Passed
Pull Request — master (#7430)
by
unknown
13:24 queued 03:43
created

PageHelper::slugCss()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Helpers;
8
9
use Chamilo\CoreBundle\Entity\AccessUrl;
10
use Chamilo\CoreBundle\Entity\Page;
11
use Chamilo\CoreBundle\Entity\PageCategory;
12
use Chamilo\CoreBundle\Entity\User;
13
use Chamilo\CoreBundle\Repository\PageCategoryRepository;
14
use Chamilo\CoreBundle\Repository\PageRepository;
15
use Chamilo\CoreBundle\Repository\SysAnnouncementRepository;
16
use Symfony\Component\HttpFoundation\Request;
17
use Symfony\Component\Security\Core\User\UserInterface;
18
19
use const PHP_URL_PATH;
20
21
class PageHelper
22
{
23
    protected PageRepository $pageRepository;
24
    protected PageCategoryRepository $pageCategoryRepository;
25
26
    /**
27
     * Repository used to read system announcements (platform news).
28
     */
29
    protected SysAnnouncementRepository $sysAnnouncementRepository;
30
31
    /**
32
     * Helper used to retrieve the current AccessUrl.
33
     */
34
    protected AccessUrlHelper $accessUrlHelper;
35
36
    public function __construct(
37
        PageRepository $pageRepository,
38
        PageCategoryRepository $pageCategoryRepository,
39
        SysAnnouncementRepository $sysAnnouncementRepository,
40
        AccessUrlHelper $accessUrlHelper
41
    ) {
42
        $this->pageRepository = $pageRepository;
43
        $this->pageCategoryRepository = $pageCategoryRepository;
44
        $this->sysAnnouncementRepository = $sysAnnouncementRepository;
45
        $this->accessUrlHelper = $accessUrlHelper;
46
    }
47
48
    public function createDefaultPages(User $user, AccessUrl $url, string $locale): bool
49
    {
50
        $categories = $this->pageCategoryRepository->findAll();
51
52
        if (!empty($categories)) {
53
            return false;
54
        }
55
56
        $category = (new PageCategory())
57
            ->setTitle('home')
58
            ->setType('grid')
59
            ->setCreator($user)
60
        ;
61
        $this->pageCategoryRepository->update($category);
62
63
        $indexCategory = (new PageCategory())
64
            ->setTitle('index')
65
            ->setType('grid')
66
            ->setCreator($user)
67
        ;
68
        $this->pageCategoryRepository->update($indexCategory);
69
70
        $indexCategory = (new PageCategory())
71
            ->setTitle('faq')
72
            ->setType('grid')
73
            ->setCreator($user)
74
        ;
75
        $this->pageCategoryRepository->update($indexCategory);
76
77
        $indexCategory = (new PageCategory())
78
            ->setTitle('demo')
79
            ->setType('grid')
80
            ->setCreator($user)
81
        ;
82
        $this->pageCategoryRepository->update($indexCategory);
83
84
        $page = (new Page())
85
            ->setTitle('Welcome')
86
            ->setContent('Welcome to Chamilo')
87
            ->setCategory($category)
88
            ->setCreator($user)
89
            ->setLocale($locale)
90
            ->setEnabled(true)
91
            ->setUrl($url)
92
        ;
93
94
        $this->pageRepository->update($page);
95
96
        $indexPage = (new Page())
97
            ->setTitle('Welcome')
98
            ->setContent('<img src="/img/document/images/mr_chamilo/svg/teaching.svg" />')
99
            ->setCategory($indexCategory)
100
            ->setCreator($user)
101
            ->setLocale($locale)
102
            ->setEnabled(true)
103
            ->setUrl($url)
104
        ;
105
        $this->pageRepository->update($indexPage);
106
107
        $footerPublicCategory = (new PageCategory())
108
            ->setTitle('footer_public')
109
            ->setType('grid')
110
            ->setCreator($user)
111
        ;
112
113
        $this->pageCategoryRepository->update($footerPublicCategory);
114
115
        $footerPrivateCategory = (new PageCategory())
116
            ->setTitle('footer_private')
117
            ->setType('grid')
118
            ->setCreator($user)
119
        ;
120
121
        $this->pageCategoryRepository->update($footerPrivateCategory);
122
123
        // Categories for extra content in admin blocks.
124
        foreach (PageCategory::ADMIN_BLOCKS_CATEGORIES as $nameBlock) {
125
            $usersAdminBlock = (new PageCategory())
126
                ->setTitle($nameBlock)
127
                ->setType('grid')
128
                ->setCreator($user)
129
            ;
130
            $this->pageCategoryRepository->update($usersAdminBlock);
131
        }
132
133
        $publicCategory = (new PageCategory())
134
            ->setTitle('public')
135
            ->setType('grid')
136
            ->setCreator($user)
137
        ;
138
139
        $this->pageCategoryRepository->update($publicCategory);
140
141
        $introductionCategory = (new PageCategory())
142
            ->setTitle('introduction')
143
            ->setType('grid')
144
            ->setCreator($user)
145
        ;
146
        $this->pageCategoryRepository->update($introductionCategory);
147
148
        return true;
149
    }
150
151
    /**
152
     * Checks if a document file URL is effectively exposed through a visible system announcement.
153
     *
154
     * This centralizes the logic used by different parts of the platform (e.g. voters, controllers)
155
     * to decide if a file coming from personal files can be considered "public" because it is
156
     * embedded inside a system announcement that is visible to the current user.
157
     *
158
     * @param string             $pathInfo   Full request path (e.g. /r/document/files/{uuid}/view)
159
     * @param string|null        $identifier File identifier extracted from the URL (usually a UUID)
160
     * @param UserInterface|null $user       Current user, or null to behave as anonymous
161
     * @param string             $locale     Current locale used to fetch announcements
162
     */
163
    public function isFilePathExposedByVisibleAnnouncement(
164
        string $pathInfo,
165
        ?string $identifier,
166
        ?UserInterface $user,
167
        string $locale
168
    ): bool {
169
        // Only relax security for the document file viewer route.
170
        if ('' === $pathInfo || !str_contains($pathInfo, '/r/document/files/')) {
171
            return false;
172
        }
173
174
        // Normalize user: if no authenticated user is provided, behave as anonymous.
175
        if (null === $user) {
176
            $anon = new User();
177
            $anon->setRoles(['ROLE_ANONYMOUS']);
178
            $user = $anon;
179
        }
180
181
        $accessUrl = $this->accessUrlHelper->getCurrent();
182
183
        // Fetch announcements that are visible for the given user, URL and locale.
184
        $announcements = $this->sysAnnouncementRepository->getAnnouncements(
185
            $user,
186
            $accessUrl,
187
            $locale
188
        );
189
190
        foreach ($announcements as $item) {
191
            $content = '';
192
193
            if (\is_array($item)) {
194
                $content = (string) ($item['content'] ?? '');
195
            } elseif (\is_object($item) && method_exists($item, 'getContent')) {
196
                $content = (string) $item->getContent();
197
            }
198
199
            if ('' === $content) {
200
                continue;
201
            }
202
203
            // Check if the announcement HTML contains the viewer path or the identifier.
204
            if (
205
                str_contains($content, $pathInfo)
206
                || ($identifier && str_contains($content, $identifier))
207
            ) {
208
                return true;
209
            }
210
        }
211
212
        return false;
213
    }
214
215
    /**
216
     * Returns CSS classes that identify the current "global page type".
217
     *
218
     * @return string[]
219
     */
220
    public function getPageTypeCssClasses(Request $request): array
221
    {
222
        $pathInfo = rtrim((string) $request->getPathInfo(), '/');
223
        if ('' === $pathInfo) {
224
            $pathInfo = '/';
225
        }
226
227
        // For legacy PHP script URLs (/main/.../*.php), Symfony may return "/" as pathInfo.
228
        // In that case, rely on REQUEST_URI (path part only) to infer the real page type.
229
        $requestUri = (string) $request->server->get('REQUEST_URI', '');
230
        $uriPath = (string) (parse_url($requestUri, PHP_URL_PATH) ?? '');
231
        $uriPath = rtrim($uriPath, '/');
232
        if ('' === $uriPath) {
233
            $uriPath = '/';
234
        }
235
236
        // Use REQUEST_URI path when pathInfo is "/" but the actual URL path is not "/".
237
        $effectivePath = $pathInfo;
238
        if ('/' === $pathInfo && '/' !== $uriPath) {
239
            $effectivePath = $uriPath;
240
        }
241
242
        $segments = array_values(array_filter(
243
            explode('/', trim($effectivePath, '/')),
244
            static fn ($v) => '' !== $v
245
        ));
246
        $seg0 = $segments[0] ?? '';
247
        $seg1 = $segments[1] ?? '';
248
        $seg2 = $segments[2] ?? '';
249
250
        // Home (only when the real URL path is "/")
251
        if ('/' === $effectivePath) {
252
            return ['page-home'];
253
        }
254
255
        if ('home' === $seg0) {
256
            return ['page-home'];
257
        }
258
259
        if ('courses' === $seg0) {
260
            return ['page-my-courses'];
261
        }
262
263
        if ('catalogue' === $seg0) {
264
            return ['page-catalogue'];
265
        }
266
267
        if ('agenda' === $seg0 || 'calendar' === $seg0) {
268
            return ['page-agenda'];
269
        }
270
271
        if ('tracking' === $seg0) {
272
            return ['page-tracking'];
273
        }
274
275
        if ('social' === $seg0) {
276
            return ['page-social'];
277
        }
278
279
        if ('account' === $seg0) {
280
            return ['page-account-security'];
281
        }
282
283
        if ('admin-dashboard' === $seg0) {
284
            return ['page-administration', 'page-administration-session'];
285
        }
286
287
        // Administration + sub-blocks (Vue)
288
        if ('admin' === $seg0) {
289
            $classes = ['page-administration'];
290
291
            if ('' !== $seg1) {
292
                // Example: /admin/settings -> page-administration page-administration-settings
293
                $classes[] = 'page-administration-'.$this->slugCss($seg1);
294
            }
295
296
            // Most Vue admin pages are platform-level.
297
            if (!\in_array('page-administration-platform', $classes, true)) {
298
                $classes[] = 'page-administration-platform';
299
            }
300
301
            return array_values(array_unique($classes));
302
        }
303
304
        // Vue "resources" routes -> optional tool markers
305
        // Example: /resources/document/... -> page-tool page-tool-document
306
        if ('resources' === $seg0 && '' !== $seg1) {
307
            return ['page-tool', 'page-tool-'.$this->slugCss($seg1)];
308
        }
309
310
        // Legacy PHP pages under /main/*
311
        if ('main' === $seg0 && '' !== $seg1) {
312
            // Tracking must share the same marker across all its pages.
313
            if ('tracking' === $seg1) {
314
                return ['page-tracking'];
315
            }
316
317
            // Legacy administration pages are NOT tools.
318
            // Examples:
319
            // - /main/admin/user_list.php    -> page-administration page-administration-user
320
            // - /main/admin/course_add.php   -> page-administration page-administration-course
321
            // - /main/admin/session_list.php -> page-administration page-administration-session
322
            if ('admin' === $seg1) {
323
                $classes = ['page-administration'];
324
325
                // Try to detect admin sub-block from the script filename (seg2), or fallback to SCRIPT_NAME.
326
                $scriptFile = $seg2;
327
                if ('' === $scriptFile) {
328
                    $scriptName = (string) $request->server->get('SCRIPT_NAME', '');
329
                    $scriptFile = basename($scriptName);
330
                }
331
332
                $block = $this->detectLegacyAdminBlock($scriptFile);
333
                $classes[] = 'page-administration-'.$block;
334
335
                // Ensure we always have a stable "platform" marker when no specific block applies.
336
                if (!\in_array('page-administration-platform', $classes, true)
337
                    && !\in_array('page-administration-user', $classes, true)
338
                    && !\in_array('page-administration-course', $classes, true)
339
                    && !\in_array('page-administration-session', $classes, true)
340
                ) {
341
                    $classes[] = 'page-administration-platform';
342
                }
343
344
                return array_values(array_unique($classes));
345
            }
346
347
            // Other legacy tools: /main/<tool>/* -> page-tool + page-tool-<tool>
348
            return ['page-tool', 'page-tool-'.$this->slugCss($seg1)];
349
        }
350
351
        // Legacy tools fallback by script name (extra safety)
352
        $script = (string) $request->server->get('SCRIPT_NAME', '');
353
        if (preg_match('#/main/([a-z_]+)/#', $script, $m)) {
354
            $tool = (string) $m[1];
355
356
            if ('tracking' === $tool) {
357
                return ['page-tracking'];
358
            }
359
360
            // Legacy administration pages are NOT tools.
361
            if ('admin' === $tool) {
362
                $classes = ['page-administration'];
363
                $classes[] = 'page-administration-'.$this->detectLegacyAdminBlock(basename($script));
364
365
                // Safe default when no specific block applies.
366
                if (!\in_array('page-administration-platform', $classes, true)
367
                    && !\in_array('page-administration-user', $classes, true)
368
                    && !\in_array('page-administration-course', $classes, true)
369
                    && !\in_array('page-administration-session', $classes, true)
370
                ) {
371
                    $classes[] = 'page-administration-platform';
372
                }
373
374
                return array_values(array_unique($classes));
375
            }
376
377
            return ['page-tool', 'page-tool-'.$this->slugCss($tool)];
378
        }
379
380
        // Generic fallback: page-<first segment>
381
        if ('' !== $seg0) {
382
            return ['page-'.$this->slugCss($seg0)];
383
        }
384
385
        return ['page-generic'];
386
    }
387
388
    /**
389
     * Detect legacy admin block from filename.
390
     * Keeps theming markers stable without hardcoding every admin file.
391
     */
392
    private function detectLegacyAdminBlock(string $scriptFile): string
393
    {
394
        $file = strtolower($scriptFile);
395
396
        if (str_starts_with($file, 'user_') || str_starts_with($file, 'user-')) {
397
            return 'user';
398
        }
399
400
        if (str_starts_with($file, 'course_') || str_starts_with($file, 'course-')) {
401
            return 'course';
402
        }
403
404
        if (str_starts_with($file, 'session_') || str_starts_with($file, 'session-')) {
405
            return 'session';
406
        }
407
408
        return 'platform';
409
    }
410
411
    /**
412
     * Converts a string into a safe CSS class fragment.
413
     */
414
    private function slugCss(string $value): string
415
    {
416
        $value = strtolower(trim($value));
417
        $value = preg_replace('/[^a-z0-9\-_]+/', '-', $value) ?? $value;
418
        $value = trim($value, '-');
419
420
        return '' !== $value ? $value : 'generic';
421
    }
422
}
423