Passed
Pull Request — master (#6894)
by
unknown
12:02 queued 03:47
created

ChamiloHelper::addLegalTermsFields()   B

Complexity

Conditions 9
Paths 8

Size

Total Lines 75
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 46
c 0
b 0
f 0
nc 8
nop 2
dl 0
loc 75
rs 7.6226

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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

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