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

ChamiloHelper::buildUrlMapForHtmlFromPackage()   F

Complexity

Conditions 32
Paths 727

Size

Total Lines 142
Code Lines 82

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 32
eloc 82
c 0
b 0
f 0
nc 727
nop 12
dl 0
loc 142
rs 0.3791

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Helpers;
8
9
use Chamilo\CoreBundle\Entity\AbstractResource;
10
use Chamilo\CoreBundle\Entity\Asset;
11
use Chamilo\CoreBundle\Framework\Container;
12
use Chamilo\CoreBundle\Repository\ResourceRepository;
13
use ChamiloSession as Session;
14
use Database;
15
use DateInterval;
16
use DateTime;
17
use DateTimeZone;
18
use Display;
19
use DocumentManager;
20
use Event;
21
use Exception;
22
use ExtraFieldValue;
23
use FormValidator;
24
use LegalManager;
25
use MessageManager;
26
use Symfony\Component\HttpFoundation\File\UploadedFile;
27
use Template;
28
use Throwable;
29
use UserManager;
30
31
use const ENT_HTML5;
32
use const ENT_QUOTES;
33
use const PHP_ROUND_HALF_UP;
34
use const PHP_SAPI;
35
use const PHP_URL_PATH;
36
37
class ChamiloHelper
38
{
39
    public const COURSE_MANAGER = 1;
40
    public const SESSION_ADMIN = 3;
41
    public const DRH = 4;
42
    public const STUDENT = 5;
43
    public const ANONYMOUS = 6;
44
45
    private static array $configuration;
46
47
    public function setConfiguration(array $configuration): void
48
    {
49
        self::$configuration = $configuration;
50
    }
51
52
    public static function getConfigurationArray(): array
53
    {
54
        return self::$configuration;
55
    }
56
57
    public static function getConfigurationValue(string $variable): mixed
58
    {
59
        $configuration = self::getConfigurationArray();
60
        if (\array_key_exists($variable, $configuration)) {
61
            return $configuration[$variable];
62
        }
63
64
        return false;
65
    }
66
67
    /**
68
     * Returns an array of resolutions that can be used for the conversion of documents to images.
69
     */
70
    public static function getDocumentConversionSizes(): array
71
    {
72
        return [
73
            '540x405' => '540x405 (3/4)',
74
            '640x480' => '640x480 (3/4)',
75
            '720x540' => '720x540 (3/4)',
76
            '800x600' => '800x600 (3/4)',
77
            '1024x576' => '1024x576 (16/9)',
78
            '1024x768' => '1000x750 (3/4)',
79
            '1280x720' => '1280x720 (16/9)',
80
            '1280x860' => '1280x960 (3/4)',
81
            '1400x1050' => '1400x1050 (3/4)',
82
            '1600x900' => '1600x900 (16/9)',
83
        ];
84
    }
85
86
    /**
87
     * Get the platform logo path.
88
     *
89
     * @deprecated
90
     *
91
     * @throws Exception
92
     */
93
    public static function getPlatformLogoPath(
94
        string $theme = '',
95
        bool $getSysPath = false,
96
        bool $forcedGetter = false
97
    ): ?string {
98
        static $logoPath;
99
100
        // If call from CLI it should be reloaded.
101
        if ('cli' === PHP_SAPI) {
102
            $logoPath = null;
103
        }
104
105
        if (!isset($logoPath) || $forcedGetter) {
106
            $theme = empty($theme) ? api_get_visual_theme() : $theme;
107
            $accessUrlId = api_get_current_access_url_id();
108
            if ('cli' === PHP_SAPI) {
109
                $accessUrl = api_get_configuration_value('access_url');
110
                if (!empty($accessUrl)) {
111
                    $accessUrlId = $accessUrl;
112
                }
113
            }
114
            $themeDir = Template::getThemeDir($theme);
115
            $customLogoPath = $themeDir.\sprintf('images/header-logo-custom%s.png', $accessUrlId);
116
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