Passed
Push — master ( 4f222e...992626 )
by
unknown
17:45 queued 09:02
created

ChamiloHelper::isAjaxRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 5
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
            if ('true' === $svgIcons) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $svgIcons seems to be never defined.
Loading history...
134
                $originalLogoPathSVG = $themeDir.'images/header-logo.svg';
135
                if (file_exists(api_get_path(SYS_CSS_PATH).$originalLogoPathSVG)) {
136
                    if ($getSysPath) {
137
                        return api_get_path(SYS_CSS_PATH).$originalLogoPathSVG;
138
                    }
139
140
                    return api_get_path(WEB_CSS_PATH).$originalLogoPathSVG;
141
                }
142
            }
143
144
            if (file_exists(api_get_path(SYS_CSS_PATH).$originalLogoPath)) {
145
                if ($getSysPath) {
146
                    return api_get_path(SYS_CSS_PATH).$originalLogoPath;
147
                }
148
149
                return api_get_path(WEB_CSS_PATH).$originalLogoPath;
150
            }
151
            $logoPath = '';
152
        }
153
154
        return $logoPath;
155
    }
156
157
    /**
158
     * Get the platform logo.
159
     * Return a <img> if the logo image exists.
160
     * Otherwise, return a <h2> with the institution name.
161
     *
162
     * @throws Exception
163
     */
164
    public static function getPlatformLogo(
165
        string $theme = '',
166
        array $imageAttributes = [],
167
        bool $getSysPath = false,
168
        bool $forcedGetter = false
169
    ): string {
170
        $logoPath = Container::getThemeHelper()->getThemeAssetUrl('images/header-logo.svg');
171
172
        if (empty($logoPath)) {
173
            $logoPath = Container::getThemeHelper()->getThemeAssetUrl('images/header-logo.png');
174
        }
175
176
        $institution = api_get_setting('Institution');
177
        $institutionUrl = api_get_setting('InstitutionUrl');
178
        $siteName = api_get_setting('siteName');
179
180
        if (null === $logoPath) {
181
            $headerLogo = Display::url($siteName, api_get_path(WEB_PATH).'index.php');
182
183
            if (!empty($institutionUrl) && !empty($institution)) {
184
                $headerLogo .= ' - '.Display::url($institution, $institutionUrl);
185
            }
186
187
            $courseInfo = api_get_course_info();
188
            if (isset($courseInfo['extLink']) && !empty($courseInfo['extLink']['name'])) {
189
                $headerLogo .= '<span class="extLinkSeparator"> - </span>';
190
191
                if (!empty($courseInfo['extLink']['url'])) {
192
                    $headerLogo .= Display::url(
193
                        $courseInfo['extLink']['name'],
194
                        $courseInfo['extLink']['url'],
195
                        [
196
                            'class' => 'extLink',
197
                        ]
198
                    );
199
                } elseif (!empty($courseInfo['extLink']['url'])) {
200
                    $headerLogo .= $courseInfo['extLink']['url'];
201
                }
202
            }
203
204
            return Display::tag('h2', $headerLogo, [
205
                'class' => 'text-left',
206
            ]);
207
        }
208
209
        $image = Display::img($logoPath, $institution, $imageAttributes);
210
211
        return Display::url($image, api_get_path(WEB_PATH).'index.php');
212
    }
213
214
    /**
215
     * Like strip_tags(), but leaves an additional space and removes only the given tags.
216
     *
217
     * @param array $tags Tags to be removed
218
     *
219
     * @return string The original string without the given tags
220
     */
221
    public static function stripGivenTags(string $string, array $tags): string
222
    {
223
        foreach ($tags as $tag) {
224
            $string2 = preg_replace('#</\b'.$tag.'\b[^>]*>#i', ' ', $string);
225
            if ($string2 !== $string) {
226
                $string = preg_replace('/<\b'.$tag.'\b[^>]*>/i', ' ', $string2);
227
            }
228
        }
229
230
        return $string;
231
    }
232
233
    /**
234
     * Adds or Subtract a time in hh:mm:ss to a datetime.
235
     *
236
     * @param string $time      Time to add or subtract in hh:mm:ss format
237
     * @param string $datetime  Datetime to be modified as accepted by the Datetime class constructor
238
     * @param bool   $operation True for Add, False to Subtract
239
     *
240
     * @throws Exception
241
     */
242
    public static function addOrSubTimeToDateTime(
243
        string $time,
244
        string $datetime = 'now',
245
        bool $operation = true
246
    ): string {
247
        $date = new DateTime($datetime);
248
        $hours = 0;
249
        $minutes = 0;
250
        $seconds = 0;
251
        sscanf($time, '%d:%d:%d', $hours, $minutes, $seconds);
252
        $timeSeconds = isset($seconds) ? $hours * 3600 + $minutes * 60 + $seconds : $hours * 60 + $minutes;
253
        if ($operation) {
254
            $date->add(new DateInterval('PT'.$timeSeconds.'S'));
255
        } else {
256
            $date->sub(new DateInterval('PT'.$timeSeconds.'S'));
257
        }
258
259
        return $date->format('Y-m-d H:i:s');
260
    }
261
262
    /**
263
     * Returns the course id (integer) for the given course directory or the current ID if no directory is defined.
264
     *
265
     * @param string|null $directory The course directory/path that appears in the URL
266
     *
267
     * @throws Exception
268
     */
269
    public static function getCourseIdByDirectory(?string $directory = null): int
270
    {
271
        if (!empty($directory)) {
272
            $directory = Database::escape_string($directory);
273
            $row = Database::select(
274
                'id',
275
                Database::get_main_table(TABLE_MAIN_COURSE),
276
                [
277
                    'where' => [
278
                        'directory = ?' => [$directory],
279
                    ],
280
                ],
281
                'first'
282
            );
283
284
            if (\is_array($row) && isset($row['id'])) {
285
                return $row['id'];
286
            }
287
288
            return 0;
289
        }
290
291
        return (int) Session::read('_real_cid', 0);
292
    }
293
294
    /**
295
     * Check if the current HTTP request is by AJAX.
296
     */
297
    public static function isAjaxRequest(): bool
298
    {
299
        $requestedWith = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? null;
300
301
        return 'XMLHttpRequest' === $requestedWith;
302
    }
303
304
    /**
305
     * Get a variable name for language file from a text.
306
     */
307
    public static function getLanguageVar(string $text, string $prefix = ''): string
308
    {
309
        $text = api_replace_dangerous_char($text);
310
        $text = str_replace(['-', ' ', '.'], '_', $text);
311
        $text = preg_replace('/_+/', '_', $text);
312
        // $text = str_replace('_', '', $text);
313
        $text = api_underscore_to_camel_case($text);
314
315
        return $prefix.$text;
316
    }
317
318
    /**
319
     * Get the stylesheet path for HTML blocks created with CKEditor.
320
     */
321
    public static function getEditorBlockStylePath(): string
322
    {
323
        $visualTheme = api_get_visual_theme();
324
325
        $cssFile = api_get_path(SYS_CSS_PATH).\sprintf('themes/%s/editor_content.css', $visualTheme);
326
327
        if (is_file($cssFile)) {
328
            return api_get_path(WEB_CSS_PATH).\sprintf('themes/%s/editor_content.css', $visualTheme);
329
        }
330
331
        return api_get_path(WEB_CSS_PATH).'editor_content.css';
332
    }
333
334
    /**
335
     * Get a list of colors from the palette at main/palette/pchart/default.color
336
     * and return it as an array of strings.
337
     *
338
     * @param bool     $decimalOpacity Whether to return the opacity as 0..100 or 0..1
339
     * @param bool     $wrapInRGBA     Whether to return it as 1,1,1,100 or rgba(1,1,1,100)
340
     * @param int|null $fillUpTo       If the number of colors is smaller than this number, generate more colors
341
     *
342
     * @return array An array of string colors
343
     */
344
    public static function getColorPalette(
345
        bool $decimalOpacity = false,
346
        bool $wrapInRGBA = false,
347
        ?int $fillUpTo = null
348
    ): array {
349
        // Get the common colors from the palette used for pchart
350
        $paletteFile = api_get_path(SYS_CODE_PATH).'palettes/pchart/default.color';
351
        $palette = file($paletteFile);
352
        if ($decimalOpacity) {
353
            // Because the pchart palette has transparency as integer values
354
            // (0..100) and chartjs uses percentage (0.0..1.0), we need to divide
355
            // the last value by 100, which is a bit overboard for just one chart
356
            foreach ($palette as $index => $color) {
357
                $components = explode(',', trim($color));
358
                $components[3] = round((int) $components[3] / 100, 1, PHP_ROUND_HALF_UP);
359
                $palette[$index] = implode(',', $components);
360
            }
361
        }
362
        if ($wrapInRGBA) {
363
            foreach ($palette as $index => $color) {
364
                $color = trim($color);
365
                $palette[$index] = 'rgba('.$color.')';
366
            }
367
        }
368
        // If we want more colors, loop through existing colors
369
        $count = \count($palette);
370
        if (isset($fillUpTo) && $fillUpTo > $count) {
371
            for ($i = $count; $i < $fillUpTo; $i++) {
372
                $palette[$i] = $palette[$i % $count];
373
            }
374
        }
375
376
        return $palette;
377
    }
378
379
    /**
380
     * Get the local time for the midnight.
381
     *
382
     * @param null|string $utcTime Optional. The time to ve converted.
383
     *                             See api_get_local_time.
384
     *
385
     * @throws Exception
386
     */
387
    public static function getServerMidnightTime(?string $utcTime = null): DateTime
388
    {
389
        $localTime = api_get_local_time($utcTime);
390
        $localTimeZone = api_get_timezone();
391
392
        $localMidnight = new DateTime($localTime, new DateTimeZone($localTimeZone));
393
        $localMidnight->modify('midnight');
394
395
        return $localMidnight;
396
    }
397
398
    /**
399
     * Get JavaScript code necessary to load quiz markers-rolls in medialement's Markers Rolls plugin.
400
     */
401
    public static function getQuizMarkersRollsJS(): string
402
    {
403
        $webCodePath = api_get_path(WEB_CODE_PATH);
404
        $cidReq = api_get_cidreq(true, true, 'embeddable');
405
        $colorPalette = self::getColorPalette(false, true);
406
407
        return "
408
            var \$originalNode = $(originalNode),
409
                    qMarkersRolls = \$originalNode.data('q-markersrolls') || [],
410
                    qMarkersColor = \$originalNode.data('q-markersrolls-color') || '$colorPalette[0]';
411
412
                if (0 == qMarkersRolls.length) {
413
                    return;
414
                }
415
416
                instance.options.markersRollsColor = qMarkersColor;
417
                instance.options.markersRollsWidth = 2;
418
                instance.options.markersRolls = {};
419
420
                qMarkersRolls.forEach(function (qMarkerRoll) {
421
                    var url = '{$webCodePath}exercise/exercise_submit.php?$cidReq&'
422
                        + $.param({
423
                            exerciseId: qMarkerRoll[1],
424
                            learnpath_id: 0,
425
                            learnpath_item_id: 0,
426
                            learnpath_item_view_id: 0
427
                        });
428
429
                    instance.options.markersRolls[qMarkerRoll[0]] = url;
430
                });
431
432
                instance.buildmarkersrolls(instance, instance.controls, instance.layers, instance.media);
433
        ";
434
    }
435
436
    /**
437
     * Performs a redirection to the specified URL.
438
     *
439
     * This method sends a direct HTTP Location header to the client,
440
     * causing the browser to navigate to the specified URL. It should be
441
     * used with caution and only in scenarios where Symfony's standard
442
     * response handling is not applicable. The method terminates script
443
     * execution after sending the header.
444
     */
445
    public static function redirectTo(string $url): void
446
    {
447
        if (!empty($url)) {
448
            header("Location: $url");
449
450
            exit;
451
        }
452
    }
453
454
    /**
455
     * Checks if the current user has accepted the Terms & Conditions.
456
     */
457
    public static function userHasAcceptedTerms(): bool
458
    {
459
        $termRegistered = Session::read('term_and_condition');
460
461
        return isset($termRegistered['user_id']);
462
    }
463
464
    /**
465
     * Redirects to the Terms and Conditions page.
466
     */
467
    public static function redirectToTermsAndConditions(): void
468
    {
469
        $url = self::getTermsAndConditionsUrl();
470
        self::redirectTo($url);
471
    }
472
473
    /**
474
     * Returns the URL of the Terms and Conditions page.
475
     */
476
    public static function getTermsAndConditionsUrl(): string
477
    {
478
        return api_get_path(WEB_PATH).'main/auth/tc.php';
479
    }
480
481
    /**
482
     * Returns the URL of the Registration page.
483
     */
484
    public static function getRegistrationUrl(): string
485
    {
486
        return api_get_path(WEB_PATH).'main/auth/registration.php';
487
    }
488
489
    /**
490
     * Adds legal terms acceptance fields into a registration form.
491
     */
492
    public static function addLegalTermsFields(FormValidator $form, bool $userAlreadyRegisteredShowTerms): void
493
    {
494
        if ('true' !== api_get_setting('allow_terms_conditions') || $userAlreadyRegisteredShowTerms) {
495
            return;
496
        }
497
498
        // Check if T&C should be shown during registration
499
        $loadMode = api_get_setting('workflows.load_term_conditions_section');
500
501
        if ('course' === $loadMode) {
502
            // skip adding terms on registration page
503
            return;
504
        }
505
506
        $languageIso = api_get_language_isocode();
507
        $languageId = api_get_language_id($languageIso);
508
509
        $termPreview = LegalManager::get_last_condition($languageId);
510
511
        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...
512
            // Do NOT load fallback terms in another language
513
            return;
514
        }
515
516
        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...
517
            return;
518
        }
519
520
        // hidden inputs to track version/language
521
        $form->addElement('hidden', 'legal_accept_type', $termPreview['version'].':'.$termPreview['language_id']);
522
        $form->addElement('hidden', 'legal_info', $termPreview['id'].':'.$termPreview['language_id']);
523
524
        if (1 == $termPreview['type']) {
525
            // simple checkbox linking out to full T&C
526
            $form->addElement(
527
                'checkbox',
528
                'legal_accept',
529
                null,
530
                'I have read and agree to the <a href="tc.php" target="_blank">Terms and Conditions</a>'
531
            );
532
            $form->addRule('legal_accept', 'This field is required', 'required');
533
        } else {
534
            // full inline T&C panel with scroll
535
            $preview = LegalManager::show_last_condition($termPreview);
536
            $form->addHtml(
537
                '<div style="
538
                background-color: #f3f4f6;
539
                border: 1px solid #d1d5db;
540
                padding: 1rem;
541
                max-height: 16rem;
542
                overflow-y: auto;
543
                border-radius: 0.375rem;
544
                margin-bottom: 1rem;
545
            ">'
546
                .$preview.
547
                '</div>'
548
            );
549
550
            // any extra labels
551
            $extra = new ExtraFieldValue('terms_and_condition');
552
            $values = $extra->getAllValuesByItem($termPreview['id']);
553
            foreach ($values as $value) {
554
                if (!empty($value['field_value'])) {
555
                    $form->addLabel($value['display_text'], $value['field_value']);
556
                }
557
            }
558
559
            // acceptance checkbox
560
            $form->addElement(
561
                'checkbox',
562
                'legal_accept',
563
                null,
564
                'I have read and agree to the Terms and Conditions'
565
            );
566
            $form->addRule('legal_accept', 'This field is required', 'required');
567
        }
568
    }
569
570
    /**
571
     * Persists the user's acceptance of the terms & conditions.
572
     *
573
     * @param string $legalAcceptType version:language_id
574
     */
575
    public static function saveUserTermsAcceptance(int $userId, string $legalAcceptType): void
576
    {
577
        // Split and build the stored value**
578
        [$version, $languageId] = explode(':', $legalAcceptType);
579
        $timestamp = time();
580
        $toSave = (int) $version.':'.(int) $languageId.':'.$timestamp;
581
582
        // Save in extra-field**
583
        UserManager::update_extra_field_value($userId, 'legal_accept', $toSave);
584
585
        // Log event
586
        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

586
        Event::/** @scrutinizer ignore-call */ 
587
               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...
587
            LOG_TERM_CONDITION_ACCEPTED,
588
            LOG_USER_OBJECT,
589
            api_get_user_info($userId),
590
            api_get_utc_datetime()
591
        );
592
593
        $bossList = UserManager::getStudentBossList($userId);
594
        if (!empty($bossList)) {
595
            $bossIds = array_column($bossList, 'boss_id');
596
            $current = api_get_user_info($userId);
597
            $dateStr = api_get_local_time($timestamp);
598
599
            foreach ($bossIds as $bossId) {
600
                $subject = \sprintf(get_lang('User %s signed the agreement.'), $current['complete_name']);
601
                $content = \sprintf(get_lang('User %s signed the agreement the %s.'), $current['complete_name'], $dateStr);
602
                MessageManager::send_message_simple($bossId, $subject, $content, $userId);
603
            }
604
        }
605
    }
606
607
    /**
608
     * Displays the Terms and Conditions page.
609
     */
610
    public static function displayLegalTermsPage(string $returnUrl = '/home', bool $canAccept = true, string $infoMessage = ''): void
611
    {
612
        $iso = api_get_language_isocode();
613
        $langId = api_get_language_id($iso);
614
        $term = LegalManager::get_last_condition($langId);
615
616
        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...
617
            // No T&C for current language → show a message
618
            Display::display_header(get_lang('Terms and Conditions'));
619
            echo '<div class="max-w-3xl mx-auto text-gray-90 text-lg text-center">'
620
                .get_lang('No terms and conditions available for this language.')
621
                .'</div>';
622
            Display::display_footer();
623
624
            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...
625
        }
626
627
        Display::display_header(get_lang('Terms and Conditions'));
628
629
        if (!empty($term['content'])) {
630
            echo '<div class="max-w-3xl mx-auto bg-white shadow p-8 rounded">';
631
            echo '<h1 class="text-2xl font-bold text-primary mb-6">'.get_lang('Terms and Conditions').'</h1>';
632
633
            if (!empty($infoMessage)) {
634
                echo '<div class="mb-4">'.$infoMessage.'</div>';
635
            }
636
637
            echo '<div class="prose prose-sm max-w-none mb-6">'.$term['content'].'</div>';
638
639
            $extra = new ExtraFieldValue('terms_and_condition');
640
            foreach ($extra->getAllValuesByItem($term['id']) as $field) {
641
                if (!empty($field['field_value'])) {
642
                    echo '<div class="mb-4">';
643
                    echo '<h3 class="text-lg font-semibold text-primary">'.$field['display_text'].'</h3>';
644
                    echo '<p class="text-gray-90 mt-1">'.$field['field_value'].'</p>';
645
                    echo '</div>';
646
                }
647
            }
648
649
            echo '<form method="post" action="tc.php?return='.urlencode($returnUrl).'" class="space-y-6">';
650
            echo '<input type="hidden" name="legal_accept_type" value="'.$term['version'].':'.$term['language_id'].'">';
651
            echo '<input type="hidden" name="return" value="'.htmlspecialchars($returnUrl).'">';
652
653
            if ($canAccept) {
654
                $hide = 'true' === api_get_setting('registration.hide_legal_accept_checkbox');
655
                if ($hide) {
656
                    echo '<input type="hidden" name="legal_accept" value="1">';
657
                } else {
658
                    echo '<label class="flex items-start space-x-2">';
659
                    echo '<input type="checkbox" name="legal_accept" value="1" required class="rounded border-gray-300 text-primary focus:ring-primary">';
660
                    echo '<span class="text-gray-90 text-sm">'.get_lang('I have read and agree to the').' ';
661
                    echo '<a href="tc.php?preview=1" target="_blank" class="text-primary hover:underline">'.get_lang('Terms and Conditions').'</a>';
662
                    echo '</span>';
663
                    echo '</label>';
664
                }
665
666
                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>';
667
            } else {
668
                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>';
669
            }
670
671
            echo '</form>';
672
            echo '</div>';
673
        } else {
674
            echo '<div class="text-center text-gray-90 text-lg">'.get_lang('Coming soon...').'</div>';
675
        }
676
677
        Display::display_footer();
678
679
        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...
680
    }
681
682
    /**
683
     * Try to add a legacy file to a Resource using the repository's addFile() API.
684
     * Falls back to attachLegacyFileToResource() if addFile() is not available.
685
     *
686
     * Returns true on success, false otherwise. Logs in English.
687
     */
688
    public static function addLegacyFileToResource(
689
        string $filePath,
690
        ResourceRepository $repo,
691
        AbstractResource $resource,
692
        string $fileName = '',
693
        string $description = ''
694
    ): bool {
695
        $class = $resource::class;
696
        $basename = basename($filePath);
697
698
        if (!self::legacyFileUsable($filePath)) {
699
            error_log("LEGACY_FILE: Cannot attach to {$class} – file not found or unreadable: {$basename}");
700
701
            return false;
702
        }
703
704
        // If the repository doesn't expose addFile(), use the Asset flow.
705
        if (!method_exists($repo, 'addFile')) {
706
            error_log('LEGACY_FILE: Repository '.$repo::class.' has no addFile(), falling back to Asset flow');
707
708
            return self::attachLegacyFileToResource($filePath, $resource, $fileName);
709
        }
710
711
        try {
712
            $mimeType = self::legacyDetectMime($filePath);
713
            $finalName = '' !== $fileName ? $fileName : $basename;
714
715
            // UploadedFile in "test mode" (last arg true) avoids PHP upload checks.
716
            $uploaded = new UploadedFile($filePath, $finalName, $mimeType, null, true);
717
            $repo->addFile($resource, $uploaded, $description);
718
719
            return true;
720
        } catch (Throwable $e) {
721
            error_log('LEGACY_FILE EXCEPTION (addFile): '.$e->getMessage());
722
723
            return false;
724
        }
725
    }
726
727
    /**
728
     * Create an Asset for a legacy file and attach it to the resource's node.
729
     * Generic path that works for any AbstractResource with a ResourceNode.
730
     *
731
     * Returns true on success, false otherwise. Logs in English.
732
     */
733
    public static function attachLegacyFileToResource(
734
        string $filePath,
735
        AbstractResource $resource,
736
        string $fileName = ''
737
    ): bool {
738
        $class = $resource::class;
739
        $basename = basename($filePath);
740
741
        if (!self::legacyFileUsable($filePath)) {
742
            error_log("LEGACY_FILE: Cannot attach Asset to {$class} – file not found or unreadable: {$basename}");
743
744
            return false;
745
        }
746
747
        if (!method_exists($resource, 'getResourceNode') || null === $resource->getResourceNode()) {
748
            error_log("LEGACY_FILE: Resource has no ResourceNode – cannot attach Asset (class: {$class})");
749
750
            return false;
751
        }
752
753
        try {
754
            $assetRepo = Container::getAssetRepository();
755
756
            // Prefer a dedicated helper if available.
757
            if (method_exists($assetRepo, 'createFromLocalPath')) {
758
                $asset = $assetRepo->createFromLocalPath(
759
                    $filePath,
760
                    '' !== $fileName ? $fileName : $basename
761
                );
762
            } else {
763
                // Fallback: simulate an upload-like array for createFromRequest().
764
                $mimeType = self::legacyDetectMime($filePath);
765
                $fakeUpload = [
766
                    'tmp_name' => $filePath,
767
                    'name' => '' !== $fileName ? $fileName : $basename,
768
                    'type' => $mimeType,
769
                    'size' => @filesize($filePath) ?: null,
770
                    'error' => 0,
771
                ];
772
773
                $asset = (new Asset())
774
                    ->setTitle($fakeUpload['name'])
775
                    ->setCompressed(false)
776
                ;
777
778
                // AssetRepository::createFromRequest(Asset $asset, array $uploadLike)
779
                $assetRepo->createFromRequest($asset, $fakeUpload);
780
            }
781
782
            // Attach to the resource's node.
783
            if (method_exists($assetRepo, 'attachToNode')) {
784
                $assetRepo->attachToNode($asset, $resource->getResourceNode());
785
786
                return true;
787
            }
788
789
            // If the resource repository exposes a direct helper:
790
            $repo = self::guessResourceRepository($resource);
791
            if ($repo && method_exists($repo, 'attachAssetToResource')) {
792
                $repo->attachAssetToResource($resource, $asset);
793
794
                return true;
795
            }
796
797
            error_log('LEGACY_FILE: No method to attach Asset to node (missing attachToNode/attachAssetToResource)');
798
799
            return false;
800
        } catch (Throwable $e) {
801
            error_log('LEGACY_FILE EXCEPTION (Asset attach): '.$e->getMessage());
802
803
            return false;
804
        }
805
    }
806
807
    private static function legacyFileUsable(string $filePath): bool
808
    {
809
        return is_file($filePath) && is_readable($filePath);
810
    }
811
812
    private static function legacyDetectMime(string $filePath): string
813
    {
814
        $mime = @mime_content_type($filePath);
815
816
        return $mime ?: 'application/octet-stream';
817
    }
818
819
    /**
820
     * Best-effort guess to find the resource repository via Doctrine.
821
     * Returns null if the repo is not a ResourceRepository.
822
     */
823
    private static function guessResourceRepository(AbstractResource $resource): ?ResourceRepository
824
    {
825
        try {
826
            $em = Database::getManager();
827
            $repo = $em->getRepository($resource::class);
828
829
            return $repo instanceof ResourceRepository ? $repo : null;
830
        } catch (Throwable $e) {
831
            return null;
832
        }
833
    }
834
835
    /**
836
     * Scan HTML for legacy /courses/<dir>/document/... references found in a ZIP,
837
     * ensure those files are created as Documents, and return URL maps to rewrite the HTML.
838
     *
839
     * Returns: ['byRel' => [ "document/..." => "public-url" ],
840
     *           'byBase'=> [ "file.ext"     => "public-url" ] ]
841
     *
842
     * @param mixed $docRepo
843
     * @param mixed $courseEntity
844
     * @param mixed $session
845
     * @param mixed $group
846
     */
847
    public static function buildUrlMapForHtmlFromPackage(
848
        string $html,
849
        string $courseDir,
850
        string $srcRoot,
851
        array &$folders,
852
        callable $ensureFolder,
853
        $docRepo,
854
        $courseEntity,
855
        $session,
856
        $group,
857
        int $session_id,
858
        int $file_option,
859
        ?callable $dbg = null
860
    ): array {
861
        $byRel = [];
862
        $byBase = [];
863
864
        $DBG = $dbg ?: static function ($m, $c = []): void { /* no-op */ };
865
866
        // src|href pointing to …/courses/<dir>/document/... (host optional)
867
        $depRegex = '/(?P<attr>src|href)\s*=\s*["\'](?P<full>(?:(?P<scheme>https?:)?\/\/[^"\']+)?(?P<path>\/courses\/[^\/]+\/document\/[^"\']+\.[a-z0-9]{1,8}))["\']/i';
868
869
        if (!preg_match_all($depRegex, $html, $mm) || empty($mm['full'])) {
870
            return ['byRel' => $byRel, 'byBase' => $byBase];
871
        }
872
873
        // Normalize a full URL to a "document/..." relative path inside the package
874
        $toRel = static function (string $full) use ($courseDir): string {
875
            $urlPath = parse_url(html_entity_decode($full, ENT_QUOTES | ENT_HTML5), PHP_URL_PATH) ?: $full;
876
            $urlPath = preg_replace('#^/courses/([^/]+)/#i', '/courses/'.$courseDir.'/', $urlPath);
877
            $rel = preg_replace('#^/courses/'.preg_quote($courseDir, '#').'/#i', '', $urlPath) ?: $urlPath;
878
879
            return ltrim($rel, '/'); // "document/..."
880
        };
881
882
        foreach ($mm['full'] as $fullUrl) {
883
            $rel = $toRel($fullUrl); // e.g. "document/img.png"
884
            if (!str_starts_with($rel, 'document/')) {
885
                continue;
886
            }   // STRICT: only /document/*
887
            if (isset($byRel[$rel])) {
888
                continue;
889
            }
890
891
            $basename = basename(parse_url($fullUrl, PHP_URL_PATH) ?: $fullUrl);
892
            $byBase[$basename] = $byBase[$basename] ?? null;
893
894
            $parentRelPath = '/'.trim(\dirname('/'.$rel), '/'); // "/document" or "/document/foo"
895
            $depTitle = basename($rel);
896
            $depAbs = rtrim($srcRoot, '/').'/'.$rel;
897
898
            // Do NOT create a top-level "/document" root
899
            $parentId = 0;
900
            if ('/document' !== $parentRelPath) {
901
                $parentId = $folders[$parentRelPath] ?? 0;
902
                if (!$parentId) {
903
                    $parentId = $ensureFolder($parentRelPath);
904
                    $folders[$parentRelPath] = $parentId;
905
                    $DBG('helper.ensureFolder', ['parentRelPath' => $parentRelPath, 'parentId' => $parentId]);
906
                }
907
            } else {
908
                $parentRelPath = '/';
909
            }
910
911
            if (!is_file($depAbs) || !is_readable($depAbs)) {
912
                $DBG('helper.dep.missing', ['rel' => $rel, 'abs' => $depAbs]);
913
914
                continue;
915
            }
916
917
            // Collision check under parent
918
            $parentRes = $parentId ? $docRepo->find($parentId) : $courseEntity;
919
            $findExisting = function ($t) use ($docRepo, $parentRes, $courseEntity, $session, $group) {
920
                $e = $docRepo->findCourseResourceByTitle($t, $parentRes->getResourceNode(), $courseEntity, $session, $group);
921
922
                return $e && method_exists($e, 'getIid') ? $e->getIid() : null;
923
            };
924
925
            $finalTitle = $depTitle;
926
            $existsIid = $findExisting($finalTitle);
927
            if ($existsIid) {
928
                $FILE_SKIP = \defined('FILE_SKIP') ? FILE_SKIP : 2;
929
                if ($file_option === $FILE_SKIP) {
930
                    $existingDoc = $docRepo->find($existsIid);
931
                    if ($existingDoc) {
932
                        $url = $docRepo->getResourceFileUrl($existingDoc);
933
                        if ($url) {
934
                            $byRel[$rel] = $url;
935
                            $byBase[$basename] = $byBase[$basename] ?: $url;
936
                            $DBG('helper.dep.reuse', ['rel' => $rel, 'iid' => $existsIid, 'url' => $url]);
937
                        }
938
                    }
939
940
                    continue;
941
                }
942
                // Rename on collision
943
                $pi = pathinfo($depTitle);
944
                $name = $pi['filename'] ?? $depTitle;
945
                $ext2 = isset($pi['extension']) && '' !== $pi['extension'] ? '.'.$pi['extension'] : '';
946
                $i = 1;
947
                while ($findExisting($finalTitle)) {
948
                    $finalTitle = $name.'_'.$i.$ext2;
949
                    $i++;
950
                }
951
            }
952
953
            // Create the non-HTML dependency from the package
954
            try {
955
                $entity = DocumentManager::addDocument(
956
                    ['real_id' => $courseEntity->getId(), 'code' => method_exists($courseEntity, 'getCode') ? $courseEntity->getCode() : null],
957
                    $parentRelPath, // metadata path (no "/document" root)
958
                    'file',
959
                    (int) (@filesize($depAbs) ?: 0),
960
                    $finalTitle,
961
                    null,
962
                    0,
963
                    null,
964
                    0,
965
                    (int) $session_id,
966
                    0,
967
                    false,
968
                    '',
969
                    $parentId,
970
                    $depAbs
971
                );
972
                $iid = method_exists($entity, 'getIid') ? $entity->getIid() : 0;
973
                $url = $docRepo->getResourceFileUrl($entity);
974
975
                $DBG('helper.dep.created', ['rel' => $rel, 'iid' => $iid, 'url' => $url]);
976
977
                if ($url) {
978
                    $byRel[$rel] = $url;
979
                    $byBase[$basename] = $byBase[$basename] ?: $url;
980
                }
981
            } catch (Throwable $e) {
982
                $DBG('helper.dep.error', ['rel' => $rel, 'err' => $e->getMessage()]);
983
            }
984
        }
985
986
        $byBase = array_filter($byBase);
987
988
        return ['byRel' => $byRel, 'byBase' => $byBase];
989
    }
990
991
    /**
992
     * Rewrite src|href that point to /courses/<dir>/document/... using:
993
     *  - exact match by relative path ("document/...") via $urlMapByRel
994
     *  - basename fallback ("file.ext") via $urlMapByBase
995
     *
996
     * Returns: ['html'=>..., 'replaced'=>N, 'misses'=>M]
997
     */
998
    public static function rewriteLegacyCourseUrlsWithMap(
999
        string $html,
1000
        string $courseDir,
1001
        array $urlMapByRel,
1002
        array $urlMapByBase
1003
    ): array {
1004
        $replaced = 0;
1005
        $misses = 0;
1006
1007
        $pattern = '/(?P<attr>src|href)\s*=\s*["\'](?P<full>(?:(?P<scheme>https?:)?\/\/[^"\']+)?(?P<path>\/courses\/(?P<dir>[^\/]+)\/document\/[^"\']+\.[a-z0-9]{1,8}))["\']/i';
1008
1009
        $html = preg_replace_callback($pattern, function ($m) use ($courseDir, $urlMapByRel, $urlMapByBase, &$replaced, &$misses) {
1010
            $attr = $m['attr'];
1011
            $fullUrl = html_entity_decode($m['full'], ENT_QUOTES | ENT_HTML5);
1012
            $path = $m['path']; // /courses/<dir>/document/...
1013
            $matchDir = $m['dir'];
1014
1015
            // Normalize to current course directory
1016
            $effectivePath = $path;
1017
            if (0 !== strcasecmp($matchDir, $courseDir)) {
1018
                $effectivePath = preg_replace('#^/courses/'.preg_quote($matchDir, '#').'/#i', '/courses/'.$courseDir.'/', $path) ?: $path;
1019
            }
1020
1021
            // "document/...."
1022
            $relInPackage = preg_replace('#^/courses/'.preg_quote($courseDir, '#').'/#i', '', $effectivePath) ?: $effectivePath;
1023
            $relInPackage = ltrim($relInPackage, '/'); // document/...
1024
1025
            // 1) exact rel match
1026
            if (isset($urlMapByRel[$relInPackage])) {
1027
                $newUrl = $urlMapByRel[$relInPackage];
1028
                $replaced++;
1029
1030
                return $attr.'="'.htmlspecialchars($newUrl, ENT_QUOTES | ENT_HTML5).'"';
1031
            }
1032
1033
            // 2) basename fallback
1034
            $base = basename(parse_url($effectivePath, PHP_URL_PATH) ?: $effectivePath);
1035
            if (isset($urlMapByBase[$base])) {
1036
                $newUrl = $urlMapByBase[$base];
1037
                $replaced++;
1038
1039
                return $attr.'="'.htmlspecialchars($newUrl, ENT_QUOTES | ENT_HTML5).'"';
1040
            }
1041
1042
            // Not found → keep original
1043
            $misses++;
1044
1045
            return $m[0];
1046
        }, $html);
1047
1048
        return ['html' => $html, 'replaced' => $replaced, 'misses' => $misses];
1049
    }
1050
}
1051