Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.26% covered (success)
97.26%
71 / 73
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Import
97.26% covered (success)
97.26%
71 / 73
60.00% covered (warning)
60.00%
3 / 5
21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 import
97.67% covered (success)
97.67%
42 / 43
0.00% covered (danger)
0.00%
0 / 1
4
 parseCSV
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 isCSVFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 validateCSV
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
13
1<?php
2
3/**
4 * Class for importing records from a csv file.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Jan Harms <model_railroader@gmx-topmail.de>
12 * @copyright 2024-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2024-01-05
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Faq;
21
22use phpMyFAQ\Category;
23use phpMyFAQ\Configuration;
24use phpMyFAQ\Core\Exception;
25use phpMyFAQ\Entity\FaqEntity;
26use phpMyFAQ\Faq;
27use phpMyFAQ\Filter;
28use phpMyFAQ\User\CurrentUser;
29use Symfony\Component\HttpFoundation\File\UploadedFile;
30
31/**
32 * Class FaqImport
33 *
34 * @package phpMyFAQ\Faq
35 */
36
37readonly class Import
38{
39    public function __construct(
40        private Configuration $configuration,
41    ) {
42    }
43
44    /**
45     * Imports a record with the given record data.
46     *
47     * @param array $record Record data
48     * @throws Exception
49     */
50    public function import(array $record): bool
51    {
52        $user = CurrentUser::getCurrentUser($this->configuration);
53        [$currentUser, $currentGroups] = CurrentUser::getCurrentUserGroupId($user);
54
55        $categoryId = Filter::filterVar($record[0], FILTER_VALIDATE_INT);
56        $question = Filter::filterVar($record[1], FILTER_SANITIZE_SPECIAL_CHARS, '');
57        $answer = Filter::filterVar($record[2], FILTER_SANITIZE_SPECIAL_CHARS, '');
58        $keywords = Filter::filterVar($record[3], FILTER_SANITIZE_SPECIAL_CHARS, '');
59        $languageCode = Filter::filterVar($record[4], FILTER_SANITIZE_SPECIAL_CHARS, '');
60        $author = Filter::filterVar($record[5], FILTER_SANITIZE_SPECIAL_CHARS, '');
61        $email = Filter::filterEmail($record[6]);
62        $email = is_string($email) ? $email : '';
63        $isActive = Filter::filterVar($record[7], FILTER_VALIDATE_BOOLEAN);
64        $isSticky = Filter::filterVar($record[8], FILTER_VALIDATE_BOOLEAN);
65
66        $faq = new Faq($this->configuration);
67        $faq->setUser($currentUser);
68        $faq->setGroups($currentGroups);
69
70        $category = new Category($this->configuration, $currentGroups, true);
71        $category->setUser($currentUser);
72        $category->setGroups($currentGroups);
73        $category->setLanguage($languageCode);
74
75        if ($faq->hasTitleAHash($question)) {
76            throw new Exception('It is not allowed, that the question title ' . $question . ' contains a hash.');
77        }
78
79        $categories = [(int) $categoryId];
80        $isActive = !is_null($isActive);
81        $isSticky = !is_null($isSticky);
82
83        $faqEntity = new FaqEntity();
84        $faqEntity
85            ->setLanguage($languageCode)
86            ->setQuestion($question)
87            ->setAnswer($answer)
88            ->setKeywords($keywords)
89            ->setAuthor($author)
90            ->setEmail($email)
91            ->setActive($isActive)
92            ->setSticky($isSticky)
93            ->setComment(false)
94            ->setNotes('');
95
96        $faqEntity = $faq->create($faqEntity);
97
98        $faqId = $faqEntity->getId();
99        if ($faqId === null) {
100            return false;
101        }
102
103        $faqMetaData = new MetaData($this->configuration);
104        $faqMetaData->setFaqId($faqId)->setFaqLanguage($languageCode)->setCategories($categories)->save();
105
106        return true;
107    }
108
109    /**
110     * Returns the data from a csv file.
111     *
112     * @param resource $handle
113     *
114     * @return array<int<0, max>, array> $csvData
115     */
116    public function parseCSV($handle): array
117    {
118        $csvData = [];
119        while (($record = fgetcsv(stream: $handle, separator: ',', enclosure: '"', escape: '')) !== false) {
120            $csvData[] = $record;
121        }
122
123        return $csvData;
124    }
125
126    /**
127     * Returns true if given Symfony FileBag-object is a csv file. Returns false if not.
128     *
129     *
130     */
131    public function isCSVFile(UploadedFile $uploadedFile): bool
132    {
133        $allowedExtensions = ['csv'];
134        $fileExtension = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_EXTENSION);
135
136        return in_array(strtolower($fileExtension), $allowedExtensions, strict: true);
137    }
138
139    /**
140     * Checks if the given csv-Data matches all requirements.
141     *
142     * @param array<array-key, array<array-key, mixed>> $csvData
143     */
144    public function validateCSV(array $csvData): bool
145    {
146        foreach ($csvData as $row) {
147            if (count($row) !== 9) {
148                return false;
149            }
150
151            $requiredColumns = [0, 1, 2, 4, 5, 6, 7, 8];
152            foreach ($requiredColumns as $requiredColumn) {
153                if (!array_key_exists($requiredColumn, $row)) {
154                    return false;
155                }
156
157                $value = $row[$requiredColumn];
158                if (
159                    $value === null
160                    || $value === ''
161                    || $value === '0'
162                    || $value === 0
163                    || $value === false
164                    || $value === []
165                ) {
166                    return false;
167                }
168            }
169
170            $activatedColumn = 7;
171            $importantFAQColumn = 8;
172            $validBooleanValues = ['true', 'false'];
173
174            if (
175                !in_array(strtolower((string) $row[$activatedColumn]), $validBooleanValues, strict: true)
176                || !in_array(strtolower((string) $row[$importantFAQColumn]), $validBooleanValues, strict: true)
177            ) {
178                return false;
179            }
180        }
181
182        return true;
183    }
184}