Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.28% covered (success)
98.28%
57 / 58
80.00% covered (success)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
AttachmentFactory
98.28% covered (success)
98.28%
57 / 58
80.00% covered (success)
80.00%
4 / 5
20
0.00% covered (danger)
0.00%
0 / 1
 create
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 fetchByRecordId
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 fetchByRecordIdPaginated
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
6
 countByRecordId
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 init
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
1<?php
2
3/**
4 * Attachment handler class.
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    Anatoliy Belsky <ab@php.net>
12 * @since     2009-08-21
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @copyright 2009-2026 phpMyFAQ Team
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Attachment;
21
22use phpMyFAQ\Configuration;
23use phpMyFAQ\Database;
24use phpMyFAQ\Enums\AttachmentStorageType;
25use phpMyFAQ\Language;
26
27/**
28 * Class AttachmentFactory
29 *
30 * @package phpMyFAQ\Attachment
31 */
32class AttachmentFactory
33{
34    /**
35     * Default encryption key.
36     */
37    private static ?string $defaultKey = null;
38
39    /**
40     * Storage type.
41     */
42    private static ?int $storageType = 0;
43
44    /**
45     * File encryption is enabled.
46     */
47    private static ?bool $encryptionEnabled = null;
48
49    /**
50     * Create an attachment exemplar.
51     *
52     * @param int|null    $attachmentId  ID
53     * @param string|null $key Key
54     * @throws AttachmentException
55     */
56    public static function create(?int $attachmentId = null, ?string $key = null): File
57    {
58        $return = match (self::$storageType) {
59            AttachmentStorageType::FILESYSTEM->value, AttachmentStorageType::S3->value => new File($attachmentId),
60            default => throw new AttachmentException('Unknown attachment storage type'),
61        };
62
63        /*
64         * If encryption isn't enabled, just ignoring all keys
65         */
66        if (self::$encryptionEnabled) {
67            $key ??= self::$defaultKey;
68        }
69
70        $return->setKey($key);
71
72        return $return;
73    }
74
75    /**
76     * Fetch all record attachments.
77     *
78     * @param int           $recordId ID of the record
79     *
80     * @throws AttachmentException
81     * @return File[]
82     */
83    public static function fetchByRecordId(Configuration $configuration, int $recordId): array
84    {
85        $files = [];
86
87        $sql = sprintf(
88            "SELECT id FROM %sfaqattachment WHERE record_id = %d AND record_lang = '%s'",
89            Database::getTablePrefix(),
90            $recordId,
91            Language::$language,
92        );
93
94        $result = $configuration->getDb()->fetchAll($configuration->getDb()->query($sql));
95
96        if ($result !== null && $result !== []) {
97            foreach ($result as $item) {
98                $files[] = self::create((int) $item->id);
99            }
100        }
101
102        return $files;
103    }
104
105    /**
106     * Fetch record attachments with pagination and sorting support.
107     *
108     * @param Configuration $configuration Configuration instance
109     * @param int $recordId ID of the record
110     * @param int $limit Number of items to fetch
111     * @param int $offset Starting offset
112     * @param string $sortField Field to sort by (id, filename, mime_type, filesize, created)
113     * @param string $sortOrder Sort order (ASC or DESC)
114     *
115     * @throws AttachmentException
116     * @return array Array of attachment data with filename and URL
117     */
118    public static function fetchByRecordIdPaginated(
119        Configuration $configuration,
120        int $recordId,
121        int $limit = 25,
122        int $offset = 0,
123        string $sortField = 'id',
124        string $sortOrder = 'ASC',
125    ): array {
126        $files = [];
127
128        // Validate sort field to prevent SQL injection
129        $allowedSortFields = ['id', 'filename', 'mime_type', 'filesize', 'created'];
130        if (!in_array($sortField, $allowedSortFields, strict: true)) {
131            $sortField = 'id';
132        }
133
134        // Validate sort order
135        $sortOrder = strtoupper($sortOrder) === 'DESC' ? 'DESC' : 'ASC';
136
137        $sql = sprintf(
138            "SELECT id FROM %sfaqattachment WHERE record_id = %d AND record_lang = '%s' ORDER BY %s %s LIMIT %d OFFSET %d",
139            Database::getTablePrefix(),
140            $recordId,
141            $configuration->getDb()->escape(Language::$language),
142            $sortField,
143            $sortOrder,
144            $limit,
145            $offset,
146        );
147
148        $result = $configuration->getDb()->fetchAll($configuration->getDb()->query($sql));
149
150        if ($result !== null && $result !== []) {
151            foreach ($result as $item) {
152                $attachment = self::create((int) $item->id);
153                $files[] = [
154                    'filename' => $attachment->getFilename(),
155                    'url' => $configuration->getDefaultUrl() . $attachment->buildUrl(),
156                ];
157            }
158        }
159
160        return $files;
161    }
162
163    /**
164     * Count total number of attachments for a record.
165     *
166     * @param Configuration $configuration Configuration instance
167     * @param int $recordId ID of the record
168     * @return int Total count of attachments
169     */
170    public static function countByRecordId(Configuration $configuration, int $recordId): int
171    {
172        $sql = sprintf(
173            "SELECT COUNT(*) as total FROM %sfaqattachment WHERE record_id = %d AND record_lang = '%s'",
174            Database::getTablePrefix(),
175            $recordId,
176            $configuration->getDb()->escape(Language::$language),
177        );
178
179        $result = $configuration->getDb()->query($sql);
180        $row = $configuration->getDb()->fetchObject($result);
181
182        return $row instanceof \stdClass ? (int) ($row->total ?? 0) : 0;
183    }
184
185    /**
186     * Initializing the factory with global attachment settings.
187     *
188     * @param string $defaultKey        Default key
189     * @param bool   $encryptionEnabled Enabled encryption?
190     * @param int|null $storageType Optional storage type (defaults to filesystem)
191     */
192    public static function init(string $defaultKey, bool $encryptionEnabled, ?int $storageType = null): void
193    {
194        if (null === self::$defaultKey) {
195            self::$defaultKey = $defaultKey;
196        }
197
198        if (null === self::$encryptionEnabled) {
199            self::$encryptionEnabled = $encryptionEnabled;
200        }
201
202        if ($storageType !== null) {
203            self::$storageType = $storageType;
204        }
205    }
206}