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

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

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

Loading history...
512
            // Do NOT load fallback terms in another language
513
            return;
514
        }
515
516
        if (!$termPreview) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $termPreview of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
517
            return;
518
        }
519
520
        // hidden inputs to track version/language
521
        $form->addElement('hidden', 'legal_accept_type', $termPreview['version'].':'.$termPreview['language_id']);
522
        $form->addElement('hidden', 'legal_info', $termPreview['id'].':'.$termPreview['language_id']);
523
524
        if (1 == $termPreview['type']) {
525
            // simple checkbox linking out to full T&C
526
            $form->addElement(
527
                'checkbox',
528
                'legal_accept',
529
                null,
530
                'I have read and agree to the <a href="tc.php" target="_blank">Terms and Conditions</a>'
531
            );
532
            $form->addRule('legal_accept', 'This field is required', 'required');
533
        } else {
534
            // full inline T&C panel with scroll
535
            $preview = LegalManager::show_last_condition($termPreview);
536
            $form->addHtml(
537
                '<div style="
538
                background-color: #f3f4f6;
539
                border: 1px solid #d1d5db;
540
                padding: 1rem;
541
                max-height: 16rem;
542
                overflow-y: auto;
543
                border-radius: 0.375rem;
544
                margin-bottom: 1rem;
545
            ">'
546
                .$preview.
547
                '</div>'
548
            );
549
550
            // any extra labels
551
            $extra = new ExtraFieldValue('terms_and_condition');
552
            $values = $extra->getAllValuesByItem($termPreview['id']);
553
            foreach ($values as $value) {
554
                if (!empty($value['field_value'])) {
555
                    $form->addLabel($value['display_text'], $value['field_value']);
556
                }
557
            }
558
559
            // acceptance checkbox
560
            $form->addElement(
561
                'checkbox',
562
                'legal_accept',
563
                null,
564
                'I have read and agree to the Terms and Conditions'
565
            );
566
            $form->addRule('legal_accept', 'This field is required', 'required');
567
        }
568
    }
569
570
    /**
571
     * Persists the user's acceptance of the terms & conditions.
572
     *
573
     * @param string $legalAcceptType version:language_id
574
     */
575
    public static function saveUserTermsAcceptance(int $userId, string $legalAcceptType): void
576
    {
577
        // Split and build the stored value**
578
        [$version, $languageId] = explode(':', $legalAcceptType);
579
        $timestamp = time();
580
        $toSave = (int) $version.':'.(int) $languageId.':'.$timestamp;
581
582
        // Save in extra-field**
583
        UserManager::update_extra_field_value($userId, 'legal_accept', $toSave);
584
585
        // Log event
586
        Event::addEvent(
0 ignored issues
show
Bug introduced by
The method addEvent() does not exist on Event. ( Ignorable by Annotation )

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

586
        Event::/** @scrutinizer ignore-call */ 
587
               addEvent(

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

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

Loading history...
587
            LOG_TERM_CONDITION_ACCEPTED,
588
            LOG_USER_OBJECT,
589
            api_get_user_info($userId),
590
            api_get_utc_datetime()
591
        );
592
593
        $bossList = UserManager::getStudentBossList($userId);
594
        if (!empty($bossList)) {
595
            $bossIds = array_column($bossList, 'boss_id');
596
            $current = api_get_user_info($userId);
597
            $dateStr = api_get_local_time($timestamp);
598
599
            foreach ($bossIds as $bossId) {
600
                $subject = \sprintf(get_lang('User %s signed the agreement.'), $current['complete_name']);
601
                $content = \sprintf(get_lang('User %s signed the agreement the %s.'), $current['complete_name'], $dateStr);
602
                MessageManager::send_message_simple($bossId, $subject, $content, $userId);
603
            }
604
        }
605
    }
606
607
    /**
608
     * Displays the Terms and Conditions page.
609
     */
610
    public static function displayLegalTermsPage(string $returnUrl = '/home', bool $canAccept = true, string $infoMessage = ''): void
611
    {
612
        $iso = api_get_language_isocode();
613
        $langId = api_get_language_id($iso);
614
        $term = LegalManager::get_last_condition($langId);
615
616
        if (!$term) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $term of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
617
            // No T&C for current language → show a message
618
            Display::display_header(get_lang('Terms and Conditions'));
619
            echo '<div class="max-w-3xl mx-auto text-gray-90 text-lg text-center">'
620
                .get_lang('No terms and conditions available for this language.')
621
                .'</div>';
622
            Display::display_footer();
623
624
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

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

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

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

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