Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
AttachmentService
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
8 / 8
22
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAttachment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canDownloadAttachment
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 checkGroupPermission
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 checkUserPermission
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getUserRights
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getAttachmentErrorMessage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getGenericErrorMessage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * Attachment service for handling attachment retrieval and permissions.
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    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2025 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     2025-01-01
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Attachment;
21
22use phpMyFAQ\Configuration;
23use phpMyFAQ\Faq\Permission;
24use phpMyFAQ\Permission\MediumPermission;
25use phpMyFAQ\Translation;
26use phpMyFAQ\User\CurrentUser;
27
28/**
29 * Service for attachment operations and permission checks.
30 */
31final readonly class AttachmentService
32{
33    public function __construct(
34        private Configuration $configuration,
35        private CurrentUser $currentUser,
36        private Permission $faqPermission,
37    ) {
38    }
39
40    /**
41     * Retrieves an attachment by ID.
42     *
43     * @throws AttachmentException
44     */
45    public function getAttachment(int $attachmentId): File
46    {
47        return AttachmentFactory::create($attachmentId);
48    }
49
50    /**
51     * Checks if the current user has permission to download an attachment.
52     */
53    public function canDownloadAttachment(AbstractAttachment $attachment): bool
54    {
55        // Allow downloads for guests if configured
56        if ($this->configuration->get('records.allowDownloadsForGuests')) {
57            return true;
58        }
59
60        // Check group and user permissions
61        $hasGroupPermission = $this->checkGroupPermission($attachment);
62        $hasUserPermission = $this->checkUserPermission($attachment);
63        $userRights = $this->getUserRights();
64
65        return $hasGroupPermission && $hasUserPermission && ($userRights['dlattachment'] ?? false);
66    }
67
68    /**
69     * Checks group permission for an attachment.
70     */
71    private function checkGroupPermission(AbstractAttachment $attachment): bool
72    {
73        if (!$this->currentUser->perm instanceof MediumPermission) {
74            return true;
75        }
76
77        $groupPermission = $this->faqPermission->get(Permission::GROUP, $attachment->getRecordId());
78
79        if ($groupPermission === []) {
80            return false;
81        }
82
83        // -1 means "all groups"
84        if (in_array(-1, $groupPermission, strict: true)) {
85            return true;
86        }
87
88        foreach ($this->currentUser->perm->getUserGroups($this->currentUser->getUserId()) as $userGroup) {
89            if (!in_array($userGroup, $groupPermission, strict: true)) {
90                continue;
91            }
92
93            return true;
94        }
95
96        return false;
97    }
98
99    /**
100     * Checks user permission for an attachment.
101     */
102    private function checkUserPermission(AbstractAttachment $attachment): bool
103    {
104        $userPermission = $this->faqPermission->get(Permission::USER, $attachment->getRecordId());
105
106        // -1 means "all users"
107        if (in_array(-1, $userPermission, strict: true)) {
108            return true;
109        }
110
111        return in_array($this->currentUser->getUserId(), $userPermission, strict: true);
112    }
113
114    /**
115     * Gets all user rights.
116     *
117     * @return array<string, bool>
118     */
119    private function getUserRights(): array
120    {
121        $permission = [];
122
123        if (!$this->currentUser->isLoggedIn()) {
124            return $permission;
125        }
126
127        // Read all rights, set false
128        $allRights = $this->currentUser->perm->getAllRightsData();
129        foreach ($allRights as $right) {
130            $permission[(string) $right['name']] = false;
131        }
132
133        // Check user rights, set true
134        $allUserRights = $this->currentUser->perm->getAllUserRights($this->currentUser->getUserId());
135        foreach ($allRights as $allRight) {
136            if (!in_array($allRight['right_id'], $allUserRights, strict: true)) {
137                continue;
138            }
139
140            $permission[(string) $allRight['name']] = true;
141        }
142
143        return $permission;
144    }
145
146    /**
147     * Gets an error message for attachment exceptions.
148     */
149    public function getAttachmentErrorMessage(AttachmentException $attachmentException): string
150    {
151        return Translation::getString(key: 'msgAttachmentInvalid') . ' (' . $attachmentException->getMessage() . ')';
152    }
153
154    /**
155     * Gets generic attachment error message.
156     */
157    public function getGenericErrorMessage(): string
158    {
159        $message = Translation::get(key: 'msgAttachmentInvalid');
160
161        return is_string($message) ? $message : '';
162    }
163}