Passed
Pull Request — master (#7182)
by
unknown
10:46
created

ChamiloHelper::buildUrlMapForHtmlFromPackage()   F

Complexity

Conditions 35
Paths 1883

Size

Total Lines 163
Code Lines 96

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 35
eloc 96
nc 1883
nop 12
dl 0
loc 163
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Helpers;
8
9
use Chamilo\CoreBundle\Entity\AbstractResource;
10
use Chamilo\CoreBundle\Entity\Asset;
11
use Chamilo\CoreBundle\Framework\Container;
12
use Chamilo\CoreBundle\Repository\ResourceRepository;
13
use ChamiloSession as Session;
14
use Database;
15
use DateInterval;
16
use DateTime;
17
use DateTimeZone;
18
use Display;
19
use DocumentManager;
20
use Event;
21
use Exception;
22
use ExtraFieldValue;
23
use FormValidator;
24
use LegalManager;
25
use MessageManager;
26
use Symfony\Component\HttpFoundation\File\UploadedFile;
27
use Template;
28
use Throwable;
29
use UserManager;
30
31
use const ENT_HTML5;
32
use const ENT_QUOTES;
33
use const PHP_ROUND_HALF_UP;
34
use const PHP_SAPI;
35
use const PHP_URL_PATH;
36
37
class ChamiloHelper
38
{
39
    public const COURSE_MANAGER = 1;
40
    public const SESSION_ADMIN = 3;
41
    public const DRH = 4;
42
    public const STUDENT = 5;
43
    public const ANONYMOUS = 6;
44
45
    private static array $configuration;
46
47
    public function setConfiguration(array $configuration): void
48
    {
49
        self::$configuration = $configuration;
50
    }
51
52
    public static function getConfigurationArray(): array
53
    {
54
        return self::$configuration;
55
    }
56
57
    public static function getConfigurationValue(string $variable): mixed
58
    {
59
        $configuration = self::getConfigurationArray();
60
        if (\array_key_exists($variable, $configuration)) {
61
            return $configuration[$variable];
62
        }
63
64
        return false;
65
    }
66
67
    /**
68
     * Returns an array of resolutions that can be used for the conversion of documents to images.
69
     */
70
    public static function getDocumentConversionSizes(): array
71
    {
72
        return [
73
            '540x405' => '540x405 (3/4)',
74
            '640x480' => '640x480 (3/4)',
75
            '720x540' => '720x540 (3/4)',
76
            '800x600' => '800x600 (3/4)',
77
            '1024x576' => '1024x576 (16/9)',
78
            '1024x768' => '1000x750 (3/4)',
79
            '1280x720' => '1280x720 (16/9)',
80
            '1280x860' => '1280x960 (3/4)',
81
            '1400x1050' => '1400x1050 (3/4)',
82
            '1600x900' => '1600x900 (16/9)',
83
        ];
84
    }
85
86
    /**
87
     * Get the platform logo path.
88
     *
89
     * @deprecated
90
     *
91
     * @throws Exception
92
     */
93
    public static function getPlatformLogoPath(
94
        string $theme = '',
95
        bool $getSysPath = false,
96
        bool $forcedGetter = false
97
    ): ?string {
98
        static $logoPath;
99
100
        // If call from CLI it should be reloaded.
101
        if ('cli' === PHP_SAPI) {
102
            $logoPath = null;
103
        }
104
105
        if (!isset($logoPath) || $forcedGetter) {
106
            $theme = empty($theme) ? api_get_visual_theme() : $theme;
107
            $accessUrlId = api_get_current_access_url_id();
108
            if ('cli' === PHP_SAPI) {
109
                $accessUrl = api_get_configuration_value('access_url');
110
                if (!empty($accessUrl)) {
111
                    $accessUrlId = $accessUrl;
112
                }
113
            }
114
            $themeDir = Template::getThemeDir($theme);
115
            $customLogoPath = $themeDir.\sprintf('images/header-logo-custom%s.png', $accessUrlId);
116
            $customLogoPathSVG = substr($customLogoPath, 0, -3).'svg';
117
            if (file_exists(api_get_path(SYS_PUBLIC_PATH).\sprintf('css/%s', $customLogoPathSVG))) {
118
                if ($getSysPath) {
119
                    return api_get_path(SYS_PUBLIC_PATH).\sprintf('css/%s', $customLogoPathSVG);
120
                }
121
122
                return api_get_path(WEB_CSS_PATH).$customLogoPathSVG;
123
            }
124
            if (file_exists(api_get_path(SYS_PUBLIC_PATH).\sprintf('css/%s', $customLogoPath))) {
125
                if ($getSysPath) {
126
                    return api_get_path(SYS_PUBLIC_PATH).\sprintf('css/%s', $customLogoPath);
127
                }
128
129
                return api_get_path(WEB_CSS_PATH).$customLogoPath;
130
            }
131
132
            $originalLogoPath = $themeDir.'images/header-logo.png';
133
            $originalLogoPathSVG = $themeDir.'images/header-logo.svg';
134
            if (file_exists(api_get_path(SYS_CSS_PATH).$originalLogoPathSVG)) {
135
                if ($getSysPath) {
136
                    return api_get_path(SYS_CSS_PATH).$originalLogoPathSVG;
137
                }
138
139
                return api_get_path(WEB_CSS_PATH).$originalLogoPathSVG;
140
            }
141
142
            if (file_exists(api_get_path(SYS_CSS_PATH).$originalLogoPath)) {
143
                if ($getSysPath) {
144
                    return api_get_path(SYS_CSS_PATH).$originalLogoPath;
145
                }
146
147
                return api_get_path(WEB_CSS_PATH).$originalLogoPath;
148
            }
149
            $logoPath = '';
150
        }
151
152
        return $logoPath;
153
    }
154
155
    /**
156
     * Get the platform logo.
157
     * Return a <img> if the logo image exists.
158
     * Otherwise, return a <h2> with the institution name.
159
     *
160
     * @throws Exception
161
     */
162
    public static function getPlatformLogo(
163
        string $theme = '',
164
        array $imageAttributes = [],
165
        bool $getSysPath = false,
166
        bool $forcedGetter = false
167
    ): string {
168
        $logoPath = Container::getThemeHelper()->getThemeAssetUrl('images/header-logo.svg');
169
170
        if (empty($logoPath)) {
171
            $logoPath = Container::getThemeHelper()->getThemeAssetUrl('images/header-logo.png');
172
        }
173
174
        $institution = api_get_setting('Institution');
175
        $institutionUrl = api_get_setting('InstitutionUrl');
176
        $siteName = api_get_setting('siteName');
177
178
        if (null === $logoPath) {
179
            $headerLogo = Display::url($siteName, api_get_path(WEB_PATH).'index.php');
180
181
            if (!empty($institutionUrl) && !empty($institution)) {
182
                $headerLogo .= ' - '.Display::url($institution, $institutionUrl);
183
            }
184
185
            $courseInfo = api_get_course_info();
186
            if (isset($courseInfo['extLink']) && !empty($courseInfo['extLink']['name'])) {
187
                $headerLogo .= '<span class="extLinkSeparator"> - </span>';
188
189
                if (!empty($courseInfo['extLink']['url'])) {
190
                    $headerLogo .= Display::url(
191
                        $courseInfo['extLink']['name'],
192
                        $courseInfo['extLink']['url'],
193
                        [
194
                            'class' => 'extLink',
195
                        ]
196
                    );
197
                } elseif (!empty($courseInfo['extLink']['url'])) {
198
                    $headerLogo .= $courseInfo['extLink']['url'];
199
                }
200
            }
201
202
            return Display::tag('h2', $headerLogo, [
203
                'class' => 'text-left',
204
            ]);
205
        }
206
207
        $image = Display::img($logoPath, $institution, $imageAttributes);
208
209
        return Display::url($image, api_get_path(WEB_PATH).'index.php');
210
    }
211
212
    /**
213
     * Like strip_tags(), but leaves an additional space and removes only the given tags.
214
     *
215
     * @param array $tags Tags to be removed
216
     *
217
     * @return string The original string without the given tags
218
     */
219
    public static function stripGivenTags(string $string, array $tags): string
220
    {
221
        foreach ($tags as $tag) {
222
            $string2 = preg_replace('#</\b'.$tag.'\b[^>]*>#i', ' ', $string);
223
            if ($string2 !== $string) {
224
                $string = preg_replace('/<\b'.$tag.'\b[^>]*>/i', ' ', $string2);
225
            }
226
        }
227
228
        return $string;
229
    }
230
231
    /**
232
     * Adds or Subtract a time in hh:mm:ss to a datetime.
233
     *
234
     * @param string $time      Time to add or subtract in hh:mm:ss format
235
     * @param string $datetime  Datetime to be modified as accepted by the Datetime class constructor
236
     * @param bool   $operation True for Add, False to Subtract
237
     *
238
     * @throws Exception
239
     */
240
    public static function addOrSubTimeToDateTime(
241
        string $time,
242
        string $datetime = 'now',
243
        bool $operation = true
244
    ): string {
245
        $date = new DateTime($datetime);
246
        $hours = 0;
247
        $minutes = 0;
248
        $seconds = 0;
249
        sscanf($time, '%d:%d:%d', $hours, $minutes, $seconds);
250
        $timeSeconds = isset($seconds) ? $hours * 3600 + $minutes * 60 + $seconds : $hours * 60 + $minutes;
251
        if ($operation) {
252
            $date->add(new DateInterval('PT'.$timeSeconds.'S'));
253
        } else {
254
            $date->sub(new DateInterval('PT'.$timeSeconds.'S'));
255
        }
256
257
        return $date->format('Y-m-d H:i:s');
258
    }
259
260
    /**
261
     * Returns the course id (integer) for the given course directory or the current ID if no directory is defined.
262
     *
263
     * @param string|null $directory The course directory/path that appears in the URL
264
     *
265
     * @throws Exception
266
     */
267
    public static function getCourseIdByDirectory(?string $directory = null): int
268
    {
269
        if (!empty($directory)) {
270
            $directory = Database::escape_string($directory);
271
            $row = Database::select(
272
                'id',
273
                Database::get_main_table(TABLE_MAIN_COURSE),
274
                [
275
                    'where' => [
276
                        'directory = ?' => [$directory],
277
                    ],
278
                ],
279
                'first'
280
            );
281
282
            if (\is_array($row) && isset($row['id'])) {
283
                return $row['id'];
284
            }
285
286
            return 0;
287
        }
288
289
        return (int) Session::read('_real_cid', 0);
290
    }
291
292
    /**
293
     * Check if the current HTTP request is by AJAX.
294
     */
295
    public static function isAjaxRequest(): bool
296
    {
297
        $requestedWith = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? null;
298
299
        return 'XMLHttpRequest' === $requestedWith;
300
    }
301
302
    /**
303
     * Get a variable name for language file from a text.
304
     */
305
    public static function getLanguageVar(string $text, string $prefix = ''): string
306
    {
307
        $text = api_replace_dangerous_char($text);
308
        $text = str_replace(['-', ' ', '.'], '_', $text);
309
        $text = preg_replace('/_+/', '_', $text);
310
        // $text = str_replace('_', '', $text);
311
        $text = api_underscore_to_camel_case($text);
312
313
        return $prefix.$text;
314
    }
315
316
    /**
317
     * Get the stylesheet path for HTML blocks created with CKEditor.
318
     */
319
    public static function getEditorBlockStylePath(): string
320
    {
321
        $visualTheme = api_get_visual_theme();
322
323
        $cssFile = api_get_path(SYS_CSS_PATH).\sprintf('themes/%s/editor_content.css', $visualTheme);
324
325
        if (is_file($cssFile)) {
326
            return api_get_path(WEB_CSS_PATH).\sprintf('themes/%s/editor_content.css', $visualTheme);
327
        }
328
329
        return api_get_path(WEB_CSS_PATH).'editor_content.css';
330
    }
331
332
    /**
333
     * Get a list of colors from the palette at main/palette/pchart/default.color
334
     * and return it as an array of strings.
335
     *
336
     * @param bool     $decimalOpacity Whether to return the opacity as 0..100 or 0..1
337
     * @param bool     $wrapInRGBA     Whether to return it as 1,1,1,100 or rgba(1,1,1,100)
338
     * @param int|null $fillUpTo       If the number of colors is smaller than this number, generate more colors
339
     *
340
     * @return array An array of string colors
341
     */
342
    public static function getColorPalette(
343
        bool $decimalOpacity = false,
344
        bool $wrapInRGBA = false,
345
        ?int $fillUpTo = null
346
    ): array {
347
        // Get the common colors from the palette used for pchart
348
        $paletteFile = api_get_path(SYS_CODE_PATH).'palettes/pchart/default.color';
349
        $palette = file($paletteFile);
350
        if ($decimalOpacity) {
351
            // Because the pchart palette has transparency as integer values
352
            // (0..100) and chartjs uses percentage (0.0..1.0), we need to divide
353
            // the last value by 100, which is a bit overboard for just one chart
354
            foreach ($palette as $index => $color) {
355
                $components = explode(',', trim($color));
356
                $components[3] = round((int) $components[3] / 100, 1, PHP_ROUND_HALF_UP);
357
                $palette[$index] = implode(',', $components);
358
            }
359
        }
360
        if ($wrapInRGBA) {
361
            foreach ($palette as $index => $color) {
362
                $color = trim($color);
363
                $palette[$index] = 'rgba('.$color.')';
364
            }
365
        }
366
        // If we want more colors, loop through existing colors
367
        $count = \count($palette);
368
        if (isset($fillUpTo) && $fillUpTo > $count) {
369
            for ($i = $count; $i < $fillUpTo; $i++) {
370
                $palette[$i] = $palette[$i % $count];
371
            }
372
        }
373
374
        return $palette;
375
    }
376
377
    /**
378
     * Get the local time for the midnight.
379
     *
380
     * @param null|string $utcTime Optional. The time to ve converted.
381
     *                             See api_get_local_time.
382
     *
383
     * @throws Exception
384
     */
385
    public static function getServerMidnightTime(?string $utcTime = null): DateTime
386
    {
387
        $localTime = api_get_local_time($utcTime);
388
        $localTimeZone = api_get_timezone();
389
390
        $localMidnight = new DateTime($localTime, new DateTimeZone($localTimeZone));
391
        $localMidnight->modify('midnight');
392
393
        return $localMidnight;
394
    }
395
396
    /**
397
     * Get JavaScript code necessary to load quiz markers-rolls in medialement's Markers Rolls plugin.
398
     */
399
    public static function getQuizMarkersRollsJS(): string
400
    {
401
        $webCodePath = api_get_path(WEB_CODE_PATH);
402
        $cidReq = api_get_cidreq(true, true, 'embeddable');
403
        $colorPalette = self::getColorPalette(false, true);
404
405
        return "
406
            var \$originalNode = $(originalNode),
407
                    qMarkersRolls = \$originalNode.data('q-markersrolls') || [],
408
                    qMarkersColor = \$originalNode.data('q-markersrolls-color') || '$colorPalette[0]';
409
410
                if (0 == qMarkersRolls.length) {
411
                    return;
412
                }
413
414
                instance.options.markersRollsColor = qMarkersColor;
415
                instance.options.markersRollsWidth = 2;
416
                instance.options.markersRolls = {};
417
418
                qMarkersRolls.forEach(function (qMarkerRoll) {
419
                    var url = '{$webCodePath}exercise/exercise_submit.php?$cidReq&'
420
                        + $.param({
421
                            exerciseId: qMarkerRoll[1],
422
                            learnpath_id: 0,
423
                            learnpath_item_id: 0,
424
                            learnpath_item_view_id: 0
425
                        });
426
427
                    instance.options.markersRolls[qMarkerRoll[0]] = url;
428
                });
429
430
                instance.buildmarkersrolls(instance, instance.controls, instance.layers, instance.media);
431
        ";
432
    }
433
434
    /**
435
     * Performs a redirection to the specified URL.
436
     *
437
     * This method sends a direct HTTP Location header to the client,
438
     * causing the browser to navigate to the specified URL. It should be
439
     * used with caution and only in scenarios where Symfony's standard
440
     * response handling is not applicable. The method terminates script
441
     * execution after sending the header.
442
     */
443
    public static function redirectTo(string $url): void
444
    {
445
        if (!empty($url)) {
446
            header("Location: $url");
447
448
            exit;
449
        }
450
    }
451
452
    /**
453
     * Checks if the current user has accepted the Terms & Conditions.
454
     */
455
    public static function userHasAcceptedTerms(): bool
456
    {
457
        $termRegistered = Session::read('term_and_condition');
458
459
        return isset($termRegistered['user_id']);
460
    }
461
462
    /**
463
     * Redirects to the Terms and Conditions page.
464
     */
465
    public static function redirectToTermsAndConditions(): void
466
    {
467
        $url = self::getTermsAndConditionsUrl();
468
        self::redirectTo($url);
469
    }
470
471
    /**
472
     * Returns the URL of the Terms and Conditions page.
473
     */
474
    public static function getTermsAndConditionsUrl(): string
475
    {
476
        return api_get_path(WEB_PATH).'main/auth/tc.php';
477
    }
478
479
    /**
480
     * Returns the URL of the Registration page.
481
     */
482
    public static function getRegistrationUrl(): string
483
    {
484
        return api_get_path(WEB_PATH).'main/auth/registration.php';
485
    }
486
487
    /**
488
     * Adds legal terms acceptance fields into a registration form.
489
     */
490
    public static function addLegalTermsFields(FormValidator $form, bool $userAlreadyRegisteredShowTerms): void
491
    {
492
        if ('true' !== api_get_setting('allow_terms_conditions') || $userAlreadyRegisteredShowTerms) {
493
            return;
494
        }
495
496
        $languageIso = api_get_language_isocode();
497
        $languageId = api_get_language_id($languageIso);
498
        $termPreview = LegalManager::get_last_condition($languageId);
499
500
        if (!$termPreview) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $termPreview of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
501
            $defaultIso = (string) api_get_setting('language.platform_language');
502
            if ($defaultIso === '' || $defaultIso === 'false') {
503
                $defaultIso = (string) api_get_setting('platformLanguage');
504
            }
505
506
            $defaultLangId = api_get_language_id($defaultIso);
507
            if ($defaultLangId > 0 && $defaultLangId !== $languageId) {
508
                $languageId = $defaultLangId;
509
                $termPreview = LegalManager::get_last_condition($languageId);
510
            }
511
        }
512
513
        if (!$termPreview) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $termPreview of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
514
            return; // Still nothing -> show nothing
515
        }
516
517
        $version = (int) ($termPreview['version'] ?? 0);
518
        $langId  = (int) ($termPreview['language_id'] ?? $languageId);
519
520
        // Track acceptance context
521
        $form->addElement('hidden', 'legal_accept_type', $version.':'.$langId);
522
        $form->addElement('hidden', 'legal_info', ((int) ($termPreview['id'] ?? 0)).':'.$langId);
523
524
        // Fetch ALL legal records for the same version + language (includes GDPR sections now)
525
        $table = Database::get_main_table(TABLE_MAIN_LEGAL);
526
527
        $rows = Database::select(
528
            '*',
529
            $table,
530
            [
531
                'where' => [
532
                    'language_id = ? AND version = ?' => [$langId, $version],
533
                ],
534
                'order' => 'id ASC',
535
            ]
536
        );
537
538
        if (!is_array($rows) || empty($rows)) {
539
            return;
540
        }
541
542
        $fullHtml = '';
543
544
        foreach ($rows as $row) {
545
            $content = trim((string) ($row['content'] ?? ''));
546
            if ($content === '') {
547
                continue;
548
            }
549
550
            // Optional title support if available in the table/schema
551
            $title = trim((string) ($row['title'] ?? ($row['name'] ?? '')));
552
            if ($title !== '') {
553
                $fullHtml .= '<div class="mt-4">';
554
                $fullHtml .= '<h4 class="text-base font-semibold text-gray-90">'.htmlspecialchars($title, ENT_QUOTES | ENT_HTML5).'</h4>';
555
                $fullHtml .= '<div class="mt-1 text-sm text-gray-90">'.$content.'</div>';
556
                $fullHtml .= '</div>';
557
            } else {
558
                $fullHtml .= '<div class="mt-4 text-sm text-gray-90">'.$content.'</div>';
559
            }
560
        }
561
562
        if (trim(strip_tags($fullHtml)) === '') {
563
            // Nothing meaningful to show
564
            return;
565
        }
566
567
        // Render the whole block at the bottom
568
        $form->addHtml('
569
        <div class="mt-6">
570
            <div class="text-lg font-semibold text-gray-90 mb-2">'.get_lang('Terms and Conditions').'</div>
571
            <div class="bg-gray-15 border border-gray-25 rounded-xl p-4 max-h-72 overflow-y-auto shadow-sm">
572
                '.$fullHtml.'
573
            </div>
574
        </div>
575
    ');
576
577
        // Acceptance checkbox (or hidden accept if configured)
578
        $hideAccept = 'true' === api_get_setting('registration.hide_legal_accept_checkbox');
579
        if ($hideAccept) {
580
            $form->addElement('hidden', 'legal_accept', '1');
581
        } else {
582
            $form->addElement(
583
                'checkbox',
584
                'legal_accept',
585
                null,
586
                'I have read and agree to the <a href="tc.php" target="_blank" rel="noopener noreferrer">Terms and Conditions</a>'
587
            );
588
            $form->addRule('legal_accept', 'This field is required', 'required');
589
        }
590
    }
591
592
    /**
593
     * Persists the user's acceptance of the terms & conditions.
594
     *
595
     * @param string $legalAcceptType version:language_id
596
     */
597
    public static function saveUserTermsAcceptance(int $userId, string $legalAcceptType): void
598
    {
599
        // Split and build the stored value**
600
        [$version, $languageId] = explode(':', $legalAcceptType);
601
        $timestamp = time();
602
        $toSave = (int) $version.':'.(int) $languageId.':'.$timestamp;
603
604
        // Save in extra-field**
605
        UserManager::update_extra_field_value($userId, 'legal_accept', $toSave);
606
607
        // Log event
608
        Event::addEvent(
0 ignored issues
show
Bug introduced by
The method addEvent() does not exist on Event. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

608
        Event::/** @scrutinizer ignore-call */ 
609
               addEvent(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
609
            LOG_TERM_CONDITION_ACCEPTED,
610
            LOG_USER_OBJECT,
611
            api_get_user_info($userId),
612
            api_get_utc_datetime()
613
        );
614
615
        $bossList = UserManager::getStudentBossList($userId);
616
        if (!empty($bossList)) {
617
            $bossIds = array_column($bossList, 'boss_id');
618
            $current = api_get_user_info($userId);
619
            $dateStr = api_get_local_time($timestamp);
620
621
            foreach ($bossIds as $bossId) {
622
                $subject = \sprintf(get_lang('User %s signed the agreement.'), $current['complete_name']);
623
                $content = \sprintf(get_lang('User %s signed the agreement the %s.'), $current['complete_name'], $dateStr);
624
                MessageManager::send_message_simple($bossId, $subject, $content, $userId);
625
            }
626
        }
627
    }
628
629
    /**
630
     * Displays the Terms and Conditions page.
631
     */
632
    public static function displayLegalTermsPage(string $returnUrl = '/home', bool $canAccept = true, string $infoMessage = ''): void
633
    {
634
        $iso = api_get_language_isocode();
635
        $langId = api_get_language_id($iso);
636
        $term = LegalManager::get_last_condition($langId);
637
638
        if (!$term) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $term of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
639
            // No T&C for current language → show a message
640
            Display::display_header(get_lang('Terms and Conditions'));
641
            echo '<div class="max-w-3xl mx-auto text-gray-90 text-lg text-center">'
642
                .get_lang('No terms and conditions available for this language.')
643
                .'</div>';
644
            Display::display_footer();
645
646
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
647
        }
648
649
        Display::display_header(get_lang('Terms and Conditions'));
650
651
        if (!empty($term['content'])) {
652
            echo '<div class="max-w-3xl mx-auto bg-white shadow p-8 rounded">';
653
            echo '<h1 class="text-2xl font-bold text-primary mb-6">'.get_lang('Terms and Conditions').'</h1>';
654
655
            if (!empty($infoMessage)) {
656
                echo '<div class="mb-4">'.$infoMessage.'</div>';
657
            }
658
659
            echo '<div class="prose prose-sm max-w-none mb-6">'.$term['content'].'</div>';
660
661
            $extra = new ExtraFieldValue('terms_and_condition');
662
            foreach ($extra->getAllValuesByItem($term['id']) as $field) {
663
                if (!empty($field['field_value'])) {
664
                    echo '<div class="mb-4">';
665
                    echo '<h3 class="text-lg font-semibold text-primary">'.$field['display_text'].'</h3>';
666
                    echo '<p class="text-gray-90 mt-1">'.$field['field_value'].'</p>';
667
                    echo '</div>';
668
                }
669
            }
670
671
            echo '<form method="post" action="tc.php?return='.urlencode($returnUrl).'" class="space-y-6">';
672
            echo '<input type="hidden" name="legal_accept_type" value="'.$term['version'].':'.$term['language_id'].'">';
673
            echo '<input type="hidden" name="return" value="'.htmlspecialchars($returnUrl).'">';
674
675
            if ($canAccept) {
676
                $hide = 'true' === api_get_setting('registration.hide_legal_accept_checkbox');
677
                if ($hide) {
678
                    echo '<input type="hidden" name="legal_accept" value="1">';
679
                } else {
680
                    echo '<label class="flex items-start space-x-2">';
681
                    echo '<input type="checkbox" name="legal_accept" value="1" required class="rounded border-gray-300 text-primary focus:ring-primary">';
682
                    echo '<span class="text-gray-90 text-sm">'.get_lang('I have read and agree to the').' ';
683
                    echo '<a href="tc.php?preview=1" target="_blank" class="text-primary hover:underline">'.get_lang('Terms and Conditions').'</a>';
684
                    echo '</span>';
685
                    echo '</label>';
686
                }
687
688
                echo '<div><button type="submit" class="inline-block bg-primary text-white font-semibold px-6 py-3 rounded hover:opacity-90 transition">'.get_lang('Accept Terms and Conditions').'</button></div>';
689
            } else {
690
                echo '<div><button type="button" class="inline-block bg-gray-400 text-white font-semibold px-6 py-3 rounded cursor-not-allowed" disabled>'.get_lang('Accept Terms and Conditions').'</button></div>';
691
            }
692
693
            echo '</form>';
694
            echo '</div>';
695
        } else {
696
            echo '<div class="text-center text-gray-90 text-lg">'.get_lang('Coming soon...').'</div>';
697
        }
698
699
        Display::display_footer();
700
701
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
702
    }
703
704
    /**
705
     * Try to add a legacy file to a Resource using the repository's addFile() API.
706
     * Falls back to attachLegacyFileToResource() if addFile() is not available.
707
     *
708
     * Returns true on success, false otherwise. Logs in English.
709
     */
710
    public static function addLegacyFileToResource(
711
        string $filePath,
712
        ResourceRepository $repo,
713
        AbstractResource $resource,
714
        string $fileName = '',
715
        string $description = ''
716
    ): bool {
717
        $class = $resource::class;
718
        $basename = basename($filePath);
719
720
        if (!self::legacyFileUsable($filePath)) {
721
            error_log("LEGACY_FILE: Cannot attach to {$class} – file not found or unreadable: {$basename}");
722
723
            return false;
724
        }
725
726
        // If the repository doesn't expose addFile(), use the Asset flow.
727
        if (!method_exists($repo, 'addFile')) {
728
            error_log('LEGACY_FILE: Repository '.$repo::class.' has no addFile(), falling back to Asset flow');
729
730
            return self::attachLegacyFileToResource($filePath, $resource, $fileName);
731
        }
732
733
        try {
734
            $mimeType = self::legacyDetectMime($filePath);
735
            $finalName = '' !== $fileName ? $fileName : $basename;
736
737
            // UploadedFile in "test mode" (last arg true) avoids PHP upload checks.
738
            $uploaded = new UploadedFile($filePath, $finalName, $mimeType, null, true);
739
            $repo->addFile($resource, $uploaded, $description);
740
741
            return true;
742
        } catch (Throwable $e) {
743
            error_log('LEGACY_FILE EXCEPTION (addFile): '.$e->getMessage());
744
745
            return false;
746
        }
747
    }
748
749
    /**
750
     * Create an Asset for a legacy file and attach it to the resource's node.
751
     * Generic path that works for any AbstractResource with a ResourceNode.
752
     *
753
     * Returns true on success, false otherwise. Logs in English.
754
     */
755
    public static function attachLegacyFileToResource(
756
        string $filePath,
757
        AbstractResource $resource,
758
        string $fileName = ''
759
    ): bool {
760
        $class = $resource::class;
761
        $basename = basename($filePath);
762
763
        if (!self::legacyFileUsable($filePath)) {
764
            error_log("LEGACY_FILE: Cannot attach Asset to {$class} – file not found or unreadable: {$basename}");
765
766
            return false;
767
        }
768
769
        if (!method_exists($resource, 'getResourceNode') || null === $resource->getResourceNode()) {
770
            error_log("LEGACY_FILE: Resource has no ResourceNode – cannot attach Asset (class: {$class})");
771
772
            return false;
773
        }
774
775
        try {
776
            $assetRepo = Container::getAssetRepository();
777
778
            // Prefer a dedicated helper if available.
779
            if (method_exists($assetRepo, 'createFromLocalPath')) {
780
                $asset = $assetRepo->createFromLocalPath(
781
                    $filePath,
782
                    '' !== $fileName ? $fileName : $basename
783
                );
784
            } else {
785
                // Fallback: simulate an upload-like array for createFromRequest().
786
                $mimeType = self::legacyDetectMime($filePath);
787
                $fakeUpload = [
788
                    'tmp_name' => $filePath,
789
                    'name' => '' !== $fileName ? $fileName : $basename,
790
                    'type' => $mimeType,
791
                    'size' => @filesize($filePath) ?: null,
792
                    'error' => 0,
793
                ];
794
795
                $asset = (new Asset())
796
                    ->setTitle($fakeUpload['name'])
797
                    ->setCompressed(false)
798
                ;
799
800
                // AssetRepository::createFromRequest(Asset $asset, array $uploadLike)
801
                $assetRepo->createFromRequest($asset, $fakeUpload);
802
            }
803
804
            // Attach to the resource's node.
805
            if (method_exists($assetRepo, 'attachToNode')) {
806
                $assetRepo->attachToNode($asset, $resource->getResourceNode());
807
808
                return true;
809
            }
810
811
            // If the resource repository exposes a direct helper:
812
            $repo = self::guessResourceRepository($resource);
813
            if ($repo && method_exists($repo, 'attachAssetToResource')) {
814
                $repo->attachAssetToResource($resource, $asset);
815
816
                return true;
817
            }
818
819
            error_log('LEGACY_FILE: No method to attach Asset to node (missing attachToNode/attachAssetToResource)');
820
821
            return false;
822
        } catch (Throwable $e) {
823
            error_log('LEGACY_FILE EXCEPTION (Asset attach): '.$e->getMessage());
824
825
            return false;
826
        }
827
    }
828
829
    private static function legacyFileUsable(string $filePath): bool
830
    {
831
        return is_file($filePath) && is_readable($filePath);
832
    }
833
834
    private static function legacyDetectMime(string $filePath): string
835
    {
836
        $mime = @mime_content_type($filePath);
837
838
        return $mime ?: 'application/octet-stream';
839
    }
840
841
    /**
842
     * Best-effort guess to find the resource repository via Doctrine.
843
     * Returns null if the repo is not a ResourceRepository.
844
     */
845
    private static function guessResourceRepository(AbstractResource $resource): ?ResourceRepository
846
    {
847
        try {
848
            $em = Database::getManager();
849
            $repo = $em->getRepository($resource::class);
850
851
            return $repo instanceof ResourceRepository ? $repo : null;
852
        } catch (Throwable $e) {
853
            return null;
854
        }
855
    }
856
857
    /**
858
     * Scan HTML for legacy /courses/<dir>/document/... references found in a ZIP,
859
     * ensure those files are created as Documents, and return URL maps to rewrite the HTML.
860
     *
861
     * Returns: ['byRel' => [ "document/..." => "public-url" ],
862
     *           'byBase'=> [ "file.ext"     => "public-url" ] ]
863
     *
864
     * @param mixed $docRepo
865
     * @param mixed $courseEntity
866
     * @param mixed $session
867
     * @param mixed $group
868
     */
869
    public static function buildUrlMapForHtmlFromPackage(
870
        string $html,
871
        string $courseDir,
872
        string $srcRoot,
873
        array &$folders,
874
        callable $ensureFolder,
875
        $docRepo,
876
        $courseEntity,
877
        $session,
878
        $group,
879
        int $session_id,
880
        int $file_option,
881
        ?callable $dbg = null
882
    ): array {
883
        $byRel = [];
884
        $byBase = [];
885
        $iidByRel = [];
886
        $iidByBase = [];
887
888
        $DBG = $dbg ?: static function ($m, $c = []): void { /* no-op */ };
889
890
        // src|href pointing to …/courses/<dir>/document/... (host optional)
891
        $depRegex = '/(?P<attr>src|href)\s*=\s*["\'](?P<full>(?:(?P<scheme>https?:)?\/\/[^"\']+)?(?P<path>\/(?:app\/)?courses\/[^\/]+\/document\/[^"\']+\.[a-z0-9]{1,8}))["\']/i';
892
893
        if (!preg_match_all($depRegex, $html, $mm) || empty($mm['full'])) {
894
            return ['byRel' => $byRel, 'byBase' => $byBase];
895
        }
896
897
        // Normalize a full URL to a "document/..." relative path inside the package
898
        $toRel = static function (string $full) use ($courseDir): string {
899
            $urlPath = parse_url(html_entity_decode($full, ENT_QUOTES | ENT_HTML5), PHP_URL_PATH) ?: $full;
900
            $urlPath = preg_replace('#^/(?:app/)?courses/([^/]+)/#i', '/courses/'.$courseDir.'/', $urlPath);
901
            $rel = preg_replace('#^/(?:app/)?courses/'.preg_quote($courseDir, '#').'/#i', '', $urlPath) ?: $urlPath;
902
903
            return ltrim($rel, '/'); // "document/..."
904
        };
905
906
        foreach ($mm['full'] as $fullUrl) {
907
            $rel = $toRel($fullUrl); // e.g. "document/img.png"
908
            // Do not auto-create HTML files here (they are handled by the main import loop).
909
            $ext = strtolower(pathinfo($rel, PATHINFO_EXTENSION));
910
            if (in_array($ext, ['html', 'htm'], true)) {
911
                continue;
912
            }
913
            if (!str_starts_with($rel, 'document/')) {
914
                continue;
915
            }   // STRICT: only /document/*
916
            if (isset($byRel[$rel])) {
917
                continue;
918
            }
919
920
            $basename = basename(parse_url($fullUrl, PHP_URL_PATH) ?: $fullUrl);
921
            $byBase[$basename] = $byBase[$basename] ?? null;
922
923
            // Convert "document/..." (package rel) to destination rel "/..."
924
            $dstRel = '/'.ltrim(substr($rel, strlen('document/')), '/'); // e.g. "/Videos/img.png"
925
            $depTitle = basename($dstRel);
926
            $depAbs = rtrim($srcRoot, '/').'/'.$rel;
927
928
            // Destination parent folder (no "/document" prefix in destination)
929
            $parentRelPath = rtrim(\dirname($dstRel), '/');
930
            if ('' === $parentRelPath || '.' === $parentRelPath) {
931
                $parentRelPath = '/';
932
            }
933
934
            $parentId = 0;
935
            if ('/' !== $parentRelPath) {
936
                $parentId = $folders[$parentRelPath] ?? 0;
937
                if (!$parentId) {
938
                    $parentId = $ensureFolder($parentRelPath);
939
                    $folders[$parentRelPath] = $parentId;
940
                    $DBG('helper.ensureFolder', ['parentRelPath' => $parentRelPath, 'parentId' => $parentId]);
941
                }
942
            }
943
944
            if (!is_file($depAbs) || !is_readable($depAbs)) {
945
                $DBG('helper.dep.missing', ['rel' => $rel, 'abs' => $depAbs]);
946
947
                continue;
948
            }
949
950
            // Collision check under parent
951
            $parentRes = $parentId ? $docRepo->find($parentId) : $courseEntity;
952
            $findExisting = function ($t) use ($docRepo, $parentRes, $courseEntity, $session, $group) {
953
                $e = $docRepo->findCourseResourceByTitle($t, $parentRes->getResourceNode(), $courseEntity, $session, $group);
954
955
                return $e && method_exists($e, 'getIid') ? $e->getIid() : null;
956
            };
957
958
            $finalTitle = $depTitle;
959
            $existsIid = $findExisting($finalTitle);
960
            if ($existsIid) {
961
                $FILE_SKIP = \defined('FILE_SKIP') ? FILE_SKIP : 2;
962
                if ($file_option === $FILE_SKIP) {
963
                    $existingDoc = $docRepo->find($existsIid);
964
                    if ($existingDoc) {
965
                        $url = $docRepo->getResourceFileUrl($existingDoc);
966
                        if ($url) {
967
                            $byRel[$rel] = $url;
968
                            $byBase[$basename] = $byBase[$basename] ?: $url;
969
970
                            $iidByRel[$rel] = (int) $existsIid;
971
                            $iidByBase[$basename] = $iidByBase[$basename] ?? (int) $existsIid;
972
973
                            $DBG('helper.dep.reuse', ['rel' => $rel, 'iid' => $existsIid, 'url' => $url]);
974
                        }
975
                    }
976
977
                    continue;
978
                }
979
                // Rename on collision
980
                $pi = pathinfo($depTitle);
981
                $name = $pi['filename'] ?? $depTitle;
982
                $ext2 = isset($pi['extension']) && '' !== $pi['extension'] ? '.'.$pi['extension'] : '';
983
                $i = 1;
984
                while ($findExisting($finalTitle)) {
985
                    $finalTitle = $name.'_'.$i.$ext2;
986
                    $i++;
987
                }
988
            }
989
990
            // Create the non-HTML dependency from the package
991
            try {
992
                $entity = DocumentManager::addDocument(
993
                    ['real_id' => $courseEntity->getId(), 'code' => method_exists($courseEntity, 'getCode') ? $courseEntity->getCode() : null],
994
                    $dstRel, // metadata path (no "/document" root)
995
                    'file',
996
                    (int) (@filesize($depAbs) ?: 0),
997
                    $finalTitle,
998
                    null,
999
                    0,
1000
                    null,
1001
                    0,
1002
                    (int) $session_id,
1003
                    0,
1004
                    false,
1005
                    '',
1006
                    $parentId,
1007
                    $depAbs
1008
                );
1009
                $iid = method_exists($entity, 'getIid') ? $entity->getIid() : 0;
1010
                $url = $docRepo->getResourceFileUrl($entity);
1011
                $iidByRel[$rel] = (int) $iid;
1012
                $iidByBase[$basename] = $iidByBase[$basename] ?? (int) $iid;
1013
1014
                $DBG('helper.dep.created', ['rel' => $rel, 'iid' => $iid, 'url' => $url]);
1015
1016
                if ($url) {
1017
                    $byRel[$rel] = $url;
1018
                    $byBase[$basename] = $byBase[$basename] ?: $url;
1019
                }
1020
            } catch (Throwable $e) {
1021
                $DBG('helper.dep.error', ['rel' => $rel, 'err' => $e->getMessage()]);
1022
            }
1023
        }
1024
1025
        $byBase = array_filter($byBase);
1026
1027
        return [
1028
            'byRel' => $byRel,
1029
            'byBase' => $byBase,
1030
            'iidByRel' => $iidByRel,
1031
            'iidByBase' => $iidByBase,
1032
        ];
1033
    }
1034
1035
    /**
1036
     * Rewrite src|href that point to /courses/<dir>/document/... using:
1037
     *  - exact match by relative path ("document/...") via $urlMapByRel
1038
     *  - basename fallback ("file.ext") via $urlMapByBase
1039
     *
1040
     * Returns: ['html'=>..., 'replaced'=>N, 'misses'=>M]
1041
     */
1042
    public static function rewriteLegacyCourseUrlsWithMap(
1043
        string $html,
1044
        string $courseDir,
1045
        array $urlMapByRel,
1046
        array $urlMapByBase
1047
    ): array {
1048
        $replaced = 0;
1049
        $misses = 0;
1050
1051
        $pattern = '/(?P<attr>src|href)\s*=\s*["\'](?P<full>(?:(?P<scheme>https?:)?\/\/[^"\']+)?(?P<path>\/(?:app\/)?courses\/(?P<dir>[^\/]+)\/document\/[^"\']+\.[a-z0-9]{1,8}))["\']/i';
1052
1053
        $html = preg_replace_callback($pattern, function ($m) use ($courseDir, $urlMapByRel, $urlMapByBase, &$replaced, &$misses) {
1054
            $attr = $m['attr'];
1055
            $fullUrl = html_entity_decode($m['full'], ENT_QUOTES | ENT_HTML5);
1056
            $path = $m['path']; // /courses/<dir>/document/...
1057
            $matchDir = $m['dir'];
1058
1059
            // Normalize to current course directory
1060
            $effectivePath = $path;
1061
            if (0 !== strcasecmp($matchDir, $courseDir)) {
1062
                $effectivePath = preg_replace(
1063
                    '#^/(?:app/)?courses/'.preg_quote($matchDir, '#').'/#i',
1064
                    '/courses/'.$courseDir.'/',
1065
                    $path
1066
                ) ?: $path;
1067
            }
1068
1069
            $relInPackage = preg_replace(
1070
                '#^/(?:app/)?courses/'.preg_quote($courseDir, '#').'/#i',
1071
                '',
1072
                $effectivePath
1073
            ) ?: $effectivePath;
1074
1075
            $relInPackage = ltrim($relInPackage, '/'); // document/...
1076
1077
            // 1) exact rel match
1078
            if (isset($urlMapByRel[$relInPackage])) {
1079
                $newUrl = $urlMapByRel[$relInPackage];
1080
                $replaced++;
1081
1082
                return $attr.'="'.htmlspecialchars($newUrl, ENT_QUOTES | ENT_HTML5).'"';
1083
            }
1084
1085
            // 2) basename fallback
1086
            $base = basename(parse_url($effectivePath, PHP_URL_PATH) ?: $effectivePath);
1087
            if (isset($urlMapByBase[$base])) {
1088
                $newUrl = $urlMapByBase[$base];
1089
                $replaced++;
1090
1091
                return $attr.'="'.htmlspecialchars($newUrl, ENT_QUOTES | ENT_HTML5).'"';
1092
            }
1093
1094
            // Not found → keep original
1095
            $misses++;
1096
1097
            return $m[0];
1098
        }, $html);
1099
1100
        return ['html' => $html, 'replaced' => $replaced, 'misses' => $misses];
1101
    }
1102
}
1103