Passed
Pull Request — master (#6922)
by
unknown
09:08 queued 22s
created

CertificateController::readCertificateHtml()   D

Complexity

Conditions 25
Paths 28

Size

Total Lines 95
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 25
eloc 53
nc 28
nop 2
dl 0
loc 95
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Controller;
8
9
use Chamilo\CoreBundle\Entity\GradebookCertificate;
10
use Chamilo\CoreBundle\Framework\Container;
11
use Chamilo\CoreBundle\Helpers\UserHelper;
12
use Chamilo\CoreBundle\Repository\GradebookCertificateRepository;
13
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
14
use Chamilo\CoreBundle\Settings\SettingsManager;
15
use Mpdf\Mpdf;
16
use Mpdf\MpdfException;
17
use Mpdf\Output\Destination;
18
use RuntimeException;
19
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
20
use Symfony\Component\HttpFoundation\Response;
21
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
22
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
23
use Symfony\Component\Routing\Attribute\Route;
24
25
#[Route('/certificates')]
26
class CertificateController extends AbstractController
27
{
28
    public function __construct(
29
        private readonly GradebookCertificateRepository $certificateRepository,
30
        private readonly SettingsManager $settingsManager,
31
        private readonly UserHelper $userHelper,
32
        private readonly ResourceNodeRepository $resourceNodeRepository,
33
    ) {}
34
35
    #[Route('/{hash}.html', name: 'chamilo_certificate_public_view', methods: ['GET'])]
36
    public function view(string $hash): Response
37
    {
38
        // Resolve certificate row (keeps legacy path logic working)
39
        [$certificate] = $this->resolveCertificateByHash($hash);
40
41
        // Permission checks
42
        $this->assertCertificateAccess($certificate);
43
44
        // Read HTML from resource storage (new) or personal-file (legacy)
45
        $html = $this->readCertificateHtml($certificate, $hash);
46
        $html = str_replace(' media="screen"', '', $html);
47
48
        return new Response('<!DOCTYPE html>'.$html, 200, [
49
            'Content-Type' => 'text/html; charset=UTF-8',
50
        ]);
51
    }
52
53
    #[Route('/{hash}.pdf', name: 'chamilo_certificate_public_pdf', methods: ['GET'])]
54
    public function downloadPdf(string $hash): Response
55
    {
56
        // Resolve certificate row
57
        [$certificate] = $this->resolveCertificateByHash($hash);
58
59
        // Permission checks
60
        $this->assertCertificateAccess($certificate);
61
62
        // Read HTML and render PDF
63
        $html = $this->readCertificateHtml($certificate, $hash);
64
        $html = str_replace(' media="screen"', '', $html);
65
66
        try {
67
            $mpdf = new Mpdf([
68
                'format'  => 'A4',
69
                'tempDir' => api_get_path(SYS_ARCHIVE_PATH).'mpdf/',
70
            ]);
71
            $mpdf->WriteHTML($html);
72
            $pdfBinary = $mpdf->Output('', Destination::STRING_RETURN);
73
74
            return new Response(
75
                $pdfBinary,
76
                200,
77
                [
78
                    'Content-Type'        => 'application/pdf',
79
                    'Content-Disposition' => 'attachment; filename="certificate.pdf"',
80
                ]
81
            );
82
        } catch (MpdfException $e) {
83
            throw new RuntimeException('Failed to generate PDF: '.$e->getMessage(), 500, $e);
84
        }
85
    }
86
87
    /**
88
     * Resolve the certificate row via path.
89
     *
90
     * @return array{0: GradebookCertificate, 1: string}
91
     *
92
     * @throws NotFoundHttpException
93
     */
94
    private function resolveCertificateByHash(string $hash): array
95
    {
96
        $filename   = $hash.'.html';
97
        $candidates = [$filename, '/'.$filename, $hash, '/'.$hash];
98
99
        $certificate = null;
100
        $matchedPath = '';
101
102
        foreach ($candidates as $cand) {
103
            $row = $this->certificateRepository->findOneBy(['pathCertificate' => $cand]);
104
            if ($row) {
105
                $certificate = $row;
106
                $matchedPath = $cand;
107
                break;
108
            }
109
        }
110
111
        if (!$certificate instanceof GradebookCertificate) {
112
            throw new NotFoundHttpException('The requested certificate does not exist.');
113
        }
114
115
        return [$certificate, $matchedPath];
116
    }
117
118
    /**
119
     * Owner/admin OR (public+published) OR (session admin if allowed).
120
     *
121
     * @throws AccessDeniedHttpException
122
     */
123
    private function assertCertificateAccess(GradebookCertificate $certificate): void
124
    {
125
        $allowPublic       = 'true' === $this->settingsManager->getSetting('certificate.allow_public_certificates', true);
126
        $allowSessionAdmin = 'true' === $this->settingsManager->getSetting('certificate.session_admin_can_download_all_certificates', true);
127
128
        $user         = $this->userHelper->getCurrent();
129
        $securityUser = $this->getUser();
130
131
        $isOwner         = $securityUser && method_exists($securityUser, 'getId') && $user->getId() === $securityUser->getId();
132
        $isPlatformAdmin = method_exists($user, 'isAdmin') && $user->isAdmin();
133
134
        if ($isOwner || $isPlatformAdmin) {
135
            return;
136
        }
137
138
        $isPublic           = ($allowPublic && $certificate->getPublish());
139
        $isSessAdminAllowed = ($allowSessionAdmin && method_exists($user, 'isSessionAdmin') && $user->isSessionAdmin());
140
141
        if (!$isPublic && !$isSessAdminAllowed) {
142
            throw new AccessDeniedHttpException('The requested certificate is not public.');
143
        }
144
    }
145
146
    /**
147
     * Returns certificate HTML from resource-node (new flow) or personal file (legacy).
148
     *
149
     * It tries multiple physical paths to accommodate different storage layouts:
150
     *  1) node->getPath() + ResourceFile->title
151
     *  2) node->getPath() + ResourceFile->original_name
152
     *  3) sharded path "resource/<a>/<b>/<c>/<file>" using title
153
     *  4) sharded path "resource/<a>/<b>/<c>/<file>" using original_name
154
     *  5) final fallback: generic getResourceNodeFileContent()
155
     *  6) legacy fallback: PersonalFile by title
156
     *
157
     * @throws NotFoundHttpException
158
     */
159
    private function readCertificateHtml(GradebookCertificate $certificate, string $hash): string
160
    {
161
        // Preferred flow: read from ResourceNode
162
        if ($certificate->hasResourceNode()) {
163
            $node = $certificate->getResourceNode();
164
            $fs   = $this->resourceNodeRepository->getFileSystem();
165
166
            if ($fs) {
0 ignored issues
show
introduced by
$fs is of type League\Flysystem\FilesystemOperator, thus it always evaluated to true.
Loading history...
167
                $basePath = rtrim((string) $node->getPath(), '/');
168
169
                // Helper to create sharded path: resource/7/4/3/<filename>
170
                $sharded = static function (string $filename): string {
171
                    $a = $filename[0] ?? '_';
172
                    $b = $filename[1] ?? '_';
173
                    $c = $filename[2] ?? '_';
174
175
                    return sprintf('resource/%s/%s/%s/%s', $a, $b, $c, $filename);
176
                };
177
178
                // Try via ResourceFile->title first (this is usually the stored physical filename)
179
                foreach ($node->getResourceFiles() as $rf) {
180
                    $title = (string) $rf->getTitle();
181
                    if ($title !== '') {
182
                        if ($basePath !== '') {
183
                            $p = $basePath.'/'.$title;
184
                            if ($fs->fileExists($p)) {
185
                                $content = $fs->read($p);
186
                                if ($content !== false && $content !== null) {
187
                                    return $content;
188
                                }
189
                            }
190
                        }
191
192
                        $p2 = $sharded($title);
193
                        if ($fs->fileExists($p2)) {
194
                            $content = $fs->read($p2);
195
                            if ($content !== false && $content !== null) {
196
                                return $content;
197
                            }
198
                        }
199
                    }
200
                }
201
202
                // Try via ResourceFile->original_name
203
                foreach ($node->getResourceFiles() as $rf) {
204
                    $orig = (string) $rf->getOriginalName();
205
                    if ($orig !== '') {
206
                        if ($basePath !== '') {
207
                            $p = $basePath.'/'.$orig;
208
                            if ($fs->fileExists($p)) {
209
                                $content = $fs->read($p);
210
                                if ($content !== false && $content !== null) {
211
                                    return $content;
212
                                }
213
                            }
214
                        }
215
216
                        $p2 = $sharded($orig);
217
                        if ($fs->fileExists($p2)) {
218
                            $content = $fs->read($p2);
219
                            if ($content !== false && $content !== null) {
220
                                return $content;
221
                            }
222
                        }
223
                    }
224
                }
225
            }
226
227
            // Final resource fallback (may still fail if no default file is set)
228
            try {
229
                return $this->resourceNodeRepository->getResourceNodeFileContent($node);
230
            } catch (\Throwable $e) {
231
                // Continue to legacy fallback
232
            }
233
        }
234
235
        // Legacy flow: PersonalFile by title
236
        $filename   = $hash.'.html';
237
        $candidates = [$filename, '/'.$filename, $hash, '/'.$hash];
238
239
        $personalFileRepo = Container::getPersonalFileRepository();
240
        $pf = null;
241
        foreach ($candidates as $cand) {
242
            $row = $personalFileRepo->findOneBy(['title' => $cand]);
243
            if ($row) {
244
                $pf = $row;
245
                break;
246
            }
247
        }
248
249
        if (!$pf) {
250
            throw new NotFoundHttpException('The certificate file was not found.');
251
        }
252
253
        return $personalFileRepo->getResourceFileContent($pf);
254
    }
255
}
256