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

ChamiloHelper::getQuizMarkersRollsJS()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 21
rs 10
c 0
b 0
f 0
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