Passed
Pull Request — master (#7119)
by
unknown
09:22
created

learnpath::getFinalItemForm()   F

Complexity

Conditions 40
Paths > 20000

Size

Total Lines 304
Code Lines 142

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 40
eloc 142
c 0
b 0
f 0
nc 1180672
nop 0
dl 0
loc 304
rs 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\Course;
6
use Chamilo\CoreBundle\Entity\ResourceLink;
7
use Chamilo\CoreBundle\Entity\ResourceNode;
8
use Chamilo\CoreBundle\Entity\Session as SessionEntity;
9
use Chamilo\CoreBundle\Entity\TrackEExercise;
10
use Chamilo\CoreBundle\Entity\User;
11
use Chamilo\CoreBundle\Enums\ObjectIcon;
12
use Chamilo\CoreBundle\Event\Events;
13
use Chamilo\CoreBundle\Event\LearningPathEndedEvent;
14
use Chamilo\CoreBundle\Framework\Container;
15
use Chamilo\CoreBundle\Repository\TrackEDefaultRepository;
16
use Chamilo\CoreBundle\Helpers\ThemeHelper;
17
use Chamilo\CourseBundle\Component\CourseCopy\CourseArchiver;
18
use Chamilo\CourseBundle\Component\CourseCopy\CourseBuilder;
19
use Chamilo\CourseBundle\Component\CourseCopy\CourseRestorer;
20
use Chamilo\CourseBundle\Entity\CDocument;
21
use Chamilo\CourseBundle\Entity\CForumThread;
22
use Chamilo\CourseBundle\Entity\CLink;
23
use Chamilo\CourseBundle\Entity\CLp;
24
use Chamilo\CourseBundle\Entity\CLpCategory;
25
use Chamilo\CourseBundle\Entity\CLpItem;
26
use Chamilo\CourseBundle\Entity\CLpItemView;
27
use Chamilo\CourseBundle\Entity\CLpRelUser;
28
use Chamilo\CourseBundle\Entity\CQuiz;
29
use Chamilo\CourseBundle\Entity\CStudentPublication;
30
use Chamilo\CourseBundle\Entity\CSurvey;
31
use Chamilo\CourseBundle\Entity\CTool;
32
use Chamilo\CourseBundle\Repository\CDocumentRepository;
33
use Chamilo\CourseBundle\Repository\CLpRelUserRepository;
34
use ChamiloSession as Session;
35
use Doctrine\Common\Collections\Criteria;
36
use PhpZip\ZipFile;
37
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
38
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
39
40
/**
41
 * Class learnpath
42
 * This class defines the parent attributes and methods for Chamilo learnpaths
43
 * and SCORM learnpaths. It is used by the scorm class.
44
 *
45
 * @todo decouple class
46
 *
47
 * @author  Yannick Warnier <[email protected]>
48
 * @author  Julio Montoya   <[email protected]> Several improvements and fixes
49
 */
50
class learnpath
51
{
52
    public const MAX_LP_ITEM_TITLE_LENGTH = 36;
53
    public const STATUS_CSS_CLASS_NAME = [
54
        'not attempted' => 'scorm_not_attempted',
55
        'incomplete' => 'scorm_not_attempted',
56
        'failed' => 'scorm_failed',
57
        'completed' => 'scorm_completed',
58
        'passed' => 'scorm_completed',
59
        'succeeded' => 'scorm_completed',
60
        'browsed' => 'scorm_completed',
61
    ];
62
63
    public $attempt = 0; // The number for the current ID view.
64
    public $cc; // Course (code) this learnpath is located in. @todo change name for something more comprensible ...
65
    public $current; // Id of the current item the user is viewing.
66
    public $current_score; // The score of the current item.
67
    public $current_time_start; // The time the user loaded this resource (this does not mean he can see it yet).
68
    public $current_time_stop; // The time the user closed this resource.
69
    public $default_status = 'not attempted';
70
    public $encoding = 'UTF-8';
71
    public $error = '';
72
    public $force_commit = false; // For SCORM only- if true will send a scorm LMSCommit() request on each LMSSetValue()
73
    public $index; // The index of the active learnpath_item in $ordered_items array.
74
    /** @var learnpathItem[] */
75
    public $items = [];
76
    public $last; // item_id of last item viewed in the learning path.
77
    public $last_item_seen = 0; // In case we have already come in this lp, reuse the last item seen if authorized.
78
    public $license; // Which license this course has been given - not used yet on 20060522.
79
    public $lp_id; // DB iid for this learnpath.
80
    public $lp_view_id; // DB ID for lp_view
81
    public $maker; // Which maker has conceived the content (ENI, Articulate, ...).
82
    public $message = '';
83
    public $mode = 'embedded'; // Holds the video display mode (fullscreen or embedded).
84
    public $name; // Learnpath name (they generally have one).
85
    public $ordered_items = []; // List of the learnpath items in the order they are to be read.
86
    public $path = ''; // Path inside the scorm directory (if scorm).
87
    public $theme; // The current theme of the learning path.
88
    public $accumulateScormTime; // Flag to decide whether to accumulate SCORM time or not
89
    public $accumulateWorkTime; // The min time of learnpath
90
91
    // Tells if all the items of the learnpath can be tried again. Defaults to "no" (=1).
92
    public $prevent_reinit = 1;
93
94
    // Describes the mode of progress bar display.
95
    public $seriousgame_mode = 0;
96
    public $progress_bar_mode = '%';
97
98
    // Percentage progress as saved in the db.
99
    public $progress_db = 0;
100
    public $proximity; // Wether the content is distant or local or unknown.
101
    public $refs_list = []; //list of items by ref => db_id. Used only for prerequisites match.
102
    // !!!This array (refs_list) is built differently depending on the nature of the LP.
103
    // If SCORM, uses ref, if Chamilo, uses id to keep a unique value.
104
    public $type; //type of learnpath. Could be 'chamilo', 'scorm', 'scorm2004', 'aicc', ...
105
    // TODO: Check if this type variable is useful here (instead of just in the controller script).
106
    public $user_id; //ID of the user that is viewing/using the course
107
    public $update_queue = [];
108
    public $scorm_debug = 0;
109
    public $arrMenu = []; // Array for the menu items.
110
    public $debug = 0; // Logging level.
111
    public $lp_session_id = 0;
112
    public $lp_view_session_id = 0; // The specific view might be bound to a session.
113
    public $prerequisite = 0;
114
    public $use_max_score = 1; // 1 or 0
115
    public $subscribeUsers = 0; // Subscribe users or not
116
    public $created_on = '';
117
    public $modified_on = '';
118
    public $published_on = '';
119
    public $expired_on = '';
120
    public $ref;
121
    public $course_int_id;
122
    public $course_info;
123
    public $categoryId;
124
    public $scormUrl;
125
    public $entity;
126
    public $auto_forward_video = 1;
127
128
    public $js_lib;
129
    public $author;
130
    public $hide_toc_frame;
131
    public $max_ordered_items;
132
133
    public function __construct(CLp $entity = null, $course_info, $user_id)
134
    {
135
        $debug = $this->debug;
136
        $user_id = (int) $user_id;
137
        $this->encoding = api_get_system_encoding();
138
        $lp_id = 0;
139
        if (null !== $entity) {
140
            $lp_id = $entity->getIid();
141
        }
142
        $course_info = empty($course_info) ? api_get_course_info() : $course_info;
143
        $course_id = (int) $course_info['real_id'];
144
        $this->course_info = $course_info;
145
        $this->set_course_int_id($course_id);
146
        if (empty($lp_id) || empty($course_id)) {
147
            $this->error = "Parameter is empty: LpId:'$lp_id', courseId: '$lp_id'";
148
        } else {
149
            //$this->entity = $entity;
150
            $this->lp_id = $lp_id;
151
            $this->type = $entity->getLpType();
152
            $this->name = stripslashes($entity->getTitle());
153
            $this->proximity = $entity->getContentLocal();
154
            $this->theme = $entity->getTheme();
155
            $this->maker = $entity->getContentLocal();
156
            $this->prevent_reinit = $entity->getPreventReinit();
157
            $this->seriousgame_mode = $entity->getSeriousgameMode();
158
            $this->license = $entity->getContentLicense();
159
            $this->scorm_debug = $entity->getDebug();
160
            $this->js_lib = $entity->getJsLib();
161
            $this->path = $entity->getPath();
162
            $this->author = $entity->getAuthor();
163
            $this->hide_toc_frame = $entity->getHideTocFrame();
164
            //$this->lp_session_id = $entity->getSessionId();
165
            $this->use_max_score = $entity->getUseMaxScore();
166
            $this->subscribeUsers = $entity->getSubscribeUsers();
167
            $this->created_on = $entity->getCreatedOn()->format('Y-m-d H:i:s');
168
            $this->modified_on = $entity->getModifiedOn()->format('Y-m-d H:i:s');
169
            $this->ref = $entity->getRef();
170
            $this->auto_forward_video = $entity->getAutoForwardVideo();
171
            $this->categoryId = 0;
172
            if ($entity->getCategory()) {
173
                $this->categoryId = $entity->getCategory()->getIid();
174
            }
175
176
            if ($entity->hasAsset()) {
177
                $asset = $entity->getAsset();
178
                $this->scormUrl = Container::getAssetRepository()->getAssetUrl($asset).'/'.$entity->getPath().'/';
179
            }
180
181
            $this->accumulateScormTime = $entity->getAccumulateWorkTime();
182
183
            if (!empty($entity->getPublishedOn())) {
184
                $this->published_on = $entity->getPublishedOn()->format('Y-m-d H:i:s');
185
            }
186
187
            if (!empty($entity->getExpiredOn())) {
188
                $this->expired_on = $entity->getExpiredOn()->format('Y-m-d H:i:s');
189
            }
190
            if (2 == $this->type) {
191
                if (1 == $entity->getForceCommit()) {
192
                    $this->force_commit = true;
193
                }
194
            }
195
            $this->mode = $entity->getDefaultViewMod();
196
197
            // Check user ID.
198
            if (empty($user_id)) {
199
                $this->error = 'User ID is empty';
200
            } else {
201
                $this->user_id = $user_id;
202
            }
203
204
            // End of variables checking.
205
            $session_id = api_get_session_id();
206
            //  Get the session condition for learning paths of the base + session.
207
            $session = api_get_session_condition($session_id);
208
            // Now get the latest attempt from this user on this LP, if available, otherwise create a new one.
209
            $lp_table = Database::get_course_table(TABLE_LP_VIEW);
210
211
            // Selecting by view_count descending allows to get the highest view_count first.
212
            $sql = "SELECT * FROM $lp_table
213
                    WHERE
214
                        c_id = $course_id AND
215
                        lp_id = $lp_id AND
216
                        user_id = $user_id
217
                        $session
218
                    ORDER BY view_count DESC";
219
            $res = Database::query($sql);
220
221
            if (Database::num_rows($res) > 0) {
222
                $row = Database::fetch_array($res);
223
                $this->attempt = $row['view_count'];
224
                $this->lp_view_id = $row['iid'];
225
                $this->last_item_seen = $row['last_item'];
226
                $this->progress_db = $row['progress'];
227
                $this->lp_view_session_id = $row['session_id'];
228
            } elseif (!api_is_invitee()) {
229
                $this->attempt = 1;
230
                $params = [
231
                    'c_id' => $course_id,
232
                    'lp_id' => $lp_id,
233
                    'user_id' => $user_id,
234
                    'view_count' => 1,
235
                    //'session_id' => $session_id,
236
                    'last_item' => 0,
237
                ];
238
                if (!empty($session_id)) {
239
                    $params['session_id'] = $session_id;
240
                }
241
                $this->last_item_seen = 0;
242
                $this->lp_view_session_id = $session_id;
243
                $this->lp_view_id = Database::insert($lp_table, $params);
244
            }
245
246
            $criteria = new Criteria();
247
            $criteria
248
                ->where($criteria->expr()->neq('path', 'root'))
249
                ->orderBy(
250
                    [
251
                        'parent' => Criteria::ASC,
252
                        'displayOrder' => Criteria::ASC,
253
                    ]
254
                );
255
            $items = $entity->getItems()->matching($criteria);
256
            $lp_item_id_list = [];
257
            foreach ($items as $item) {
258
                $itemId = $item->getIid();
259
                $lp_item_id_list[] = $itemId;
260
261
                switch ($this->type) {
262
                    case CLp::AICC_TYPE:
263
                        $oItem = new aiccItem('db', $itemId, $course_id);
264
                        if (is_object($oItem)) {
265
                            $oItem->set_lp_view($this->lp_view_id);
266
                            $oItem->set_prevent_reinit($this->prevent_reinit);
267
                            // Don't use reference here as the next loop will make the pointed object change.
268
                            $this->items[$itemId] = $oItem;
269
                            $this->refs_list[$oItem->ref] = $itemId;
270
                        }
271
                        break;
272
                    case CLp::SCORM_TYPE:
273
                        $oItem = new scormItem('db', $itemId);
274
                        if (is_object($oItem)) {
275
                            $oItem->set_lp_view($this->lp_view_id);
276
                            $oItem->set_prevent_reinit($this->prevent_reinit);
277
                            // Don't use reference here as the next loop will make the pointed object change.
278
                            $this->items[$itemId] = $oItem;
279
                            $this->refs_list[$oItem->ref] = $itemId;
280
                        }
281
                        break;
282
                    case CLp::LP_TYPE:
283
                    default:
284
                        $oItem = new learnpathItem(null, $item);
285
                        if (is_object($oItem)) {
286
                            // Moved down to when we are sure the item_view exists.
287
                            //$oItem->set_lp_view($this->lp_view_id);
288
                            $oItem->set_prevent_reinit($this->prevent_reinit);
289
                            // Don't use reference here as the next loop will make the pointed object change.
290
                            $this->items[$itemId] = $oItem;
291
                            $this->refs_list[$itemId] = $itemId;
292
                        }
293
                        break;
294
                }
295
296
                // Setting the object level with variable $this->items[$i][parent]
297
                foreach ($this->items as $itemLPObject) {
298
                    $level = self::get_level_for_item($this->items, $itemLPObject->db_id);
299
                    $itemLPObject->level = $level;
300
                }
301
302
                // Setting the view in the item object.
303
                if (isset($this->items[$itemId]) && is_object($this->items[$itemId])) {
304
                    $this->items[$itemId]->set_lp_view($this->lp_view_id);
305
                    if (TOOL_HOTPOTATOES == $this->items[$itemId]->get_type()) {
306
                        $this->items[$itemId]->current_start_time = 0;
307
                        $this->items[$itemId]->current_stop_time = 0;
308
                    }
309
                }
310
            }
311
312
            if (!empty($lp_item_id_list)) {
313
                $lp_item_id_list_to_string = implode("','", $lp_item_id_list);
314
                if (!empty($lp_item_id_list_to_string)) {
315
                    // Get last viewing vars.
316
                    $itemViewTable = Database::get_course_table(TABLE_LP_ITEM_VIEW);
317
                    // This query should only return one or zero result.
318
                    $sql = "SELECT lp_item_id, status
319
                            FROM $itemViewTable
320
                            WHERE
321
                                lp_view_id = ".$this->get_view_id()." AND
322
                                lp_item_id IN ('".$lp_item_id_list_to_string."')
323
                            ORDER BY view_count DESC ";
324
                    $status_list = [];
325
                    $res = Database::query($sql);
326
                    while ($row = Database:: fetch_array($res)) {
327
                        $status_list[$row['lp_item_id']] = $row['status'];
328
                    }
329
330
                    foreach ($lp_item_id_list as $item_id) {
331
                        if (isset($status_list[$item_id])) {
332
                            $status = $status_list[$item_id];
333
334
                            if (is_object($this->items[$item_id])) {
335
                                $this->items[$item_id]->set_status($status);
336
                                if (empty($status)) {
337
                                    $this->items[$item_id]->set_status(
338
                                        $this->default_status
339
                                    );
340
                                }
341
                            }
342
                        } else {
343
                            if (!api_is_invitee()) {
344
                                if (isset($this->items[$item_id]) && is_object($this->items[$item_id])) {
345
                                    $this->items[$item_id]->set_status(
346
                                        $this->default_status
347
                                    );
348
                                }
349
350
                                if (!empty($this->lp_view_id)) {
351
                                    // Add that row to the lp_item_view table so that
352
                                    // we have something to show in the stats page.
353
                                    $params = [
354
                                        'lp_item_id' => $item_id,
355
                                        'lp_view_id' => $this->lp_view_id,
356
                                        'view_count' => 1,
357
                                        'status' => 'not attempted',
358
                                        'start_time' => time(),
359
                                        'total_time' => 0,
360
                                        'score' => 0,
361
                                    ];
362
                                    Database::insert($itemViewTable, $params);
363
364
                                    $this->items[$item_id]->set_lp_view(
365
                                        $this->lp_view_id
366
                                    );
367
                                }
368
                            }
369
                        }
370
                    }
371
                }
372
            }
373
374
            $this->ordered_items = self::get_flat_ordered_items_list($entity, null);
375
            $this->max_ordered_items = 0;
376
            foreach ($this->ordered_items as $index => $dummy) {
377
                if ($index > $this->max_ordered_items && !empty($dummy)) {
378
                    $this->max_ordered_items = $index;
379
                }
380
            }
381
            // TODO: Define the current item better.
382
            $this->first();
383
            if ($debug) {
384
                error_log('lp_view_session_id '.$this->lp_view_session_id);
385
                error_log('End of learnpath constructor for learnpath '.$this->get_id());
386
            }
387
        }
388
    }
389
390
    /**
391
     * @return int
392
     */
393
    public function get_course_int_id()
394
    {
395
        return $this->course_int_id ?? api_get_course_int_id();
396
    }
397
398
    /**
399
     * @param $course_id
400
     *
401
     * @return int
402
     */
403
    public function set_course_int_id($course_id)
404
    {
405
        return $this->course_int_id = (int) $course_id;
406
    }
407
408
    /**
409
     * Function rewritten based on old_add_item() from Yannick Warnier.
410
     * Due the fact that users can decide where the item should come, I had to overlook this function and
411
     * I found it better to rewrite it. Old function is still available.
412
     * Added also the possibility to add a description.
413
     *
414
     * @param CLpItem $parent
415
     * @param int     $previousId
416
     * @param string  $type
417
     * @param int     $id resource ID (ref)
418
     * @param string  $title
419
     * @param string  $description
420
     * @param int     $prerequisites
421
     * @param int     $maxTimeAllowed
422
     * @param int     $userId
423
     *
424
     * @return int
425
     */
426
    public function add_item(
427
        ?CLpItem $parent,
428
        $previousId,
429
        $type,
430
        $id,
431
        $title,
432
        $description = '',
433
        $prerequisites = 0,
434
        $maxTimeAllowed = 0
435
    ) {
436
        $type = empty($type) ? 'dir' : $type;
437
        $course_id = $this->course_info['real_id'];
438
        if (empty($course_id)) {
439
            // Sometimes Oogie doesn't catch the course info but sets $this->cc
440
            $this->course_info = api_get_course_info($this->cc);
441
            $course_id = $this->course_info['real_id'];
442
        }
443
        $id = (int) $id;
444
        $maxTimeAllowed = (int) $maxTimeAllowed;
445
        if (empty($maxTimeAllowed)) {
446
            $maxTimeAllowed = 0;
447
        }
448
        $maxScore = 100;
449
        if ('quiz' === $type && $id) {
450
            // Disabling the exercise if we add it inside a LP
451
            $exercise = new Exercise($course_id);
452
            $exercise->read($id);
453
            $maxScore = $exercise->getMaxScore();
454
455
            $exercise->disable();
456
            $exercise->save();
457
            $title = $exercise->get_formated_title();
458
        }
459
460
        $lpItem = (new CLpItem())
461
            ->setTitle($title)
462
            ->setDescription($description)
463
            ->setPath($id)
464
            ->setLp(api_get_lp_entity($this->get_id()))
465
            ->setItemType($type)
466
            ->setMaxScore($maxScore)
467
            ->setMaxTimeAllowed($maxTimeAllowed)
468
            ->setPrerequisite($prerequisites)
469
            //->setDisplayOrder($display_order + 1)
470
            //->setNextItemId((int) $next)
471
            //->setPreviousItemId($previous)
472
        ;
473
474
        if (!empty($parent))  {
475
            $lpItem->setParent($parent);
476
        }
477
        $em = Database::getManager();
478
        $em->persist($lpItem);
479
        $em->flush();
480
481
        $new_item_id = $lpItem->getIid();
482
        if ($new_item_id) {
483
            // @todo fix upload audio.
484
            // Upload audio.
485
            /*if (!empty($_FILES['mp3']['name'])) {
486
                // Create the audio folder if it does not exist yet.
487
                $filepath = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document/';
488
                if (!is_dir($filepath.'audio')) {
489
                    mkdir(
490
                        $filepath.'audio',
491
                        api_get_permissions_for_new_directories()
492
                    );
493
                    DocumentManager::addDocument(
494
                        $_course,
495
                        '/audio',
496
                        'folder',
497
                        0,
498
                        'audio',
499
                        '',
500
                        0,
501
                        true,
502
                        null,
503
                        $sessionId,
504
                        $userId
505
                    );
506
                }
507
508
                $file_path = handle_uploaded_document(
509
                    $_course,
510
                    $_FILES['mp3'],
511
                    api_get_path(SYS_COURSE_PATH).$_course['path'].'/document',
512
                    '/audio',
513
                    $userId,
514
                    '',
515
                    '',
516
                    '',
517
                    '',
518
                    false
519
                );
520
521
                // Getting the filename only.
522
                $file_components = explode('/', $file_path);
523
                $file = $file_components[count($file_components) - 1];
524
525
                // Store the mp3 file in the lp_item table.
526
                $sql = "UPDATE $tbl_lp_item SET
527
                          audio = '".Database::escape_string($file)."'
528
                        WHERE iid = '".intval($new_item_id)."'";
529
                Database::query($sql);
530
            }*/
531
        }
532
533
        return $new_item_id;
534
    }
535
536
    /**
537
     * Static admin function allowing addition of a learnpath to a course.
538
     *
539
     * @param string $courseCode
540
     * @param string $name
541
     * @param string $description
542
     * @param string $learnpath
543
     * @param string $origin
544
     * @param string $zipname       Zip file containing the learnpath or directory containing the learnpath
545
     * @param string $published_on
546
     * @param string $expired_on
547
     * @param int    $categoryId
548
     * @param int    $userId
549
     *
550
     * @return CLp
551
     */
552
    public static function add_lp(
553
        $courseCode,
554
        $name,
555
        $description = '',
556
        $learnpath = 'guess',
557
        $origin = 'zip',
558
        $zipname = '',
559
        $published_on = '',
560
        $expired_on = '',
561
        $categoryId = 0,
562
        $userId = 0
563
    ) {
564
        global $charset;
565
566
        if (!empty($courseCode)) {
567
            $courseInfo = api_get_course_info($courseCode);
568
            $course_id = $courseInfo['real_id'];
569
        } else {
570
            $course_id = api_get_course_int_id();
571
            $courseInfo = api_get_course_info();
572
        }
573
574
        $categoryId = (int) $categoryId;
575
576
        if (empty($published_on)) {
577
            $published_on = null;
578
        } else {
579
            $published_on = api_get_utc_datetime($published_on, true, true);
580
        }
581
582
        if (empty($expired_on)) {
583
            $expired_on = null;
584
        } else {
585
            $expired_on = api_get_utc_datetime($expired_on, true, true);
586
        }
587
588
        $description = Database::escape_string(api_htmlentities($description, ENT_QUOTES));
589
        $type = 1;
590
        switch ($learnpath) {
591
            case 'guess':
592
            case 'aicc':
593
                break;
594
            case 'dokeos':
595
            case 'chamilo':
596
                $type = 1;
597
                break;
598
        }
599
600
        $sessionEntity = api_get_session_entity();
601
        $courseEntity = api_get_course_entity($courseInfo['real_id']);
602
        $lp = null;
603
        switch ($origin) {
604
            case 'zip':
605
                // Check zip name string. If empty, we are currently creating a new Chamilo learnpath.
606
                break;
607
            case 'manual':
608
            default:
609
                /*$get_max = "SELECT MAX(display_order)
610
                            FROM $tbl_lp WHERE c_id = $course_id";
611
                $res_max = Database::query($get_max);
612
                if (Database::num_rows($res_max) < 1) {
613
                    $dsp = 1;
614
                } else {
615
                    $row = Database::fetch_array($res_max);
616
                    $dsp = $row[0] + 1;
617
                }*/
618
619
                $category = null;
620
                if (!empty($categoryId)) {
621
                    $category = Container::getLpCategoryRepository()->find($categoryId);
622
                }
623
624
                $lpRepo = Container::getLpRepository();
625
626
                $lp = (new CLp())
627
                    ->setLpType($type)
628
                    ->setTitle($name)
629
                    ->setDescription($description)
630
                    ->setCategory($category)
631
                    ->setPublishedOn($published_on)
632
                    ->setExpiredOn($expired_on)
633
                    ->setParent($courseEntity)
634
                    ->addCourseLink($courseEntity, $sessionEntity)
635
                ;
636
                $lpRepo->createLp($lp);
637
638
                break;
639
        }
640
641
        return $lp;
642
    }
643
644
    /**
645
     * Auto completes the parents of an item in case it's been completed or passed.
646
     *
647
     * @param int $item Optional ID of the item from which to look for parents
648
     */
649
    public function autocomplete_parents($item)
650
    {
651
        $debug = $this->debug;
652
653
        if (empty($item)) {
654
            $item = $this->current;
655
        }
656
657
        $currentItem = $this->getItem($item);
658
        if ($currentItem) {
659
            $parent_id = $currentItem->get_parent();
660
            $parent = $this->getItem($parent_id);
661
            if ($parent) {
662
                // if $item points to an object and there is a parent.
663
                if ($debug) {
664
                    error_log(
665
                        'Autocompleting parent of item '.$item.' '.
666
                        $currentItem->get_title().'" (item '.$parent_id.' "'.$parent->get_title().'") ',
667
                        0
668
                    );
669
                }
670
671
                // New experiment including failed and browsed in completed status.
672
                //$current_status = $currentItem->get_status();
673
                //if ($currentItem->is_done() || $current_status == 'browsed' || $current_status == 'failed') {
674
                // Fixes chapter auto complete
675
                if (true) {
676
                    // If the current item is completed or passes or succeeded.
677
                    $updateParentStatus = true;
678
                    if ($debug) {
679
                        error_log('Status of current item is alright');
680
                    }
681
682
                    foreach ($parent->get_children() as $childItemId) {
683
                        $childItem = $this->getItem($childItemId);
684
685
                        // If children was not set try to get the info
686
                        if (empty($childItem->db_item_view_id)) {
687
                            $childItem->set_lp_view($this->lp_view_id);
688
                        }
689
690
                        // Check all his brothers (parent's children) for completion status.
691
                        if ($childItemId != $item) {
692
                            if ($debug) {
693
                                error_log(
694
                                    'Looking at brother #'.$childItemId.' "'.$childItem->get_title().'", status is '.$childItem->get_status(),
695
                                    0
696
                                );
697
                            }
698
                            // Trying completing parents of failed and browsed items as well.
699
                            if ($childItem->status_is(
700
                                [
701
                                    'completed',
702
                                    'passed',
703
                                    'succeeded',
704
                                    'browsed',
705
                                    'failed',
706
                                ]
707
                            )
708
                            ) {
709
                                // Keep completion status to true.
710
                                continue;
711
                            } else {
712
                                if ($debug > 2) {
713
                                    error_log(
714
                                        'Found one incomplete child of parent #'.$parent_id.': child #'.$childItemId.' "'.$childItem->get_title().'", is '.$childItem->get_status().' db_item_view_id:#'.$childItem->db_item_view_id,
715
                                        0
716
                                    );
717
                                }
718
                                $updateParentStatus = false;
719
                                break;
720
                            }
721
                        }
722
                    }
723
724
                    if ($updateParentStatus) {
725
                        // If all the children were completed:
726
                        $parent->set_status('completed');
727
                        $parent->save(false, $this->prerequisites_match($parent->get_id()));
728
                        // Force the status to "completed"
729
                        //$this->update_queue[$parent->get_id()] = $parent->get_status();
730
                        $this->update_queue[$parent->get_id()] = 'completed';
731
                        if ($debug) {
732
                            error_log(
733
                                'Added parent #'.$parent->get_id().' "'.$parent->get_title().'" to update queue status: completed '.
734
                                print_r($this->update_queue, 1),
735
                                0
736
                            );
737
                        }
738
                        // Recursive call.
739
                        $this->autocomplete_parents($parent->get_id());
740
                    }
741
                }
742
            } else {
743
                if ($debug) {
744
                    error_log("Parent #$parent_id does not exists");
745
                }
746
            }
747
        } else {
748
            if ($debug) {
749
                error_log("#$item is an item that doesn't have parents");
750
            }
751
        }
752
    }
753
754
    /**
755
     * Closes the current resource.
756
     *
757
     * Stops the timer
758
     * Saves into the database if required
759
     * Clears the current resource data from this object
760
     *
761
     * @return bool True on success, false on failure
762
     */
763
    public function close()
764
    {
765
        if (empty($this->lp_id)) {
766
            $this->error = 'Trying to close this learnpath but no ID is set';
767
768
            return false;
769
        }
770
        $this->current_time_stop = time();
771
        $this->ordered_items = [];
772
        $this->index = 0;
773
        unset($this->lp_id);
774
        //unset other stuff
775
        return true;
776
    }
777
778
    /**
779
     * Static admin function allowing removal of a learnpath.
780
     *
781
     * @param array  $courseInfo
782
     * @param int    $id         Learnpath ID
783
     * @param string $delete     Whether to delete data or keep it (default: 'keep', others: 'remove')
784
     *
785
     * @return bool True on success, false on failure (might change that to return number of elements deleted)
786
     */
787
    public function delete($courseInfo = null, $id = null, $delete = 'keep')
788
    {
789
        $course_id = api_get_course_int_id();
790
        if (!empty($courseInfo)) {
791
            $course_id = isset($courseInfo['real_id']) ? $courseInfo['real_id'] : $course_id;
792
        }
793
794
        // Prevent deleting a different LP than the current one if an explicit ID was passed
795
        if (!empty($id) && ($id != $this->lp_id)) {
796
            return false;
797
        }
798
799
        $course  = api_get_course_entity();
800
        $session = api_get_session_entity();
801
802
        /** @var CLp|null $lp */
803
        $lp = Container::getLpRepository()->find($this->lp_id);
804
805
        // Detach the asset to avoid FK constraint
806
        $asset = $lp ? $lp->getAsset() : null;
807
        if ($asset && $lp) {
808
            $lp->setAsset(null);
809
            $em = Database::getManager();
810
            $em->persist($lp);
811
            $em->flush();
812
        }
813
814
        // 2) Now delete the asset and its folder
815
        if ($asset) {
816
            Container::getAssetRepository()->delete($asset);
817
        }
818
819
        // Remove resource links (course/session context)
820
        Database::getManager()
821
            ->getRepository(ResourceLink::class)
822
            ->removeByResourceInContext($lp, $course, $session);
823
824
        // Remove from gradebook if present
825
        $link_info = GradebookUtils::isResourceInCourseGradebook(
826
            api_get_course_int_id(),
827
            4,
828
            $id,
829
            api_get_session_id()
830
        );
831
832
        if (!empty($link_info)) {
833
            GradebookUtils::remove_resource_from_course_gradebook($link_info['id']);
834
        }
835
836
        // Tracking event
837
        $trackRepo    = Container::$container->get(TrackEDefaultRepository::class);
838
        $resourceNode = $lp ? $lp->getResourceNode() : null;
839
        if ($resourceNode) {
840
            $trackRepo->registerResourceEvent(
841
                $resourceNode,
842
                'deletion',
843
                api_get_user_id(),
844
                api_get_course_int_id(),
845
                api_get_session_id()
846
            );
847
        }
848
849
        // Purge the SCORM ZIP registered under Documents/Learning paths (teacher-only folder)
850
        //    This keeps the course storage meter consistent after LP deletion.
851
        try {
852
            if ($lp && $course) {
853
                $em = Database::getManager();
854
                /** @var CDocumentRepository $docRepo */
855
                $docRepo = $em->getRepository(CDocument::class);
856
                $docRepo->purgeScormZip($course, $lp);
857
            }
858
        } catch (\Throwable $e) {
859
            // Do not block LP deletion if purge fails; just log the error.
860
            error_log('[learnpath::delete] Failed to purge SCORM ZIP from Documents: '.$e->getMessage());
861
        }
862
    }
863
864
    /**
865
     * Removes all the children of one item - dangerous!
866
     *
867
     * @param int $id Element ID of which children have to be removed
868
     *
869
     * @return int Total number of children removed
870
     */
871
    public function delete_children_items($id)
872
    {
873
        $course_id = $this->course_info['real_id'];
874
875
        $num = 0;
876
        $id = (int) $id;
877
        if (empty($id) || empty($course_id)) {
878
            return false;
879
        }
880
        $lp_item = Database::get_course_table(TABLE_LP_ITEM);
881
        $sql = "SELECT * FROM $lp_item
882
                WHERE parent_item_id = $id";
883
        $res = Database::query($sql);
884
        while ($row = Database::fetch_array($res)) {
885
            $num += $this->delete_children_items($row['iid']);
886
            $sql = "DELETE FROM $lp_item
887
                    WHERE iid = ".$row['iid'];
888
            Database::query($sql);
889
            $num++;
890
        }
891
892
        return $num;
893
    }
894
895
    /**
896
     * Removes an item from the current learnpath.
897
     *
898
     * @param int $id Elem ID (0 if first)
899
     *
900
     * @return int Number of elements moved
901
     *
902
     * @todo implement resource removal
903
     */
904
    public function delete_item($id)
905
    {
906
        $course_id = api_get_course_int_id();
907
        $id = (int) $id;
908
        // TODO: Implement the resource removal.
909
        if (empty($id) || empty($course_id)) {
910
            return false;
911
        }
912
913
        $repo = Container::getLpItemRepository();
914
        $item = $repo->find($id);
915
        if (null === $item) {
916
            return false;
917
        }
918
919
        $em = Database::getManager();
920
        $repo->removeFromTree($item);
921
        $em->flush();
922
        $lp_item = Database::get_course_table(TABLE_LP_ITEM);
923
924
        //Removing prerequisites since the item will not longer exist
925
        $sql_all = "UPDATE $lp_item SET prerequisite = ''
926
                    WHERE prerequisite = '$id'";
927
        Database::query($sql_all);
928
929
        $sql = "UPDATE $lp_item
930
                SET previous_item_id = ".$this->getLastInFirstLevel()."
931
                WHERE lp_id = {$this->lp_id} AND item_type = '".TOOL_LP_FINAL_ITEM."'";
932
        Database::query($sql);
933
934
        // Remove from search engine if enabled.
935
        if ('true' === api_get_setting('search_enabled')) {
936
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
937
            $sql = 'SELECT * FROM %s
938
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
939
                    LIMIT 1';
940
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp, $id);
941
            $res = Database::query($sql);
942
            if (Database::num_rows($res) > 0) {
943
                $row2 = Database::fetch_array($res);
944
                $di = new ChamiloIndexer();
945
                $di->remove_document($row2['search_did']);
946
            }
947
            $sql = 'DELETE FROM %s
948
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
949
                    LIMIT 1';
950
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp, $id);
951
            Database::query($sql);
952
        }
953
    }
954
955
    /**
956
     * Updates an item's content in place.
957
     *
958
     * @param int    $id               Element ID
959
     * @param int    $parent           Parent item ID
960
     * @param int    $previous         Previous item ID
961
     * @param string $title            Item title
962
     * @param string $description      Item description
963
     * @param string $prerequisites    Prerequisites (optional)
964
     * @param array  $audio            The array resulting of the $_FILES[mp3] element
965
     * @param int    $max_time_allowed
966
     * @param string $url
967
     *
968
     * @return bool True on success, false on error
969
     */
970
    public function edit_item(
971
        $id,
972
        $parent,
973
        $previous,
974
        $title,
975
        $description,
976
        $prerequisites = '0',
977
        $audio = [],
978
        $max_time_allowed = 0,
979
        $url = ''
980
    ) {
981
        $_course = api_get_course_info();
982
        $id = (int) $id;
983
984
        if (empty($id) || empty($_course)) {
985
            return false;
986
        }
987
        $repo = Container::getLpItemRepository();
988
        /** @var CLpItem $item */
989
        $item = $repo->find($id);
990
        if (null === $item) {
991
            return false;
992
        }
993
994
        $item
995
            ->setTitle($title)
996
            ->setDescription($description)
997
            ->setPrerequisite($prerequisites)
998
            ->setMaxTimeAllowed((int) $max_time_allowed)
999
        ;
1000
1001
        $em = Database::getManager();
1002
        if (!empty($parent)) {
1003
            $parent = $repo->find($parent);
1004
            $item->setParent($parent);
1005
        } else {
1006
            $item->setParent(null);
1007
        }
1008
1009
        if (!empty($previous)) {
1010
            $previous = $repo->find($previous);
1011
            $repo->persistAsNextSiblingOf( $item, $previous);
1012
        } else {
1013
            $em->persist($item);
1014
        }
1015
1016
        $em->flush();
1017
1018
        if ('link' === $item->getItemType()) {
1019
            $link = new Link();
1020
            $linkId = $item->getPath();
1021
            $link->updateLink($linkId, $url);
1022
        }
1023
    }
1024
1025
    /**
1026
     * Updates an item's prereq in place.
1027
     *
1028
     * @param int    $id              Element ID
1029
     * @param string $prerequisite_id Prerequisite Element ID
1030
     * @param int    $minScore        Prerequisite min score
1031
     * @param int    $maxScore        Prerequisite max score
1032
     *
1033
     * @return bool True on success, false on error
1034
     */
1035
    public function edit_item_prereq($id, $prerequisite_id, $minScore = 0, $maxScore = 100)
1036
    {
1037
        $id = (int) $id;
1038
1039
        if (empty($id)) {
1040
            return false;
1041
        }
1042
        $prerequisite_id = (int) $prerequisite_id;
1043
1044
        if (empty($minScore) || $minScore < 0) {
1045
            $minScore = 0;
1046
        }
1047
1048
        if (empty($maxScore) || $maxScore < 0) {
1049
            $maxScore = 100;
1050
        }
1051
1052
        $minScore = (float) $minScore;
1053
        $maxScore = (float) $maxScore;
1054
1055
        if (empty($prerequisite_id)) {
1056
            $prerequisite_id = 'NULL';
1057
            $minScore = 0;
1058
            $maxScore = 100;
1059
        }
1060
1061
        $table = Database::get_course_table(TABLE_LP_ITEM);
1062
        $sql = " UPDATE $table
1063
                 SET
1064
                    prerequisite = $prerequisite_id ,
1065
                    prerequisite_min_score = $minScore ,
1066
                    prerequisite_max_score = $maxScore
1067
                 WHERE iid = $id";
1068
        Database::query($sql);
1069
1070
        return true;
1071
    }
1072
1073
    /**
1074
     * Get the specific prefix index terms of this learning path.
1075
     *
1076
     * @param string $prefix
1077
     *
1078
     * @return array Array of terms
1079
     */
1080
    public function get_common_index_terms_by_prefix($prefix)
1081
    {
1082
        $terms = get_specific_field_values_list_by_prefix(
1083
            $prefix,
1084
            $this->cc,
1085
            TOOL_LEARNPATH,
1086
            $this->lp_id
1087
        );
1088
        $prefix_terms = [];
1089
        if (!empty($terms)) {
1090
            foreach ($terms as $term) {
1091
                $prefix_terms[] = $term['value'];
1092
            }
1093
        }
1094
1095
        return $prefix_terms;
1096
    }
1097
1098
    /**
1099
     * Gets the number of items currently completed.
1100
     *
1101
     * @param bool Flag to determine the failed status is not considered progressed
1102
     *
1103
     * @return int The number of items currently completed
1104
     */
1105
    public function get_complete_items_count(bool $failedStatusException = false): int
1106
    {
1107
        $i = 0;
1108
        $completedStatusList = [
1109
            'completed',
1110
            'passed',
1111
            'succeeded',
1112
            'browsed',
1113
        ];
1114
1115
        if (!$failedStatusException) {
1116
            $completedStatusList[] = 'failed';
1117
        }
1118
1119
        foreach ($this->items as $id => $dummy) {
1120
            // Trying failed and browsed considered "progressed" as well.
1121
            if ($this->items[$id]->status_is($completedStatusList) &&
1122
                'dir' !== $this->items[$id]->get_type()
1123
            ) {
1124
                $i++;
1125
            }
1126
        }
1127
1128
        return $i;
1129
    }
1130
1131
    /**
1132
     * Gets the current item ID.
1133
     *
1134
     * @return int The current learnpath item id
1135
     */
1136
    public function get_current_item_id()
1137
    {
1138
        $current = 0;
1139
        if (!empty($this->current)) {
1140
            $current = (int) $this->current;
1141
        }
1142
1143
        return $current;
1144
    }
1145
1146
    /**
1147
     * Force to get the first learnpath item id.
1148
     *
1149
     * @return int The current learnpath item id
1150
     */
1151
    public function get_first_item_id()
1152
    {
1153
        $current = 0;
1154
        if (is_array($this->ordered_items)) {
1155
            $current = $this->ordered_items[0];
1156
        }
1157
1158
        return $current;
1159
    }
1160
1161
    /**
1162
     * Gets the total number of items available for viewing in this SCORM.
1163
     *
1164
     * @return int The total number of items
1165
     */
1166
    public function get_total_items_count()
1167
    {
1168
        return count($this->items);
1169
    }
1170
1171
    /**
1172
     * Gets the total number of items available for viewing in this SCORM but without chapters.
1173
     *
1174
     * @return int The total no-chapters number of items
1175
     */
1176
    public function getTotalItemsCountWithoutDirs()
1177
    {
1178
        $total = 0;
1179
        $typeListNotToCount = self::getChapterTypes();
1180
        foreach ($this->items as $temp2) {
1181
            if (!in_array($temp2->get_type(), $typeListNotToCount)) {
1182
                $total++;
1183
            }
1184
        }
1185
1186
        return $total;
1187
    }
1188
1189
    /**
1190
     *  Sets the first element URL.
1191
     */
1192
    public function first()
1193
    {
1194
        if ($this->debug > 0) {
1195
            error_log('In learnpath::first()', 0);
1196
            error_log('$this->last_item_seen '.$this->last_item_seen);
1197
        }
1198
1199
        // Test if the last_item_seen exists and is not a dir.
1200
        if (0 == count($this->ordered_items)) {
1201
            $this->index = 0;
1202
        }
1203
1204
        if (!empty($this->last_item_seen) &&
1205
            !empty($this->items[$this->last_item_seen]) &&
1206
            'dir' !== $this->items[$this->last_item_seen]->get_type()
1207
            //with this change (below) the LP will NOT go to the next item, it will take lp item we left
1208
            //&& !$this->items[$this->last_item_seen]->is_done()
1209
        ) {
1210
            if ($this->debug > 2) {
1211
                error_log(
1212
                    'In learnpath::first() - Last item seen is '.$this->last_item_seen.' of type '.
1213
                    $this->items[$this->last_item_seen]->get_type()
1214
                );
1215
            }
1216
            $index = -1;
1217
            foreach ($this->ordered_items as $myindex => $item_id) {
1218
                if ($item_id == $this->last_item_seen) {
1219
                    $index = $myindex;
1220
                    break;
1221
                }
1222
            }
1223
            if (-1 == $index) {
1224
                // Index hasn't changed, so item not found - panic (this shouldn't happen).
1225
                if ($this->debug > 2) {
1226
                    error_log('Last item ('.$this->last_item_seen.') was found in items but not in ordered_items, panic!', 0);
1227
                }
1228
1229
                return false;
1230
            } else {
1231
                $this->last = $this->last_item_seen;
1232
                $this->current = $this->last_item_seen;
1233
                $this->index = $index;
1234
            }
1235
        } else {
1236
            if ($this->debug > 2) {
1237
                error_log('In learnpath::first() - No last item seen', 0);
1238
            }
1239
            $index = 0;
1240
            // Loop through all ordered items and stop at the first item that is
1241
            // not a directory *and* that has not been completed yet.
1242
            while (!empty($this->ordered_items[$index]) &&
1243
                is_a($this->items[$this->ordered_items[$index]], 'learnpathItem') &&
1244
                (
1245
                    'dir' === $this->items[$this->ordered_items[$index]]->get_type() ||
1246
                    true === $this->items[$this->ordered_items[$index]]->is_done()
1247
                ) && $index < $this->max_ordered_items
1248
            ) {
1249
                $index++;
1250
            }
1251
1252
            $this->last = $this->current;
1253
            // current is
1254
            $this->current = isset($this->ordered_items[$index]) ? $this->ordered_items[$index] : null;
1255
            $this->index = $index;
1256
            if ($this->debug > 2) {
1257
                error_log('$index '.$index);
1258
                error_log('In learnpath::first() - No last item seen');
1259
                error_log('New last = '.$this->last.'('.$this->ordered_items[$index].')');
1260
            }
1261
        }
1262
        if ($this->debug > 2) {
1263
            error_log('In learnpath::first() - First item is '.$this->get_current_item_id());
1264
        }
1265
    }
1266
1267
    /**
1268
     * Gets the js library from the database.
1269
     *
1270
     * @return string The name of the javascript library to be used
1271
     */
1272
    public function get_js_lib()
1273
    {
1274
        $lib = '';
1275
        if (!empty($this->js_lib)) {
1276
            $lib = $this->js_lib;
1277
        }
1278
1279
        return $lib;
1280
    }
1281
1282
    /**
1283
     * Gets the learnpath database ID.
1284
     *
1285
     * @return int Learnpath ID in the lp table
1286
     */
1287
    public function get_id()
1288
    {
1289
        if (!empty($this->lp_id)) {
1290
            return (int) $this->lp_id;
1291
        }
1292
1293
        return 0;
1294
    }
1295
1296
    /**
1297
     * Gets the last element URL.
1298
     *
1299
     * @return string URL to load into the viewer
1300
     */
1301
    public function get_last()
1302
    {
1303
        // This is just in case the lesson doesn't cointain a valid scheme, just to avoid "Notices"
1304
        if (count($this->ordered_items) > 0) {
1305
            $this->index = count($this->ordered_items) - 1;
1306
1307
            return $this->ordered_items[$this->index];
1308
        }
1309
1310
        return false;
1311
    }
1312
1313
    /**
1314
     * Get the last element in the first level.
1315
     * Unlike learnpath::get_last this function doesn't consider the subsection' elements.
1316
     *
1317
     * @return mixed
1318
     */
1319
    public function getLastInFirstLevel()
1320
    {
1321
        try {
1322
            $lastId = Database::getManager()
1323
                ->createQuery('SELECT i.iid FROM ChamiloCourseBundle:CLpItem i
1324
                WHERE i.lp = :lp AND i.parent IS NULL AND i.itemType != :type ORDER BY i.displayOrder DESC')
1325
                ->setMaxResults(1)
1326
                ->setParameters(['lp' => $this->lp_id, 'type' => TOOL_LP_FINAL_ITEM])
1327
                ->getSingleScalarResult();
1328
1329
            return $lastId;
1330
        } catch (Exception $exception) {
1331
            return 0;
1332
        }
1333
    }
1334
1335
    /**
1336
     * Gets the navigation bar for the learnpath display screen.
1337
     *
1338
     * @param string $barId
1339
     *
1340
     * @return string The HTML string to use as a navigation bar
1341
     */
1342
    public function get_navigation_bar($barId = '')
1343
    {
1344
        if (empty($barId)) {
1345
            $barId = 'control-top';
1346
        }
1347
        $lpId = $this->lp_id;
1348
        $mycurrentitemid = $this->get_current_item_id();
1349
        $reportingText = get_lang('Reporting');
1350
        $previousText = get_lang('Previous');
1351
        $nextText = get_lang('Next');
1352
        $fullScreenText = get_lang('Back to normal screen');
1353
1354
        $settings = api_get_setting('lp.lp_view_settings', true);
1355
        $display = $settings['display'] ?? false;
1356
        $icon = Display::getMdiIcon('information');
1357
1358
        $reportingIcon = '
1359
            <a class="icon-toolbar"
1360
                id="stats_link"
1361
                href="lp_controller.php?action=stats&'.api_get_cidreq(true).'&lp_id='.$lpId.'"
1362
                onclick="window.parent.API.save_asset(); return true;"
1363
                target="content_name" title="'.$reportingText.'">
1364
                '.$icon.'<span class="sr-only">'.$reportingText.'</span>
1365
            </a>';
1366
1367
        if (!empty($display)) {
1368
            $showReporting = isset($display['show_reporting_icon']) ? $display['show_reporting_icon'] : true;
1369
            if (false === $showReporting) {
1370
                $reportingIcon = '';
1371
            }
1372
        }
1373
1374
        $hideArrows = false;
1375
        if (isset($settings['display']) && isset($settings['display']['hide_lp_arrow_navigation'])) {
1376
            $hideArrows = $settings['display']['hide_lp_arrow_navigation'];
1377
        }
1378
1379
        $previousIcon = '';
1380
        $nextIcon = '';
1381
        if (false === $hideArrows) {
1382
            $icon = Display::getMdiIcon('chevron-left');
1383
            $previousIcon = '
1384
                <button class="icon-toolbar" id="scorm-previous" type="button"
1385
                    onclick="switch_item('.$mycurrentitemid.',\'previous\');return false;" title="'.$previousText.'">
1386
                    '.$icon.'<span class="sr-only">'.$previousText.'</span>
1387
                </button>';
1388
1389
            $icon = Display::getMdiIcon('chevron-right');
1390
            $nextIcon = '
1391
                <button class="icon-toolbar" id="scorm-next" type="button"
1392
                    onclick="switch_item('.$mycurrentitemid.',\'next\');return false;" title="'.$nextText.'">
1393
                    '.$icon.'<span class="sr-only">'.$nextText.'</span>
1394
                </button>';
1395
        }
1396
1397
        if ('fullscreen' === $this->mode) {
1398
            $icon = Display::getMdiIcon('view-column');
1399
            $navbar = '
1400
                  <span id="'.$barId.'" class="buttons">
1401
                    '.$reportingIcon.'
1402
                    '.$previousIcon.'
1403
                    '.$nextIcon.'
1404
                    <a class="icon-toolbar" id="view-embedded"
1405
                        href="lp_controller.php?action=mode&mode=embedded" target="_top" title="'.$fullScreenText.'">
1406
                        '.$icon.'<span class="sr-only">'.$fullScreenText.'</span>
1407
                    </a>
1408
                  </span>';
1409
        } else {
1410
            $navbar = '
1411
                 <span id="'.$barId.'" class="buttons text-right">
1412
                    '.$reportingIcon.'
1413
                    '.$previousIcon.'
1414
                    '.$nextIcon.'
1415
                </span>';
1416
        }
1417
1418
        return $navbar;
1419
    }
1420
1421
    /**
1422
     * Gets the next resource in queue (url).
1423
     *
1424
     * @return string URL to load into the viewer
1425
     */
1426
    public function get_next_index()
1427
    {
1428
        // TODO
1429
        $index = $this->index;
1430
        $index++;
1431
        while (
1432
            !empty($this->ordered_items[$index]) && ('dir' == $this->items[$this->ordered_items[$index]]->get_type()) &&
1433
            $index < $this->max_ordered_items
1434
        ) {
1435
            $index++;
1436
            if ($index == $this->max_ordered_items) {
1437
                if ('dir' === $this->items[$this->ordered_items[$index]]->get_type()) {
1438
                    return $this->index;
1439
                }
1440
1441
                return $index;
1442
            }
1443
        }
1444
        if (empty($this->ordered_items[$index])) {
1445
            return $this->index;
1446
        }
1447
1448
        return $index;
1449
    }
1450
1451
    /**
1452
     * Gets item_id for the next element.
1453
     *
1454
     * @return int Next item (DB) ID
1455
     */
1456
    public function get_next_item_id()
1457
    {
1458
        $new_index = $this->get_next_index();
1459
        if (!empty($new_index)) {
1460
            if (isset($this->ordered_items[$new_index])) {
1461
                return $this->ordered_items[$new_index];
1462
            }
1463
        }
1464
1465
        return 0;
1466
    }
1467
1468
    /**
1469
     * Returns the package type ('scorm','aicc','scorm2004','ppt'...).
1470
     *
1471
     * Generally, the package provided is in the form of a zip file, so the function
1472
     * has been written to test a zip file. If not a zip, the function will return the
1473
     * default return value: ''
1474
     *
1475
     * @param string $filePath the path to the file
1476
     * @param string $file_name the original name of the file
1477
     *
1478
     * @return string 'scorm','aicc','scorm2004','error-empty-package'
1479
     *                if the package is empty, or '' if the package cannot be recognized
1480
     */
1481
    public static function getPackageType($filePath, $file_name)
1482
    {
1483
        // Get name of the zip file without the extension.
1484
        $file_info = pathinfo($file_name);
1485
        $extension = $file_info['extension']; // Extension only.
1486
        if (!empty($_POST['ppt2lp']) && !in_array(strtolower($extension), [
1487
                'dll',
1488
                'exe',
1489
            ])) {
1490
            return 'oogie';
1491
        }
1492
        if (!empty($_POST['woogie']) && !in_array(strtolower($extension), [
1493
                'dll',
1494
                'exe',
1495
            ])) {
1496
            return 'woogie';
1497
        }
1498
1499
        $zipFile = new ZipFile();
1500
        $zipFile->openFile($filePath);
1501
        $zipContentArray = $zipFile->getEntries();
1502
        $package_type = '';
1503
        $manifest = '';
1504
        $aicc_match_crs = 0;
1505
        $aicc_match_au = 0;
1506
        $aicc_match_des = 0;
1507
        $aicc_match_cst = 0;
1508
        $countItems = 0;
1509
        // The following loop should be stopped as soon as we found the right imsmanifest.xml (how to recognize it?).
1510
        if ($zipContentArray) {
1511
            $countItems = count($zipContentArray);
1512
            if ($countItems > 0) {
1513
                foreach ($zipContentArray as $thisContent) {
1514
                    $fileName = basename($thisContent->getName());
1515
                    if (preg_match('~.(php.*|phtml)$~i', $fileName)) {
1516
                        // New behaviour: Don't do anything. These files will be removed in scorm::import_package.
1517
                    } elseif (false !== stristr($fileName, 'imsmanifest.xml')) {
1518
                        $manifest = $fileName; // Just the relative directory inside scorm/
1519
                        $package_type = 'scorm';
1520
                        break; // Exit the foreach loop.
1521
                    } elseif (
1522
                        preg_match('/aicc\//i', $fileName) ||
1523
                        in_array(
1524
                            strtolower(pathinfo($fileName, PATHINFO_EXTENSION)),
1525
                            ['crs', 'au', 'des', 'cst']
1526
                        )
1527
                    ) {
1528
                        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
1529
                        switch ($ext) {
1530
                            case 'crs':
1531
                                $aicc_match_crs = 1;
1532
                                break;
1533
                            case 'au':
1534
                                $aicc_match_au = 1;
1535
                                break;
1536
                            case 'des':
1537
                                $aicc_match_des = 1;
1538
                                break;
1539
                            case 'cst':
1540
                                $aicc_match_cst = 1;
1541
                                break;
1542
                            default:
1543
                                break;
1544
                        }
1545
                        //break; // Don't exit the loop, because if we find an imsmanifest afterwards, we want it, not the AICC.
1546
                    } else {
1547
                        $package_type = '';
1548
                    }
1549
                }
1550
            }
1551
        }
1552
1553
        if (empty($package_type) && 4 == ($aicc_match_crs + $aicc_match_au + $aicc_match_des + $aicc_match_cst)) {
1554
            // If found an aicc directory... (!= false means it cannot be false (error) or 0 (no match)).
1555
            $package_type = 'aicc';
1556
        }
1557
1558
        // Try with chamilo course builder
1559
        if (empty($package_type)) {
1560
            // Sometimes users will try to upload an empty zip, or a zip with
1561
            // only a folder. Catch that and make the calling function aware.
1562
            // If the single file was the imsmanifest.xml, then $package_type
1563
            // would be 'scorm' and we wouldn't be here.
1564
            if ($countItems < 2) {
1565
                return 'error-empty-package';
1566
            }
1567
            $package_type = 'chamilo';
1568
        }
1569
1570
        return $package_type;
1571
    }
1572
1573
    /**
1574
     * Gets the previous resource in queue (url). Also initialises time values for this viewing.
1575
     *
1576
     * @return string URL to load into the viewer
1577
     */
1578
    public function get_previous_index()
1579
    {
1580
        $index = $this->index;
1581
        if (isset($this->ordered_items[$index - 1])) {
1582
            $index--;
1583
            while (isset($this->ordered_items[$index]) &&
1584
                ('dir' === $this->items[$this->ordered_items[$index]]->get_type())
1585
            ) {
1586
                $index--;
1587
                if ($index < 0) {
1588
                    return $this->index;
1589
                }
1590
            }
1591
        }
1592
1593
        return $index;
1594
    }
1595
1596
    /**
1597
     * Gets item_id for the next element.
1598
     *
1599
     * @return int Previous item (DB) ID
1600
     */
1601
    public function get_previous_item_id()
1602
    {
1603
        $index = $this->get_previous_index();
1604
1605
        return $this->ordered_items[$index];
1606
    }
1607
1608
    /**
1609
     * Returns the HTML necessary to print a mediaplayer block inside a page.
1610
     *
1611
     * @param int    $lpItemId
1612
     * @param string $autostart
1613
     *
1614
     * @return string The mediaplayer HTML
1615
     */
1616
    public function get_mediaplayer($lpItemId, $autostart = 'true')
1617
    {
1618
        $courseInfo = api_get_course_info();
1619
        $lpItemId = (int) $lpItemId;
1620
1621
        if (empty($courseInfo) || empty($lpItemId)) {
1622
            return '';
1623
        }
1624
        $item = $this->items[$lpItemId] ?? null;
1625
1626
        if (empty($item)) {
1627
            return '';
1628
        }
1629
1630
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
1631
        $tbl_lp_item_view = Database::get_course_table(TABLE_LP_ITEM_VIEW);
1632
        $itemViewId = (int) $item->db_item_view_id;
1633
1634
        // Getting all the information about the item.
1635
        $sql = "SELECT lp_view.status
1636
                FROM $tbl_lp_item as lpi
1637
                INNER JOIN $tbl_lp_item_view as lp_view
1638
                ON (lpi.iid = lp_view.lp_item_id)
1639
                WHERE
1640
                    lp_view.iid = $itemViewId AND
1641
                    lpi.iid = $lpItemId
1642
                ";
1643
        $result = Database::query($sql);
1644
        $row = Database::fetch_assoc($result);
1645
        $output = '';
1646
        $audio = $item->audio;
1647
1648
        if (!empty($audio)) {
1649
            $list = $_SESSION['oLP']->get_toc();
1650
1651
            switch ($item->get_type()) {
1652
                case 'quiz':
1653
                    $type_quiz = false;
1654
                    foreach ($list as $toc) {
1655
                        if ($toc['id'] == $_SESSION['oLP']->current) {
1656
                            $type_quiz = true;
1657
                        }
1658
                    }
1659
1660
                    if ($type_quiz) {
1661
                        if (1 == $_SESSION['oLP']->prevent_reinit) {
1662
                            $autostart_audio = 'completed' === $row['status'] ? 'false' : 'true';
1663
                        } else {
1664
                            $autostart_audio = $autostart;
1665
                        }
1666
                    }
1667
                    break;
1668
                case TOOL_READOUT_TEXT:
1669
                    $autostart_audio = 'false';
1670
                    break;
1671
                default:
1672
                    $autostart_audio = 'true';
1673
            }
1674
1675
            $file = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document'.$audio;
1676
            $url = api_get_path(WEB_COURSE_PATH).$courseInfo['path'].'/document'.$audio.'?'.api_get_cidreq();
1677
1678
            $player = Display::getMediaPlayer(
1679
                $file,
1680
                [
1681
                    'id' => 'lp_audio_media_player',
1682
                    'url' => $url,
1683
                    'autoplay' => $autostart_audio,
1684
                    'width' => '100%',
1685
                ]
1686
            );
1687
1688
            // The mp3 player.
1689
            $output = '<div id="container">';
1690
            $output .= $player;
1691
            $output .= '</div>';
1692
        }
1693
1694
        return $output;
1695
    }
1696
1697
    /**
1698
     * @param int    $studentId
1699
     * @param int    $prerequisite
1700
     * @param Course $course
1701
     * @param int    $sessionId
1702
     *
1703
     * @return bool
1704
     */
1705
    public static function isBlockedByPrerequisite(
1706
        $studentId,
1707
        $prerequisite,
1708
        Course $course,
1709
        $sessionId
1710
    ) {
1711
        $courseId = $course->getId();
1712
1713
        $allow = ('true' === api_get_setting('lp.allow_teachers_to_access_blocked_lp_by_prerequisite'));
1714
        if ($allow) {
1715
            if (api_is_allowed_to_edit() ||
1716
                api_is_platform_admin(true) ||
1717
                api_is_drh() ||
1718
                api_is_coach($sessionId, $courseId, false)
1719
            ) {
1720
                return false;
1721
            }
1722
        }
1723
1724
        $isBlocked = false;
1725
        if (!empty($prerequisite)) {
1726
            $progress = self::getProgress(
1727
                $prerequisite,
1728
                $studentId,
1729
                $courseId,
1730
                $sessionId
1731
            );
1732
            if ($progress < 100) {
1733
                $isBlocked = true;
1734
            }
1735
1736
            if (Tracking::minimumTimeAvailable($sessionId, $courseId)) {
1737
                // Block if it does not exceed minimum time
1738
                // Minimum time (in minutes) to pass the learning path
1739
                $accumulateWorkTime = self::getAccumulateWorkTimePrerequisite($prerequisite, $courseId);
1740
1741
                if ($accumulateWorkTime > 0) {
1742
                    // Total time in course (sum of times in learning paths from course)
1743
                    $accumulateWorkTimeTotal = self::getAccumulateWorkTimeTotal($courseId);
1744
1745
                    // Connect with the plugin_licences_course_session table
1746
                    // which indicates what percentage of the time applies
1747
                    // Minimum connection percentage
1748
                    $perc = 100;
1749
                    // Time from the course
1750
                    $tc = $accumulateWorkTimeTotal;
1751
1752
                    // Percentage of the learning paths
1753
                    $pl = $accumulateWorkTime / $accumulateWorkTimeTotal;
1754
                    // Minimum time for each learning path
1755
                    $accumulateWorkTime = ($pl * $tc * $perc / 100);
1756
1757
                    // Spent time (in seconds) so far in the learning path
1758
                    $lpTimeList = Tracking::getCalculateTime($studentId, $courseId, $sessionId);
1759
                    $lpTime = isset($lpTimeList[TOOL_LEARNPATH][$prerequisite]) ? $lpTimeList[TOOL_LEARNPATH][$prerequisite] : 0;
1760
1761
                    if ($lpTime < ($accumulateWorkTime * 60)) {
1762
                        $isBlocked = true;
1763
                    }
1764
                }
1765
            }
1766
        }
1767
1768
        return $isBlocked;
1769
    }
1770
1771
    /**
1772
     * Checks if the learning path is visible for student after the progress
1773
     * of its prerequisite is completed, considering the time availability and
1774
     * the LP visibility.
1775
     */
1776
    public static function is_lp_visible_for_student(CLp $lp, $student_id, Course $course, SessionEntity $session = null): bool
1777
    {
1778
        $sessionId = $session ? $session->getId() : 0;
1779
        $courseId = $course->getId();
1780
        $visibility = $lp->isVisible($course, $session);
1781
1782
        // If the item was deleted.
1783
        if (false === $visibility) {
1784
            return false;
1785
        }
1786
1787
        $now = time();
1788
        if ($lp->hasCategory()) {
1789
            $category = $lp->getCategory();
1790
1791
            if (false === self::categoryIsVisibleForStudent(
1792
                    $category,
1793
                    api_get_user_entity($student_id),
1794
                    $course,
1795
                    $session
1796
                )) {
1797
                return false;
1798
            }
1799
1800
            $prerequisite = $lp->getPrerequisite();
1801
            $is_visible = true;
1802
1803
            $isBlocked = self::isBlockedByPrerequisite(
1804
                $student_id,
1805
                $prerequisite,
1806
                $course,
1807
                $sessionId
1808
            );
1809
1810
            if ($isBlocked) {
1811
                $is_visible = false;
1812
            }
1813
1814
            // Also check the time availability of the LP
1815
            if ($is_visible) {
1816
                // Adding visibility restrictions
1817
                if (null !== $lp->getPublishedOn()) {
1818
                    if ($now < $lp->getPublishedOn()->getTimestamp()) {
1819
                        $is_visible = false;
1820
                    }
1821
                }
1822
                // Blocking empty start times see BT#2800
1823
                global $_custom;
1824
                if (isset($_custom['lps_hidden_when_no_start_date']) &&
1825
                    $_custom['lps_hidden_when_no_start_date']
1826
                ) {
1827
                    if (null !== $lp->getPublishedOn()) {
1828
                        $is_visible = false;
1829
                    }
1830
                }
1831
1832
                if (null !== $lp->getExpiredOn()) {
1833
                    if ($now > $lp->getExpiredOn()->getTimestamp()) {
1834
                        $is_visible = false;
1835
                    }
1836
                }
1837
            }
1838
1839
            if ($is_visible) {
1840
                $subscriptionSettings = self::getSubscriptionSettings();
1841
1842
                // Check if the subscription users/group to a LP is ON
1843
                if (1 == $lp->getSubscribeUsers() &&
1844
                    true === $subscriptionSettings['allow_add_users_to_lp']
1845
                ) {
1846
                    // Try group
1847
                    $is_visible = false;
1848
                    // Checking only the user visibility
1849
                    $userVisibility = self::isUserSubscribedToLp($lp, $student_id, $course, $session);
1850
1851
                    if (true === $userVisibility) {
1852
                        return true;
1853
                    }
1854
1855
                    // Try with groups
1856
                    $groupVisibility = self::isGroupSubscribedToLp($lp, $student_id, $course, $session);
1857
                    if (true === $groupVisibility) {
1858
                        return true;
1859
                    }
1860
                }
1861
            }
1862
1863
            return $is_visible;
1864
        } else {
1865
1866
            $is_visible = true;
1867
            $subscriptionSettings = self::getSubscriptionSettings();
1868
            // Check if the subscription users/group to a LP is ON
1869
            if (1 == $lp->getSubscribeUsers() &&
1870
                true === $subscriptionSettings['allow_add_users_to_lp']
1871
            ) {
1872
                $is_visible = false;
1873
                $userVisibility = self::isUserSubscribedToLp($lp, $student_id, $course, $session);
1874
1875
                if (true === $userVisibility) {
1876
                    return true;
1877
                }
1878
1879
                // Try with groups
1880
                $groupVisibility = self::isGroupSubscribedToLp($lp, $student_id, $course, $session);
1881
                if (true === $groupVisibility) {
1882
                    return true;
1883
                }
1884
            }
1885
1886
            return $is_visible;
1887
        }
1888
1889
        return true;
0 ignored issues
show
Unused Code introduced by
return true is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1890
    }
1891
1892
    public static function isGroupSubscribedToLp(
1893
        CLp $lp,
1894
        int $studentId,
1895
        Course $course,
1896
        SessionEntity $session = null
1897
    ): bool {
1898
1899
        // Subscribed groups to a LP
1900
        $links = $lp->getResourceNode()->getResourceLinks();
1901
        $selectedChoices = [];
1902
        foreach ($links as $link) {
1903
            if (null !== $link->getGroup()) {
1904
                $selectedChoices[] = $link->getGroup()->getIid();
1905
            }
1906
        }
1907
1908
        $isVisible = false;
1909
        $userGroups = GroupManager::getAllGroupPerUserSubscription($studentId, $course->getId());
1910
        if (!empty($userGroups)) {
1911
            foreach ($userGroups as $groupInfo) {
1912
                $groupId = $groupInfo['iid'];
1913
                if (in_array($groupId, $selectedChoices)) {
1914
                    $isVisible = true;
1915
                    break;
1916
                }
1917
            }
1918
        }
1919
1920
        return $isVisible;
1921
    }
1922
1923
    public static function isUserSubscribedToLp(
1924
        CLp $lp,
1925
        int $studentId,
1926
        Course $course,
1927
        SessionEntity $session = null
1928
    ): bool {
1929
1930
        $isVisible = true;
1931
        $em = Database::getManager();
1932
1933
        /** @var CLpRelUserRepository $cLpRelUserRepo */
1934
        $cLpRelUserRepo = $em->getRepository(CLpRelUser::class);
1935
1936
        // Getting subscribed users to a LP.
1937
        $subscribedUsersInLp = $cLpRelUserRepo->getUsersSubscribedToItem(
1938
            $lp,
1939
            $course,
1940
            $session
1941
        );
1942
1943
        $selectedChoices = [];
1944
        foreach ($subscribedUsersInLp as $users) {
1945
            /** @var \Chamilo\CourseBundle\Entity\CLpRelUser $users */
1946
            $selectedChoices[] = $users->getUser()->getId();
1947
        }
1948
1949
        if (!api_is_allowed_to_edit() && !in_array($studentId, $selectedChoices)) {
1950
            $isVisible = false;
1951
        }
1952
1953
        return $isVisible;
1954
    }
1955
1956
    /**
1957
     * @param int $lpId
1958
     * @param int $userId
1959
     * @param int $courseId
1960
     * @param int $sessionId
1961
     *
1962
     * @return int
1963
     */
1964
    public static function getProgress($lpId, $userId, $courseId, $sessionId = 0)
1965
    {
1966
        $lpId = (int) $lpId;
1967
        $userId = (int) $userId;
1968
        $courseId = (int) $courseId;
1969
        $sessionId = (int) $sessionId;
1970
1971
        $sessionCondition = api_get_session_condition($sessionId);
1972
        $table = Database::get_course_table(TABLE_LP_VIEW);
1973
        $sql = "SELECT progress FROM $table
1974
                WHERE
1975
                    c_id = $courseId AND
1976
                    lp_id = $lpId AND
1977
                    user_id = $userId $sessionCondition ";
1978
        $res = Database::query($sql);
1979
1980
        $progress = 0;
1981
        if (Database::num_rows($res) > 0) {
1982
            $row = Database::fetch_array($res);
1983
            $progress = (int) $row['progress'];
1984
        }
1985
1986
        return $progress;
1987
    }
1988
1989
    /**
1990
     * @param array $lpList
1991
     * @param int   $userId
1992
     * @param int   $courseId
1993
     * @param int   $sessionId
1994
     *
1995
     * @return array
1996
     */
1997
    public static function getProgressFromLpList($lpList, $userId, $courseId, $sessionId = 0)
1998
    {
1999
        $lpList = array_map('intval', $lpList);
2000
        if (empty($lpList)) {
2001
            return [];
2002
        }
2003
2004
        $lpList = implode("','", $lpList);
2005
2006
        $userId = (int) $userId;
2007
        $courseId = (int) $courseId;
2008
        $sessionId = (int) $sessionId;
2009
2010
        $sessionCondition = api_get_session_condition($sessionId);
2011
        $table = Database::get_course_table(TABLE_LP_VIEW);
2012
        $sql = "SELECT lp_id, progress FROM $table
2013
                WHERE
2014
                    c_id = $courseId AND
2015
                    lp_id IN ('".$lpList."') AND
2016
                    user_id = $userId $sessionCondition ";
2017
        $res = Database::query($sql);
2018
2019
        if (Database::num_rows($res) > 0) {
2020
            $list = [];
2021
            while ($row = Database::fetch_array($res)) {
2022
                $list[$row['lp_id']] = $row['progress'];
2023
            }
2024
2025
            return $list;
2026
        }
2027
2028
        return [];
2029
    }
2030
2031
    /**
2032
     * Displays a progress bar
2033
     * completed so far.
2034
     *
2035
     * @param int    $percentage Progress value to display
2036
     * @param string $text_add   Text to display near the progress value
2037
     *
2038
     * @return string HTML string containing the progress bar
2039
     */
2040
    public static function get_progress_bar($percentage = -1, $text_add = '')
2041
    {
2042
        $text = $percentage.$text_add;
2043
2044
        return '<div class="p-progressbar p-progressbar-determinate"
2045
            role="progressbar" aria-valuenow="'.$percentage.'" aria-valuemin="0" aria-valuemax="100">
2046
            <div id="progress_bar_value" class="p-progressbar-value" style="width: '.$text.';">
2047
                 <div class="p-progressbar-label">'.$text.'</div>
2048
            </div>
2049
        </div>';
2050
    }
2051
2052
    /**
2053
     * @param string $mode can be '%' or 'abs'
2054
     *                     otherwise this value will be used $this->progress_bar_mode
2055
     *
2056
     * @return string
2057
     */
2058
    public function getProgressBar($mode = null)
2059
    {
2060
        [$percentage, $text_add] = $this->get_progress_bar_text($mode);
2061
2062
        return self::get_progress_bar($percentage, $text_add);
2063
    }
2064
2065
    /**
2066
     * Gets the progress bar info to display inside the progress bar.
2067
     * Also used by scorm_api.php.
2068
     *
2069
     * @param string $mode Mode of display (can be '%' or 'abs').abs means
2070
     *                     we display a number of completed elements per total elements
2071
     * @param int    $add  Additional steps to fake as completed
2072
     *
2073
     * @return array Percentage or number and symbol (% or /xx)
2074
     */
2075
    public function get_progress_bar_text($mode = '', $add = 0)
2076
    {
2077
        if (empty($mode)) {
2078
            $mode = $this->progress_bar_mode;
2079
        }
2080
        $text = '';
2081
        $percentage = 0;
2082
        // If the option to use the score as progress is set for this learning
2083
        // path, then the rules are completely different: we assume only one
2084
        // item exists and the progress of the LP depends on the score
2085
        $scoreAsProgressSetting = ('true' === api_get_setting('lp.lp_score_as_progress_enable'));
2086
        if (true === $scoreAsProgressSetting) {
2087
            $scoreAsProgress = $this->getUseScoreAsProgress();
2088
            if ($scoreAsProgress) {
2089
                // Get single item's score
2090
                $itemId = $this->get_current_item_id();
2091
                $item = $this->getItem($itemId);
2092
                $score = $item->get_score();
2093
                $maxScore = $item->get_max();
2094
                if ($mode = '%') {
2095
                    if (!empty($maxScore)) {
2096
                        $percentage = ((float) $score / (float) $maxScore) * 100;
2097
                    }
2098
                    $percentage = number_format($percentage, 0);
2099
                    $text = '%';
2100
                } else {
2101
                    $percentage = $score;
2102
                    $text = '/'.$maxScore;
2103
                }
2104
2105
                return [$percentage, $text];
2106
            }
2107
        }
2108
        // otherwise just continue the normal processing of progress
2109
        $total_items = $this->getTotalItemsCountWithoutDirs();
2110
        $completeItems = $this->get_complete_items_count();
2111
        if (0 != $add) {
2112
            $completeItems += $add;
2113
        }
2114
        if ($completeItems > $total_items) {
2115
            $completeItems = $total_items;
2116
        }
2117
        if ('%' === $mode) {
2118
            if ($total_items > 0) {
2119
                $percentage = ((float) $completeItems / (float) $total_items) * 100;
2120
            }
2121
            $percentage = number_format($percentage, 0);
2122
            $text = '%';
2123
        } elseif ('abs' === $mode) {
2124
            $percentage = $completeItems;
2125
            $text = '/'.$total_items;
2126
        }
2127
2128
        return [
2129
            $percentage,
2130
            $text,
2131
        ];
2132
    }
2133
2134
    /**
2135
     * Gets the progress bar mode.
2136
     *
2137
     * @return string The progress bar mode attribute
2138
     */
2139
    public function get_progress_bar_mode()
2140
    {
2141
        if (!empty($this->progress_bar_mode)) {
2142
            return $this->progress_bar_mode;
2143
        }
2144
2145
        return '%';
2146
    }
2147
2148
    /**
2149
     * Gets the learnpath theme (remote or local).
2150
     *
2151
     * @return string Learnpath theme
2152
     */
2153
    public function get_theme()
2154
    {
2155
        if (!empty($this->theme)) {
2156
            return $this->theme;
2157
        }
2158
2159
        return '';
2160
    }
2161
2162
    /**
2163
     * Gets the learnpath session id.
2164
     *
2165
     * @return int
2166
     */
2167
    public function get_lp_session_id()
2168
    {
2169
        $lp = Container::getLpRepository()->find($this->lp_id);
2170
        if ($lp) {
2171
            /* @var ResourceNode $resourceNode */
2172
            $resourceNode = $lp->getResourceNode();
2173
            if ($resourceNode) {
0 ignored issues
show
introduced by
$resourceNode is of type Chamilo\CoreBundle\Entity\ResourceNode, thus it always evaluated to true.
Loading history...
2174
                $link = $resourceNode->getResourceLinks()->first();
2175
                if ($link && $link->getSession()) {
2176
2177
                    return (int) $link->getSession()->getId();
2178
                }
2179
            }
2180
        }
2181
2182
        return 0;
2183
    }
2184
2185
    /**
2186
     * Generate a new prerequisites string for a given item. If this item was a sco and
2187
     * its prerequisites were strings (instead of IDs), then transform those strings into
2188
     * IDs, knowing that SCORM IDs are kept in the "ref" field of the lp_item table.
2189
     * Prefix all item IDs that end-up in the prerequisites string by "ITEM_" to use the
2190
     * same rule as the scormExport() method.
2191
     *
2192
     * @param int $item_id Item ID
2193
     *
2194
     * @return string Prerequisites string ready for the export as SCORM
2195
     */
2196
    public function get_scorm_prereq_string($item_id)
2197
    {
2198
        if ($this->debug > 0) {
2199
            error_log('In learnpath::get_scorm_prereq_string()');
2200
        }
2201
        if (!is_object($this->items[$item_id])) {
2202
            return false;
2203
        }
2204
        /** @var learnpathItem $oItem */
2205
        $oItem = $this->items[$item_id];
2206
        $prereq = $oItem->get_prereq_string();
2207
2208
        if (empty($prereq)) {
2209
            return '';
2210
        }
2211
        if (preg_match('/^\d+$/', $prereq) &&
2212
            isset($this->items[$prereq]) &&
2213
            is_object($this->items[$prereq])
2214
        ) {
2215
            // If the prerequisite is a simple integer ID and this ID exists as an item ID,
2216
            // then simply return it (with the ITEM_ prefix).
2217
            //return 'ITEM_' . $prereq;
2218
            return $this->items[$prereq]->ref;
2219
        } else {
2220
            if (isset($this->refs_list[$prereq])) {
2221
                // It's a simple string item from which the ID can be found in the refs list,
2222
                // so we can transform it directly to an ID for export.
2223
                return $this->items[$this->refs_list[$prereq]]->ref;
2224
            } elseif (isset($this->refs_list['ITEM_'.$prereq])) {
2225
                return $this->items[$this->refs_list['ITEM_'.$prereq]]->ref;
2226
            } else {
2227
                // The last case, if it's a complex form, then find all the IDs (SCORM strings)
2228
                // and replace them, one by one, by the internal IDs (chamilo db)
2229
                // TODO: Modify the '*' replacement to replace the multiplier in front of it
2230
                // by a space as well.
2231
                $find = [
2232
                    '&',
2233
                    '|',
2234
                    '~',
2235
                    '=',
2236
                    '<>',
2237
                    '{',
2238
                    '}',
2239
                    '*',
2240
                    '(',
2241
                    ')',
2242
                ];
2243
                $replace = [
2244
                    ' ',
2245
                    ' ',
2246
                    ' ',
2247
                    ' ',
2248
                    ' ',
2249
                    ' ',
2250
                    ' ',
2251
                    ' ',
2252
                    ' ',
2253
                    ' ',
2254
                ];
2255
                $prereq_mod = str_replace($find, $replace, $prereq);
2256
                $ids = explode(' ', $prereq_mod);
2257
                foreach ($ids as $id) {
2258
                    $id = trim($id);
2259
                    if (isset($this->refs_list[$id])) {
2260
                        $prereq = preg_replace(
2261
                            '/[^a-zA-Z_0-9]('.$id.')[^a-zA-Z_0-9]/',
2262
                            'ITEM_'.$this->refs_list[$id],
2263
                            $prereq
2264
                        );
2265
                    }
2266
                }
2267
2268
                return $prereq;
2269
            }
2270
        }
2271
    }
2272
2273
    /**
2274
     * Returns the XML DOM document's node.
2275
     *
2276
     * @param resource $children Reference to a list of objects to search for the given ITEM_*
2277
     * @param string   $id       The identifier to look for
2278
     *
2279
     * @return mixed The reference to the element found with that identifier. False if not found
2280
     */
2281
    public function get_scorm_xml_node(&$children, $id)
2282
    {
2283
        for ($i = 0; $i < $children->length; $i++) {
2284
            $item_temp = $children->item($i);
2285
            if ('item' === $item_temp->nodeName) {
2286
                if ($item_temp->getAttribute('identifier') == $id) {
2287
                    return $item_temp;
2288
                }
2289
            }
2290
            $subchildren = $item_temp->childNodes;
2291
            if ($subchildren && $subchildren->length > 0) {
2292
                $val = $this->get_scorm_xml_node($subchildren, $id);
2293
                if (is_object($val)) {
2294
                    return $val;
2295
                }
2296
            }
2297
        }
2298
2299
        return false;
2300
    }
2301
2302
    /**
2303
     * Gets the status list for all LP's items.
2304
     *
2305
     * @return array Array of [index] => [item ID => current status]
2306
     */
2307
    public function get_items_status_list()
2308
    {
2309
        $list = [];
2310
        foreach ($this->ordered_items as $item_id) {
2311
            $list[] = [
2312
                $item_id => $this->items[$item_id]->get_status(),
2313
            ];
2314
        }
2315
2316
        return $list;
2317
    }
2318
2319
    /**
2320
     * Return the number of interactions for the given learnpath Item View ID.
2321
     * This method can be used as static.
2322
     *
2323
     * @param int $lp_iv_id  Item View ID
2324
     * @param int $course_id course id
2325
     *
2326
     * @return int
2327
     */
2328
    public static function get_interactions_count_from_db($lp_iv_id, $course_id)
2329
    {
2330
        $table = Database::get_course_table(TABLE_LP_IV_INTERACTION);
2331
        $lp_iv_id = (int) $lp_iv_id;
2332
        $course_id = (int) $course_id;
2333
2334
        $sql = "SELECT count(*) FROM $table
2335
                WHERE c_id = $course_id AND lp_iv_id = $lp_iv_id";
2336
        $res = Database::query($sql);
2337
        $num = 0;
2338
        if (Database::num_rows($res)) {
2339
            $row = Database::fetch_array($res);
2340
            $num = $row[0];
2341
        }
2342
2343
        return $num;
2344
    }
2345
2346
    /**
2347
     * Return the interactions as an array for the given lp_iv_id.
2348
     * This method can be used as static.
2349
     *
2350
     * @param int $lp_iv_id Learnpath Item View ID
2351
     *
2352
     * @return array
2353
     *
2354
     * @todo    Transcode labels instead of switching to HTML (which requires to know the encoding of the LP)
2355
     */
2356
    public static function get_iv_interactions_array($lp_iv_id, $course_id = 0)
2357
    {
2358
        $course_id = empty($course_id) ? api_get_course_int_id() : (int) $course_id;
2359
        $list = [];
2360
        $table = Database::get_course_table(TABLE_LP_IV_INTERACTION);
2361
        $lp_iv_id = (int) $lp_iv_id;
2362
2363
        if (empty($lp_iv_id) || empty($course_id)) {
2364
            return [];
2365
        }
2366
2367
        $sql = "SELECT * FROM $table
2368
                WHERE c_id = ".$course_id." AND lp_iv_id = $lp_iv_id
2369
                ORDER BY order_id ASC";
2370
        $res = Database::query($sql);
2371
        $num = Database::num_rows($res);
2372
        if ($num > 0) {
2373
            $list[] = [
2374
                'order_id' => api_htmlentities(get_lang('Order'), ENT_QUOTES),
2375
                'id' => api_htmlentities(get_lang('Interaction ID'), ENT_QUOTES),
2376
                'type' => api_htmlentities(get_lang('Type'), ENT_QUOTES),
2377
                'time' => api_htmlentities(get_lang('Time (finished at...)'), ENT_QUOTES),
2378
                'correct_responses' => api_htmlentities(get_lang('Correct answers'), ENT_QUOTES),
2379
                'student_response' => api_htmlentities(get_lang('Learner answers'), ENT_QUOTES),
2380
                'result' => api_htmlentities(get_lang('Result'), ENT_QUOTES),
2381
                'latency' => api_htmlentities(get_lang('Time spent'), ENT_QUOTES),
2382
                'student_response_formatted' => '',
2383
            ];
2384
            while ($row = Database::fetch_array($res)) {
2385
                $studentResponseFormatted = urldecode($row['student_response']);
2386
                $content_student_response = explode('__|', $studentResponseFormatted);
2387
                if (count($content_student_response) > 0) {
2388
                    if (count($content_student_response) >= 3) {
2389
                        // Pop the element off the end of array.
2390
                        array_pop($content_student_response);
2391
                    }
2392
                    $studentResponseFormatted = implode(',', $content_student_response);
2393
                }
2394
2395
                $list[] = [
2396
                    'order_id' => $row['order_id'] + 1,
2397
                    'id' => urldecode($row['interaction_id']), //urldecode because they often have %2F or stuff like that
2398
                    'type' => $row['interaction_type'],
2399
                    'time' => $row['completion_time'],
2400
                    'correct_responses' => '', // Hide correct responses from students.
2401
                    'student_response' => $row['student_response'],
2402
                    'result' => $row['result'],
2403
                    'latency' => $row['latency'],
2404
                    'student_response_formatted' => $studentResponseFormatted,
2405
                ];
2406
            }
2407
        }
2408
2409
        return $list;
2410
    }
2411
2412
    /**
2413
     * Return the number of objectives for the given learnpath Item View ID.
2414
     * This method can be used as static.
2415
     *
2416
     * @param int $lp_iv_id  Item View ID
2417
     * @param int $course_id Course ID
2418
     *
2419
     * @return int Number of objectives
2420
     */
2421
    public static function get_objectives_count_from_db($lp_iv_id, $course_id)
2422
    {
2423
        $table = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
2424
        $course_id = (int) $course_id;
2425
        $lp_iv_id = (int) $lp_iv_id;
2426
        $sql = "SELECT count(*) FROM $table
2427
                WHERE c_id = $course_id AND lp_iv_id = $lp_iv_id";
2428
        //@todo seems that this always returns 0
2429
        $res = Database::query($sql);
2430
        $num = 0;
2431
        if (Database::num_rows($res)) {
2432
            $row = Database::fetch_array($res);
2433
            $num = $row[0];
2434
        }
2435
2436
        return $num;
2437
    }
2438
2439
    /**
2440
     * Return the objectives as an array for the given lp_iv_id.
2441
     * This method can be used as static.
2442
     *
2443
     * @param int $lpItemViewId Learnpath Item View ID
2444
     * @param int $course_id
2445
     *
2446
     * @return array
2447
     *
2448
     * @todo    Translate labels
2449
     */
2450
    public static function get_iv_objectives_array($lpItemViewId = 0, $course_id = 0)
2451
    {
2452
        $course_id = empty($course_id) ? api_get_course_int_id() : (int) $course_id;
2453
        $lpItemViewId = (int) $lpItemViewId;
2454
2455
        if (empty($course_id) || empty($lpItemViewId)) {
2456
            return [];
2457
        }
2458
2459
        $table = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
2460
        $sql = "SELECT * FROM $table
2461
                WHERE c_id = $course_id AND lp_iv_id = $lpItemViewId
2462
                ORDER BY order_id ASC";
2463
        $res = Database::query($sql);
2464
        $num = Database::num_rows($res);
2465
        $list = [];
2466
        if ($num > 0) {
2467
            $list[] = [
2468
                'order_id' => api_htmlentities(get_lang('Order'), ENT_QUOTES),
2469
                'objective_id' => api_htmlentities(get_lang('Objective ID'), ENT_QUOTES),
2470
                'score_raw' => api_htmlentities(get_lang('Objective raw score'), ENT_QUOTES),
2471
                'score_max' => api_htmlentities(get_lang('Objective max score'), ENT_QUOTES),
2472
                'score_min' => api_htmlentities(get_lang('Objective min score'), ENT_QUOTES),
2473
                'status' => api_htmlentities(get_lang('Objective status'), ENT_QUOTES),
2474
            ];
2475
            while ($row = Database::fetch_array($res)) {
2476
                $list[] = [
2477
                    'order_id' => $row['order_id'] + 1,
2478
                    'objective_id' => urldecode($row['objective_id']), // urldecode() because they often have %2F
2479
                    'score_raw' => $row['score_raw'],
2480
                    'score_max' => $row['score_max'],
2481
                    'score_min' => $row['score_min'],
2482
                    'status' => $row['status'],
2483
                ];
2484
            }
2485
        }
2486
2487
        return $list;
2488
    }
2489
2490
    /**
2491
     * Generate and return the table of contents for this learnpath. The (flat) table returned can be
2492
     * used by get_html_toc() to be ready to display.
2493
     */
2494
    public function get_toc(): array
2495
    {
2496
        $toc = [];
2497
        foreach ($this->ordered_items as $item_id) {
2498
            // TODO: Change this link generation and use new function instead.
2499
            $toc[] = [
2500
                'id' => $item_id,
2501
                'title' => $this->items[$item_id]->get_title(),
2502
                'status' => $this->items[$item_id]->get_status(false),
2503
                'status_class' => self::getStatusCSSClassName($this->items[$item_id]->get_status(false)),
2504
                'level' => $this->items[$item_id]->get_level(),
2505
                'type' => $this->items[$item_id]->get_type(),
2506
                'description' => $this->items[$item_id]->get_description(),
2507
                'path' => $this->items[$item_id]->get_path(),
2508
                'parent' => $this->items[$item_id]->get_parent(),
2509
            ];
2510
        }
2511
2512
        return $toc;
2513
    }
2514
2515
    /**
2516
     * Returns the CSS class name associated with a given item status.
2517
     *
2518
     * @param $status string an item status
2519
     *
2520
     * @return string CSS class name
2521
     */
2522
    public static function getStatusCSSClassName($status)
2523
    {
2524
        if (array_key_exists($status, self::STATUS_CSS_CLASS_NAME)) {
2525
            return self::STATUS_CSS_CLASS_NAME[$status];
2526
        }
2527
2528
        return '';
2529
    }
2530
2531
    /**
2532
     * Generate and return the table of contents for this learnpath. The JS
2533
     * table returned is used inside of scorm_api.php.
2534
     *
2535
     * @param string $varname
2536
     *
2537
     * @return string A JS array variable construction
2538
     */
2539
    public function get_items_details_as_js($varname = 'olms.lms_item_types')
2540
    {
2541
        $toc = $varname.' = new Array();';
2542
        foreach ($this->ordered_items as $item_id) {
2543
            $toc .= $varname."['i$item_id'] = '".$this->items[$item_id]->get_type()."';";
2544
        }
2545
2546
        return $toc;
2547
    }
2548
2549
    /**
2550
     * Gets the learning path type.
2551
     *
2552
     * @param bool $get_name Return the name? If false, return the ID. Default is false.
2553
     *
2554
     * @return mixed Type ID or name, depending on the parameter
2555
     */
2556
    public function get_type($get_name = false)
2557
    {
2558
        $res = false;
2559
        if (!empty($this->type) && (!$get_name)) {
2560
            $res = $this->type;
2561
        }
2562
2563
        return $res;
2564
    }
2565
2566
    /**
2567
     * Gets the learning path type as static method.
2568
     *
2569
     * @param int $lp_id
2570
     *
2571
     * @return mixed Type ID or name, depending on the parameter
2572
     */
2573
    public static function get_type_static($lp_id = 0)
2574
    {
2575
        $tbl_lp = Database::get_course_table(TABLE_LP_MAIN);
2576
        $lp_id = (int) $lp_id;
2577
        $sql = "SELECT lp_type FROM $tbl_lp
2578
                WHERE iid = $lp_id";
2579
        $res = Database::query($sql);
2580
        if (false === $res) {
2581
            return null;
2582
        }
2583
        if (Database::num_rows($res) <= 0) {
2584
            return null;
2585
        }
2586
        $row = Database::fetch_array($res);
2587
2588
        return $row['lp_type'];
2589
    }
2590
2591
    /**
2592
     * Gets a flat list of item IDs ordered for display (level by level ordered by order_display)
2593
     * This method can be used as abstract and is recursive.
2594
     *
2595
     * @param CLp $lp
2596
     * @param int $parent    Parent ID of the items to look for
2597
     *
2598
     * @return array Ordered list of item IDs (empty array on error)
2599
     */
2600
    public static function get_flat_ordered_items_list(CLp $lp, $parent = 0, $withExportFlag = false)
2601
    {
2602
        $parent = (int) $parent;
2603
        $lpItemRepo = Container::getLpItemRepository();
2604
        if (empty($parent)) {
2605
            $rootItem = $lpItemRepo->getRootItem($lp->getIid());
2606
            if (null !== $rootItem) {
2607
                $parent = $rootItem->getIid();
2608
            }
2609
        }
2610
2611
        if (empty($parent)) {
2612
            return [];
2613
        }
2614
2615
        $criteria = new Criteria();
2616
        $criteria
2617
            ->where($criteria->expr()->neq('path', 'root'))
2618
            ->orderBy(['displayOrder' => Criteria::ASC]);
2619
        $items = $lp->getItems()->matching($criteria);
2620
        $items = $items->filter(function (CLpItem $element) use ($parent) {
2621
            if ('root' === $element->getPath()) {
2622
                return false;
2623
            }
2624
            if (null !== $element->getParent()) {
2625
                return $element->getParent()->getIid() === $parent;
2626
            }
2627
            return false;
2628
        });
2629
2630
        if (!$withExportFlag) {
2631
            $ids = [];
2632
            foreach ($items as $item) {
2633
                $itemId = $item->getIid();
2634
                $ids[] = $itemId;
2635
                $subIds = self::get_flat_ordered_items_list($lp, $itemId, false);
2636
                foreach ($subIds as $subId) {
2637
                    $ids[] = $subId;
2638
                }
2639
            }
2640
            return $ids;
2641
        }
2642
2643
        $list = [];
2644
        foreach ($items as $item) {
2645
            $itemId = $item->getIid();
2646
            $list[] = [
2647
                'iid'            => $itemId,
2648
                'export_allowed' => $item->isExportAllowed() ? 1 : 0,
2649
            ];
2650
            $subList = self::get_flat_ordered_items_list($lp, $itemId, true);
2651
            foreach ($subList as $subEntry) {
2652
                $list[] = $subEntry;
2653
            }
2654
        }
2655
2656
        return $list;
2657
    }
2658
2659
    public static function getChapterTypes(): array
2660
    {
2661
        return [
2662
            'dir',
2663
        ];
2664
    }
2665
2666
    /**
2667
     * Uses the table generated by get_toc() and returns an HTML-formatted string ready to display.
2668
     *
2669
     * @return array HTML TOC ready to display
2670
     */
2671
    public function getListArrayToc()
2672
    {
2673
        $lpItemRepo = Container::getLpItemRepository();
2674
        $itemRoot = $lpItemRepo->getRootItem($this->get_id());
2675
        $options = [
2676
            'decorate' => false,
2677
        ];
2678
2679
        return $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
2680
    }
2681
2682
    /**
2683
     * Returns an HTML-formatted string ready to display with teacher buttons
2684
     * in LP view menu.
2685
     *
2686
     * @return string HTML TOC ready to display
2687
     */
2688
    public function get_teacher_toc_buttons()
2689
    {
2690
        $isAllow = api_is_allowed_to_edit(null, true, false, false);
2691
        $hideIcons = api_get_configuration_value('hide_teacher_icons_lp');
2692
        $html = '';
2693
        if ($isAllow && false == $hideIcons) {
2694
            if ($this->get_lp_session_id() == api_get_session_id()) {
2695
                $html .= '<div id="actions_lp" class="actions_lp"><hr>';
2696
                $html .= '<div class="flex flex-wrap gap-1 justify-center">';
2697
                $html .= "<a
2698
                    class='btn btn-sm btn--plain'
2699
                    href='lp_controller.php?".api_get_cidreq()."&action=add_item&type=step&lp_id=".$this->lp_id."&isStudentView=false'
2700
                    target='_parent'>".
2701
                    Display::getMdiIcon('pencil').get_lang('Edit')."</a>";
2702
                $html .= '<a
2703
                    class="btn btn-sm btn--plain"
2704
                    href="lp_controller.php?'.api_get_cidreq()."&action=edit&lp_id=".$this->lp_id.'&isStudentView=false">'.
2705
                    Display::getMdiIcon('hammer-wrench').get_lang('Settings').'</a>';
2706
                $html .= '</div>';
2707
                $html .= '</div>';
2708
            }
2709
        }
2710
2711
        return $html;
2712
    }
2713
2714
    /**
2715
     * Gets the learnpath name/title.
2716
     *
2717
     * @return string Learnpath name/title
2718
     */
2719
    public function get_name()
2720
    {
2721
        if (!empty($this->name)) {
2722
            return $this->name;
2723
        }
2724
2725
        return 'N/A';
2726
    }
2727
2728
    /**
2729
     * @return string
2730
     */
2731
    public function getNameNoTags()
2732
    {
2733
        return strip_tags($this->get_name());
2734
    }
2735
2736
    /**
2737
     * Gets a link to the resource from the present location, depending on item ID.
2738
     *
2739
     * @param string $type         Type of link expected
2740
     * @param int    $item_id      Learnpath item ID
2741
     * @param bool   $provided_toc
2742
     *
2743
     * @return string $provided_toc Link to the lp_item resource
2744
     */
2745
    public function get_link($type = 'http', $item_id = 0, $provided_toc = false)
2746
    {
2747
        $course_id = $this->get_course_int_id();
2748
        $item_id = (int) $item_id;
2749
2750
        if (empty($item_id)) {
2751
            $item_id = $this->get_current_item_id();
2752
2753
            if (empty($item_id)) {
2754
                //still empty, this means there was no item_id given and we are not in an object context or
2755
                //the object property is empty, return empty link
2756
                $this->first();
2757
2758
                return '';
2759
            }
2760
        }
2761
2762
        $file = '';
2763
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
2764
        $lp_item_table = Database::get_course_table(TABLE_LP_ITEM);
2765
        $lp_item_view_table = Database::get_course_table(TABLE_LP_ITEM_VIEW);
2766
2767
        $sql = "SELECT
2768
                    l.lp_type as ltype,
2769
                    l.path as lpath,
2770
                    li.item_type as litype,
2771
                    li.path as lipath,
2772
                    li.parameters as liparams
2773
        		FROM $lp_table l
2774
                INNER JOIN $lp_item_table li
2775
                ON (li.lp_id = l.iid)
2776
        		WHERE
2777
        		    li.iid = $item_id
2778
        		";
2779
        $res = Database::query($sql);
2780
        if (Database::num_rows($res) > 0) {
2781
            $row = Database::fetch_array($res);
2782
            $lp_type = $row['ltype'];
2783
            $lp_path = $row['lpath'];
2784
            $lp_item_type = $row['litype'];
2785
            $lp_item_path = $row['lipath'];
2786
            $lp_item_params = $row['liparams'];
2787
            if (empty($lp_item_params) && false !== strpos($lp_item_path, '?')) {
2788
                [$lp_item_path, $lp_item_params] = explode('?', $lp_item_path);
2789
            }
2790
            //$sys_course_path = api_get_path(SYS_COURSE_PATH).api_get_course_path();
2791
            if ('http' === $type) {
2792
                //web path
2793
                //$course_path = api_get_path(WEB_COURSE_PATH).api_get_course_path();
2794
            } else {
2795
                //$course_path = $sys_course_path; //system path
2796
            }
2797
2798
            // Fixed issue BT#1272 - If the item type is a Chamilo Item (quiz, link, etc),
2799
            // then change the lp type to thread it as a normal Chamilo LP not a SCO.
2800
            if (in_array(
2801
                $lp_item_type,
2802
                ['quiz', 'document', 'final_item', 'link', 'forum', 'thread', 'student_publication', 'survey']
2803
            )
2804
            ) {
2805
                $lp_type = CLp::LP_TYPE;
2806
            }
2807
2808
            // Now go through the specific cases to get the end of the path
2809
            // @todo Use constants instead of int values.
2810
            switch ($lp_type) {
2811
                case CLp::LP_TYPE:
2812
                    $file = self::rl_get_resource_link_for_learnpath(
2813
                        $course_id,
2814
                        $this->get_id(),
2815
                        $item_id,
2816
                        $this->get_view_id()
2817
                    );
2818
                    switch ($lp_item_type) {
2819
                        case 'document':
2820
                            // Shows a button to download the file instead of just downloading the file directly.
2821
                            $documentPathInfo = pathinfo($file);
2822
                            if (isset($documentPathInfo['extension'])) {
2823
                                $parsed = parse_url($documentPathInfo['extension']);
2824
                                if (isset($parsed['path'])) {
2825
                                    $extension = $parsed['path'];
2826
                                    $extensionsToDownload = [
2827
                                        'zip',
2828
                                        'ppt',
2829
                                        'pptx',
2830
                                        'ods',
2831
                                        'xlsx',
2832
                                        'xls',
2833
                                        'csv',
2834
                                        'doc',
2835
                                        'docx',
2836
                                        'dot',
2837
                                    ];
2838
2839
                                    if (in_array($extension, $extensionsToDownload)) {
2840
                                        $file = api_get_path(WEB_CODE_PATH).
2841
                                            'lp/embed.php?type=download&source=file&lp_item_id='.$item_id.'&'.api_get_cidreq();
2842
                                    }
2843
                                }
2844
                            }
2845
                            break;
2846
                        case 'dir':
2847
                            $file = 'lp_content.php?type=dir';
2848
                            break;
2849
                        case 'link':
2850
                            if (Link::is_youtube_link($file)) {
2851
                                $src = Link::get_youtube_video_id($file);
2852
                                $file = api_get_path(WEB_CODE_PATH).'lp/embed.php?type=youtube&source='.$src;
2853
                            } elseif (Link::isVimeoLink($file)) {
2854
                                $src = Link::getVimeoLinkId($file);
2855
                                $file = api_get_path(WEB_CODE_PATH).'lp/embed.php?type=vimeo&source='.$src;
2856
                            } else {
2857
                                // If the current site is HTTPS and the link is
2858
                                // HTTP, browsers will refuse opening the link
2859
                                $urlId = api_get_current_access_url_id();
2860
                                $url = api_get_access_url($urlId, false);
2861
                                $protocol = substr($url['url'], 0, 5);
2862
                                if ('https' === $protocol) {
2863
                                    $linkProtocol = substr($file, 0, 5);
2864
                                    if ('http:' === $linkProtocol) {
2865
                                        //this is the special intervention case
2866
                                        $file = api_get_path(WEB_CODE_PATH).
2867
                                            'lp/embed.php?type=nonhttps&source='.urlencode($file);
2868
                                    }
2869
                                }
2870
                            }
2871
                            break;
2872
                        case 'quiz':
2873
                            // Check how much attempts of a exercise exits in lp
2874
                            $lp_item_id = $this->get_current_item_id();
2875
                            $lp_view_id = $this->get_view_id();
2876
2877
                            $prevent_reinit = null;
2878
                            if (isset($this->items[$this->current])) {
2879
                                $prevent_reinit = $this->items[$this->current]->get_prevent_reinit();
2880
                            }
2881
2882
                            if (empty($provided_toc)) {
2883
                                $list = $this->get_toc();
2884
                            } else {
2885
                                $list = $provided_toc;
2886
                            }
2887
2888
                            $type_quiz = false;
2889
                            foreach ($list as $toc) {
2890
                                if ($toc['id'] == $lp_item_id && 'quiz' === $toc['type']) {
2891
                                    $type_quiz = true;
2892
                                }
2893
                            }
2894
2895
                            if ($type_quiz) {
2896
                                $lp_item_id = (int) $lp_item_id;
2897
                                $lp_view_id = (int) $lp_view_id;
2898
                                $sql = "SELECT count(*) FROM $lp_item_view_table
2899
                                        WHERE
2900
                                            lp_item_id='".$lp_item_id."' AND
2901
                                            lp_view_id ='".$lp_view_id."' AND
2902
                                            status='completed'";
2903
                                $result = Database::query($sql);
2904
                                $row_count = Database:: fetch_row($result);
2905
                                $count_item_view = (int) $row_count[0];
2906
                                $not_multiple_attempt = 0;
2907
                                if (1 === $prevent_reinit && $count_item_view > 0) {
2908
                                    $not_multiple_attempt = 1;
2909
                                }
2910
                                $file .= '&not_multiple_attempt='.$not_multiple_attempt;
2911
                            }
2912
                            break;
2913
                    }
2914
2915
                    $tmp_array = explode('/', $file);
2916
                    $document_name = $tmp_array[count($tmp_array) - 1];
2917
                    if (strpos($document_name, '_DELETED_')) {
2918
                        $file = 'blank.php?error=document_deleted';
2919
                    }
2920
                    break;
2921
                case CLp::SCORM_TYPE:
2922
                    if ('dir' !== $lp_item_type) {
2923
                        // Quite complex here:
2924
                        // We want to make sure 'http://' (and similar) links can
2925
                        // be loaded as is (withouth the Chamilo path in front) but
2926
                        // some contents use this form: resource.htm?resource=http://blablabla
2927
                        // which means we have to find a protocol at the path's start, otherwise
2928
                        // it should not be considered as an external URL.
2929
                        // if ($this->prerequisites_match($item_id)) {
2930
                        if (0 != preg_match('#^[a-zA-Z]{2,5}://#', $lp_item_path)) {
2931
                            if ($this->debug > 2) {
2932
                                error_log('In learnpath::get_link() '.__LINE__.' - Found match for protocol in '.$lp_item_path, 0);
2933
                            }
2934
                            // Distant url, return as is.
2935
                            $file = $lp_item_path;
2936
                        } else {
2937
                            if ($this->debug > 2) {
2938
                                error_log('In learnpath::get_link() '.__LINE__.' - No starting protocol in '.$lp_item_path);
2939
                            }
2940
                            // Prevent getting untranslatable urls.
2941
                            $lp_item_path = preg_replace('/%2F/', '/', $lp_item_path);
2942
                            $lp_item_path = preg_replace('/%3A/', ':', $lp_item_path);
2943
2944
                            /*$asset = $this->getEntity()->getAsset();
2945
                            $folder = Container::getAssetRepository()->getFolder($asset);
2946
                            $hasFile = Container::getAssetRepository()->getFileSystem()->has($folder.$lp_item_path);
2947
                            $file = null;
2948
                            if ($hasFile) {
2949
                                $file = Container::getAssetRepository()->getAssetUrl($asset).'/'.$lp_item_path;
2950
                            }*/
2951
                            $file = $this->scormUrl.$lp_item_path;
2952
2953
                            // Prepare the path.
2954
                            /*$file = $course_path.'/scorm/'.$lp_path.'/'.$lp_item_path;
2955
                            // TODO: Fix this for urls with protocol header.
2956
                            $file = str_replace('//', '/', $file);
2957
                            $file = str_replace(':/', '://', $file);
2958
                            if ('/' === substr($lp_path, -1)) {
2959
                                $lp_path = substr($lp_path, 0, -1);
2960
                            }*/
2961
                            /*if (!$hasFile) {
2962
                                // if file not found.
2963
                                $decoded = html_entity_decode($lp_item_path);
2964
                                [$decoded] = explode('?', $decoded);
2965
                                if (!is_file(realpath($sys_course_path.'/scorm/'.$lp_path.'/'.$decoded))) {
2966
                                    $file = self::rl_get_resource_link_for_learnpath(
2967
                                        $course_id,
2968
                                        $this->get_id(),
2969
                                        $item_id,
2970
                                        $this->get_view_id()
2971
                                    );
2972
                                    if (empty($file)) {
2973
                                        $file = 'blank.php?error=document_not_found';
2974
                                    } else {
2975
                                        $tmp_array = explode('/', $file);
2976
                                        $document_name = $tmp_array[count($tmp_array) - 1];
2977
                                        if (strpos($document_name, '_DELETED_')) {
2978
                                            $file = 'blank.php?error=document_deleted';
2979
                                        } else {
2980
                                            $file = 'blank.php?error=document_not_found';
2981
                                        }
2982
                                    }
2983
                                } else {
2984
                                    $file = $course_path.'/scorm/'.$lp_path.'/'.$decoded;
2985
                                }
2986
                            }*/
2987
                        }
2988
2989
                        // We want to use parameters if they were defined in the imsmanifest
2990
                        if (false === strpos($file, 'blank.php')) {
2991
                            $lp_item_params = ltrim($lp_item_params, '?');
2992
                            $file .= (false === strstr($file, '?') ? '?' : '').$lp_item_params;
2993
                        }
2994
                    } else {
2995
                        $file = 'lp_content.php?type=dir';
2996
                    }
2997
                    break;
2998
                case CLp::AICC_TYPE:
2999
                    // Formatting AICC HACP append URL.
3000
                    $aicc_append = '?aicc_sid='.
3001
                        urlencode(session_id()).'&aicc_url='.urlencode(api_get_path(WEB_CODE_PATH).'lp/aicc_hacp.php').'&';
3002
                    if (!empty($lp_item_params)) {
3003
                        $aicc_append .= $lp_item_params.'&';
3004
                    }
3005
                    if ('dir' !== $lp_item_type) {
3006
                        // Quite complex here:
3007
                        // We want to make sure 'http://' (and similar) links can
3008
                        // be loaded as is (withouth the Chamilo path in front) but
3009
                        // some contents use this form: resource.htm?resource=http://blablabla
3010
                        // which means we have to find a protocol at the path's start, otherwise
3011
                        // it should not be considered as an external URL.
3012
                        if (0 != preg_match('#^[a-zA-Z]{2,5}://#', $lp_item_path)) {
3013
                            if ($this->debug > 2) {
3014
                                error_log('In learnpath::get_link() '.__LINE__.' - Found match for protocol in '.$lp_item_path, 0);
3015
                            }
3016
                            // Distant url, return as is.
3017
                            $file = $lp_item_path;
3018
                            // Enabled and modified by Ivan Tcholakov, 16-OCT-2008.
3019
                            /*
3020
                            if (stristr($file,'<servername>') !== false) {
3021
                                $file = str_replace('<servername>', $course_path.'/scorm/'.$lp_path.'/', $lp_item_path);
3022
                            }
3023
                            */
3024
                            if (false !== stripos($file, '<servername>')) {
3025
                                //$file = str_replace('<servername>',$course_path.'/scorm/'.$lp_path.'/',$lp_item_path);
3026
                                $web_course_path = str_replace('https://', '', str_replace('http://', '', $course_path));
3027
                                $file = str_replace('<servername>', $web_course_path.'/scorm/'.$lp_path, $lp_item_path);
3028
                            }
3029
3030
                            $file .= $aicc_append;
3031
                        } else {
3032
                            if ($this->debug > 2) {
3033
                                error_log('In learnpath::get_link() '.__LINE__.' - No starting protocol in '.$lp_item_path, 0);
3034
                            }
3035
                            // Prevent getting untranslatable urls.
3036
                            $lp_item_path = preg_replace('/%2F/', '/', $lp_item_path);
3037
                            $lp_item_path = preg_replace('/%3A/', ':', $lp_item_path);
3038
                            // Prepare the path - lp_path might be unusable because it includes the "aicc" subdir name.
3039
                            $file = $course_path.'/scorm/'.$lp_path.'/'.$lp_item_path;
3040
                            // TODO: Fix this for urls with protocol header.
3041
                            $file = str_replace('//', '/', $file);
3042
                            $file = str_replace(':/', '://', $file);
3043
                            $file .= $aicc_append;
3044
                        }
3045
                    } else {
3046
                        $file = 'lp_content.php?type=dir';
3047
                    }
3048
                    break;
3049
                case 4:
3050
                default:
3051
                    break;
3052
            }
3053
            // Replace &amp; by & because &amp; will break URL with params
3054
            $file = !empty($file) ? str_replace('&amp;', '&', $file) : '';
3055
        }
3056
        if ($this->debug > 2) {
3057
            error_log('In learnpath::get_link() - returning "'.$file.'" from get_link', 0);
3058
        }
3059
3060
        return $file;
3061
    }
3062
3063
    /**
3064
     * Gets the latest usable view or generate a new one.
3065
     *
3066
     * @param int $attempt_num Optional attempt number. If none given, takes the highest from the lp_view table
3067
     * @param int $userId      The user ID, as $this->get_user_id() is not always available
3068
     *
3069
     * @return int DB lp_view id
3070
     */
3071
    public function get_view($attempt_num = 0, $userId = null)
3072
    {
3073
        $search = '';
3074
        $attempt_num = (int) $attempt_num;
3075
        // Use $attempt_num to enable multi-views management (disabled so far).
3076
        if (!empty($attempt_num)) {
3077
            $search = 'AND view_count = '.$attempt_num;
3078
        }
3079
3080
        $course_id = api_get_course_int_id();
3081
        $sessionId = api_get_session_id();
3082
3083
        // Check user ID.
3084
        if (empty($userId)) {
3085
            if (empty($this->get_user_id())) {
3086
                $this->error = 'User ID is empty in learnpath::get_view()';
3087
3088
                return null;
3089
            } else {
3090
                $userId = $this->get_user_id();
3091
            }
3092
        }
3093
        $sessionCondition = api_get_session_condition($sessionId);
3094
3095
        // When missing $attempt_num, search for a unique lp_view record for this lp and user.
3096
        $table = Database::get_course_table(TABLE_LP_VIEW);
3097
        $sql = "SELECT iid FROM $table
3098
        		WHERE
3099
        		    c_id = $course_id AND
3100
        		    lp_id = ".$this->get_id()." AND
3101
        		    user_id = ".$userId."
3102
        		    $sessionCondition
3103
        		    $search
3104
                ORDER BY view_count DESC";
3105
        $res = Database::query($sql);
3106
        if (Database::num_rows($res) > 0) {
3107
            $row = Database::fetch_array($res);
3108
            $this->lp_view_id = $row['iid'];
3109
        } elseif (!api_is_invitee()) {
3110
            $params = [
3111
                'c_id' => $course_id,
3112
                'lp_id' => $this->get_id(),
3113
                'user_id' => $this->get_user_id(),
3114
                'view_count' => 1,
3115
                'last_item' => 0,
3116
            ];
3117
            if (!empty($sessionId)) {
3118
                $params['session_id']  = $sessionId;
3119
            }
3120
            $this->lp_view_id = Database::insert($table, $params);
3121
        }
3122
3123
        return $this->lp_view_id;
3124
    }
3125
3126
    /**
3127
     * Gets the current view id.
3128
     *
3129
     * @return int View ID (from lp_view)
3130
     */
3131
    public function get_view_id()
3132
    {
3133
        if (!empty($this->lp_view_id)) {
3134
            return (int) $this->lp_view_id;
3135
        }
3136
3137
        return 0;
3138
    }
3139
3140
    /**
3141
     * Gets the update queue.
3142
     *
3143
     * @return array Array containing IDs of items to be updated by JavaScript
3144
     */
3145
    public function get_update_queue()
3146
    {
3147
        return $this->update_queue;
3148
    }
3149
3150
    /**
3151
     * Gets the user ID.
3152
     *
3153
     * @return int User ID
3154
     */
3155
    public function get_user_id()
3156
    {
3157
        if (!empty($this->user_id)) {
3158
            return (int) $this->user_id;
3159
        }
3160
3161
        return false;
3162
    }
3163
3164
    /**
3165
     * Checks if any of the items has an audio element attached.
3166
     *
3167
     * @return bool True or false
3168
     */
3169
    public function has_audio()
3170
    {
3171
        $has = false;
3172
        foreach ($this->items as $i => $item) {
3173
            if (!empty($this->items[$i]->audio)) {
3174
                $has = true;
3175
                break;
3176
            }
3177
        }
3178
3179
        return $has;
3180
    }
3181
3182
    /**
3183
     * Updates learnpath attributes to point to the next element
3184
     * The last part is similar to set_current_item but processing the other way around.
3185
     */
3186
    public function next()
3187
    {
3188
        if ($this->debug > 0) {
3189
            error_log('In learnpath::next()', 0);
3190
        }
3191
        $this->last = $this->get_current_item_id();
3192
        $this->items[$this->last]->save(
3193
            false,
3194
            $this->prerequisites_match($this->last)
3195
        );
3196
        $this->autocomplete_parents($this->last);
3197
        $new_index = $this->get_next_index();
3198
        if ($this->debug > 2) {
3199
            error_log('New index: '.$new_index, 0);
3200
        }
3201
        $this->index = $new_index;
3202
        if ($this->debug > 2) {
3203
            error_log('Now having orderedlist['.$new_index.'] = '.$this->ordered_items[$new_index], 0);
3204
        }
3205
        $this->current = $this->ordered_items[$new_index];
3206
        if ($this->debug > 2) {
3207
            error_log('new item id is '.$this->current.'-'.$this->get_current_item_id(), 0);
3208
        }
3209
    }
3210
3211
    /**
3212
     * Open a resource = initialise all local variables relative to this resource. Depending on the child
3213
     * class, this might be redefined to allow several behaviours depending on the document type.
3214
     *
3215
     * @param int $id Resource ID
3216
     */
3217
    public function open($id)
3218
    {
3219
        // TODO:
3220
        // set the current resource attribute to this resource
3221
        // switch on element type (redefine in child class?)
3222
        // set status for this item to "opened"
3223
        // start timer
3224
        // initialise score
3225
        $this->index = 0; //or = the last item seen (see $this->last)
3226
    }
3227
3228
    /**
3229
     * Check that all prerequisites are fulfilled. Returns true and an
3230
     * empty string on success, returns false
3231
     * and the prerequisite string on error.
3232
     * This function is based on the rules for aicc_script language as
3233
     * described in the SCORM 1.2 CAM documentation page 108.
3234
     *
3235
     * @param int $itemId Optional item ID. If none given, uses the current open item.
3236
     *
3237
     * @return bool true if prerequisites are matched, false otherwise - Empty string if true returned, prerequisites
3238
     *              string otherwise
3239
     */
3240
    public function prerequisites_match($itemId = null)
3241
    {
3242
        $allow = ('true' === api_get_setting('lp.allow_teachers_to_access_blocked_lp_by_prerequisite'));
3243
        if ($allow) {
3244
            if (api_is_allowed_to_edit() ||
3245
                api_is_platform_admin(true) ||
3246
                api_is_drh() ||
3247
                api_is_coach(api_get_session_id(), api_get_course_int_id())
3248
            ) {
3249
                return true;
3250
            }
3251
        }
3252
3253
        $debug = $this->debug;
3254
        if ($debug > 0) {
3255
            error_log('In learnpath::prerequisites_match()');
3256
        }
3257
3258
        if (empty($itemId)) {
3259
            $itemId = $this->current;
3260
        }
3261
3262
        $currentItem = $this->getItem($itemId);
3263
3264
        if ($currentItem) {
3265
            if (2 == $this->type) {
3266
                // Getting prereq from scorm
3267
                $prereq_string = $this->get_scorm_prereq_string($itemId);
3268
            } else {
3269
                $prereq_string = $currentItem->get_prereq_string();
3270
            }
3271
3272
            if (empty($prereq_string)) {
3273
                if ($debug > 0) {
3274
                    error_log('Found prereq_string is empty return true');
3275
                }
3276
3277
                return true;
3278
            }
3279
3280
            // Clean spaces.
3281
            $prereq_string = str_replace(' ', '', $prereq_string);
3282
            if ($debug > 0) {
3283
                error_log('Found prereq_string: '.$prereq_string, 0);
3284
            }
3285
3286
            // Now send to the parse_prereq() function that will check this component's prerequisites.
3287
            $result = $currentItem->parse_prereq(
3288
                $prereq_string,
3289
                $this->items,
3290
                $this->refs_list,
3291
                $this->get_user_id()
3292
            );
3293
3294
            if (false === $result) {
3295
                $this->set_error_msg($currentItem->prereq_alert);
3296
            }
3297
        } else {
3298
            $result = true;
3299
            if ($debug > 1) {
3300
                error_log('$this->items['.$itemId.'] was not an object', 0);
3301
            }
3302
        }
3303
3304
        if ($debug > 1) {
3305
            error_log('End of prerequisites_match(). Error message is now '.$this->error, 0);
3306
        }
3307
3308
        return $result;
3309
    }
3310
3311
    /**
3312
     * Updates learnpath attributes to point to the previous element
3313
     * The last part is similar to set_current_item but processing the other way around.
3314
     */
3315
    public function previous()
3316
    {
3317
        $this->last = $this->get_current_item_id();
3318
        $this->items[$this->last]->save(
3319
            false,
3320
            $this->prerequisites_match($this->last)
3321
        );
3322
        $this->autocomplete_parents($this->last);
3323
        $new_index = $this->get_previous_index();
3324
        $this->index = $new_index;
3325
        $this->current = $this->ordered_items[$new_index];
3326
    }
3327
3328
    /**
3329
     * Publishes a learnpath. This basically means show or hide the learnpath
3330
     * to normal users.
3331
     * Can be used as abstract.
3332
     *
3333
     * @param int $id         Learnpath ID
3334
     * @param int $visibility New visibility (1 = visible/published, 0= invisible/draft)
3335
     *
3336
     * @return bool
3337
     */
3338
    public static function toggleVisibility($id, $visibility = 1)
3339
    {
3340
        $repo = Container::getLpRepository();
3341
        $lp = $repo->find($id);
3342
3343
        if (!$lp) {
3344
            return false;
3345
        }
3346
3347
        $visibility = (int) $visibility;
3348
3349
        $course = api_get_course_entity();
3350
        $session = api_get_session_entity();
3351
3352
        if (1 === $visibility) {
3353
            $repo->setVisibilityPublished($lp, $course, $session);
3354
        } else {
3355
            $repo->setVisibilityDraft($lp, $course, $session);
3356
        }
3357
3358
        return true;
3359
    }
3360
3361
    /**
3362
     * Publishes a learnpath category.
3363
     * This basically means show or hide the learnpath category to normal users.
3364
     *
3365
     * @param int $id
3366
     * @param int $visibility
3367
     *
3368
     * @return bool
3369
     */
3370
    public static function toggleCategoryVisibility($id, $visibility = 1)
3371
    {
3372
        $repo = Container::getLpCategoryRepository();
3373
        $resource = $repo->find($id);
3374
3375
        if (!$resource) {
3376
            return false;
3377
        }
3378
3379
        $visibility = (int) $visibility;
3380
3381
        $course = api_get_course_entity();
3382
        $session = api_get_session_entity();
3383
3384
        if (1 === $visibility) {
3385
            $repo->setVisibilityPublished($resource, $course, $session);
3386
        } else {
3387
            $repo->setVisibilityDraft($resource, $course, $session);
3388
            self::toggleCategoryPublish($id, 0);
3389
        }
3390
3391
        return false;
3392
    }
3393
3394
    /**
3395
     * Publishes a learnpath. This basically means show or hide the learnpath
3396
     * on the course homepage.
3397
     *
3398
     * @param int    $id            Learnpath id
3399
     * @param string $setVisibility New visibility (v/i - visible/invisible)
3400
     *
3401
     * @return bool
3402
     */
3403
    public static function togglePublish($id, $setVisibility = 'v')
3404
    {
3405
        $addShortcut = false;
3406
        if ('v' === $setVisibility) {
3407
            $addShortcut = true;
3408
        }
3409
        $repo = Container::getLpRepository();
3410
        /** @var CLp|null $lp */
3411
        $lp = $repo->find($id);
3412
        if (null === $lp) {
3413
            return false;
3414
        }
3415
        $repoShortcut = Container::getShortcutRepository();
3416
        if ($addShortcut) {
3417
            $repoShortcut->addShortCut($lp, api_get_user_entity(), api_get_course_entity(), api_get_session_entity());
3418
        } else {
3419
            $repoShortcut->removeShortCut($lp);
3420
        }
3421
3422
        return true;
3423
    }
3424
3425
    /**
3426
     * Show or hide the learnpath category on the course homepage.
3427
     *
3428
     * @param int $id
3429
     * @param int $setVisibility
3430
     *
3431
     * @return bool
3432
     */
3433
    public static function toggleCategoryPublish($id, $setVisibility = 1)
3434
    {
3435
        $setVisibility = (int) $setVisibility;
3436
        $addShortcut = false;
3437
        if (1 === $setVisibility) {
3438
            $addShortcut = true;
3439
        }
3440
3441
        $repo = Container::getLpCategoryRepository();
3442
        /** @var CLpCategory|null $lp */
3443
        $category = $repo->find($id);
3444
3445
        if (null === $category) {
3446
            return false;
3447
        }
3448
3449
        $repoShortcut = Container::getShortcutRepository();
3450
        if ($addShortcut) {
3451
            $courseEntity = api_get_course_entity(api_get_course_int_id());
3452
            $repoShortcut->addShortCut($category, api_get_user_entity(), $courseEntity, api_get_session_entity());
3453
        } else {
3454
            $repoShortcut->removeShortCut($category);
3455
        }
3456
3457
        return true;
3458
    }
3459
3460
    /**
3461
     * Check if the learnpath category is visible for a user.
3462
     *
3463
     * @return bool
3464
     */
3465
    public static function categoryIsVisibleForStudent(
3466
        CLpCategory $category,
3467
        User $user,
3468
        Course $course,
3469
        SessionEntity $session = null
3470
    ) {
3471
        $isAllowedToEdit = api_is_allowed_to_edit(null, true);
3472
3473
        if ($isAllowedToEdit) {
3474
            return true;
3475
        }
3476
3477
        $categoryVisibility = $category->isVisible($course, $session);
3478
3479
        if (!$categoryVisibility) {
3480
            return false;
3481
        }
3482
3483
        $subscriptionSettings = self::getSubscriptionSettings();
3484
3485
        if (false === $subscriptionSettings['allow_add_users_to_lp_category']) {
3486
            return true;
3487
        }
3488
3489
        $noUserSubscribed = false;
3490
        $noGroupSubscribed = true;
3491
        $users = $category->getUsers();
3492
        if (empty($users) || !$users->count()) {
3493
            $noUserSubscribed = true;
3494
        } elseif ($category->hasUserAdded($user)) {
3495
            return true;
3496
        }
3497
3498
        //$groups = GroupManager::getAllGroupPerUserSubscription($user->getId());
3499
3500
        return $noGroupSubscribed && $noUserSubscribed;
3501
    }
3502
3503
    /**
3504
     * Check if a learnpath category is published as course tool.
3505
     *
3506
     * @param int $courseId
3507
     *
3508
     * @return bool
3509
     */
3510
    public static function categoryIsPublished(CLpCategory $category, $courseId)
3511
    {
3512
        return false;
3513
        $link = self::getCategoryLinkForTool($category->getId());
0 ignored issues
show
Unused Code introduced by
$link = self::getCategor...ool($category->getId()) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
3514
        $em = Database::getManager();
3515
3516
        $tools = $em
3517
            ->createQuery("
3518
                SELECT t FROM ChamiloCourseBundle:CTool t
3519
                WHERE t.course = :course AND
3520
                    t.name = :name AND
3521
                    t.image LIKE 'lp_category.%' AND
3522
                    t.link LIKE :link
3523
            ")
3524
            ->setParameters([
3525
                'course' => $courseId,
3526
                'name' => strip_tags($category->getTitle()),
3527
                'link' => "$link%",
3528
            ])
3529
            ->getResult();
3530
3531
        /** @var CTool $tool */
3532
        $tool = current($tools);
3533
3534
        return $tool ? $tool->getVisibility() : false;
3535
    }
3536
3537
    /**
3538
     * Restart the whole learnpath. Return the URL of the first element.
3539
     * Make sure the results are saved with anoter method. This method should probably be redefined in children classes.
3540
     * To use a similar method  statically, use the create_new_attempt() method.
3541
     *
3542
     * @return bool
3543
     */
3544
    public function restart()
3545
    {
3546
        if ($this->debug > 0) {
3547
            error_log('In learnpath::restart()', 0);
3548
        }
3549
        // TODO
3550
        // Call autosave method to save the current progress.
3551
        //$this->index = 0;
3552
        if (api_is_invitee()) {
3553
            return false;
3554
        }
3555
        $session_id = api_get_session_id();
3556
        $course_id = api_get_course_int_id();
3557
        $lp_view_table = Database::get_course_table(TABLE_LP_VIEW);
3558
        $sql = "INSERT INTO $lp_view_table (c_id, lp_id, user_id, view_count, session_id)
3559
                VALUES ($course_id, ".$this->lp_id.",".$this->get_user_id().",".($this->attempt + 1).", $session_id)";
3560
        if ($this->debug > 2) {
3561
            error_log('Inserting new lp_view for restart: '.$sql, 0);
3562
        }
3563
        Database::query($sql);
3564
        $view_id = Database::insert_id();
3565
3566
        if ($view_id) {
3567
            $this->lp_view_id = $view_id;
3568
            $this->attempt = $this->attempt + 1;
3569
        } else {
3570
            $this->error = 'Could not insert into item_view table...';
3571
3572
            return false;
3573
        }
3574
        $this->autocomplete_parents($this->current);
3575
        foreach ($this->items as $index => $dummy) {
3576
            $this->items[$index]->restart();
3577
            $this->items[$index]->set_lp_view($this->lp_view_id);
3578
        }
3579
        $this->first();
3580
3581
        return true;
3582
    }
3583
3584
    /**
3585
     * Saves the current item.
3586
     *
3587
     * @return bool
3588
     */
3589
    public function save_current()
3590
    {
3591
        $debug = $this->debug;
3592
        // TODO: Do a better check on the index pointing to the right item (it is supposed to be working
3593
        // on $ordered_items[] but not sure it's always safe to use with $items[]).
3594
        if ($debug) {
3595
            error_log('save_current() saving item '.$this->current, 0);
3596
            error_log(''.print_r($this->items, true), 0);
3597
        }
3598
        if (isset($this->items[$this->current]) &&
3599
            is_object($this->items[$this->current])
3600
        ) {
3601
            if ($debug) {
3602
                error_log('Before save last_scorm_session_time: '.$this->items[$this->current]->getLastScormSessionTime());
3603
            }
3604
3605
            $res = $this->items[$this->current]->save(
3606
                false,
3607
                $this->prerequisites_match($this->current)
3608
            );
3609
            $this->autocomplete_parents($this->current);
3610
            $status = $this->items[$this->current]->get_status();
3611
            $this->update_queue[$this->current] = $status;
3612
3613
            if ($debug) {
3614
                error_log('After save last_scorm_session_time: '.$this->items[$this->current]->getLastScormSessionTime());
3615
            }
3616
3617
            return $res;
3618
        }
3619
3620
        return false;
3621
    }
3622
3623
    /**
3624
     * Saves the given item.
3625
     *
3626
     * @param int  $item_id      Optional (will take from $_REQUEST if null)
3627
     * @param bool $from_outside Save from url params (true) or from current attributes (false). Default true
3628
     *
3629
     * @return bool
3630
     */
3631
    public function save_item($item_id = null, $from_outside = true)
3632
    {
3633
        $debug = $this->debug;
3634
        if ($debug) {
3635
            error_log('In learnpath::save_item('.$item_id.','.intval($from_outside).')', 0);
3636
        }
3637
        // TODO: Do a better check on the index pointing to the right item (it is supposed to be working
3638
        // on $ordered_items[] but not sure it's always safe to use with $items[]).
3639
        if (empty($item_id)) {
3640
            $item_id = (int) $_REQUEST['id'];
3641
        }
3642
3643
        if (empty($item_id)) {
3644
            $item_id = $this->get_current_item_id();
3645
        }
3646
        if (isset($this->items[$item_id]) &&
3647
            is_object($this->items[$item_id])
3648
        ) {
3649
            if ($debug) {
3650
                error_log('Object exists');
3651
            }
3652
3653
            // Saving the item.
3654
            $res = $this->items[$item_id]->save(
3655
                $from_outside,
3656
                $this->prerequisites_match($item_id)
3657
            );
3658
3659
            if ($debug) {
3660
                error_log('update_queue before:');
3661
                error_log(print_r($this->update_queue, 1));
3662
            }
3663
            $this->autocomplete_parents($item_id);
3664
3665
            $status = $this->items[$item_id]->get_status();
3666
            $this->update_queue[$item_id] = $status;
3667
3668
            if ($debug) {
3669
                error_log('get_status(): '.$status);
3670
                error_log('update_queue after:');
3671
                error_log(print_r($this->update_queue, 1));
3672
            }
3673
3674
            return $res;
3675
        }
3676
3677
        return false;
3678
    }
3679
3680
    /**
3681
     * Saves the last item seen's ID only in case.
3682
     */
3683
    public function save_last()
3684
    {
3685
        $course_id = api_get_course_int_id();
3686
        $debug = $this->debug;
3687
        if ($debug) {
3688
            error_log('In learnpath::save_last()', 0);
3689
        }
3690
        $session_condition = api_get_session_condition(
3691
            api_get_session_id(),
3692
            true,
3693
            false
3694
        );
3695
        $table = Database::get_course_table(TABLE_LP_VIEW);
3696
3697
        $userId = $this->get_user_id();
3698
        if (empty($userId)) {
3699
            $userId = api_get_user_id();
3700
            if ($debug) {
3701
                error_log('$this->get_user_id() was empty, used api_get_user_id() instead in '.__FILE__.' line '.__LINE__);
3702
            }
3703
        }
3704
        if (isset($this->current) && !api_is_invitee()) {
3705
            if ($debug) {
3706
                error_log('Saving current item ('.$this->current.') for later review', 0);
3707
            }
3708
            $sql = "UPDATE $table SET
3709
                        last_item = ".$this->get_current_item_id()."
3710
                    WHERE
3711
                        c_id = $course_id AND
3712
                        lp_id = ".$this->get_id()." AND
3713
                        user_id = ".$userId." ".$session_condition;
3714
3715
            if ($debug) {
3716
                error_log('Saving last item seen : '.$sql, 0);
3717
            }
3718
            Database::query($sql);
3719
        }
3720
3721
        if (!api_is_invitee()) {
3722
            // Save progress.
3723
            [$progress] = $this->get_progress_bar_text('%');
3724
            $scoreAsProgressSetting = ('true' === api_get_setting('lp.lp_score_as_progress_enable'));
3725
            $scoreAsProgress = $this->getUseScoreAsProgress();
3726
            if ($scoreAsProgress && $scoreAsProgressSetting && (null === $score || empty($score) || -1 == $score)) {
3727
                if ($debug) {
3728
                    error_log("Return false: Dont save score: $score");
3729
                    error_log("progress: $progress");
3730
                }
3731
3732
                return false;
3733
            }
3734
3735
            if ($scoreAsProgress && $scoreAsProgressSetting) {
3736
                $storedProgress = self::getProgress(
3737
                    $this->get_id(),
3738
                    $userId,
3739
                    $course_id,
3740
                    $this->get_lp_session_id()
3741
                );
3742
3743
                // Check if the stored progress is higher than the new value
3744
                if ($storedProgress >= $progress) {
3745
                    if ($debug) {
3746
                        error_log("Return false: New progress value is lower than stored value - Current value: $storedProgress - New value: $progress [lp ".$this->get_id()." - user ".$userId."]");
3747
                    }
3748
3749
                    return false;
3750
                }
3751
            }
3752
            if ($progress >= 0 && $progress <= 100) {
3753
                $progress = (int) $progress;
3754
                $sql = "UPDATE $table SET
3755
                            progress = $progress
3756
                        WHERE
3757
                            c_id = $course_id AND
3758
                            lp_id = ".$this->get_id()." AND
3759
                            user_id = ".$userId." ".$session_condition;
3760
                // Ignore errors as some tables might not have the progress field just yet.
3761
                Database::query($sql);
3762
                $this->progress_db = $progress;
3763
3764
                if (100 == $progress) {
3765
                    Container::getEventDispatcher()->dispatch(
3766
                        new LearningPathEndedEvent(['lp_view_id' => $this->lp_view_id]),
3767
                        Events::LP_ENDED
3768
                    );
3769
                }
3770
            }
3771
        }
3772
    }
3773
3774
    /**
3775
     * Sets the current item ID (checks if valid and authorized first).
3776
     *
3777
     * @param int $item_id New item ID. If not given or not authorized, defaults to current
3778
     */
3779
    public function set_current_item($item_id = null)
3780
    {
3781
        $debug = $this->debug;
3782
        if ($debug) {
3783
            error_log('In learnpath::set_current_item('.$item_id.')', 0);
3784
        }
3785
        if (empty($item_id)) {
3786
            if ($debug) {
3787
                error_log('No new current item given, ignore...', 0);
3788
            }
3789
            // Do nothing.
3790
        } else {
3791
            if ($debug) {
3792
                error_log('New current item given is '.$item_id.'...', 0);
3793
            }
3794
            if (is_numeric($item_id)) {
3795
                $item_id = (int) $item_id;
3796
                // TODO: Check in database here.
3797
                $this->last = $this->current;
3798
                $this->current = $item_id;
3799
                // TODO: Update $this->index as well.
3800
                foreach ($this->ordered_items as $index => $item) {
3801
                    if ($item == $this->current) {
3802
                        $this->index = $index;
3803
                        break;
3804
                    }
3805
                }
3806
                if ($debug) {
3807
                    error_log('set_current_item('.$item_id.') done. Index is now : '.$this->index);
3808
                }
3809
            } else {
3810
                if ($debug) {
3811
                    error_log('set_current_item('.$item_id.') failed. Not a numeric value: ');
3812
                }
3813
            }
3814
        }
3815
    }
3816
3817
    /**
3818
     * Set index specified prefix terms for all items in this path.
3819
     *
3820
     * @param string $terms_string Comma-separated list of terms
3821
     * @param string $prefix       Xapian term prefix
3822
     *
3823
     * @return bool False on error, true otherwise
3824
     */
3825
    public function set_terms_by_prefix($terms_string, $prefix)
3826
    {
3827
        $course_id = api_get_course_int_id();
3828
        if ('true' !== api_get_setting('search_enabled')) {
3829
            return false;
3830
        }
3831
3832
        if (!extension_loaded('xapian')) {
3833
            return false;
3834
        }
3835
3836
        $terms_string = trim($terms_string);
3837
        $terms = explode(',', $terms_string);
3838
        array_walk($terms, 'trim_value');
3839
        $stored_terms = $this->get_common_index_terms_by_prefix($prefix);
3840
3841
        // Don't do anything if no change, verify only at DB, not the search engine.
3842
        if ((0 == count(array_diff($terms, $stored_terms))) && (0 == count(array_diff($stored_terms, $terms)))) {
3843
            return false;
3844
        }
3845
3846
        require_once 'xapian.php'; // TODO: Try catch every xapian use or make wrappers on API.
3847
        require_once api_get_path(LIBRARY_PATH).'search/xapian/XapianQuery.php';
3848
3849
        $items_table = Database::get_course_table(TABLE_LP_ITEM);
3850
        // TODO: Make query secure agains XSS : use member attr instead of post var.
3851
        $lp_id = (int) $_POST['lp_id'];
3852
        $sql = "SELECT * FROM $items_table WHERE c_id = $course_id AND lp_id = $lp_id";
3853
        $result = Database::query($sql);
3854
        $di = new ChamiloIndexer();
3855
3856
        while ($lp_item = Database::fetch_array($result)) {
3857
            // Get search_did.
3858
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
3859
            $sql = 'SELECT * FROM %s
3860
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
3861
                    LIMIT 1';
3862
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp_id, $lp_item['id']);
3863
3864
            //echo $sql; echo '<br>';
3865
            $res = Database::query($sql);
3866
            if (Database::num_rows($res) > 0) {
3867
                $se_ref = Database::fetch_array($res);
3868
                // Compare terms.
3869
                $doc = $di->get_document($se_ref['search_did']);
3870
                $xapian_terms = xapian_get_doc_terms($doc, $prefix);
3871
                $xterms = [];
3872
                foreach ($xapian_terms as $xapian_term) {
3873
                    $xterms[] = substr($xapian_term['name'], 1);
3874
                }
3875
3876
                $dterms = $terms;
3877
                $missing_terms = array_diff($dterms, $xterms);
3878
                $deprecated_terms = array_diff($xterms, $dterms);
3879
3880
                // Save it to search engine.
3881
                foreach ($missing_terms as $term) {
3882
                    $doc->add_term($prefix.$term, 1);
3883
                }
3884
                foreach ($deprecated_terms as $term) {
3885
                    $doc->remove_term($prefix.$term);
3886
                }
3887
                $di->getDb()->replace_document((int) $se_ref['search_did'], $doc);
3888
                $di->getDb()->flush();
3889
            }
3890
        }
3891
3892
        return true;
3893
    }
3894
3895
    /**
3896
     * Sets the previous item ID to a given ID. Generally, this should be set to the previous 'current' item.
3897
     *
3898
     * @param int $id DB ID of the item
3899
     */
3900
    public function set_previous_item($id)
3901
    {
3902
        if ($this->debug > 0) {
3903
            error_log('In learnpath::set_previous_item()', 0);
3904
        }
3905
        $this->last = $id;
3906
    }
3907
3908
    /**
3909
     * Sets and saves the expired_on date.
3910
     *
3911
     * @return bool Returns true if author's name is not empty
3912
     */
3913
    public function set_modified_on()
3914
    {
3915
        $this->modified_on = api_get_utc_datetime();
3916
        $table = Database::get_course_table(TABLE_LP_MAIN);
3917
        $lp_id = $this->get_id();
3918
        $sql = "UPDATE $table SET modified_on = '".$this->modified_on."'
3919
                WHERE iid = $lp_id";
3920
        Database::query($sql);
3921
3922
        return true;
3923
    }
3924
3925
    /**
3926
     * Sets the object's error message.
3927
     *
3928
     * @param string $error Error message. If empty, reinits the error string
3929
     */
3930
    public function set_error_msg($error = '')
3931
    {
3932
        if ($this->debug > 0) {
3933
            error_log('In learnpath::set_error_msg()', 0);
3934
        }
3935
        if (empty($error)) {
3936
            $this->error = '';
3937
        } else {
3938
            $this->error .= $error;
3939
        }
3940
    }
3941
3942
    /**
3943
     * Launches the current item if not 'sco'
3944
     * (starts timer and make sure there is a record ready in the DB).
3945
     *
3946
     * @param bool $allow_new_attempt Whether to allow a new attempt or not
3947
     *
3948
     * @return bool
3949
     */
3950
    public function start_current_item($allow_new_attempt = false)
3951
    {
3952
        $debug = $this->debug;
3953
        if ($debug) {
3954
            error_log('In learnpath::start_current_item()');
3955
            error_log('current: '.$this->current);
3956
        }
3957
        if (0 != $this->current && isset($this->items[$this->current]) &&
3958
            is_object($this->items[$this->current])
3959
        ) {
3960
            $type = $this->get_type();
3961
            $item_type = $this->items[$this->current]->get_type();
3962
            if ($debug) {
3963
                error_log('item type: '.$item_type);
3964
                error_log('lp type: '.$type);
3965
            }
3966
            if ((2 == $type && 'sco' !== $item_type) ||
3967
                (3 == $type && 'au' !== $item_type) ||
3968
                (1 == $type && TOOL_QUIZ != $item_type && TOOL_HOTPOTATOES != $item_type)
3969
            ) {
3970
                $this->items[$this->current]->open($allow_new_attempt);
3971
                $this->autocomplete_parents($this->current);
3972
                $prereq_check = $this->prerequisites_match($this->current);
3973
                if ($debug) {
3974
                    error_log('start_current_item will save item with prereq: '.$prereq_check);
3975
                }
3976
                $this->items[$this->current]->save(false, $prereq_check);
3977
            }
3978
            // If sco, then it is supposed to have been updated by some other call.
3979
            if ('sco' === $item_type) {
3980
                $this->items[$this->current]->restart();
3981
            }
3982
        }
3983
        if ($debug) {
3984
            error_log('lp_view_session_id');
3985
            error_log($this->lp_view_session_id);
3986
            error_log('api session id');
3987
            error_log(api_get_session_id());
3988
            error_log('End of learnpath::start_current_item()');
3989
        }
3990
3991
        return true;
3992
    }
3993
3994
    /**
3995
     * Stops the processing and counters for the old item (as held in $this->last).
3996
     *
3997
     * @return bool True/False
3998
     */
3999
    public function stop_previous_item()
4000
    {
4001
        $debug = $this->debug;
4002
        if ($debug) {
4003
            error_log('In learnpath::stop_previous_item()', 0);
4004
        }
4005
4006
        if (0 != $this->last && $this->last != $this->current &&
4007
            isset($this->items[$this->last]) && is_object($this->items[$this->last])
4008
        ) {
4009
            if ($debug) {
4010
                error_log('In learnpath::stop_previous_item() - '.$this->last.' is object');
4011
            }
4012
            switch ($this->get_type()) {
4013
                case '3':
4014
                    if ('au' != $this->items[$this->last]->get_type()) {
4015
                        if ($debug) {
4016
                            error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 3 is <> au');
4017
                        }
4018
                        $this->items[$this->last]->close();
4019
                    } else {
4020
                        if ($debug) {
4021
                            error_log('In learnpath::stop_previous_item() - Item is an AU, saving is managed by AICC signals');
4022
                        }
4023
                    }
4024
                    break;
4025
                case '2':
4026
                    if ('sco' != $this->items[$this->last]->get_type()) {
4027
                        if ($debug) {
4028
                            error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 2 is <> sco');
4029
                        }
4030
                        $this->items[$this->last]->close();
4031
                    } else {
4032
                        if ($debug) {
4033
                            error_log('In learnpath::stop_previous_item() - Item is a SCO, saving is managed by SCO signals');
4034
                        }
4035
                    }
4036
                    break;
4037
                case '1':
4038
                default:
4039
                    if ($debug) {
4040
                        error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 1 is asset');
4041
                    }
4042
                    $this->items[$this->last]->close();
4043
                    break;
4044
            }
4045
        } else {
4046
            if ($debug) {
4047
                error_log('In learnpath::stop_previous_item() - No previous element found, ignoring...');
4048
            }
4049
4050
            return false;
4051
        }
4052
4053
        return true;
4054
    }
4055
4056
    /**
4057
     * Updates the default view mode from fullscreen to embedded and inversely.
4058
     *
4059
     * @return string The current default view mode ('fullscreen' or 'embedded')
4060
     */
4061
    public function update_default_view_mode()
4062
    {
4063
        $table = Database::get_course_table(TABLE_LP_MAIN);
4064
        $sql = "SELECT * FROM $table
4065
                WHERE iid = ".$this->get_id();
4066
        $res = Database::query($sql);
4067
        if (Database::num_rows($res) > 0) {
4068
            $row = Database::fetch_array($res);
4069
            $default_view_mode = $row['default_view_mod'];
4070
            $view_mode = $default_view_mode;
4071
            switch ($default_view_mode) {
4072
                case 'fullscreen': // default with popup
4073
                    $view_mode = 'embedded';
4074
                    break;
4075
                case 'embedded': // default view with left menu
4076
                    $view_mode = 'embedframe';
4077
                    break;
4078
                case 'embedframe': //folded menu
4079
                    $view_mode = 'impress';
4080
                    break;
4081
                case 'impress':
4082
                    $view_mode = 'fullscreen';
4083
                    break;
4084
            }
4085
            $sql = "UPDATE $table SET default_view_mod = '$view_mode'
4086
                    WHERE iid = ".$this->get_id();
4087
            Database::query($sql);
4088
            $this->mode = $view_mode;
4089
4090
            return $view_mode;
4091
        }
4092
4093
        return -1;
4094
    }
4095
4096
    /**
4097
     * Updates the default behaviour about auto-commiting SCORM updates.
4098
     *
4099
     * @return bool True if auto-commit has been set to 'on', false otherwise
4100
     */
4101
    public function update_default_scorm_commit()
4102
    {
4103
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
4104
        $sql = "SELECT * FROM $lp_table
4105
                WHERE iid = ".$this->get_id();
4106
        $res = Database::query($sql);
4107
        if (Database::num_rows($res) > 0) {
4108
            $row = Database::fetch_array($res);
4109
            $force = $row['force_commit'];
4110
            if (1 == $force) {
4111
                $force = 0;
4112
                $force_return = false;
4113
            } elseif (0 == $force) {
4114
                $force = 1;
4115
                $force_return = true;
4116
            }
4117
            $sql = "UPDATE $lp_table SET force_commit = $force
4118
                    WHERE iid = ".$this->get_id();
4119
            Database::query($sql);
4120
            $this->force_commit = $force_return;
4121
4122
            return $force_return;
4123
        }
4124
4125
        return -1;
4126
    }
4127
4128
    /**
4129
     * Updates the order of learning paths (goes through all of them by order and fills the gaps).
4130
     *
4131
     * @return bool True on success, false on failure
4132
     */
4133
    public function update_display_order()
4134
    {
4135
        return;
4136
        $course_id = api_get_course_int_id();
0 ignored issues
show
Unused Code introduced by
$course_id = api_get_course_int_id() is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
4137
        $table = Database::get_course_table(TABLE_LP_MAIN);
4138
        $sql = "SELECT * FROM $table
4139
                WHERE c_id = $course_id
4140
                ORDER BY display_order";
4141
        $res = Database::query($sql);
4142
        if (false === $res) {
4143
            return false;
4144
        }
4145
4146
        $num = Database::num_rows($res);
4147
        // First check the order is correct, globally (might be wrong because
4148
        // of versions < 1.8.4).
4149
        if ($num > 0) {
4150
            $i = 1;
4151
            while ($row = Database::fetch_array($res)) {
4152
                if ($row['display_order'] != $i) {
4153
                    // If we find a gap in the order, we need to fix it.
4154
                    $sql = "UPDATE $table SET display_order = $i
4155
                            WHERE iid = ".$row['iid'];
4156
                    Database::query($sql);
4157
                }
4158
                $i++;
4159
            }
4160
        }
4161
4162
        return true;
4163
    }
4164
4165
    /**
4166
     * Updates the "prevent_reinit" value that enables control on reinitialising items on second view.
4167
     *
4168
     * @return bool True if prevent_reinit has been set to 'on', false otherwise (or 1 or 0 in this case)
4169
     */
4170
    public function update_reinit()
4171
    {
4172
        $force = $this->prevent_reinit;
4173
        if (1 == $force) {
4174
            $force = 0;
4175
        } elseif (0 == $force) {
4176
            $force = 1;
4177
        }
4178
4179
        $table = Database::get_course_table(TABLE_LP_MAIN);
4180
        $sql = "UPDATE $table SET prevent_reinit = $force
4181
                WHERE iid = ".$this->get_id();
4182
        Database::query($sql);
4183
        $this->prevent_reinit = $force;
4184
4185
        return $force;
4186
    }
4187
4188
    /**
4189
     * Determine the attempt_mode thanks to prevent_reinit and seriousgame_mode db flag.
4190
     *
4191
     * @return string 'single', 'multi' or 'seriousgame'
4192
     *
4193
     * @author ndiechburg <[email protected]>
4194
     */
4195
    public function get_attempt_mode()
4196
    {
4197
        //Set default value for seriousgame_mode
4198
        if (!isset($this->seriousgame_mode)) {
4199
            $this->seriousgame_mode = 0;
4200
        }
4201
        // Set default value for prevent_reinit
4202
        if (!isset($this->prevent_reinit)) {
4203
            $this->prevent_reinit = 1;
4204
        }
4205
        if (1 == $this->seriousgame_mode && 1 == $this->prevent_reinit) {
4206
            return 'seriousgame';
4207
        }
4208
        if (0 == $this->seriousgame_mode && 1 == $this->prevent_reinit) {
4209
            return 'single';
4210
        }
4211
        if (0 == $this->seriousgame_mode && 0 == $this->prevent_reinit) {
4212
            return 'multiple';
4213
        }
4214
4215
        return 'single';
4216
    }
4217
4218
    /**
4219
     * Register the attempt mode into db thanks to flags prevent_reinit and seriousgame_mode flags.
4220
     *
4221
     * @param string 'seriousgame', 'single' or 'multiple'
0 ignored issues
show
Documentation Bug introduced by
The doc comment 'seriousgame', 'single' at position 0 could not be parsed: Unknown type name ''seriousgame'' at position 0 in 'seriousgame', 'single'.
Loading history...
4222
     *
4223
     * @return bool
4224
     *
4225
     * @author ndiechburg <[email protected]>
4226
     */
4227
    public function set_attempt_mode($mode)
4228
    {
4229
        switch ($mode) {
4230
            case 'seriousgame':
4231
                $sg_mode = 1;
4232
                $prevent_reinit = 1;
4233
                break;
4234
            case 'single':
4235
                $sg_mode = 0;
4236
                $prevent_reinit = 1;
4237
                break;
4238
            case 'multiple':
4239
                $sg_mode = 0;
4240
                $prevent_reinit = 0;
4241
                break;
4242
            default:
4243
                $sg_mode = 0;
4244
                $prevent_reinit = 0;
4245
                break;
4246
        }
4247
        $this->prevent_reinit = $prevent_reinit;
4248
        $this->seriousgame_mode = $sg_mode;
4249
        $table = Database::get_course_table(TABLE_LP_MAIN);
4250
        $sql = "UPDATE $table SET
4251
                prevent_reinit = $prevent_reinit ,
4252
                seriousgame_mode = $sg_mode
4253
                WHERE iid = ".$this->get_id();
4254
        $res = Database::query($sql);
4255
        if ($res) {
4256
            return true;
4257
        } else {
4258
            return false;
4259
        }
4260
    }
4261
4262
    /**
4263
     * Switch between multiple attempt, single attempt or serious_game mode (only for scorm).
4264
     *
4265
     * @author ndiechburg <[email protected]>
4266
     */
4267
    public function switch_attempt_mode()
4268
    {
4269
        $mode = $this->get_attempt_mode();
4270
        switch ($mode) {
4271
            case 'single':
4272
                $next_mode = 'multiple';
4273
                break;
4274
            case 'multiple':
4275
                $next_mode = 'seriousgame';
4276
                break;
4277
            case 'seriousgame':
4278
            default:
4279
                $next_mode = 'single';
4280
                break;
4281
        }
4282
        $this->set_attempt_mode($next_mode);
4283
    }
4284
4285
    /**
4286
     * Switch the lp in ktm mode. This is a special scorm mode with unique attempt
4287
     * but possibility to do again a completed item.
4288
     *
4289
     * @return bool true if seriousgame_mode has been set to 1, false otherwise
4290
     *
4291
     * @author ndiechburg <[email protected]>
4292
     */
4293
    public function set_seriousgame_mode()
4294
    {
4295
        $table = Database::get_course_table(TABLE_LP_MAIN);
4296
        $force = $this->seriousgame_mode;
4297
        if (1 == $force) {
4298
            $force = 0;
4299
        } elseif (0 == $force) {
4300
            $force = 1;
4301
        }
4302
        $sql = "UPDATE $table SET seriousgame_mode = $force
4303
                WHERE iid = ".$this->get_id();
4304
        Database::query($sql);
4305
        $this->seriousgame_mode = $force;
4306
4307
        return $force;
4308
    }
4309
4310
    /**
4311
     * Updates the "scorm_debug" value that shows or hide the debug window.
4312
     *
4313
     * @return bool True if scorm_debug has been set to 'on', false otherwise (or 1 or 0 in this case)
4314
     */
4315
    public function update_scorm_debug()
4316
    {
4317
        $table = Database::get_course_table(TABLE_LP_MAIN);
4318
        $force = $this->scorm_debug;
4319
        if (1 == $force) {
4320
            $force = 0;
4321
        } elseif (0 == $force) {
4322
            $force = 1;
4323
        }
4324
        $sql = "UPDATE $table SET debug = $force
4325
                WHERE iid = ".$this->get_id();
4326
        Database::query($sql);
4327
        $this->scorm_debug = $force;
4328
4329
        return $force;
4330
    }
4331
4332
    /**
4333
     * Function that creates a html list of learning path items so that we can add audio files to them.
4334
     *
4335
     * @author Kevin Van Den Haute
4336
     *
4337
     * @return string
4338
     */
4339
    public function overview()
4340
    {
4341
        $return = '';
4342
        $update_audio = $_GET['updateaudio'] ?? null;
4343
4344
        // we need to start a form when we want to update all the mp3 files
4345
        if ('true' == $update_audio) {
4346
            $return .= '<form action="'.api_get_self().'?'.api_get_cidreq().'&updateaudio='.Security::remove_XSS(
4347
                    $_GET['updateaudio']
4348
                ).'&action='.Security::remove_XSS(
4349
                    $_GET['action']
4350
                ).'&lp_id='.$_SESSION['oLP']->lp_id.'" method="post" enctype="multipart/form-data" name="updatemp3" id="updatemp3">';
4351
        }
4352
        $return .= '<div id="message"></div>';
4353
        if (0 == count($this->items)) {
4354
            $return .= Display::return_message(
4355
                get_lang(
4356
                    'You should add some items to your learning path, otherwise you won\'t be able to attach audio files to them'
4357
                ),
4358
                'normal'
4359
            );
4360
        } else {
4361
            $return_audio = '<table class="table table-hover table-striped data_table">';
4362
            $return_audio .= '<tr>';
4363
            $return_audio .= '<th width="40%">'.get_lang('Title').'</th>';
4364
            $return_audio .= '<th>'.get_lang('Audio').'</th>';
4365
            $return_audio .= '</tr>';
4366
4367
            if ('true' != $update_audio) {
4368
                /*$return .= '<div class="col-md-12">';
4369
                $return .= self::return_new_tree($update_audio);
4370
                $return .= '</div>';*/
4371
                $return .= Display::div(
4372
                    Display::url(get_lang('Save'), '#', ['id' => 'listSubmit', 'class' => 'btn btn--primary']),
4373
                    ['style' => 'float:left; margin-top:15px;width:100%']
4374
                );
4375
            } else {
4376
                //$return_audio .= self::return_new_tree($update_audio);
4377
                $return .= $return_audio.'</table>';
4378
            }
4379
4380
            // We need to close the form when we are updating the mp3 files.
4381
            if ('true' == $update_audio) {
4382
                $return .= '<div class="footer-audio">';
4383
                $return .= Display::button(
4384
                    'save_audio',
4385
                    '<em class="fa fa-file-audio-o"></em> '.get_lang('Save audio and organization'),
4386
                    ['class' => 'btn btn--primary', 'type' => 'submit']
4387
                );
4388
                $return .= '</div>';
4389
            }
4390
        }
4391
4392
        // We need to close the form when we are updating the mp3 files.
4393
        if ('true' === $update_audio && isset($this->arrMenu) && 0 != count($this->arrMenu)) {
4394
            $return .= '</form>';
4395
        }
4396
4397
        return $return;
4398
    }
4399
4400
    public function showBuildSideBar($updateAudio = false, $dropElementHere = false, $type = null)
4401
    {
4402
        $sureToDelete = trim(get_lang('Are you sure to delete'));
4403
        $ajax_url = api_get_path(WEB_AJAX_PATH).'lp.ajax.php?lp_id='.$this->get_id().'&'.api_get_cidreq();
4404
4405
        $content = '
4406
    <script>
4407
    $(function() {
4408
        function enforceFinalAtEndDOM() {
4409
            var $root = $("#lp_item_list");
4410
            $root.find("li.final-item").each(function() {
4411
                $root.append(this);
4412
            });
4413
        }
4414
4415
        function refreshTree() {
4416
            var params = "&a=get_lp_item_tree";
4417
            $.get(
4418
                "'.$ajax_url.'",
4419
                params,
4420
                function(result) {
4421
                    $("#lp_item_list").html(result);
4422
                    enforceFinalAtEndDOM();
4423
                    nestedSortable();
4424
                }
4425
            );
4426
        }
4427
4428
        const nestedQuery = ".nested-sortable";
4429
        const identifier  = "id";
4430
        const root        = document.getElementById("lp_item_list");
4431
4432
        function serialize(sortable) {
4433
            var out    = [];
4434
            var finals = [];
4435
            var children = [].slice.call(sortable.children);
4436
4437
            for (var i = 0; i < children.length; i++) {
4438
                var li = children[i];
4439
                if (!li || !li.dataset) continue;
4440
4441
                var id = li.dataset[identifier];
4442
                if (!id) continue;
4443
4444
                var isFinal =
4445
                    li.classList.contains("final-item") ||
4446
                    li.dataset.type === "final_item" ||
4447
                    li.dataset.fixed === "final";
4448
4449
                var parentLi = $(li).closest("ul.nested-sortable").closest("li")[0];
4450
                var parentId = (parentLi && parentLi.dataset) ? parentLi.dataset[identifier] : null;
4451
                var rec = { id: id, parent_id: isFinal ? null : parentId };
4452
                var nested = li.querySelector(nestedQuery);
4453
                if (nested && !isFinal) {
4454
                    out = out.concat( serialize(nested) );
4455
                }
4456
4457
                (isFinal ? finals : out).push(rec);
4458
            }
4459
4460
            return out.concat(finals);
4461
        }
4462
4463
        function nestedSortable() {
4464
            let lists = document.getElementsByClassName("nested-sortable");
4465
            Array.prototype.forEach.call(lists, function(ul) {
4466
                Sortable.create(ul, {
4467
                    group: "nested",
4468
                    put: ["nested-sortable", ".lp_resource", ".nested-source"],
4469
                    animation: 150,
4470
                    swapThreshold: 0.65,
4471
                    dataIdAttr: "data-id",
4472
                    filter: ".disable_drag",
4473
                    onMove: function (evt) {
4474
                        var t = evt.dragged && evt.dragged.dataset ? evt.dragged.dataset.type : null;
4475
                        if (t === "final_item") return false;
4476
                    },
4477
                    onEnd: function(evt) {
4478
                        if (evt.item && (evt.item.classList.contains("final-item") || evt.item.dataset.type === "final_item")) {
4479
                            enforceFinalAtEndDOM();
4480
                            return;
4481
                        }
4482
                        enforceFinalAtEndDOM();
4483
4484
                        let list  = serialize(root);
4485
                        let order = "&a=update_lp_item_order&new_order=" + JSON.stringify(list);
4486
4487
                        $.get(
4488
                            "'.$ajax_url.'",
4489
                            order,
4490
                            function(reponse) {
4491
                                $("#message").html(reponse);
4492
                                refreshTree();
4493
                            }
4494
                        );
4495
                    },
4496
                });
4497
            });
4498
        }
4499
4500
        nestedSortable();
4501
4502
        let resources = document.getElementsByClassName("lp_resource");
4503
        Array.prototype.forEach.call(resources, function(resource) {
4504
            Sortable.create(resource, {
4505
                group: {
4506
                    name: "nested",
4507
                    pull: true,
4508
                    put: false
4509
                },
4510
                sort: false,
4511
                filter: ".disable_drag",
4512
                animation: 150,
4513
                fallbackOnBody: true,
4514
                swapThreshold: 0.65,
4515
                dataIdAttr: "data-id",
4516
                onRemove: function(evt) {
4517
                    var itemEl   = evt.item;
4518
                    var newIndex = evt.newIndex;
4519
                    var id       = $(itemEl).attr("id");
4520
                    var parentId = $(itemEl).parent().parent().attr("id");
4521
                    var type     = $(itemEl).find(".link_with_id").attr("data_type");
4522
                    var title    = $(itemEl).find(".link_with_id").text();
4523
4524
                    let previousId = 0;
4525
                    if (0 !== newIndex) {
4526
                        previousId = $(itemEl).prev().attr("id");
4527
                    }
4528
4529
                    var params = {
4530
                        "a": "add_lp_item",
4531
                        "id": id,
4532
                        "parent_id": parentId,
4533
                        "previous_id": previousId,
4534
                        "type": type,
4535
                        "title" : title
4536
                    };
4537
4538
                    $.ajax({
4539
                        type: "GET",
4540
                        url: "'.$ajax_url.'",
4541
                        data: params,
4542
                        success: function(itemId) {
4543
                            $(itemEl).attr("id", itemId);
4544
                            $(itemEl).attr("data-id", itemId);
4545
4546
                            enforceFinalAtEndDOM();
4547
4548
                            let list = serialize(root);
4549
                            let listInString = JSON.stringify(list) || "[]";
4550
                            let order = "&a=update_lp_item_order&new_order=" + listInString;
4551
4552
                            $.get(
4553
                                "'.$ajax_url.'",
4554
                                order,
4555
                                function(reponse) {
4556
                                    $("#message").html(reponse);
4557
                                    refreshTree();
4558
                                }
4559
                            );
4560
                        }
4561
                    });
4562
                }
4563
            });
4564
        });
4565
    });
4566
    </script>';
4567
4568
        $content .= "
4569
    <script>
4570
        function confirmation(name) {
4571
            return confirm('$sureToDelete ' + name);
4572
        }
4573
        function refreshTree() {
4574
            var params = '&a=get_lp_item_tree';
4575
            $.get(
4576
                '".$ajax_url."',
4577
                params,
4578
                function(result) {
4579
                    $('#lp_item_list').html(result);
4580
                }
4581
            );
4582
        }
4583
4584
        $(function () {
4585
            expandColumnToggle('#hide_bar_template', { selector: '#lp_sidebar' }, { selector: '#doc_form' });
4586
4587
            $('.lp-btn-associate-forum').on('click', function (e) {
4588
                var ok = confirm('".get_lang('This action will associate a forum thread to this learning path item. Do you want to proceed?')."');
4589
                if (!ok) e.preventDefault();
4590
            });
4591
4592
            $('.lp-btn-dissociate-forum').on('click', function (e) {
4593
                var ok = confirm('".get_lang('This action will dissociate the forum thread of this learning path item. Do you want to proceed?')."');
4594
                if (!ok) e.preventDefault();
4595
            });
4596
4597
            $('#frmModel').hide();
4598
        });
4599
4600
        function deleteItem(event) {
4601
            var id = $(event).attr('data-id');
4602
            var title = $(event).attr('data-title');
4603
            var params = '&a=delete_item&id=' + id;
4604
            if (confirmation(title)) {
4605
                $.get(
4606
                    '".$ajax_url."',
4607
                    params,
4608
                    function(result) {
4609
                        refreshTree();
4610
                    }
4611
                );
4612
            }
4613
        }
4614
    </script>";
4615
4616
        $content .= $this->return_new_tree($updateAudio, $dropElementHere);
4617
        $documentId = isset($_GET['path_item']) ? (int) $_GET['path_item'] : 0;
4618
4619
        $repo = Container::getDocumentRepository();
4620
        $document = $repo->find($documentId);
4621
        if ($document) {
4622
            // Show the template list
4623
            $content .= '<div id="frmModel" class="scrollbar-inner lp-add-item"></div>';
4624
        }
4625
4626
        // Show the template list.
4627
        if (('document' === $type || 'step' === $type) && !isset($_GET['file'])) {
4628
            // Show the template list.
4629
            $content .= '<div id="frmModel" class="scrollbar-inner lp-add-item"></div>';
4630
        }
4631
4632
        return $content;
4633
    }
4634
4635
    /**
4636
     * @param bool  $updateAudio
4637
     * @param bool   $dropElement
4638
     *
4639
     * @return string
4640
     */
4641
    public function return_new_tree($updateAudio = false, $dropElement = false)
4642
    {
4643
        $list = $this->getBuildTree(false, $dropElement);
4644
        $return = Display::panelCollapse(
4645
            $this->name,
4646
            $list,
4647
            'scorm-list',
4648
            null,
4649
            'scorm-list-accordion',
4650
            'scorm-list-collapse'
4651
        );
4652
4653
        if ($updateAudio) {
4654
            //$return = $result['return_audio'];
4655
        }
4656
4657
        return $return;
4658
    }
4659
4660
    public function getBuildTree($noWrapper = false, $dropElement = false): string
4661
    {
4662
        $mainUrl = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?'.api_get_cidreq();
4663
        $upIcon = Display::getMdiIcon('arrow-up-bold', 'ch-tool-icon', '', 16, get_lang('Up'));
4664
        $disableUpIcon = Display::getMdiIcon('arrow-up-bold', 'ch-tool-icon-disabled', '', 16, get_lang('Up'));
4665
        $downIcon = Display::getMdiIcon('arrow-down-bold', 'ch-tool-icon', '', 16, get_lang('Down'));
4666
        $previewImage = Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', '', 16, get_lang('Preview'));
4667
4668
        $lpItemRepo = Container::getLpItemRepository();
4669
        $itemRoot = $lpItemRepo->getRootItem($this->get_id());
4670
4671
        $options = [
4672
            'decorate' => true,
4673
            'rootOpen' => function($tree) use ($noWrapper) {
4674
                if ($tree[0]['lvl'] === 1) {
4675
                    if ($noWrapper) {
4676
                        return '';
4677
                    }
4678
                    return '<ul id="lp_item_list" class="list-group nested-sortable">';
4679
                }
4680
4681
                return '<ul class="list-group nested-sortable">';
4682
            },
4683
            'rootClose' => function($tree) use ($noWrapper, $dropElement)  {
4684
                if ($tree[0]['lvl'] === 1) {
4685
                    if ($dropElement) {
4686
                        //return Display::return_message(get_lang('Drag and drop an element here'));
4687
                        //return $this->getDropElementHtml();
4688
                    }
4689
                    if ($noWrapper) {
4690
                        return '';
4691
                    }
4692
                }
4693
4694
                return '</ul>';
4695
            },
4696
            'childOpen' => function($child) {
4697
                $id   = $child['iid'];
4698
                $type = $child['itemType'] ?? ($child['item_type'] ?? '');
4699
                $isFinal = (TOOL_LP_FINAL_ITEM === $type);
4700
                $extraClass = $isFinal ? ' final-item disable_drag' : '';
4701
                $extraAttr  = $isFinal ? ' data-fixed="final"' : '';
4702
4703
                return '<li
4704
                    id="'.$id.'"
4705
                    data-id="'.$id.'"
4706
                    data-type="'.$type.'"
4707
                    '.$extraAttr.'
4708
                    class="flex flex-col list-group-item nested-'.$child['lvl'].$extraClass.'">';
4709
            },
4710
            'childClose' => '',
4711
            'nodeDecorator' => function ($node) use ($mainUrl, $previewImage, $upIcon, $downIcon) {
4712
                $fullTitle = $node['title'];
4713
                //$title = cut($fullTitle, self::MAX_LP_ITEM_TITLE_LENGTH);
4714
                $title = $fullTitle;
4715
                $itemId = $node['iid'];
4716
                $type = $node['itemType'];
4717
                $lpId = $this->get_id();
4718
4719
                $moveIcon = '';
4720
                if (TOOL_LP_FINAL_ITEM !== $type) {
4721
                    $moveIcon .= '<a class="moved" href="#">';
4722
                    $moveIcon .= Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
4723
                    $moveIcon .= '</a>';
4724
                }
4725
4726
                $iconName = str_replace(' ', '', $type);
4727
                $icon = '';
4728
                switch ($iconName) {
4729
                    case 'category':
4730
                    case 'chapter':
4731
                    case 'folder':
4732
                    case 'dir':
4733
                        $icon = Display::getMdiIcon(ObjectIcon::CHAPTER, 'ch-tool-icon', '', ICON_SIZE_TINY);
4734
                        break;
4735
                    default:
4736
                        $icon = Display::getMdiIcon(ObjectIcon::SINGLE_ELEMENT, 'ch-tool-icon', '', ICON_SIZE_TINY);
4737
                        break;
4738
                }
4739
4740
                $urlPreviewLink = $mainUrl.'&action=view_item&mode=preview_document&id='.$itemId.'&lp_id='.$lpId;
4741
                $previewIcon = Display::url(
4742
                    $previewImage,
4743
                    $urlPreviewLink,
4744
                    [
4745
                        'target' => '_blank',
4746
                        'class' => 'btn btn--plain',
4747
                        'data-title' => $title,
4748
                        'title' => $title,
4749
                    ]
4750
                );
4751
                $url = $mainUrl.'&view=build&id='.$itemId.'&lp_id='.$lpId;
4752
4753
                $preRequisitesIcon = Display::url(
4754
                    Display::getMdiIcon('graph', 'ch-tool-icon', '', 16, get_lang('Prerequisites')),
4755
                    $url.'&action=edit_item_prereq',
4756
                    ['class' => '']
4757
                );
4758
4759
                $editIcon = '<a
4760
                    href="'.$mainUrl.'&action=edit_item&view=build&id='.$itemId.'&lp_id='.$lpId.'&path_item='.$node['path'].'"
4761
                    class=""
4762
                    >';
4763
                $editIcon .= Display::getMdiIcon('pencil', 'ch-tool-icon', '', 16, get_lang('Edit section description/name'));
4764
                $editIcon .= '</a>';
4765
                $orderIcons = '';
4766
                /*if ('final_item' !== $type) {
4767
                    $orderIcons = Display::url(
4768
                        $upIcon,
4769
                        'javascript:void(0)',
4770
                        ['class' => 'btn btn--plain order_items', 'data-dir' => 'up', 'data-id' => $itemId]
4771
                    );
4772
                    $orderIcons .= Display::url(
4773
                        $downIcon,
4774
                        'javascript:void(0)',
4775
                        ['class' => 'btn btn--plain order_items', 'data-dir' => 'down', 'data-id' => $itemId]
4776
                    );
4777
                }*/
4778
4779
                $deleteIcon = ' <a
4780
                    data-id = '.$itemId.'
4781
                    data-title = \''.addslashes($title).'\'
4782
                    href="javascript:void(0);"
4783
                    onclick="return deleteItem(this);"
4784
                    class="">';
4785
                $deleteIcon .= Display::getMdiIcon('delete', 'ch-tool-icon', '', 16, get_lang('Delete section'));
4786
                $deleteIcon .= '</a>';
4787
                $extra = '';
4788
4789
                if ('dir' === $type && empty($node['__children'])) {
4790
                    $level = $node['lvl'] + 1;
4791
                    $extra = '<ul class="list-group nested-sortable">
4792
                                <li class="list-group-item list-group-item-empty nested-'.$level.'"></li>
4793
                              </ul>';
4794
                }
4795
4796
                $buttons = Display::tag(
4797
                    'div',
4798
                    "<div class=\"btn-group btn-group-sm\">
4799
                                $editIcon
4800
                                $preRequisitesIcon
4801
                                $orderIcons
4802
                                $deleteIcon
4803
                               </div>",
4804
                    ['class' => 'btn-toolbar button_actions']
4805
                );
4806
4807
                return
4808
                    "<div class='flex flex-row'> $moveIcon  $icon <span class='mx-1'>$title </span></div>
4809
                    $extra
4810
                    $buttons
4811
                    "
4812
                    ;
4813
            },
4814
        ];
4815
4816
        $tree = $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
4817
4818
        if (empty($tree) && $dropElement) {
4819
            return $this->getDropElementHtml($noWrapper);
4820
        }
4821
4822
        return $tree;
4823
    }
4824
4825
    public function getDropElementHtml($noWrapper = false)
4826
    {
4827
        $li = '<li class="list-group-item">'.
4828
            Display::return_message(get_lang('Drag and drop an element here')).
4829
            '</li>';
4830
        if ($noWrapper) {
4831
            return $li;
4832
        }
4833
4834
        return
4835
            '<ul id="lp_item_list" class="list-group nested-sortable">
4836
            '.$li.'
4837
            </ul>';
4838
    }
4839
4840
    /**
4841
     * This function builds the action menu.
4842
     *
4843
     * @param bool   $returnString           Optional
4844
     * @param bool   $showRequirementButtons Optional. Allow show the requirements button
4845
     * @param bool   $isConfigPage           Optional. If is the config page, show the edit button
4846
     * @param bool   $allowExpand            Optional. Allow show the expand/contract button
4847
     * @param string $action
4848
     * @param array  $extraField
4849
     *
4850
     * @return string
4851
     */
4852
    public function build_action_menu(
4853
        $returnString = false,
4854
        $showRequirementButtons = true,
4855
        $isConfigPage = false,
4856
        $allowExpand = true,
4857
        $action = '',
4858
        $extraField = []
4859
    ) {
4860
        $actionsRight = '';
4861
        $lpId = $this->lp_id;
4862
        if (!isset($extraField['backTo']) && empty($extraField['backTo'])) {
4863
            $back = Display::url(
4864
                Display::getMdiIcon('arrow-left-bold-box', 'ch-tool-icon', '', 32, get_lang('Back to learning paths')),
4865
                'lp_controller.php?'.api_get_cidreq()
4866
            );
4867
        } else {
4868
            $back = Display::url(
4869
                Display::getMdiIcon('arrow-left-bold-box', 'ch-tool-icon', '', 32, get_lang('Back')),
4870
                $extraField['backTo']
4871
            );
4872
        }
4873
4874
        /*if ($backToBuild) {
4875
            $back = Display::url(
4876
                Display::getMdiIcon('arrow-left-bold-box', 'ch-tool-icon', null, 32, get_lang('Go back')),
4877
                "lp_controller.php?action=add_item&type=step&lp_id=$lpId&".api_get_cidreq()
4878
            );
4879
        }*/
4880
4881
        $actionsLeft = $back;
4882
4883
        $actionsLeft .= Display::url(
4884
            Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', '', 32, get_lang('Preview')),
4885
            'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4886
                'action' => 'view',
4887
                'lp_id' => $lpId,
4888
                'isStudentView' => 'true',
4889
            ])
4890
        );
4891
4892
        /*$actionsLeft .= Display::url(
4893
            Display::getMdiIcon('music-note-plus', 'ch-tool-icon', null, 32, get_lang('Add audio')),
4894
            'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4895
                'action' => 'admin_view',
4896
                'lp_id' => $lpId,
4897
                'updateaudio' => 'true',
4898
            ])
4899
        );*/
4900
4901
        $subscriptionSettings = self::getSubscriptionSettings();
4902
4903
        $request = api_request_uri();
4904
        if (false === strpos($request, 'edit')) {
4905
            $actionsLeft .= Display::url(
4906
                Display::getMdiIcon('hammer-wrench', 'ch-tool-icon', '', 32, get_lang('Course settings')),
4907
                'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4908
                    'action' => 'edit',
4909
                    'lp_id' => $lpId,
4910
                ])
4911
            );
4912
        }
4913
4914
        if ((false === strpos($request, 'build') &&
4915
            false === strpos($request, 'add_item')) ||
4916
            in_array($action, ['add_audio'], true)
4917
        ) {
4918
            $actionsLeft .= Display::url(
4919
                Display::getMdiIcon('pencil', 'ch-tool-icon', '', 32, get_lang('Edit')),
4920
                'lp_controller.php?'.http_build_query([
4921
                    'action' => 'build',
4922
                    'lp_id' => $lpId,
4923
                ]).'&'.api_get_cidreq()
4924
            );
4925
        }
4926
4927
        if (false === strpos(api_get_self(), 'lp_subscribe_users.php')) {
4928
            if (1 == $this->subscribeUsers &&
4929
                $subscriptionSettings['allow_add_users_to_lp']) {
4930
                $actionsLeft .= Display::url(
4931
                    Display::getMdiIcon('account-multiple-plus', 'ch-tool-icon', '', 32, get_lang('Subscribe users to learning path')),
4932
                    api_get_path(WEB_CODE_PATH)."lp/lp_subscribe_users.php?lp_id=$lpId&".api_get_cidreq()
4933
                );
4934
            }
4935
        }
4936
4937
        if ($allowExpand) {
4938
            /*$actionsLeft .= Display::url(
4939
                Display::getMdiIcon('arrow-expand-all', 'ch-tool-icon', null, 32, get_lang('Expand')).
4940
                Display::getMdiIcon('arrow-collapse-all', 'ch-tool-icon', null, 32, get_lang('Collapse')),
4941
                '#',
4942
                ['role' => 'button', 'id' => 'hide_bar_template']
4943
            );*/
4944
        }
4945
4946
        if ($showRequirementButtons) {
4947
            $buttons = [
4948
                [
4949
                    'title' => get_lang('Set previous step as prerequisite for each step'),
4950
                    'href' => 'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4951
                        'action' => 'set_previous_step_as_prerequisite',
4952
                        'lp_id' => $lpId,
4953
                    ]),
4954
                ],
4955
                [
4956
                    'title' => get_lang('Clear all prerequisites'),
4957
                    'href' => 'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4958
                        'action' => 'clear_prerequisites',
4959
                        'lp_id' => $lpId,
4960
                    ]),
4961
                ],
4962
            ];
4963
            $actionsRight = Display::groupButtonWithDropDown(
4964
                get_lang('Prerequisites options'),
4965
                $buttons,
4966
                true
4967
            );
4968
        }
4969
4970
        if (api_is_platform_admin() && isset($extraField['authorlp'])) {
4971
            $actionsLeft .= Display::url(
4972
                Display::getMdiIcon('account-multiple-plus', 'ch-tool-icon', '', 32, get_lang('Author')),
4973
                'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4974
                    'action' => 'author_view',
4975
                    'lp_id' => $lpId,
4976
                ])
4977
            );
4978
        }
4979
4980
        $toolbar = Display::toolbarAction('actions-lp-controller', [$actionsLeft, $actionsRight]);
4981
4982
        if ($returnString) {
4983
            return $toolbar;
4984
        }
4985
4986
        echo $toolbar;
4987
    }
4988
4989
    /**
4990
     * Creates the default learning path folder.
4991
     *
4992
     * @param array $course
4993
     * @param int   $creatorId
4994
     *
4995
     * @return CDocument
4996
     */
4997
    public static function generate_learning_path_folder($course, $creatorId = 0)
4998
    {
4999
        // Creating learning_path folder
5000
        $dir = 'learning_path';
5001
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
5002
5003
        return create_unexisting_directory(
5004
            $course,
5005
            $creatorId,
5006
            0,
5007
            null,
5008
            0,
5009
            '',
5010
            $dir,
5011
            get_lang('Learning paths'),
5012
            0
5013
        );
5014
    }
5015
5016
    /**
5017
     * @param array  $course
5018
     * @param string $lp_name
5019
     * @param int    $creatorId
5020
     *
5021
     * @return CDocument
5022
     */
5023
    public function generate_lp_folder($course, $lp_name = '', $creatorId = 0)
5024
    {
5025
        $filepath = '';
5026
        $dir = '/learning_path/';
5027
5028
        if (empty($lp_name)) {
5029
            $lp_name = $this->name;
5030
        }
5031
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
5032
        $parent = self::generate_learning_path_folder($course, $creatorId);
5033
5034
        // Limits title size
5035
        $title = api_substr(api_replace_dangerous_char($lp_name), 0, 80);
5036
        $dir = $dir.$title;
5037
5038
        // Creating LP folder
5039
        $folder = null;
5040
        if ($parent) {
5041
            $folder = create_unexisting_directory(
5042
                $course,
5043
                $creatorId,
5044
                0,
5045
                0,
5046
                0,
5047
                $filepath,
5048
                $dir,
5049
                $lp_name,
5050
                '',
5051
                false,
5052
                false,
5053
                $parent
5054
            );
5055
        }
5056
5057
        return $folder;
5058
    }
5059
5060
    /**
5061
     * Create a new document //still needs some finetuning.
5062
     *
5063
     * @param array  $courseInfo
5064
     * @param string $content
5065
     * @param string $title
5066
     * @param string $extension
5067
     * @param int    $parentId
5068
     * @param int    $creatorId  creator id
5069
     *
5070
     * @return int
5071
     */
5072
    public function create_document(
5073
        $courseInfo,
5074
        $content = '',
5075
        $title = '',
5076
        $extension = 'html',
5077
        $parentId = 0,
5078
        $creatorId = 0,
5079
        $docFiletype = 'file'
5080
    ) {
5081
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
5082
        $sessionId = api_get_session_id();
5083
5084
        // Generates folder
5085
        $this->generate_lp_folder($courseInfo);
5086
        // stripslashes() before calling api_replace_dangerous_char() because $_POST['title']
5087
        // is already escaped twice when it gets here.
5088
        $originalTitle = !empty($title) ? $title : $_POST['title'];
5089
        if (!empty($title)) {
5090
            $title = api_replace_dangerous_char(stripslashes($title));
5091
        } else {
5092
            $title = api_replace_dangerous_char(stripslashes($_POST['title']));
5093
        }
5094
5095
        $title = disable_dangerous_file($title);
5096
        $filename = $title;
5097
        $tmp_filename = "$filename.$extension";
5098
        /*$i = 0;
5099
        while (file_exists($filepath.$tmp_filename.'.'.$extension)) {
5100
            $tmp_filename = $filename.'_'.++$i;
5101
        }*/
5102
        $filename = $tmp_filename.'.'.$extension;
5103
5104
        if ('html' === $extension) {
5105
            $content = stripslashes($content);
5106
            $content = str_replace(
5107
                api_get_path(WEB_COURSE_PATH),
5108
                api_get_path(REL_PATH).'courses/',
5109
                $content
5110
            );
5111
            $content = str_replace(
5112
                '</body>',
5113
                '<style type="text/css">body{}</style></body>',
5114
                $content
5115
            );
5116
        }
5117
5118
        $ext = strtolower((string) $extension);
5119
        if (in_array($ext, ['html','htm'], true)) {
5120
            $docFiletype = 'html';
5121
        }
5122
5123
        $document = DocumentManager::addDocument(
5124
            $courseInfo,
5125
            null,
5126
            $docFiletype,
5127
            '',
5128
            $tmp_filename,
5129
            '',
5130
            0, //readonly
5131
            true,
5132
            null,
5133
            $sessionId,
5134
            $creatorId,
5135
            false,
5136
            $content,
5137
            $parentId
5138
        );
5139
5140
        if ($document && in_array($ext, ['html','htm'], true)) {
5141
            $em = Database::getManager();
5142
            $docRepo = Container::getDocumentRepository();
5143
            $docEntity = $docRepo->find($document->getIid());
5144
            if ($docEntity && $docEntity->hasResourceNode()) {
5145
                $rf = $docEntity->getResourceNode()->getResourceFiles()->first();
5146
                if ($rf && $rf->getMimeType() !== 'text/html') {
5147
                    $rf->setMimeType('text/html');
5148
                    $em->flush();
5149
                }
5150
            }
5151
        }
5152
5153
        $document_id = $document->getIid();
5154
        if ($document_id) {
5155
            $new_comment = isset($_POST['comment']) ? trim($_POST['comment']) : '';
5156
            $new_title = $originalTitle;
5157
5158
            if ($new_comment || $new_title) {
5159
                $tbl_doc = Database::get_course_table(TABLE_DOCUMENT);
5160
                $ct = '';
5161
                if ($new_comment) {
5162
                    $ct .= ", comment='".Database::escape_string($new_comment)."'";
5163
                }
5164
                if ($new_title) {
5165
                    $ct .= ", title='".Database::escape_string($new_title)."' ";
5166
                }
5167
5168
                $sql = "UPDATE $tbl_doc SET ".substr($ct, 1)."
5169
                        WHERE iid = $document_id ";
5170
                Database::query($sql);
5171
            }
5172
        }
5173
5174
        return $document_id;
5175
    }
5176
5177
    /**
5178
     * Edit a document based on $_POST and $_GET parameters 'dir' and 'path'.
5179
     */
5180
    public function edit_document()
5181
    {
5182
        $repo = Container::getDocumentRepository();
5183
        if (isset($_REQUEST['document_id']) && !empty($_REQUEST['document_id'])) {
5184
            $id = (int) $_REQUEST['document_id'];
5185
            /** @var CDocument $document */
5186
            $document = $repo->find($id);
5187
            if ($document->getResourceNode()->hasEditableTextContent()) {
5188
                $repo->updateResourceFileContent($document, $_REQUEST['content_lp']);
5189
            }
5190
            $document->setTitle($_REQUEST['title']);
5191
            $repo->update($document);
5192
        }
5193
    }
5194
5195
    /**
5196
     * Displays the selected item, with a panel for manipulating the item.
5197
     *
5198
     * @param CLpItem $lpItem
5199
     * @param string  $msg
5200
     * @param bool    $show_actions
5201
     *
5202
     * @return string
5203
     */
5204
    public function display_item($lpItem, $msg = null, $show_actions = true)
5205
    {
5206
        $course_id = api_get_course_int_id();
5207
        $return = '';
5208
5209
        if (null === $lpItem) {
5210
            return '';
5211
        }
5212
        $item_id = $lpItem->getIid();
5213
        $itemType = $lpItem->getItemType();
5214
        $lpId = $lpItem->getLp()->getIid();
5215
        $path = $lpItem->getPath();
5216
5217
        Session::write('parent_item_id', 'dir' === $itemType ? $item_id : 0);
5218
5219
        // Prevents wrong parent selection for document, see Bug#1251.
5220
        if ('dir' !== $itemType) {
5221
            Session::write('parent_item_id', $lpItem->getParentItemId());
5222
        }
5223
5224
        if ($show_actions) {
5225
            $return .= $this->displayItemMenu($lpItem);
5226
        }
5227
        $return .= '<div style="padding:10px;">';
5228
5229
        if ('' != $msg) {
5230
            $return .= $msg;
5231
        }
5232
5233
        $return .= '<h3>'.$lpItem->getTitle().'</h3>';
5234
5235
        switch ($itemType) {
5236
            case TOOL_THREAD:
5237
                $link = $this->rl_get_resource_link_for_learnpath(
5238
                    $course_id,
5239
                    $lpId,
5240
                    $item_id,
5241
                    0
5242
                );
5243
                $return .= Display::url(
5244
                    get_lang('Go to thread'),
5245
                    $link,
5246
                    ['class' => 'btn btn--primary']
5247
                );
5248
                break;
5249
            case TOOL_FORUM:
5250
                $return .= Display::url(
5251
                    get_lang('Go to the forum'),
5252
                    api_get_path(WEB_CODE_PATH).'forum/viewforum.php?'.api_get_cidreq().'&forum='.$path,
5253
                    ['class' => 'btn btn--primary']
5254
                );
5255
                break;
5256
            case TOOL_QUIZ:
5257
                if (!empty($path)) {
5258
                    $exercise = new Exercise();
5259
                    $exercise->read($path);
5260
                    $return .= $exercise->description.'<br />';
5261
                    $return .= Display::url(
5262
                        get_lang('Go to exercise'),
5263
                        api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$exercise->id,
5264
                        ['class' => 'btn btn--primary']
5265
                    );
5266
                }
5267
                break;
5268
            case TOOL_LP_FINAL_ITEM:
5269
                $return .= $this->getSavedFinalItem();
5270
                break;
5271
            case TOOL_DOCUMENT:
5272
            case 'video':
5273
            case TOOL_READOUT_TEXT:
5274
                $repo = Container::getDocumentRepository();
5275
                /** @var CDocument $document */
5276
                $document = $repo->find($lpItem->getPath());
5277
                $return .= $this->display_document($document, true, true);
5278
                break;
5279
        }
5280
        $return .= '</div>';
5281
5282
        return $return;
5283
    }
5284
5285
    /**
5286
     * Shows the needed forms for editing a specific item.
5287
     *
5288
     * @param CLpItem $lpItem
5289
     *
5290
     * @throws Exception
5291
     *
5292
     *
5293
     * @return string
5294
     */
5295
    public function display_edit_item($lpItem, $excludeExtraFields = [])
5296
    {
5297
        $return = '';
5298
        if (empty($lpItem)) {
5299
            return '';
5300
        }
5301
        $itemType = $lpItem->getItemType();
5302
        $path = $lpItem->getPath();
5303
5304
        switch ($itemType) {
5305
            case 'dir':
5306
            case 'asset':
5307
            case 'sco':
5308
                if (isset($_GET['view']) && 'build' === $_GET['view']) {
5309
                    $return .= $this->displayItemMenu($lpItem);
5310
                    $return .= $this->display_item_form($lpItem, 'edit');
5311
                } else {
5312
                    $return .= $this->display_item_form($lpItem, 'edit_item');
5313
                }
5314
                break;
5315
            case TOOL_LP_FINAL_ITEM:
5316
            case TOOL_DOCUMENT:
5317
            case 'video':
5318
            case TOOL_READOUT_TEXT:
5319
                $return .= $this->displayItemMenu($lpItem);
5320
                $return .= $this->displayDocumentForm('edit', $lpItem);
5321
                break;
5322
            case TOOL_LINK:
5323
                $link = null;
5324
                if (!empty($path)) {
5325
                    $repo = Container::getLinkRepository();
5326
                    $link = $repo->find($path);
5327
                }
5328
                $return .= $this->displayItemMenu($lpItem);
5329
                $return .= $this->display_link_form('edit', $lpItem, $link);
5330
5331
                break;
5332
            case TOOL_QUIZ:
5333
                if (!empty($path)) {
5334
                    $repo = Container::getQuizRepository();
5335
                    $resource = $repo->find($path);
5336
                }
5337
                $return .= $this->displayItemMenu($lpItem);
5338
                $return .= $this->display_quiz_form('edit', $lpItem, $resource);
5339
                break;
5340
            case TOOL_STUDENTPUBLICATION:
5341
                if (!empty($path)) {
5342
                    $repo = Container::getStudentPublicationRepository();
5343
                    $resource = $repo->find($path);
5344
                }
5345
                $return .= $this->displayItemMenu($lpItem);
5346
                $return .= $this->display_student_publication_form('edit', $lpItem, $resource);
5347
                break;
5348
            case TOOL_FORUM:
5349
                if (!empty($path)) {
5350
                    $repo = Container::getForumRepository();
5351
                    $resource = $repo->find($path);
5352
                }
5353
                $return .= $this->displayItemMenu($lpItem);
5354
                $return .= $this->display_forum_form('edit', $lpItem, $resource);
5355
                break;
5356
            case TOOL_THREAD:
5357
                if (!empty($path)) {
5358
                    $repo = Container::getForumPostRepository();
5359
                    $resource = $repo->find($path);
5360
                }
5361
                $return .= $this->displayItemMenu($lpItem);
5362
                $return .= $this->display_thread_form('edit', $lpItem, $resource);
5363
                break;
5364
        }
5365
5366
        return $return;
5367
    }
5368
5369
    /**
5370
     * Function that displays a list with al the resources that
5371
     * could be added to the learning path.
5372
     *
5373
     * @throws Exception
5374
     */
5375
    public function displayResources(): string
5376
    {
5377
        // Get all the docs.
5378
        $documents = $this->get_documents(true);
5379
5380
        // Get all the exercises.
5381
        $exercises = $this->get_exercises();
5382
5383
        // Get all the links.
5384
        $links = $this->get_links();
5385
5386
        // Get all the student publications.
5387
        $works = $this->get_student_publications();
5388
5389
        // Get all the forums.
5390
        $forums = $this->get_forums();
5391
5392
        // Get all surveys
5393
        $surveys = $this->getSurveys();
5394
5395
        // Get the final item form (see BT#11048) .
5396
        $finish = $this->getFinalItemForm();
5397
        $size = ICON_SIZE_MEDIUM; //ICON_SIZE_BIG
5398
        $headers = [
5399
            Display::getMdiIcon('bookshelf', 'ch-tool-icon-gradient', '', 64, get_lang('Documents')),
5400
            Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon-gradient', '', 64, get_lang('Tests')),
5401
            Display::getMdiIcon('file-link', 'ch-tool-icon-gradient', '', 64, get_lang('Links')),
5402
            Display::getMdiIcon('inbox-full', 'ch-tool-icon-gradient', '', 64, get_lang('Assignments')),
5403
            Display::getMdiIcon('comment-quote', 'ch-tool-icon-gradient', '', 64, get_lang('Forums')),
5404
            Display::getMdiIcon('bookmark-multiple', 'ch-tool-icon-gradient', '', 64, get_lang('Add section')),
5405
            Display::getMdiIcon('form-dropdown', 'ch-tool-icon-gradient', '', 64, get_lang('Create survey')),
5406
            Display::getMdiIcon('certificate', 'ch-tool-icon-gradient', '', 64, get_lang('Certificate')),
5407
        ];
5408
        $content = '';
5409
        /*$content = Display::return_message(
5410
            get_lang('Click on the [Learner view] button to see your learning path'),
5411
            'normal'
5412
        );*/
5413
        $section = $this->displayNewSectionForm();
5414
        $selected = isset($_REQUEST['lp_build_selected']) ? (int) $_REQUEST['lp_build_selected'] : 0;
5415
5416
        return Display::tabs(
5417
            $headers,
5418
            [
5419
                $documents,
5420
                $exercises,
5421
                $links,
5422
                $works,
5423
                $forums,
5424
                $section,
5425
                $surveys,
5426
                $finish,
5427
            ],
5428
            'resource_tab',
5429
            ['class' => 'lp-resource-tabs'],
5430
            [],
5431
            $selected
5432
        );
5433
    }
5434
5435
    /**
5436
     * Returns the extension of a document.
5437
     *
5438
     * @param string $filename
5439
     *
5440
     * @return string Extension (part after the last dot)
5441
     */
5442
    public function get_extension($filename)
5443
    {
5444
        $explode = explode('.', $filename);
5445
5446
        return $explode[count($explode) - 1];
5447
    }
5448
5449
    /**
5450
     * @return string
5451
     */
5452
    public function getCurrentBuildingModeURL()
5453
    {
5454
        $pathItem = isset($_GET['path_item']) ? (int) $_GET['path_item'] : '';
5455
        $action = isset($_GET['action']) ? Security::remove_XSS($_GET['action']) : '';
5456
        $id = isset($_GET['id']) ? (int) $_GET['id'] : '';
5457
        $view = isset($_GET['view']) ? Security::remove_XSS($_GET['view']) : '';
5458
5459
        $currentUrl = api_get_self().'?'.api_get_cidreq().
5460
            '&action='.$action.'&lp_id='.$this->lp_id.'&path_item='.$pathItem.'&view='.$view.'&id='.$id;
5461
5462
        return $currentUrl;
5463
    }
5464
5465
    /**
5466
     * Displays a document by id.
5467
     *
5468
     * @param CDocument $document
5469
     * @param bool      $show_title
5470
     * @param bool      $iframe
5471
     * @param bool      $edit_link
5472
     *
5473
     * @return string
5474
     */
5475
    public function display_document($document, $show_title = false, $iframe = true, $edit_link = false)
5476
    {
5477
        $return = '';
5478
        if (!$document) {
5479
            return '';
5480
        }
5481
5482
        $repo = Container::getDocumentRepository();
5483
5484
        // TODO: Add a path filter.
5485
        if ($iframe) {
5486
            $url = $repo->getResourceFileUrl($document);
5487
5488
            $return .= '<iframe
5489
                id="learnpath_preview_frame"
5490
                frameborder="0"
5491
                height="400"
5492
                width="100%"
5493
                scrolling="auto"
5494
                src="'.$url.'"></iframe>';
5495
        } else {
5496
            $return = $repo->getResourceFileContent($document);
5497
        }
5498
5499
        return $return;
5500
    }
5501
5502
    /**
5503
     * Return HTML form to add/edit a link item.
5504
     *
5505
     * @param string  $action (add/edit)
5506
     * @param CLpItem $lpItem
5507
     * @param CLink   $link
5508
     *
5509
     * @throws Exception
5510
     *
5511
     *
5512
     * @return string HTML form
5513
     */
5514
    public function display_link_form($action, $lpItem, $link)
5515
    {
5516
        $item_url = '';
5517
        if ($link) {
5518
            $item_url = stripslashes($link->getUrl());
5519
        }
5520
        $form = new FormValidator(
5521
            'edit_link',
5522
            'POST',
5523
            $this->getCurrentBuildingModeURL()
5524
        );
5525
5526
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5527
5528
        $urlAttributes = ['class' => 'learnpath_item_form'];
5529
        $urlAttributes['disabled'] = 'disabled';
5530
        $form->addElement('url', 'url', get_lang('URL'), $urlAttributes);
5531
        $form->setDefault('url', $item_url);
5532
5533
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5534
5535
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
5536
    }
5537
5538
    /**
5539
     * Return HTML form to add/edit a quiz.
5540
     *
5541
     * @param string  $action   Action (add/edit)
5542
     * @param CLpItem $lpItem   Item ID if already exists
5543
     * @param CQuiz   $exercise Extra information (quiz ID if integer)
5544
     *
5545
     * @throws Exception
5546
     *
5547
     * @return string HTML form
5548
     */
5549
    public function display_quiz_form($action, $lpItem, $exercise)
5550
    {
5551
        $form = new FormValidator(
5552
            'quiz_form',
5553
            'POST',
5554
            $this->getCurrentBuildingModeURL()
5555
        );
5556
5557
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5558
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5559
5560
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
5561
    }
5562
5563
    /**
5564
     * Return the form to display the forum edit/add option.
5565
     *
5566
     * @param CLpItem $lpItem
5567
     *
5568
     * @throws Exception
5569
     *
5570
     * @return string HTML form
5571
     */
5572
    public function display_forum_form($action, $lpItem, $resource)
5573
    {
5574
        $form = new FormValidator(
5575
            'forum_form',
5576
            'POST',
5577
            $this->getCurrentBuildingModeURL()
5578
        );
5579
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5580
5581
        if ('add' === $action) {
5582
            $form->addButtonSave(get_lang('Add forum to course'), 'submit_button');
5583
        } else {
5584
            $form->addButtonSave(get_lang('Edit the current forum'), 'submit_button');
5585
        }
5586
5587
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
5588
    }
5589
5590
    /**
5591
     * Return HTML form to add/edit forum threads.
5592
     *
5593
     * @param string  $action
5594
     * @param CLpItem $lpItem
5595
     * @param string  $resource
5596
     *
5597
     * @throws Exception
5598
     *
5599
     * @return string HTML form
5600
     */
5601
    public function display_thread_form($action, $lpItem, $resource)
5602
    {
5603
        $form = new FormValidator(
5604
            'thread_form',
5605
            'POST',
5606
            $this->getCurrentBuildingModeURL()
5607
        );
5608
5609
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
5610
5611
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5612
5613
        return $form->returnForm();
5614
    }
5615
5616
    /**
5617
     * Return the HTML form to display an item (generally a dir item).
5618
     *
5619
     * @param CLpItem $lpItem
5620
     * @param string  $action
5621
     *
5622
     * @throws Exception
5623
     *
5624
     *
5625
     * @return string HTML form
5626
     */
5627
    public function display_item_form(
5628
        $lpItem,
5629
        $action = 'add_item'
5630
    ) {
5631
        $item_type = $lpItem->getItemType();
5632
5633
        $url = api_get_self().'?'.api_get_cidreq().'&action='.$action.'&type='.$item_type.'&lp_id='.$this->lp_id;
5634
5635
        $form = new FormValidator('form_'.$item_type, 'POST', $url);
5636
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
5637
5638
        $form->addButtonSave(get_lang('Save section'), 'submit_button');
5639
5640
        return $form->returnForm();
5641
    }
5642
5643
    /**
5644
     * Return HTML form to add/edit a student publication (work).
5645
     *
5646
     * @param string              $action
5647
     * @param CStudentPublication $resource
5648
     *
5649
     * @throws Exception
5650
     *
5651
     * @return string HTML form
5652
     */
5653
    public function display_student_publication_form($action, CLpItem $lpItem, $resource)
5654
    {
5655
        $form = new FormValidator('frm_student_publication', 'post', '#');
5656
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
5657
5658
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5659
5660
        $return = '<div class="sectioncomment">';
5661
        $return .= $form->returnForm();
5662
        $return .= '</div>';
5663
5664
        return $return;
5665
    }
5666
5667
    public function displayNewSectionForm()
5668
    {
5669
        $action = 'add_item';
5670
        $item_type = 'dir';
5671
5672
        $lpItem = (new CLpItem())
5673
            ->setTitle('')
5674
            ->setItemType('dir')
5675
        ;
5676
5677
        $url = api_get_self().'?'.api_get_cidreq().'&action='.$action.'&type='.$item_type.'&lp_id='.$this->lp_id;
5678
5679
        $form = new FormValidator('form_'.$item_type, 'POST', $url);
5680
        LearnPathItemForm::setForm($form, 'add', $this, $lpItem);
5681
5682
        $form->addButtonSave(get_lang('Save section'), 'submit_button');
5683
        $form->addElement('hidden', 'type', 'dir');
5684
5685
        return $form->returnForm();
5686
    }
5687
5688
    /**
5689
     * Returns the form to update or create a document.
5690
     *
5691
     * @param string  $action (add/edit)
5692
     * @param CLpItem $lpItem
5693
     *
5694
     *
5695
     * @throws Exception
5696
     *
5697
     * @return string HTML form
5698
     */
5699
    public function displayDocumentForm($action = 'add', $lpItem = null)
5700
    {
5701
        $courseInfo = api_get_course_info();
5702
5703
        $form = new FormValidator(
5704
            'form',
5705
            'POST',
5706
            $this->getCurrentBuildingModeURL(),
5707
            '',
5708
            ['enctype' => 'multipart/form-data']
5709
        );
5710
5711
        $data = $this->generate_lp_folder($courseInfo);
5712
5713
        if (null !== $lpItem) {
5714
            LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5715
        }
5716
5717
        switch ($action) {
5718
            case 'add':
5719
                $folders = DocumentManager::get_all_document_folders($courseInfo, 0, true);
5720
                DocumentManager::build_directory_selector(
5721
                    $folders,
5722
                    '',
5723
                    [],
5724
                    true,
5725
                    $form,
5726
                    'directory_parent_id'
5727
                );
5728
                if ($data) {
5729
                    $form->setDefaults(['directory_parent_id' => $data->getIid()]);
5730
                }
5731
                break;
5732
        }
5733
5734
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5735
5736
        $script = '<script>
5737
document.addEventListener("DOMContentLoaded", function () {
5738
    var form = document.getElementById("form") || document.forms["form"];
5739
    if (!form) return;
5740
    form.addEventListener("submit", function (e) {
5741
        var btn = form.querySelector("button[name=submit_button], input[name=submit_button]");
5742
        if (!btn) return;
5743
        // async disable to avoid interfering with possible sync handlers triggered on submit
5744
        setTimeout(function () {
5745
            try {
5746
                btn.disabled = true;
5747
                if (btn.classList) btn.classList.add("disabled");
5748
            } catch (err) {
5749
                // silent
5750
            }
5751
        }, 0);
5752
    }, { once: true }); // once: true to disable only on the first submit
5753
});
5754
</script>';
5755
5756
        return $form->returnForm() . $script;
5757
    }
5758
5759
    /**
5760
     * @param array  $courseInfo
5761
     * @param string $content
5762
     * @param string $title
5763
     * @param int    $parentId
5764
     *
5765
     * @return int
5766
     */
5767
    public function createReadOutText($courseInfo, $content = '', $title = '', $parentId = 0)
5768
    {
5769
        $creatorId = api_get_user_id();
5770
        $sessionId = api_get_session_id();
5771
5772
        // Generates folder
5773
        $result = $this->generate_lp_folder($courseInfo);
5774
        $dir = $result['dir'];
5775
5776
        if (empty($parentId) || '/' === $parentId) {
5777
            $postDir = isset($_POST['dir']) ? $_POST['dir'] : $dir;
5778
            $dir = isset($_GET['dir']) ? $_GET['dir'] : $postDir; // Please, do not modify this dirname formatting.
5779
5780
            if ('/' === $parentId) {
5781
                $dir = '/';
5782
            }
5783
5784
            // Please, do not modify this dirname formatting.
5785
            if (strstr($dir, '..')) {
5786
                $dir = '/';
5787
            }
5788
5789
            if (!empty($dir[0]) && '.' == $dir[0]) {
5790
                $dir = substr($dir, 1);
5791
            }
5792
            if (!empty($dir[0]) && '/' != $dir[0]) {
5793
                $dir = '/'.$dir;
5794
            }
5795
            if (isset($dir[strlen($dir) - 1]) && '/' != $dir[strlen($dir) - 1]) {
5796
                $dir .= '/';
5797
            }
5798
        } else {
5799
            $parentInfo = DocumentManager::get_document_data_by_id(
5800
                $parentId,
5801
                $courseInfo['code']
5802
            );
5803
            if (!empty($parentInfo)) {
5804
                $dir = $parentInfo['path'].'/';
5805
            }
5806
        }
5807
5808
        $filepath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/'.$dir;
5809
5810
        if (!is_dir($filepath)) {
5811
            $dir = '/';
5812
            $filepath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/'.$dir;
5813
        }
5814
5815
        $originalTitle = !empty($title) ? $title : $_POST['title'];
5816
5817
        if (!empty($title)) {
5818
            $title = api_replace_dangerous_char(stripslashes($title));
5819
        } else {
5820
            $title = api_replace_dangerous_char(stripslashes($_POST['title']));
5821
        }
5822
5823
        $title = disable_dangerous_file($title);
5824
        $filename = $title;
5825
        $content = !empty($content) ? $content : $_POST['content_lp'];
5826
        $tmpFileName = $filename;
5827
5828
        $i = 0;
5829
        while (file_exists($filepath.$tmpFileName.'.html')) {
5830
            $tmpFileName = $filename.'_'.++$i;
5831
        }
5832
5833
        $filename = $tmpFileName.'.html';
5834
        $content = stripslashes($content);
5835
5836
        if (file_exists($filepath.$filename)) {
5837
            return 0;
5838
        }
5839
5840
        $putContent = file_put_contents($filepath.$filename, $content);
5841
5842
        if (false === $putContent) {
5843
            return 0;
5844
        }
5845
5846
        $fileSize = filesize($filepath.$filename);
5847
        $saveFilePath = $dir.$filename;
5848
5849
        $document = DocumentManager::addDocument(
5850
            $courseInfo,
5851
            $saveFilePath,
5852
            'file',
5853
            $fileSize,
5854
            $tmpFileName,
5855
            '',
5856
            0, //readonly
5857
            true,
5858
            null,
5859
            $sessionId,
5860
            $creatorId
5861
        );
5862
5863
        $documentId = $document->getIid();
5864
5865
        if (!$document) {
5866
            return 0;
5867
        }
5868
5869
        $newComment = isset($_POST['comment']) ? trim($_POST['comment']) : '';
5870
        $newTitle = $originalTitle;
5871
5872
        if ($newComment || $newTitle) {
5873
            $em = Database::getManager();
5874
5875
            if ($newComment) {
5876
                $document->setComment($newComment);
5877
            }
5878
5879
            if ($newTitle) {
5880
                $document->setTitle($newTitle);
5881
            }
5882
5883
            $em->persist($document);
5884
            $em->flush();
5885
        }
5886
5887
        return $documentId;
5888
    }
5889
5890
    /**
5891
     * Displays the menu for manipulating a step.
5892
     *
5893
     * @return string
5894
     */
5895
    public function displayItemMenu(CLpItem $lpItem)
5896
    {
5897
        $item_id = $lpItem->getIid();
5898
        $audio = $lpItem->getAudio();
5899
        $itemType = $lpItem->getItemType();
5900
        $path = $lpItem->getPath();
5901
5902
        $return = '';
5903
        $audio_player = null;
5904
        // We display an audio player if needed.
5905
        if (!empty($audio)) {
5906
            /*$webAudioPath = '../..'.api_get_path(REL_COURSE_PATH).$_course['path'].'/document/audio/'.$row['audio'];
5907
            $audio_player .= '<div class="lp_mediaplayer" id="container">'
5908
                .'<audio src="'.$webAudioPath.'" controls>'
5909
                .'</div><br>';*/
5910
        }
5911
5912
        $url = api_get_self().'?'.api_get_cidreq().'&view=build&id='.$item_id.'&lp_id='.$this->lp_id;
5913
5914
        if (TOOL_LP_FINAL_ITEM !== $itemType) {
5915
            $return .= Display::url(
5916
                Display::getMdiIcon('pencil', 'ch-tool-icon', null, 22, get_lang('Edit')),
5917
                $url.'&action=edit_item&path_item='.$path
5918
            );
5919
5920
            /*$return .= Display::url(
5921
                Display::getMdiIcon('arrow-right-bold', 'ch-tool-icon', null, 22, get_lang('Move')),
5922
                $url.'&action=move_item'
5923
            );*/
5924
        }
5925
5926
        // Commented for now as prerequisites cannot be added to chapters.
5927
        if ('dir' !== $itemType) {
5928
            $return .= Display::url(
5929
                Display::getMdiIcon('graph', 'ch-tool-icon', null, 22, get_lang('Prerequisites')),
5930
                $url.'&action=edit_item_prereq'
5931
            );
5932
        }
5933
        $return .= Display::url(
5934
            Display::getMdiIcon('delete', 'ch-tool-icon', null, 22, get_lang('Delete')),
5935
            $url.'&action=delete_item'
5936
        );
5937
5938
        /*if (in_array($itemType, [TOOL_DOCUMENT, TOOL_LP_FINAL_ITEM, TOOL_HOTPOTATOES])) {
5939
            $documentData = DocumentManager::get_document_data_by_id($path, $course_code);
5940
            if (empty($documentData)) {
5941
                // Try with iid
5942
                $table = Database::get_course_table(TABLE_DOCUMENT);
5943
                $sql = "SELECT path FROM $table
5944
                        WHERE
5945
                              c_id = ".api_get_course_int_id()." AND
5946
                              iid = ".$path." AND
5947
                              path NOT LIKE '%_DELETED_%'";
5948
                $result = Database::query($sql);
5949
                $documentData = Database::fetch_array($result);
5950
                if ($documentData) {
5951
                    $documentData['absolute_path_from_document'] = '/document'.$documentData['path'];
5952
                }
5953
            }
5954
            if (isset($documentData['absolute_path_from_document'])) {
5955
                $return .= get_lang('File').': '.$documentData['absolute_path_from_document'];
5956
            }
5957
        }*/
5958
5959
        if (!empty($audio_player)) {
5960
            $return .= $audio_player;
5961
        }
5962
5963
        return Display::toolbarAction('lp_item', [$return]);
5964
    }
5965
5966
    /**
5967
     * Creates the javascript needed for filling up the checkboxes without page reload.
5968
     *
5969
     * @return string
5970
     */
5971
    public function get_js_dropdown_array()
5972
    {
5973
        $return = 'var child_name = new Array();'."\n";
5974
        $return .= 'var child_value = new Array();'."\n\n";
5975
        $return .= 'child_name[0] = new Array();'."\n";
5976
        $return .= 'child_value[0] = new Array();'."\n\n";
5977
5978
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
5979
        $sql = "SELECT * FROM ".$tbl_lp_item."
5980
                WHERE
5981
                    lp_id = ".$this->lp_id." AND
5982
                    parent_item_id = 0
5983
                ORDER BY display_order ASC";
5984
        Database::query($sql);
5985
        $i = 0;
5986
5987
        $list = $this->getItemsForForm(true);
5988
5989
        foreach ($list as $row_zero) {
5990
            if (TOOL_LP_FINAL_ITEM !== $row_zero['item_type']) {
5991
                if (TOOL_QUIZ == $row_zero['item_type']) {
5992
                    $row_zero['title'] = Exercise::get_formated_title_variable($row_zero['title']);
5993
                }
5994
                $js_var = json_encode(get_lang('After').' '.$row_zero['title']);
5995
                $return .= 'child_name[0]['.$i.'] = '.$js_var.' ;'."\n";
5996
                $return .= 'child_value[0]['.$i++.'] = "'.$row_zero['iid'].'";'."\n";
5997
            }
5998
        }
5999
6000
        $return .= "\n";
6001
        $sql = "SELECT * FROM $tbl_lp_item
6002
                WHERE lp_id = ".$this->lp_id;
6003
        $res = Database::query($sql);
6004
        while ($row = Database::fetch_array($res)) {
6005
            $sql_parent = "SELECT * FROM ".$tbl_lp_item."
6006
                           WHERE
6007
                                parent_item_id = ".$row['iid']."
6008
                           ORDER BY display_order ASC";
6009
            $res_parent = Database::query($sql_parent);
6010
            $i = 0;
6011
            $return .= 'child_name['.$row['iid'].'] = new Array();'."\n";
6012
            $return .= 'child_value['.$row['iid'].'] = new Array();'."\n\n";
6013
6014
            while ($row_parent = Database::fetch_array($res_parent)) {
6015
                $js_var = json_encode(get_lang('After').' '.$this->cleanItemTitle($row_parent['title']));
6016
                $return .= 'child_name['.$row['iid'].']['.$i.'] =   '.$js_var.' ;'."\n";
6017
                $return .= 'child_value['.$row['iid'].']['.$i++.'] = "'.$row_parent['iid'].'";'."\n";
6018
            }
6019
            $return .= "\n";
6020
        }
6021
6022
        $return .= "
6023
            function load_cbo(id) {
6024
                if (!id) {
6025
                    return false;
6026
                }
6027
6028
                var cbo = document.getElementById('previous');
6029
                if (cbo) {
6030
                    for(var i = cbo.length - 1; i > 0; i--) {
6031
                        cbo.options[i] = null;
6032
                    }
6033
                    var k=0;
6034
                    for (var i = 1; i <= child_name[id].length; i++){
6035
                        var option = new Option(child_name[id][i - 1], child_value[id][i - 1]);
6036
                        option.style.paddingLeft = '40px';
6037
                        cbo.options[i] = option;
6038
                        k = i;
6039
                    }
6040
                    cbo.options[k].selected = true;
6041
                }
6042
6043
                //$('#previous').selectpicker('refresh');
6044
            }";
6045
6046
        return $return;
6047
    }
6048
6049
    /**
6050
     * Display the form to allow moving an item.
6051
     *
6052
     * @param CLpItem $lpItem
6053
     *
6054
     * @throws Exception
6055
     *
6056
     *
6057
     * @return string HTML form
6058
     */
6059
    public function display_move_item($lpItem)
6060
    {
6061
        $return = '';
6062
        $path = $lpItem->getPath();
6063
6064
        if ($lpItem) {
6065
            $itemType = $lpItem->getItemType();
6066
            switch ($itemType) {
6067
                case 'dir':
6068
                case 'asset':
6069
                    $return .= $this->displayItemMenu($lpItem);
6070
                    $return .= $this->display_item_form(
6071
                        $lpItem,
6072
                        get_lang('Move the current section'),
6073
                        'move',
6074
                        $row
6075
                    );
6076
                    break;
6077
                case TOOL_DOCUMENT:
6078
                case 'video':
6079
                    $return .= $this->displayItemMenu($lpItem);
6080
                    $return .= $this->displayDocumentForm('move', $lpItem);
6081
                    break;
6082
                case TOOL_LINK:
6083
                    $link = null;
6084
                    if (!empty($path)) {
6085
                        $repo = Container::getLinkRepository();
6086
                        $link = $repo->find($path);
6087
                    }
6088
                    $return .= $this->displayItemMenu($lpItem);
6089
                    $return .= $this->display_link_form('move', $lpItem, $link);
6090
                    break;
6091
                case TOOL_HOTPOTATOES:
6092
                    $return .= $this->displayItemMenu($lpItem);
6093
                    $return .= $this->display_link_form('move', $lpItem, $row);
6094
                    break;
6095
                case TOOL_QUIZ:
6096
                    $return .= $this->displayItemMenu($lpItem);
6097
                    $return .= $this->display_quiz_form('move', $lpItem, $row);
6098
                    break;
6099
                case TOOL_STUDENTPUBLICATION:
6100
                    $return .= $this->displayItemMenu($lpItem);
6101
                    $return .= $this->display_student_publication_form('move', $lpItem, $row);
6102
                    break;
6103
                case TOOL_FORUM:
6104
                    $return .= $this->displayItemMenu($lpItem);
6105
                    $return .= $this->display_forum_form('move', $lpItem, $row);
6106
                    break;
6107
                case TOOL_THREAD:
6108
                    $return .= $this->displayItemMenu($lpItem);
6109
                    $return .= $this->display_forum_form('move', $lpItem, $row);
6110
                    break;
6111
            }
6112
        }
6113
6114
        return $return;
6115
    }
6116
6117
    /**
6118
     * Return HTML form to allow prerequisites selection.
6119
     *
6120
     * @todo use FormValidator
6121
     *
6122
     * @return string HTML form
6123
     */
6124
    public function displayItemPrerequisitesForm(CLpItem $lpItem)
6125
    {
6126
        $courseId = api_get_course_int_id();
6127
        $preRequisiteId = (int) ($lpItem->getPrerequisite() ?? 0);
6128
        $currentItemId = $lpItem->getIid();
6129
6130
        $title = get_lang('Add/edit prerequisites').' '.$lpItem->getTitle();
6131
6132
        // Load scores
6133
        $tblLpItem = Database::get_course_table(TABLE_LP_ITEM);
6134
        $sql = "SELECT * FROM $tblLpItem WHERE lp_id = ".$this->lp_id;
6135
        $result = Database::query($sql);
6136
6137
        $selectedMinScore = [];
6138
        $selectedMaxScore = [];
6139
        $masteryScore = [];
6140
6141
        while ($row = Database::fetch_array($result)) {
6142
            if ($row['iid'] == $currentItemId) {
6143
                $selectedMinScore[$row['prerequisite']] = $row['prerequisite_min_score'];
6144
                $selectedMaxScore[$row['prerequisite']] = $row['prerequisite_max_score'];
6145
            }
6146
            $masteryScore[$row['iid']] = $row['mastery_score'];
6147
        }
6148
6149
        $displayOrder = $lpItem->getDisplayOrder();
6150
        $lpItemRepo = Container::getLpItemRepository();
6151
        $itemRoot = $lpItemRepo->getRootItem($this->get_id());
6152
        $em = Database::getManager();
6153
6154
        $noneChecked = (empty($preRequisiteId) || (int) $preRequisiteId === 0) ? ' checked="checked" ' : '';
6155
6156
        $return  = '<div class="space-y-6">';
6157
        $return .= Display::page_header($title);
6158
6159
        $return .= '<form method="POST" class="space-y-6">';
6160
        $return .= '  <div class="bg-white border border-gray-200 rounded-2xl shadow-sm overflow-hidden">';
6161
        $return .= '    <div class="overflow-x-auto">';
6162
        $return .= '      <table class="min-w-full divide-y divide-gray-200">';
6163
        $return .= '        <thead class="bg-gray-20">';
6164
        $return .= '          <tr>';
6165
        $return .= '            <th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">'.get_lang('Prerequisites').'</th>';
6166
        $return .= '            <th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-36">'.get_lang('minimum').'</th>';
6167
        $return .= '            <th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-36">'.get_lang('maximum').'</th>';
6168
        $return .= '          </tr>';
6169
        $return .= '        </thead>';
6170
        $return .= '        <tbody class="divide-y divide-gray-100 bg-white">';
6171
6172
        // None row
6173
        $return .= '          <tr class="hover:bg-gray-20">';
6174
        $return .= '            <td colspan="3" class="px-6 py-4">';
6175
        $return .= '              <label for="idnone" class="flex items-center gap-3 cursor-pointer select-none">';
6176
        $return .= '                <input '.$noneChecked.' id="idnone" name="prerequisites" type="radio" value="0" class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-500" />';
6177
        $return .= '                <span class="text-sm text-gray-800">'.get_lang('none').'</span>';
6178
        $return .= '              </label>';
6179
        $return .= '            </td>';
6180
        $return .= '          </tr>';
6181
6182
        $options = [
6183
            'decorate' => true,
6184
            'rootOpen' => function () { return ''; },
6185
            'rootClose' => function () { return ''; },
6186
            'childOpen' => function () { return ''; },
6187
            'childClose' => '',
6188
            'nodeDecorator' => function ($item) use (
6189
                $currentItemId,
6190
                $preRequisiteId,
6191
                $courseId,
6192
                $selectedMaxScore,
6193
                $selectedMinScore,
6194
                $masteryScore,
6195
                $displayOrder,
6196
                $lpItemRepo,
6197
                $em
6198
            ) {
6199
                $itemId = $item['iid'];
6200
                $type = $item['itemType'];
6201
6202
                if ($itemId == $currentItemId) {
6203
                    return '';
6204
                }
6205
                if ($displayOrder < $item['displayOrder']) {
6206
                    return '';
6207
                }
6208
6209
                $iconName = str_replace(' ', '', $type);
6210
                switch ($iconName) {
6211
                    case 'category':
6212
                    case 'chapter':
6213
                    case 'folder':
6214
                    case 'dir':
6215
                        $icon = Display::getMdiIcon(ObjectIcon::CHAPTER, 'ch-tool-icon', '', ICON_SIZE_TINY);
6216
                        break;
6217
                    default:
6218
                        $icon = Display::getMdiIcon(ObjectIcon::SINGLE_ELEMENT, 'ch-tool-icon', '', ICON_SIZE_TINY);
6219
                        break;
6220
                }
6221
6222
                $selectedMaxScoreValue = isset($selectedMaxScore[$itemId]) ? $selectedMaxScore[$itemId] : $item['maxScore'];
6223
                $selectedMinScoreValue = $selectedMinScore[$itemId] ?? 0;
6224
                $masteryScoreAsMinValue = $masteryScore[$itemId] ?? 0;
6225
6226
                $itemIdInt = (int) $itemId;
6227
                $refInt    = (int) ($item['ref'] ?? 0);
6228
6229
                $checked = '';
6230
                if ($preRequisiteId !== 0 && ($preRequisiteId === $itemIdInt || ($refInt !== 0 && $preRequisiteId === $refInt))) {
6231
                    $checked = ' checked="checked" ';
6232
                }
6233
6234
                $isDisabled = ('dir' === $type);
6235
                $disabledAttr = $isDisabled ? ' disabled="disabled" ' : '';
6236
                $rowHover = $isDisabled ? '' : ' hover:bg-gray-10';
6237
                $labelClass = $isDisabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer';
6238
6239
                $indentPx = (int) ($item['lvl'] ?? 0) * 20;
6240
6241
                $radio = '
6242
                <label for="id'.$itemId.'" class="flex items-center gap-3 select-none '.$labelClass.'" style="padding-left:'.$indentPx.'px;">
6243
                    <input
6244
                        '.$checked.' '.$disabledAttr.'
6245
                        id="id'.$itemId.'"
6246
                        name="prerequisites"
6247
                        type="radio"
6248
                        value="'.$itemId.'"
6249
                        class="h-4 w-4 border-gray-50 text-indigo-600 focus:ring-indigo-500"
6250
                    />
6251
                    <span class="flex items-center gap-2 text-sm text-gray-800">'.$icon.'<span>'.$item['title'].'</span></span>
6252
                </label>
6253
            ';
6254
6255
                $return = '<tr class="'.$rowHover.'">';
6256
6257
                $isScored = (TOOL_QUIZ == $type || TOOL_HOTPOTATOES == $type);
6258
6259
                // Main cell
6260
                if ($isScored) {
6261
                    $return .= '<td class="px-6 py-3">'.$radio.'</td>';
6262
                } else {
6263
                    $return .= '<td colspan="3" class="px-6 py-3">'.$radio.'</td>';
6264
                }
6265
6266
                // Score inputs
6267
                if (TOOL_QUIZ == $type) {
6268
                    $exercise = new Exercise($courseId);
6269
                    /** @var CLpItem $itemEntity */
6270
                    $itemEntity = $lpItemRepo->find($itemId);
6271
                    $exercise->read($item['path']);
6272
                    $itemEntity->setMaxScore($exercise->getMaxScore());
6273
                    $em->persist($itemEntity);
6274
                    $em->flush($itemEntity);
6275
6276
                    $item['maxScore'] = $exercise->getMaxScore();
6277
6278
                    if (empty($selectedMinScoreValue) && !empty($masteryScoreAsMinValue)) {
6279
                        $selectedMinScoreValue = $masteryScoreAsMinValue;
6280
                    }
6281
6282
                    $inputClass = 'w-28 rounded-lg border border-gray-50 shadow-sm px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500';
6283
                    $return .= '<td class="px-6 py-3">
6284
                    <input class="'.$inputClass.'" name="min_'.$itemId.'" type="number" min="0" step="any" max="'.$item['maxScore'].'" value="'.$selectedMinScoreValue.'" />
6285
                </td>';
6286
                    $return .= '<td class="px-6 py-3">
6287
                    <input class="'.$inputClass.'" name="max_'.$itemId.'" type="number" min="0" step="any" max="'.$item['maxScore'].'" value="'.$selectedMaxScoreValue.'" />
6288
                </td>';
6289
                }
6290
6291
                if (TOOL_HOTPOTATOES == $type) {
6292
                    $inputClass = 'w-28 rounded-lg border border-gray-50 shadow-sm px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500';
6293
                    $return .= '<td class="px-6 py-3">
6294
                    <input class="'.$inputClass.'" name="min_'.$itemId.'" type="number" min="0" step="any" max="'.$item['maxScore'].'" value="'.$selectedMinScoreValue.'" />
6295
                </td>';
6296
                    $return .= '<td class="px-6 py-3">
6297
                    <input class="'.$inputClass.'" name="max_'.$itemId.'" type="number" min="0" step="any" max="'.$item['maxScore'].'" value="'.$selectedMaxScoreValue.'" />
6298
                </td>';
6299
                }
6300
                $return .= '</tr>';
6301
6302
                return $return;
6303
            },
6304
        ];
6305
6306
        $tree = $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
6307
        $return .= $tree;
6308
6309
        $return .= '        </tbody>';
6310
        $return .= '      </table>';
6311
        $return .= '    </div>';
6312
6313
        // Footer actions
6314
        $return .= '    <div class="border-t border-gray-20 bg-gray-20 px-6 py-4 flex items-center justify-end">';
6315
        $return .= '      <button class="btn btn--primary" name="submit_button" type="submit">'.get_lang('Save prerequisites settings').'</button>';
6316
        $return .= '    </div>';
6317
6318
        $return .= '  </div>';
6319
        $return .= '</form>';
6320
        $return .= '</div>';
6321
6322
        return $return;
6323
    }
6324
6325
    /**
6326
     * Return HTML list to allow prerequisites selection for lp.
6327
     */
6328
    public function display_lp_prerequisites_list(FormValidator $form)
6329
    {
6330
        $lp_id = $this->lp_id;
6331
        $lp = api_get_lp_entity($lp_id);
6332
        $prerequisiteId = $lp->getPrerequisite();
6333
6334
        $repo = Container::getLpRepository();
6335
        $qb = $repo->findAllByCourse(api_get_course_entity(), api_get_session_entity());
6336
        /** @var CLp[] $lps */
6337
        $lps = $qb->getQuery()->getResult();
6338
6339
        //$session_id = api_get_session_id();
6340
        /*$session_condition = api_get_session_condition($session_id, true, true);
6341
        $sql = "SELECT * FROM $tbl_lp
6342
                WHERE c_id = $course_id $session_condition
6343
                ORDER BY display_order ";
6344
        $rs = Database::query($sql);*/
6345
6346
        $items = [get_lang('none')];
6347
        foreach ($lps as $lp) {
6348
            $myLpId = $lp->getIid();
6349
            if ($myLpId == $lp_id) {
6350
                continue;
6351
            }
6352
            $items[$myLpId] = $lp->getTitle();
6353
            /*$return .= '<option
6354
                value="'.$myLpId.'" '.(($myLpId == $prerequisiteId) ? ' selected ' : '').'>'.
6355
                $lp->getName().
6356
                '</option>';*/
6357
        }
6358
6359
        $select = $form->addSelect('prerequisites', get_lang('Prerequisites'), $items);
6360
        $select->setSelected($prerequisiteId);
6361
    }
6362
6363
    /**
6364
     * Creates a list with all the documents in it.
6365
     *
6366
     * @param bool $showInvisibleFiles
6367
     *
6368
     * @throws Exception
6369
     *
6370
     *
6371
     * @return string
6372
     */
6373
    public function get_documents($showInvisibleFiles = false)
6374
    {
6375
        $sessionId = api_get_session_id();
6376
        $documentTree = DocumentManager::get_document_preview(
6377
            api_get_course_entity(),
6378
            $this->lp_id,
6379
            null,
6380
            $sessionId,
6381
            true,
6382
            null,
6383
            null,
6384
            $showInvisibleFiles,
6385
            false,
6386
            false,
6387
            true,
6388
            false,
6389
            [],
6390
            [],
6391
            ['file', 'folder'],
6392
            true
6393
        );
6394
6395
        $form = new FormValidator(
6396
            'form_upload',
6397
            'POST',
6398
            $this->getCurrentBuildingModeURL(),
6399
            '',
6400
            ['enctype' => 'multipart/form-data']
6401
        );
6402
6403
        $folders = DocumentManager::get_all_document_folders(
6404
            api_get_course_info(),
6405
            0,
6406
            true
6407
        );
6408
6409
        $folder = $this->generate_lp_folder(api_get_course_info());
6410
6411
        DocumentManager::build_directory_selector(
6412
            $folders,
6413
            $folder->getIid(),
6414
            [],
6415
            true,
6416
            $form,
6417
            'directory_parent_id'
6418
        );
6419
6420
        $group = [
6421
            $form->createElement(
6422
                'radio',
6423
                'if_exists',
6424
                get_lang('If file exists:'),
6425
                get_lang('Do nothing'),
6426
                'nothing'
6427
            ),
6428
            $form->createElement(
6429
                'radio',
6430
                'if_exists',
6431
                null,
6432
                get_lang('Overwrite the existing file'),
6433
                'overwrite'
6434
            ),
6435
            $form->createElement(
6436
                'radio',
6437
                'if_exists',
6438
                null,
6439
                get_lang('Rename the uploaded file if it exists'),
6440
                'rename'
6441
            ),
6442
        ];
6443
        $form->addGroup($group, null, get_lang('If file exists:'));
6444
6445
        $fileExistsOption = api_get_setting('document.document_if_file_exists_option');
6446
        $defaultFileExistsOption = 'rename';
6447
        if (!empty($fileExistsOption)) {
6448
            $defaultFileExistsOption = $fileExistsOption;
6449
        }
6450
        $form->setDefaults(['if_exists' => $defaultFileExistsOption]);
6451
6452
        // Check box options
6453
        $form->addCheckBox(
6454
            'unzip',
6455
            get_lang('Options'),
6456
            get_lang('Uncompress zip')
6457
        );
6458
6459
        $url = api_get_path(WEB_AJAX_PATH).'document.ajax.php?'
6460
            .api_get_cidreq()
6461
            .'&a=upload_file&curdirpath='
6462
            .'&lp_id='.(int) $this->lp_id
6463
            .'&lp_auto_add=1';
6464
6465
        $form->addHidden('lp_id', (string) (int) $this->lp_id);
6466
        $form->addHidden('lp_auto_add', '1');
6467
6468
        $form->addMultipleUpload($url);
6469
6470
        $lpItem = (new CLpItem())
6471
            ->setTitle('')
6472
            ->setItemType(TOOL_DOCUMENT)
6473
        ;
6474
6475
        // Original document form (editor + fields).
6476
        $newForm = $this->displayDocumentForm('add', $lpItem);
6477
6478
        // New templates panel (left side).
6479
        $templatesPanel = $this->renderDocumentTemplatesPanel();
6480
6481
        if (!empty($templatesPanel)) {
6482
            // Two-column layout: templates on the left, form on the right.
6483
            $new = '
6484
            <div class="lp-document-create flex flex-col lg:flex-row gap-4">
6485
                <div class="w-full lg:w-1/3">
6486
                    '.$templatesPanel.'
6487
                </div>
6488
                <div class="w-full lg:w-2/3">
6489
                    '.$newForm.'
6490
                </div>
6491
            </div>
6492
        ';
6493
        } else {
6494
            // Fallback: only the form, as before.
6495
            $new = $newForm;
6496
        }
6497
6498
        $videosTree = $this->get_videos();
6499
        $headers = [
6500
            get_lang('Files'),
6501
            get_lang('Videos'),
6502
            get_lang('Create a new document'),
6503
            get_lang('Upload'),
6504
        ];
6505
6506
        $uploadHtml = $form->returnForm();
6507
        $refreshUrl = $this->getCurrentBuildingModeURL();
6508
6509
        $uploadHtml .= '
6510
<script>
6511
document.addEventListener("DOMContentLoaded", function () {
6512
6513
  if (typeof window.jQuery === "undefined") {
6514
    return;
6515
  }
6516
6517
  var needsReload = false;
6518
6519
  function extractJsonFromUploadData(data) {
6520
    try {
6521
      if (!data) return null;
6522
      if (data.result) return data.result;
6523
      if (data.responseJSON) return data.responseJSON;
6524
      if (data.jqXHR && data.jqXHR.responseJSON) return data.jqXHR.responseJSON;
6525
      if (data.jqXHR && data.jqXHR.responseText) {
6526
        return JSON.parse(data.jqXHR.responseText);
6527
      }
6528
      return null;
6529
    } catch (e) {
6530
      return null;
6531
    }
6532
  }
6533
6534
  // Fired per-file when upload completes
6535
  jQuery(document).on("fileuploaddone", function (e, data) {
6536
    var json = extractJsonFromUploadData(data);
6537
    if (json && (json.lp_refresh === true || json.lp_refresh === 1)) {
6538
      needsReload = true;
6539
    }
6540
  });
6541
6542
  // Fired after all uploads are done
6543
  jQuery(document).on("fileuploadstop", function () {
6544
    if (!needsReload) return;
6545
    window.location.href = '.json_encode($refreshUrl).';
6546
  });
6547
});
6548
</script>';
6549
6550
        return Display::tabs(
6551
            $headers,
6552
            [$documentTree, $videosTree, $new, $uploadHtml],
6553
            'subtab',
6554
            ['class' => 'mt-3 lp-subtabs']
6555
        );
6556
    }
6557
6558
    /**
6559
     * Render system and course templates panel for LP document editor.
6560
     */
6561
    protected function renderDocumentTemplatesPanel(): string
6562
    {
6563
        $courseId = api_get_course_int_id();
6564
        if (empty($courseId)) {
6565
            return '';
6566
        }
6567
6568
        try {
6569
            $router = Container::getRouter();
6570
            $url = $router->generate('all-templates', ['courseId' => $courseId]);
6571
        } catch (\Throwable $e) {
6572
            return '';
6573
        }
6574
6575
        if (empty($url)) {
6576
            return '';
6577
        }
6578
6579
        $url = Security::remove_XSS($url);
6580
6581
        $html  = '<div id="lp-doc-template-panel"';
6582
        $html .= ' class="lp-doc-template-panel mb-4 lg:mb-0 bg-support-2 border border-gray-25 rounded-lg p-3 text-body-2 text-gray-90"';
6583
        $html .= ' data-templates-url="'.$url.'">';
6584
        $html .= '<div class="flex items-center justify-between mb-3">';
6585
        $html .= '<span class="font-semibold">'.get_lang('Templates').'</span>';
6586
        $html .= '</div>';
6587
        $html .= '<div id="lp-doc-template-list" class="space-y-2 max-h-96 overflow-y-auto"></div>';
6588
        $html .= '</div>';
6589
6590
        $html .= <<<'JS'
6591
        <script>
6592
        (function () {
6593
            var panel = document.getElementById('lp-doc-template-panel');
6594
            if (!panel) { return; }
6595
6596
            var listEl = document.getElementById('lp-doc-template-list');
6597
            var url = panel.getAttribute('data-templates-url');
6598
            if (!url || !listEl) { return; }
6599
6600
            function encodeContent(html) {
6601
                var value = html || '';
6602
                try {
6603
                    return btoa(unescape(encodeURIComponent(value)));
6604
                } catch (e) {
6605
                    return btoa(value);
6606
                }
6607
            }
6608
6609
            function decodeContent(encoded) {
6610
                if (!encoded) { return ''; }
6611
                try {
6612
                    return decodeURIComponent(escape(atob(encoded)));
6613
                } catch (e) {
6614
                    return atob(encoded);
6615
                }
6616
            }
6617
6618
            // Try to find the main HTML editor used in LP document form
6619
            function getCkEditorInstance() {
6620
                if (typeof CKEDITOR === 'undefined' || !CKEDITOR.instances) {
6621
                    return null;
6622
                }
6623
6624
                // Prefer common field names
6625
                var preferred = ['content_lp', 'content', 'content[content]'];
6626
                for (var i = 0; i < preferred.length; i++) {
6627
                    var key = preferred[i];
6628
                    if (CKEDITOR.instances[key]) {
6629
                        return CKEDITOR.instances[key];
6630
                    }
6631
                }
6632
6633
                // Fallback: first instance found
6634
                for (var id in CKEDITOR.instances) {
6635
                    if (Object.prototype.hasOwnProperty.call(CKEDITOR.instances, id)) {
6636
                        return CKEDITOR.instances[id];
6637
                    }
6638
                }
6639
6640
                return null;
6641
            }
6642
6643
            function applyTemplate(encoded) {
6644
                var content = decodeContent(encoded);
6645
6646
                // CKEditor
6647
                var ck = getCkEditorInstance();
6648
                if (ck && ck.setData) {
6649
                    ck.setData(content);
6650
                    return;
6651
                }
6652
6653
                // TinyMCE (just in case)
6654
                if (typeof tinyMCE !== 'undefined') {
6655
                    if (tinyMCE.activeEditor && tinyMCE.activeEditor.setContent) {
6656
                        tinyMCE.activeEditor.setContent(content);
6657
                        return;
6658
                    }
6659
                    if (tinyMCE.editors && tinyMCE.editors.length && tinyMCE.editors[0].setContent) {
6660
                        tinyMCE.editors[0].setContent(content);
6661
                        return;
6662
                    }
6663
                }
6664
6665
                // FCKeditor fallback
6666
                if (typeof FCKeditorAPI !== 'undefined' && FCKeditorAPI.GetInstance) {
6667
                    var fckNames = ['content_lp', 'content'];
6668
                    for (var i = 0; i < fckNames.length; i++) {
6669
                        var inst = FCKeditorAPI.GetInstance(fckNames[i]);
6670
                        if (inst && inst.SetHTML) {
6671
                            inst.SetHTML(content);
6672
                            return;
6673
                        }
6674
                    }
6675
                }
6676
6677
                // Plain textarea fallback
6678
                var txt =
6679
                    document.getElementById('content_lp') ||
6680
                    document.getElementById('content') ||
6681
                    document.querySelector('textarea[name="content_lp"], textarea[name="content"]');
6682
6683
                if (txt) {
6684
                    txt.value = content;
6685
                } else {
6686
                    console.warn(
6687
                        'LP document templates: no editor instance or textarea found to apply template content.'
6688
                    );
6689
                }
6690
            }
6691
6692
            function findTemplateButton(target) {
6693
                while (target && target !== panel) {
6694
                    if (target.classList && target.classList.contains('lp-template-pill')) {
6695
                        return target;
6696
                    }
6697
                    target = target.parentNode;
6698
                }
6699
                return null;
6700
            }
6701
6702
            fetch(url, { credentials: 'same-origin' })
6703
                .then(function (response) {
6704
                    return response.json();
6705
                })
6706
                .then(function (data) {
6707
                    if (!Array.isArray(data)) {
6708
                        return;
6709
                    }
6710
6711
                    if (data.length === 0) {
6712
                        listEl.classList.add('text-gray-50', 'text-xs');
6713
                        listEl.textContent = 'No templates found';
6714
                        return;
6715
                    }
6716
6717
                    data.forEach(function (tpl) {
6718
                        // Try different property names for content
6719
                        var rawContent = tpl.content || tpl.contentFile || tpl.html || '';
6720
                        var encoded = encodeContent(rawContent);
6721
6722
                        var btn = document.createElement('button');
6723
                        btn.type = 'button';
6724
                        btn.className =
6725
                            'lp-template-pill w-full text-left px-3 py-2 mb-2 rounded-lg border border-gray-25 ' +
6726
                            'bg-white text-xs text-gray-90 hover:border-primary hover:bg-support-1 hover:text-primary ' +
6727
                            'transition flex flex-col';
6728
                        btn.setAttribute('data-template', encoded);
6729
6730
                        if (tpl.image) {
6731
                            var img = document.createElement('img');
6732
                            img.src = tpl.image;
6733
                            img.alt = tpl.title || '';
6734
                            img.className = 'mb-2 rounded border border-gray-25 max-h-20 w-full object-cover';
6735
                            btn.appendChild(img);
6736
                        }
6737
6738
                        var span = document.createElement('span');
6739
                        span.className = 'truncate';
6740
                        span.textContent = tpl.title || '';
6741
                        btn.appendChild(span);
6742
6743
                        listEl.appendChild(btn);
6744
                    });
6745
                })
6746
                .catch(function (error) {
6747
                    console.error('Unable to load document templates for LP editor', error);
6748
                });
6749
6750
            panel.addEventListener('click', function (event) {
6751
                var button = findTemplateButton(event.target);
6752
                if (!button) {
6753
                    return;
6754
                }
6755
                var encoded = button.getAttribute('data-template');
6756
                if (!encoded) {
6757
                    return;
6758
                }
6759
                applyTemplate(encoded);
6760
            });
6761
        })();
6762
        </script>
6763
        JS;
6764
6765
        return $html;
6766
    }
6767
6768
6769
    public function get_videos()
6770
    {
6771
        $sessionId = api_get_session_id();
6772
6773
        $documentTree = DocumentManager::get_document_preview(
6774
            api_get_course_entity(),
6775
            $this->lp_id,
6776
            null,
6777
            $sessionId,
6778
            true,
6779
            null,
6780
            null,
6781
            false,
6782
            false,
6783
            false,
6784
            true,
6785
            false,
6786
            [],
6787
            [],
6788
            'video',
6789
            true
6790
        );
6791
6792
        return $documentTree ?: get_lang('No video found');
6793
    }
6794
6795
    /**
6796
     * Creates a list with all the exercises (quiz) in it.
6797
     *
6798
     * @return string
6799
     */
6800
    public function get_exercises()
6801
    {
6802
        $course_id = api_get_course_int_id();
6803
        $session_id = api_get_session_id();
6804
        $setting = 'true' === api_get_setting('lp.show_invisible_exercise_in_lp_toc');
6805
6806
        $active = 2;
6807
        if ($setting) {
6808
            $active = 1;
6809
        }
6810
        $keyword = $_REQUEST['keyword'] ?? null;
6811
        $categoryId = $_REQUEST['category_id'] ?? null;
6812
        $course = api_get_course_entity($course_id);
6813
        $session = api_get_session_entity($session_id);
6814
6815
        $qb = Container::getQuizRepository()->findAllByCourse($course, $session, $keyword, $active, false, $categoryId);
6816
        /** @var CQuiz[] $exercises */
6817
        $exercises = $qb->getQuery()->getResult();
6818
6819
        $return = '<ul class="mt-2 bg-white list-group list-group-flush border border-gray-25 rounded lp_resource">';
6820
        $return .= '<li class="list-group-item lp_resource_element border-gray-25 disable_drag">';
6821
        $return .= Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, 32, get_lang('New test'));
6822
        $return .= '<a
6823
            href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise_admin.php?'.api_get_cidreq().'&lp_id='.$this->lp_id.'">'.
6824
            get_lang('New test').'</a>';
6825
        $return .= '</li>';
6826
6827
        $previewIcon = Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview'));
6828
        $quizIcon = Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, 16, get_lang('Test'));
6829
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6830
        $exerciseUrl = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq();
6831
        foreach ($exercises as $exercise) {
6832
            $exerciseId = $exercise->getIid();
6833
            $title = strip_tags(api_html_entity_decode($exercise->getTitle()));
6834
            $visibility = $exercise->isVisible($course, $session);
6835
6836
            $link = Display::url(
6837
                $previewIcon,
6838
                $exerciseUrl.'&exerciseId='.$exerciseId,
6839
                ['target' => '_blank']
6840
            );
6841
            $return .= '<li
6842
                class="list-group-item lp_resource_element border-gray-25"
6843
                id="'.$exerciseId.'"
6844
                data-id="'.$exerciseId.'"
6845
                title="'.$title.'">';
6846
            $return .= Display::url($moveIcon, '#', ['class' => 'moved']);
6847
            $return .= $quizIcon;
6848
            $sessionStar = '';
6849
            $return .= Display::url(
6850
                Security::remove_XSS(cut($title, 80)).$link.$sessionStar,
6851
                api_get_self().'?'.
6852
                    api_get_cidreq().'&action=add_item&type='.TOOL_QUIZ.'&file='.$exerciseId.'&lp_id='.$this->lp_id,
6853
                [
6854
                    'class' => false === $visibility ? 'moved text-muted ' : 'moved link_with_id',
6855
                    'data_type' => 'quiz',
6856
                    'data-id' => $exerciseId,
6857
                ]
6858
            );
6859
            $return .= '</li>';
6860
        }
6861
6862
        $return .= '</ul>';
6863
6864
        return $return;
6865
    }
6866
6867
    /**
6868
     * Creates a list with all the links in it.
6869
     *
6870
     * @return string
6871
     */
6872
    public function get_links()
6873
    {
6874
        $sessionId = api_get_session_id();
6875
        $repo = Container::getLinkRepository();
6876
6877
        $course = api_get_course_entity();
6878
        $session = api_get_session_entity($sessionId);
6879
        $qb = $repo->getResourcesByCourse($course, $session);
6880
        /** @var CLink[] $links */
6881
        $links = $qb->getQuery()->getResult();
6882
6883
        $selfUrl = api_get_self();
6884
        $courseIdReq = api_get_cidreq();
6885
        $userInfo = api_get_user_info();
6886
6887
        $moveEverywhereIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6888
6889
        $categorizedLinks = [];
6890
        $categories = [];
6891
6892
        foreach ($links as $link) {
6893
            $categoryId = null !== $link->getCategory() ? $link->getCategory()->getIid() : 0;
6894
            if (empty($categoryId)) {
6895
                $categories[0] = get_lang('Uncategorized');
6896
            } else {
6897
                $category = $link->getCategory();
6898
                $categories[$categoryId] = $category->getTitle();
6899
            }
6900
            $categorizedLinks[$categoryId][$link->getIid()] = $link;
6901
        }
6902
6903
        $linksHtmlCode =
6904
            '<script>
6905
            function toggle_tool(tool, id) {
6906
                if(document.getElementById(tool+"_"+id+"_content").style.display == "none"){
6907
                    document.getElementById(tool+"_"+id+"_content").style.display = "block";
6908
                    document.getElementById(tool+"_"+id+"_opener").src = "'.Display::returnIconPath('remove.gif').'";
6909
                } else {
6910
                    document.getElementById(tool+"_"+id+"_content").style.display = "none";
6911
                    document.getElementById(tool+"_"+id+"_opener").src = "'.Display::returnIconPath('add.png').'";
6912
                }
6913
            }
6914
        </script>
6915
6916
        <ul class="mt-2 bg-white list-group lp_resource">
6917
            <li class="list-group-item lp_resource_element border-gray-25 disable_drag ">
6918
                '.Display::getMdiIcon(ObjectIcon::LINK, 'ch-tool-icon', null, ICON_SIZE_SMALL).'
6919
                <a
6920
                href="'.api_get_path(WEB_CODE_PATH).'link/link.php?'.$courseIdReq.'&action=addlink&lp_id='.$this->lp_id.'"
6921
                title="'.get_lang('Add a link').'">'.
6922
                get_lang('Add a link').'
6923
                </a>
6924
            </li>';
6925
        $linkIcon = Display::getMdiIcon('file-link', 'ch-tool-icon', null, 16, get_lang('Link'));
6926
        foreach ($categorizedLinks as $categoryId => $links) {
6927
            $linkNodes = null;
6928
            /** @var CLink $link */
6929
            foreach ($links as $key => $link) {
6930
                $title = $link->getTitle();
6931
                $id = $link->getIid();
6932
                $linkUrl = Display::url(
6933
                    Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6934
                    api_get_path(WEB_CODE_PATH).'link/link_goto.php?'.api_get_cidreq().'&link_id='.$key,
6935
                    ['target' => '_blank']
6936
                );
6937
6938
                if ($link->isVisible($course, $session)) {
6939
                    $sessionStar = '';
6940
                    $url = $selfUrl.'?'.$courseIdReq.'&action=add_item&type='.TOOL_LINK.'&file='.$key.'&lp_id='.$this->lp_id;
6941
                    $link = Display::url(
6942
                        Security::remove_XSS($title).$sessionStar.$linkUrl,
6943
                        $url,
6944
                        [
6945
                            'class' => 'moved link_with_id',
6946
                            'data-id' => $key,
6947
                            'data_type' => TOOL_LINK,
6948
                            'title' => $title,
6949
                        ]
6950
                    );
6951
                    $linkNodes .=
6952
                        "<li
6953
                            class='list-group-item border-gray-25 lp_resource_element'
6954
                            id= $id
6955
                            data-id= $id
6956
                            >
6957
                         <a class='moved' href='#'>
6958
                            $moveEverywhereIcon
6959
                        </a>
6960
                        $linkIcon $link
6961
                        </li>";
6962
                }
6963
            }
6964
            $linksHtmlCode .=
6965
                '<li class="list-group-item border-gray-25 disable_drag">
6966
                    <a style="cursor:hand" onclick="javascript: toggle_tool(\''.TOOL_LINK.'\','.$categoryId.')" >
6967
                        <img src="'.Display::returnIconPath('add.png').'" id="'.TOOL_LINK.'_'.$categoryId.'_opener"
6968
                        align="absbottom" />
6969
                    </a>
6970
                    <span style="vertical-align:middle">'.Security::remove_XSS($categories[$categoryId]).'</span>
6971
                </li>
6972
            '.
6973
                $linkNodes.
6974
            '';
6975
        }
6976
        $linksHtmlCode .= '</ul>';
6977
6978
        return $linksHtmlCode;
6979
    }
6980
6981
    /**
6982
     * Creates a list with all the student publications in it.
6983
     *
6984
     * @return string
6985
     */
6986
    public function get_student_publications()
6987
    {
6988
        $return = '<ul class="mt-2 bg-white list-group list-group-flush border border-gray-25 rounded lp_resource">';
6989
        $return .= '<li class="list-group-item border-gray-25 lp_resource_element">';
6990
        $works = getWorkListTeacher(0, 100, null, null, null);
6991
        if (!empty($works)) {
6992
            $icon = Display::getMdiIcon('inbox-full', 'ch-tool-icon',null, 16, get_lang('Assignments'));
6993
            foreach ($works as $work) {
6994
                $workId = $work['iid'];
6995
                $link = Display::url(
6996
                    Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6997
                    api_get_path(WEB_CODE_PATH).'work/work_list_all.php?'.api_get_cidreq().'&id='.$workId,
6998
                    ['target' => '_blank']
6999
                );
7000
7001
                $return .= '<li
7002
                    class="list-group-item border-gray-25 lp_resource_element"
7003
                    id="'.$workId.'"
7004
                    data-id="'.$workId.'"
7005
                    >';
7006
                $return .= '<a class="moved" href="#">';
7007
                $return .= Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
7008
                $return .= '</a> ';
7009
7010
                $return .= $icon;
7011
                $return .= Display::url(
7012
                    Security::remove_XSS(cut(strip_tags($work['title']), 80)).' '.$link,
7013
                    api_get_self().'?'.
7014
                    api_get_cidreq().'&action=add_item&type='.TOOL_STUDENTPUBLICATION.'&file='.$work['iid'].'&lp_id='.$this->lp_id,
7015
                    [
7016
                        'class' => 'moved link_with_id',
7017
                        'data-id' => $work['iid'],
7018
                        'data_type' => TOOL_STUDENTPUBLICATION,
7019
                        'title' => Security::remove_XSS(cut(strip_tags($work['title']), 80)),
7020
                    ]
7021
                );
7022
                $return .= '</li>';
7023
            }
7024
        }
7025
7026
        $return .= '</ul>';
7027
7028
        return $return;
7029
    }
7030
7031
    /**
7032
     * Creates a list with all the forums in it.
7033
     *
7034
     * @return string
7035
     */
7036
    public function get_forums()
7037
    {
7038
        $forumCategories = get_forum_categories();
7039
        $forumsInNoCategory = get_forums_in_category(0);
7040
        if (!empty($forumsInNoCategory)) {
7041
            $forumCategories = array_merge(
7042
                $forumCategories,
7043
                [
7044
                    [
7045
                        'cat_id' => 0,
7046
                        'session_id' => 0,
7047
                        'visibility' => 1,
7048
                        'cat_comment' => null,
7049
                    ],
7050
                ]
7051
            );
7052
        }
7053
7054
        $a_forums = [];
7055
        $courseEntity = api_get_course_entity(api_get_course_int_id());
7056
        $sessionEntity = api_get_session_entity(api_get_session_id());
7057
7058
        foreach ($forumCategories as $forumCategory) {
7059
            // The forums in this category.
7060
            $forumsInCategory = get_forums_in_category($forumCategory->getIid());
7061
            if (!empty($forumsInCategory)) {
7062
                foreach ($forumsInCategory as $forum) {
7063
                    if ($forum->isVisible($courseEntity, $sessionEntity)) {
7064
                        $a_forums[] = $forum;
7065
                    }
7066
                }
7067
            }
7068
        }
7069
7070
        $return = '<ul class="mt-2 bg-white list-group list-group-flush border border-gray-25 rounded lp_resource">';
7071
7072
        // First add link
7073
        $return .= '<li class="list-group-item border-gray-25 lp_resource_element disable_drag">';
7074
        $return .= Display::getMdiIcon('comment-quote	', 'ch-tool-icon', null, 32, get_lang('Create a new forum'));
7075
        $return .= Display::url(
7076
            get_lang('Create a new forum'),
7077
            api_get_path(WEB_CODE_PATH).'forum/index.php?'.api_get_cidreq().'&'.http_build_query([
7078
                'action' => 'add',
7079
                'content' => 'forum',
7080
                'lp_id' => $this->lp_id,
7081
            ]),
7082
            ['title' => get_lang('Create a new forum')]
7083
        );
7084
        $return .= '</li>';
7085
7086
        $return .= '<script>
7087
            function toggle_forum(forum_id) {
7088
                if (document.getElementById("forum_"+forum_id+"_content").style.display == "none") {
7089
                    document.getElementById("forum_"+forum_id+"_content").style.display = "block";
7090
                    document.getElementById("forum_"+forum_id+"_opener").src = "'.Display::returnIconPath('remove.gif').'";
7091
                } else {
7092
                    document.getElementById("forum_"+forum_id+"_content").style.display = "none";
7093
                    document.getElementById("forum_"+forum_id+"_opener").src = "'.Display::returnIconPath('add.png').'";
7094
                }
7095
            }
7096
        </script>';
7097
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
7098
        $userRights = api_is_allowed_to_edit(false, true);
7099
        foreach ($a_forums as $forum) {
7100
            $forumSession = $forum->getFirstResourceLink()->getSession();
7101
            $isForumSession = (null !== $forumSession);
7102
            $forumId = $forum->getIid();
7103
            $title = Security::remove_XSS($forum->getTitle());
7104
            $link = Display::url(
7105
                Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
7106
                api_get_path(WEB_CODE_PATH).'forum/viewforum.php?'.api_get_cidreq().'&forum='.$forumId,
7107
                ['target' => '_blank']
7108
            );
7109
7110
            $return .= '<li
7111
                    class="list-group-item border-gray-25 lp_resource_element"
7112
                    id="'.$forumId.'"
7113
                    data-id="'.$forumId.'"
7114
                    >';
7115
            $return .= '<a class="moved" href="#">';
7116
            $return .= $moveIcon;
7117
            $return .= ' </a>';
7118
            $return .= Display::getMdiIcon('comment-quote', 'ch-tool-icon', null, 16, get_lang('Forum'));
7119
7120
            $moveLink = Display::url(
7121
                $title,
7122
                api_get_self().'?'.
7123
                api_get_cidreq().'&action=add_item&type='.TOOL_FORUM.'&forum_id='.$forumId.'&lp_id='.$this->lp_id,
7124
                [
7125
                    'class' => 'moved link_with_id',
7126
                    'data-id' => $forumId,
7127
                    'data_type' => TOOL_FORUM,
7128
                    'title' => $title,
7129
                    'style' => 'vertical-align:middle',
7130
                ]
7131
            );
7132
            $return .= '<a onclick="javascript:toggle_forum('.$forumId.');" style="cursor:hand; vertical-align:middle">
7133
                    <img
7134
                        src="'.Display::returnIconPath('add.png').'"
7135
                        id="forum_'.$forumId.'_opener" align="absbottom"
7136
                     />
7137
                </a>
7138
                '.$moveLink;
7139
            $return .= '</li>';
7140
7141
            $return .= '<div style="display:none" id="forum_'.$forumId.'_content">';
7142
            $threads = get_threads($forumId);
7143
            if (is_array($threads)) {
7144
                foreach ($threads as $thread) {
7145
                    $threadId = $thread->getIid();
7146
                    $link = Display::url(
7147
                        Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
7148
                        api_get_path(WEB_CODE_PATH).
7149
                        'forum/viewthread.php?'.api_get_cidreq().'&forum='.$forumId.'&thread='.$threadId,
7150
                        ['target' => '_blank']
7151
                    );
7152
7153
                    $return .= '<li
7154
                        class="list-group-item border-gray-25 lp_resource_element"
7155
                      id="'.$threadId.'"
7156
                        data-id="'.$threadId.'"
7157
                    >';
7158
                    $return .= '&nbsp;<a class="moved" href="#">';
7159
                    $return .= $moveIcon;
7160
                    $return .= ' </a>';
7161
                    $return .= Display::getMdiIcon('format-quote-open', 'ch-tool-icon', null, 16, get_lang('Thread'));
7162
                    $return .= '<a
7163
                        class="moved link_with_id"
7164
                        data-id="'.$threadId.'"
7165
                        data_type="'.TOOL_THREAD.'"
7166
                        title="'.$thread->getTitle().'"
7167
                        href="'.api_get_self().'?'.api_get_cidreq().'&action=add_item&type='.TOOL_THREAD.'&thread_id='.$threadId.'&lp_id='.$this->lp_id.'"
7168
                        >'.
7169
                        Security::remove_XSS($thread->getTitle()).' '.$link.'</a>';
7170
                    $return .= '</li>';
7171
                }
7172
            }
7173
            $return .= '</div>';
7174
        }
7175
        $return .= '</ul>';
7176
7177
        return $return;
7178
    }
7179
7180
    /**
7181
     * Creates a list with all the surveys in it.
7182
     *
7183
     * @return string
7184
     */
7185
    public function getSurveys()
7186
    {
7187
        $return = '<ul class="mt-2 bg-white list-group list-group-flush border border-gray-25 rounded lp_resource">';
7188
7189
        // First add link
7190
        $return .= '<li class="list-group-item border-gray-25 lp_resource_element disable_drag">';
7191
        $return .= Display::getMdiIcon('clipboard-question-outline', 'ch-tool-icon', null, 32, get_lang('Create survey'));
7192
        $return .= Display::url(
7193
            get_lang('Create survey'),
7194
            api_get_path(WEB_CODE_PATH).'survey/create_new_survey.php?'.api_get_cidreq().'&'.http_build_query([
7195
                'action' => 'add',
7196
                'lp_id' => $this->lp_id,
7197
            ]),
7198
            ['title' => get_lang('Create survey')]
7199
        );
7200
        $return .= '</li>';
7201
7202
        $surveys = SurveyManager::get_surveys(api_get_course_id(), api_get_session_id());
7203
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
7204
7205
        foreach ($surveys as $survey) {
7206
            if (!empty($survey['iid'])) {
7207
                $surveyTitle = strip_tags($survey['title']);
7208
                $return .= '<li class="list-group-item border-gray-25 lp_resource_element" id="'.$survey['iid'].'" data-id="'.$survey['iid'].'">';
7209
                $return .= '<a class="moved" href="#">';
7210
                $return .= $moveIcon;
7211
                $return .= ' </a>';
7212
                $return .= Display::getMdiIcon('poll', 'ch-tool-icon', null, 16, get_lang('Survey'));
7213
                $return .= '<a class="moved link_with_id" data-id="'.$survey['iid'].'" data_type="'.TOOL_SURVEY.'" title="'.$surveyTitle.'" href="'.api_get_self().'?'.api_get_cidreq().'&action=add_item&type='.TOOL_SURVEY.'&survey_id='.$survey['iid'].'&lp_id='.$this->lp_id.'" style="vertical-align:middle">'.$surveyTitle.'</a>';
7214
                $return .= '</li>';
7215
            }
7216
        }
7217
7218
        $return .= '</ul>';
7219
7220
        return $return;
7221
    }
7222
7223
    /**
7224
     * Temp function to be moved in main_api or the best place around for this.
7225
     * Creates a file path if it doesn't exist.
7226
     *
7227
     * @param string $path
7228
     */
7229
    public function create_path($path)
7230
    {
7231
        $path_bits = explode('/', dirname($path));
7232
7233
        // IS_WINDOWS_OS has been defined in main_api.lib.php
7234
        $path_built = IS_WINDOWS_OS ? '' : '/';
7235
        foreach ($path_bits as $bit) {
7236
            if (!empty($bit)) {
7237
                $new_path = $path_built.$bit;
7238
                if (is_dir($new_path)) {
7239
                    $path_built = $new_path.'/';
7240
                } else {
7241
                    mkdir($new_path, api_get_permissions_for_new_directories());
7242
                    $path_built = $new_path.'/';
7243
                }
7244
            }
7245
        }
7246
    }
7247
7248
    /**
7249
     * @param int    $lp_id
7250
     * @param string $status
7251
     */
7252
    public function set_autolaunch($lp_id, $status)
7253
    {
7254
        $status = (int) $status;
7255
        $em = Database::getManager();
7256
        $repo = Container::getLpRepository();
7257
7258
        $session = api_get_session_entity();
7259
        $course = api_get_course_entity();
7260
7261
        $qb = $repo->getResourcesByCourse($course, $session);
7262
        $lps = $qb->getQuery()->getResult();
7263
7264
        foreach ($lps as $lp) {
7265
            $lp->setAutoLaunch(0);
7266
            $em->persist($lp);
7267
        }
7268
7269
        $em->flush();
7270
7271
        if ($status === 1) {
7272
            $lp = $repo->find($lp_id);
7273
            if ($lp) {
7274
                $lp->setAutolaunch(1);
7275
                $em->persist($lp);
7276
            }
7277
            $em->flush();
7278
        }
7279
    }
7280
7281
    /**
7282
     * Gets previous_item_id for the next element of the lp_item table.
7283
     *
7284
     * @author Isaac flores paz
7285
     *
7286
     * @return int Previous item ID
7287
     */
7288
    public function select_previous_item_id()
7289
    {
7290
        $course_id = api_get_course_int_id();
7291
        $table_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7292
7293
        // Get the max order of the items
7294
        $sql = "SELECT max(display_order) AS display_order FROM $table_lp_item
7295
                WHERE c_id = $course_id AND lp_id = ".$this->lp_id;
7296
        $rs_max_order = Database::query($sql);
7297
        $row_max_order = Database::fetch_object($rs_max_order);
7298
        $max_order = $row_max_order->display_order;
7299
        // Get the previous item ID
7300
        $sql = "SELECT iid as previous FROM $table_lp_item
7301
                WHERE
7302
                    c_id = $course_id AND
7303
                    lp_id = ".$this->lp_id." AND
7304
                    display_order = '$max_order' ";
7305
        $rs_max = Database::query($sql);
7306
        $row_max = Database::fetch_object($rs_max);
7307
7308
        // Return the previous item ID
7309
        return $row_max->previous;
7310
    }
7311
7312
    /**
7313
     * Copies an LP.
7314
     */
7315
    public function copy()
7316
    {
7317
        // Course builder
7318
        $cb = new CourseBuilder();
7319
7320
        //Setting tools that will be copied
7321
        $cb->set_tools_to_build(['learnpaths']);
7322
7323
        //Setting elements that will be copied
7324
        $cb->set_tools_specific_id_list(
7325
            ['learnpaths' => [$this->lp_id]]
7326
        );
7327
7328
        $course = $cb->build();
7329
7330
        //Course restorer
7331
        $course_restorer = new CourseRestorer($course);
7332
        $course_restorer->set_add_text_in_items(true);
7333
        $course_restorer->set_tool_copy_settings(
7334
            ['learnpaths' => ['reset_dates' => true]]
7335
        );
7336
        $course_restorer->restore(
7337
            api_get_course_id(),
7338
            api_get_session_id(),
7339
            false,
7340
            false
7341
        );
7342
    }
7343
7344
    public static function getQuotaInfo(string $localFilePath): array
7345
    {
7346
        $post_max_raw   = ini_get('post_max_size');
7347
        $post_max_bytes = (int) rtrim($post_max_raw, 'MG') * (str_ends_with($post_max_raw,'G') ? 1024**3 : 1024**2);
7348
        $upload_max_raw = ini_get('upload_max_filesize');
7349
        $upload_max_bytes = (int) rtrim($upload_max_raw, 'MG') * (str_ends_with($upload_max_raw,'G') ? 1024**3 : 1024**2);
7350
7351
        $em     = Database::getManager();
7352
        $course = api_get_course_entity(api_get_course_int_id());
7353
7354
        $nodes  = Container::getResourceNodeRepository()->findByResourceTypeAndCourse('file', $course);
7355
        $root   = null;
7356
        foreach ($nodes as $n) {
7357
            if ($n->getParent() === null) {
7358
                $root = $n; break;
7359
            }
7360
        }
7361
        $docsSize = $root
7362
            ? Container::getDocumentRepository()->getFolderSize($root, $course)
7363
            : 0;
7364
7365
        $assetRepo = Container::getAssetRepository();
7366
        $fs        = $assetRepo->getFileSystem();
7367
        $scormSize = 0;
7368
        foreach (Container::getLpRepository()->findScormByCourse($course) as $lp) {
7369
            $asset = $lp->getAsset();
7370
            if (!$asset) {
7371
                continue;
7372
            }
7373
7374
            // Path may point to an extracted folder or a .zip file
7375
            $path = $assetRepo->getFolder($asset);
7376
            if (!$path) {
7377
                continue;
7378
            }
7379
7380
            try {
7381
                if ($fs->directoryExists($path)) {
7382
                    // Extracted SCORM folder
7383
                    $scormSize += self::getFolderSize($path);
7384
                    continue;
7385
                }
7386
                if ($fs->fileExists($path)) {
7387
                    // SCORM .zip file
7388
                    $scormSize += (int) $fs->fileSize($path);
7389
                    continue;
7390
                }
7391
7392
                // Local filesystem fallbacks
7393
                if (@is_dir($path)) {
7394
                    $scormSize += self::getFolderSize($path);
7395
                    continue;
7396
                }
7397
                if (@is_file($path)) {
7398
                    $size = @filesize($path);
7399
                    if ($size !== false) {
7400
                        $scormSize += (int) $size;
7401
                        continue;
7402
                    }
7403
                }
7404
7405
                // Only log when we truly cannot resolve the size
7406
                error_log('[Learnpath::getQuotaInfo] Unable to resolve SCORM size (path not found or unreadable): '.$path);
7407
            } catch (\Throwable $e) {
7408
                error_log('[Learnpath::getQuotaInfo] Exception while resolving SCORM size for path '.$path.' - '.$e->getMessage());
7409
            }
7410
        }
7411
7412
        $uploadedSize = filesize($localFilePath);
7413
        $existingTotal = $docsSize + $scormSize;
7414
        $combined = $existingTotal + $uploadedSize;
7415
7416
        $quotaMb = DocumentManager::get_course_quota();
7417
        $quotaBytes = $quotaMb * 1024 * 1024;
7418
7419
        return [
7420
            'post_max'      => $post_max_bytes,
7421
            'upload_max'    => $upload_max_bytes,
7422
            'docs_size'     => $docsSize,
7423
            'scorm_size'    => $scormSize,
7424
            'existing_total'=> $existingTotal,
7425
            'uploaded_size' => $uploadedSize,
7426
            'combined'      => $combined,
7427
            'quota_bytes'   => $quotaBytes,
7428
        ];
7429
    }
7430
7431
    /**
7432
     * Verify document size.
7433
     */
7434
    public static function verify_document_size(string $localFilePath): bool
7435
    {
7436
        $info = self::getQuotaInfo($localFilePath);
7437
        if ($info['uploaded_size'] > $info['post_max']
7438
            || $info['uploaded_size'] > $info['upload_max']
7439
            || $info['combined']    > $info['quota_bytes']
7440
        ) {
7441
            Container::getSession()->set('quota_info', $info);
7442
            return true;
7443
        }
7444
7445
        return false;
7446
    }
7447
7448
    private static function getFolderSize(string $path): int
7449
    {
7450
        $size     = 0;
7451
        $iterator = new \RecursiveIteratorIterator(
7452
            new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
7453
        );
7454
        foreach ($iterator as $file) {
7455
            if ($file->isFile()) {
7456
                $size += $file->getSize();
7457
            }
7458
        }
7459
        return $size;
7460
    }
7461
7462
    /**
7463
     * Clear LP prerequisites.
7464
     */
7465
    public function clearPrerequisites()
7466
    {
7467
        $course_id = $this->get_course_int_id();
7468
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7469
        $lp_id = $this->get_id();
7470
        // Cleaning prerequisites
7471
        $sql = "UPDATE $tbl_lp_item SET prerequisite = ''
7472
                WHERE lp_id = $lp_id";
7473
        Database::query($sql);
7474
7475
        // Cleaning mastery score for exercises
7476
        $sql = "UPDATE $tbl_lp_item SET mastery_score = ''
7477
                WHERE lp_id = $lp_id AND item_type = 'quiz'";
7478
        Database::query($sql);
7479
    }
7480
7481
    public function set_previous_step_as_prerequisite_for_all_items()
7482
    {
7483
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7484
        $course_id = $this->get_course_int_id();
7485
        $lp_id = $this->get_id();
7486
7487
        if (!empty($this->items)) {
7488
            $previous_item_id = null;
7489
            $previous_item_max = 0;
7490
            $previous_item_type = null;
7491
            $last_item_not_dir = null;
7492
            $last_item_not_dir_type = null;
7493
            $last_item_not_dir_max = null;
7494
7495
            foreach ($this->ordered_items as $itemId) {
7496
                $item = $this->getItem($itemId);
7497
                // if there was a previous item... (otherwise jump to set it)
7498
                if (!empty($previous_item_id)) {
7499
                    $current_item_id = $item->get_id(); //save current id
7500
                    if ('dir' != $item->get_type()) {
7501
                        // Current item is not a folder, so it qualifies to get a prerequisites
7502
                        if ('quiz' == $last_item_not_dir_type) {
7503
                            // if previous is quiz, mark its max score as default score to be achieved
7504
                            $sql = "UPDATE $tbl_lp_item SET mastery_score = '$last_item_not_dir_max'
7505
                                    WHERE c_id = $course_id AND lp_id = $lp_id AND iid = $last_item_not_dir";
7506
                            Database::query($sql);
7507
                        }
7508
                        // now simply update the prerequisite to set it to the last non-chapter item
7509
                        $sql = "UPDATE $tbl_lp_item SET prerequisite = '$last_item_not_dir'
7510
                                WHERE lp_id = $lp_id AND iid = $current_item_id";
7511
                        Database::query($sql);
7512
                        // record item as 'non-chapter' reference
7513
                        $last_item_not_dir = $item->get_id();
7514
                        $last_item_not_dir_type = $item->get_type();
7515
                        $last_item_not_dir_max = $item->get_max();
7516
                    }
7517
                } else {
7518
                    if ('dir' != $item->get_type()) {
7519
                        // Current item is not a folder (but it is the first item) so record as last "non-chapter" item
7520
                        $last_item_not_dir = $item->get_id();
7521
                        $last_item_not_dir_type = $item->get_type();
7522
                        $last_item_not_dir_max = $item->get_max();
7523
                    }
7524
                }
7525
                // Saving the item as "previous item" for the next loop
7526
                $previous_item_id = $item->get_id();
7527
                $previous_item_max = $item->get_max();
7528
                $previous_item_type = $item->get_type();
7529
            }
7530
        }
7531
    }
7532
7533
    /**
7534
     * @param array $params
7535
     *
7536
     * @return int
7537
     */
7538
    public static function createCategory($params)
7539
    {
7540
        $courseEntity = api_get_course_entity(api_get_course_int_id());
7541
7542
        $item = new CLpCategory();
7543
        $item
7544
            ->setTitle($params['name'])
7545
            ->setParent($courseEntity)
7546
            ->addCourseLink($courseEntity, api_get_session_entity())
7547
        ;
7548
7549
        $repo = Container::getLpCategoryRepository();
7550
        $repo->create($item);
7551
7552
        return $item->getIid();
7553
    }
7554
7555
    /**
7556
     * @param array $params
7557
     */
7558
    public static function updateCategory($params)
7559
    {
7560
        $em = Database::getManager();
7561
        /** @var CLpCategory $item */
7562
        $item = $em->find(CLpCategory::class, $params['id']);
7563
        if ($item) {
7564
            $item->setTitle($params['name']);
7565
            $em->persist($item);
7566
            $em->flush();
7567
        }
7568
    }
7569
7570
    public static function moveUpCategory(int $id): void
7571
    {
7572
        $em = Database::getManager();
7573
        /** @var CLpCategory $item */
7574
        $item = $em->find(CLpCategory::class, $id);
7575
        if ($item) {
7576
            $course = api_get_course_entity();
7577
            $session = api_get_session_entity();
7578
7579
            $link = $item->resourceNode->getResourceLinkByContext($course, $session);
7580
7581
            if ($link) {
7582
                $link->moveUpPosition();
7583
7584
                $em->flush();
7585
            }
7586
        }
7587
    }
7588
7589
    public static function moveDownCategory(int $id): void
7590
    {
7591
        $em = Database::getManager();
7592
        /** @var CLpCategory $item */
7593
        $item = $em->find(CLpCategory::class, $id);
7594
        if ($item) {
7595
            $course = api_get_course_entity();
7596
            $session = api_get_session_entity();
7597
7598
            $link = $item->resourceNode->getResourceLinkByContext($course, $session);
7599
7600
            if ($link) {
7601
                $link->moveDownPosition();
7602
7603
                $em->flush();
7604
            }
7605
        }
7606
    }
7607
7608
    /**
7609
     * @param int $courseId
7610
     *
7611
     * @return int
7612
     */
7613
    public static function getCountCategories($courseId)
7614
    {
7615
        if (empty($courseId)) {
7616
            return 0;
7617
        }
7618
        $repo = Container::getLpCategoryRepository();
7619
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId));
7620
        $qb->addSelect('count(resource)');
7621
7622
        return (int) $qb->getQuery()->getSingleScalarResult();
7623
    }
7624
7625
    /**
7626
     * @param int $courseId
7627
     *
7628
     * @return CLpCategory[]
7629
     */
7630
    public static function getCategories($courseId)
7631
    {
7632
        // Using doctrine extensions
7633
        $repo = Container::getLpCategoryRepository();
7634
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId), api_get_session_entity(), null, null, true, true);
7635
7636
        return $qb->getQuery()->getResult();
7637
    }
7638
7639
    public static function getCategorySessionId($id)
7640
    {
7641
        if ('true' !== api_get_setting('lp.allow_session_lp_category')) {
7642
            return 0;
7643
        }
7644
7645
        $repo = Container::getLpCategoryRepository();
7646
        /** @var CLpCategory $category */
7647
        $category = $repo->find($id);
7648
7649
        $sessionId = 0;
7650
        $link = $category->getFirstResourceLink();
7651
        if ($link && $link->getSession()) {
7652
            $sessionId = (int) $link->getSession()->getId();
7653
        }
7654
7655
        return $sessionId;
7656
    }
7657
7658
    public static function deleteCategory(int $id): bool
7659
    {
7660
        $repo = Container::getLpCategoryRepository();
7661
        /** @var CLpCategory|null $category */
7662
        $category = $repo->find($id);
7663
7664
        if (null === $category) {
7665
            return false;
7666
        }
7667
7668
        $em  = Database::getManager();
7669
        $lps = $category->getLps();
7670
7671
        // Detach all learning paths from this category
7672
        foreach ($lps as $lp) {
7673
            $lp->setCategory(null);
7674
            $em->persist($lp);
7675
        }
7676
7677
        // Remove the resource link of this category in the current context
7678
        $course  = api_get_course_entity();
7679
        $session = api_get_session_entity();
7680
7681
        $em->getRepository(ResourceLink::class)->removeByResourceInContext(
7682
            $category,
7683
            $course,
7684
            $session
7685
        );
7686
7687
        // Remove the category itself
7688
        $em->remove($category);
7689
        $em->flush();
7690
7691
        return true;
7692
    }
7693
7694
    /**
7695
     * @param int  $courseId
7696
     * @param bool $addSelectOption
7697
     *
7698
     * @return array
7699
     */
7700
    public static function getCategoryFromCourseIntoSelect($courseId, $addSelectOption = false)
7701
    {
7702
        $repo = Container::getLpCategoryRepository();
7703
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId), api_get_session_entity());
7704
        $items = $qb->getQuery()->getResult();
7705
7706
        $cats = [];
7707
        if ($addSelectOption) {
7708
            $cats = [get_lang('Select a category')];
7709
        }
7710
7711
        if (!empty($items)) {
7712
            foreach ($items as $cat) {
7713
                $cats[$cat->getIid()] = $cat->getTitle();
7714
            }
7715
        }
7716
7717
        return $cats;
7718
    }
7719
7720
    /**
7721
     * @param int   $courseId
7722
     * @param int   $lpId
7723
     * @param int   $user_id
7724
     *
7725
     * @return learnpath
7726
     */
7727
    public static function getLpFromSession(int $courseId, int $lpId, int $user_id)
7728
    {
7729
        $debug = 0;
7730
        $learnPath = null;
7731
        $lpObject = Session::read('lpobject');
7732
7733
        $repo = Container::getLpRepository();
7734
        $lp = $repo->find($lpId);
7735
        if (null !== $lpObject) {
7736
            /** @var learnpath $learnPath */
7737
            $learnPath = UnserializeApi::unserialize('lp', $lpObject);
7738
            $learnPath->entity = $lp;
7739
            if ($debug) {
7740
                error_log('getLpFromSession: unserialize');
7741
                error_log('------getLpFromSession------');
7742
                error_log('------unserialize------');
7743
                error_log("lp_view_session_id: ".$learnPath->lp_view_session_id);
7744
                error_log("api_get_sessionid: ".api_get_session_id());
7745
            }
7746
        }
7747
7748
        if (!is_object($learnPath)) {
7749
            $learnPath = new learnpath($lp, api_get_course_info_by_id($courseId), $user_id);
7750
            if ($debug) {
7751
                error_log('------getLpFromSession------');
7752
                error_log('getLpFromSession: create new learnpath');
7753
                error_log("create new LP with $courseId - $lpId - $user_id");
7754
                error_log("lp_view_session_id: ".$learnPath->lp_view_session_id);
7755
                error_log("api_get_sessionid: ".api_get_session_id());
7756
            }
7757
        }
7758
7759
        return $learnPath;
7760
    }
7761
7762
    /**
7763
     * @param int $itemId
7764
     *
7765
     * @return learnpathItem|false
7766
     */
7767
    public function getItem($itemId)
7768
    {
7769
        if (isset($this->items[$itemId]) && is_object($this->items[$itemId])) {
7770
            return $this->items[$itemId];
7771
        }
7772
7773
        return false;
7774
    }
7775
7776
    /**
7777
     * @return int
7778
     */
7779
    public function getCurrentAttempt()
7780
    {
7781
        $attempt = $this->getItem($this->get_current_item_id());
7782
        if ($attempt) {
7783
            return $attempt->get_attempt_id();
7784
        }
7785
7786
        return 0;
7787
    }
7788
7789
    /**
7790
     * @return int
7791
     */
7792
    public function getCategoryId()
7793
    {
7794
        return (int) $this->categoryId;
7795
    }
7796
7797
    /**
7798
     * Get whether this is a learning path with the possibility to subscribe
7799
     * users or not.
7800
     *
7801
     * @return int
7802
     */
7803
    public function getSubscribeUsers()
7804
    {
7805
        return $this->subscribeUsers;
7806
    }
7807
7808
    /**
7809
     * Calculate the count of stars for a user in this LP
7810
     * This calculation is based on the following rules:
7811
     * - the student gets one star when he gets to 50% of the learning path
7812
     * - the student gets a second star when the average score of all tests inside the learning path >= 50%
7813
     * - the student gets a third star when the average score of all tests inside the learning path >= 80%
7814
     * - the student gets the final star when the score for the *last* test is >= 80%.
7815
     *
7816
     * @param int $sessionId Optional. The session ID
7817
     *
7818
     * @return int The count of stars
7819
     */
7820
    public function getCalculateStars($sessionId = 0)
7821
    {
7822
        $stars = 0;
7823
        $progress = self::getProgress(
7824
            $this->lp_id,
7825
            $this->user_id,
7826
            $this->course_int_id,
7827
            $sessionId
7828
        );
7829
7830
        if ($progress >= 50) {
7831
            $stars++;
7832
        }
7833
7834
        // Calculate stars chapters evaluation
7835
        $exercisesItems = $this->getExercisesItems();
7836
7837
        if (!empty($exercisesItems)) {
7838
            $totalResult = 0;
7839
7840
            foreach ($exercisesItems as $exerciseItem) {
7841
                $exerciseResultInfo = Event::getExerciseResultsByUser(
7842
                    $this->user_id,
7843
                    $exerciseItem->path,
7844
                    $this->course_int_id,
7845
                    $sessionId,
7846
                    $this->lp_id,
7847
                    $exerciseItem->db_id
7848
                );
7849
7850
                $exerciseResultInfo = end($exerciseResultInfo);
7851
7852
                if (!$exerciseResultInfo) {
7853
                    continue;
7854
                }
7855
7856
                if (!empty($exerciseResultInfo['max_score'])) {
7857
                    $exerciseResult = $exerciseResultInfo['score'] * 100 / $exerciseResultInfo['max_score'];
7858
                } else {
7859
                    $exerciseResult = 0;
7860
                }
7861
                $totalResult += $exerciseResult;
7862
            }
7863
7864
            $totalExerciseAverage = $totalResult / (count($exercisesItems) > 0 ? count($exercisesItems) : 1);
7865
7866
            if ($totalExerciseAverage >= 50) {
7867
                $stars++;
7868
            }
7869
7870
            if ($totalExerciseAverage >= 80) {
7871
                $stars++;
7872
            }
7873
        }
7874
7875
        // Calculate star for final evaluation
7876
        $finalEvaluationItem = $this->getFinalEvaluationItem();
7877
7878
        if (!empty($finalEvaluationItem)) {
7879
            $evaluationResultInfo = Event::getExerciseResultsByUser(
7880
                $this->user_id,
7881
                $finalEvaluationItem->path,
7882
                $this->course_int_id,
7883
                $sessionId,
7884
                $this->lp_id,
7885
                $finalEvaluationItem->db_id
7886
            );
7887
7888
            $evaluationResultInfo = end($evaluationResultInfo);
7889
7890
            if ($evaluationResultInfo) {
7891
                $evaluationResult = $evaluationResultInfo['score'] * 100 / $evaluationResultInfo['max_score'];
7892
                if ($evaluationResult >= 80) {
7893
                    $stars++;
7894
                }
7895
            }
7896
        }
7897
7898
        return $stars;
7899
    }
7900
7901
    /**
7902
     * Get the items of exercise type.
7903
     *
7904
     * @return array The items. Otherwise return false
7905
     */
7906
    public function getExercisesItems()
7907
    {
7908
        $exercises = [];
7909
        foreach ($this->items as $item) {
7910
            if ('quiz' !== $item->type) {
7911
                continue;
7912
            }
7913
            $exercises[] = $item;
7914
        }
7915
7916
        array_pop($exercises);
7917
7918
        return $exercises;
7919
    }
7920
7921
    /**
7922
     * Get the item of exercise type (evaluation type).
7923
     *
7924
     * @return array The final evaluation. Otherwise return false
7925
     */
7926
    public function getFinalEvaluationItem()
7927
    {
7928
        $exercises = [];
7929
        foreach ($this->items as $item) {
7930
            if (TOOL_QUIZ !== $item->type) {
7931
                continue;
7932
            }
7933
7934
            $exercises[] = $item;
7935
        }
7936
7937
        return array_pop($exercises);
7938
    }
7939
7940
    /**
7941
     * Calculate the total points achieved for the current user in this learning path.
7942
     *
7943
     * @param int $sessionId Optional. The session Id
7944
     *
7945
     * @return int
7946
     */
7947
    public function getCalculateScore($sessionId = 0)
7948
    {
7949
        // Calculate stars chapters evaluation
7950
        $exercisesItems = $this->getExercisesItems();
7951
        $finalEvaluationItem = $this->getFinalEvaluationItem();
7952
        $totalExercisesResult = 0;
7953
        $totalEvaluationResult = 0;
7954
7955
        if (false !== $exercisesItems) {
7956
            foreach ($exercisesItems as $exerciseItem) {
7957
                $exerciseResultInfo = Event::getExerciseResultsByUser(
7958
                    $this->user_id,
7959
                    $exerciseItem->path,
7960
                    $this->course_int_id,
7961
                    $sessionId,
7962
                    $this->lp_id,
7963
                    $exerciseItem->db_id
7964
                );
7965
7966
                $exerciseResultInfo = end($exerciseResultInfo);
7967
7968
                if (!$exerciseResultInfo) {
7969
                    continue;
7970
                }
7971
7972
                $totalExercisesResult += $exerciseResultInfo['score'];
7973
            }
7974
        }
7975
7976
        if (!empty($finalEvaluationItem)) {
7977
            $evaluationResultInfo = Event::getExerciseResultsByUser(
7978
                $this->user_id,
7979
                $finalEvaluationItem->path,
7980
                $this->course_int_id,
7981
                $sessionId,
7982
                $this->lp_id,
7983
                $finalEvaluationItem->db_id
7984
            );
7985
7986
            $evaluationResultInfo = end($evaluationResultInfo);
7987
7988
            if ($evaluationResultInfo) {
7989
                $totalEvaluationResult += $evaluationResultInfo['score'];
7990
            }
7991
        }
7992
7993
        return $totalExercisesResult + $totalEvaluationResult;
7994
    }
7995
7996
    /**
7997
     * Check if URL is not allowed to be show in a iframe.
7998
     *
7999
     * @param string $src
8000
     *
8001
     * @return string
8002
     */
8003
    public function fixBlockedLinks($src)
8004
    {
8005
        $urlInfo = parse_url($src);
8006
8007
        $platformProtocol = 'https';
8008
        if (false === strpos(api_get_path(WEB_CODE_PATH), 'https')) {
8009
            $platformProtocol = 'http';
8010
        }
8011
8012
        $protocolFixApplied = false;
8013
        //Scheme validation to avoid "Notices" when the lesson doesn't contain a valid scheme
8014
        $scheme = isset($urlInfo['scheme']) ? $urlInfo['scheme'] : null;
8015
        $host = isset($urlInfo['host']) ? $urlInfo['host'] : null;
8016
8017
        if ($platformProtocol != $scheme) {
8018
            Session::write('x_frame_source', $src);
8019
            $src = 'blank.php?error=x_frames_options';
8020
            $protocolFixApplied = true;
8021
        }
8022
8023
        if (false == $protocolFixApplied) {
8024
            if (false === strpos(api_get_path(WEB_PATH), $host)) {
8025
                // Check X-Frame-Options
8026
                $ch = curl_init();
8027
                $options = [
8028
                    CURLOPT_URL => $src,
8029
                    CURLOPT_RETURNTRANSFER => true,
8030
                    CURLOPT_HEADER => true,
8031
                    CURLOPT_FOLLOWLOCATION => true,
8032
                    CURLOPT_ENCODING => "",
8033
                    CURLOPT_AUTOREFERER => true,
8034
                    CURLOPT_CONNECTTIMEOUT => 120,
8035
                    CURLOPT_TIMEOUT => 120,
8036
                    CURLOPT_MAXREDIRS => 10,
8037
                ];
8038
8039
                $proxySettings = api_get_setting('security.proxy_settings', true);
8040
                if (!empty($proxySettings) &&
8041
                    isset($proxySettings['curl_setopt_array'])
8042
                ) {
8043
                    $options[CURLOPT_PROXY] = $proxySettings['curl_setopt_array']['CURLOPT_PROXY'];
8044
                    $options[CURLOPT_PROXYPORT] = $proxySettings['curl_setopt_array']['CURLOPT_PROXYPORT'];
8045
                }
8046
8047
                curl_setopt_array($ch, $options);
8048
                $response = curl_exec($ch);
8049
                $httpCode = curl_getinfo($ch);
8050
                $headers = substr($response, 0, $httpCode['header_size']);
8051
8052
                $error = false;
8053
                if (stripos($headers, 'X-Frame-Options: DENY') > -1
8054
                    //|| stripos($headers, 'X-Frame-Options: SAMEORIGIN') > -1
8055
                ) {
8056
                    $error = true;
8057
                }
8058
8059
                if ($error) {
8060
                    Session::write('x_frame_source', $src);
8061
                    $src = 'blank.php?error=x_frames_options';
8062
                }
8063
            }
8064
        }
8065
8066
        return $src;
8067
    }
8068
8069
    /**
8070
     * Check if this LP has a created forum in the basis course.
8071
     *
8072
     * @deprecated
8073
     *
8074
     * @return bool
8075
     */
8076
    public function lpHasForum()
8077
    {
8078
        $forumTable = Database::get_course_table(TABLE_FORUM);
8079
        $itemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
8080
8081
        $fakeFrom = "
8082
            $forumTable f
8083
            INNER JOIN $itemProperty ip
8084
            ON (f.forum_id = ip.ref AND f.c_id = ip.c_id)
8085
        ";
8086
8087
        $resultData = Database::select(
8088
            'COUNT(f.iid) AS qty',
8089
            $fakeFrom,
8090
            [
8091
                'where' => [
8092
                    'ip.visibility != ? AND ' => 2,
8093
                    'ip.tool = ? AND ' => TOOL_FORUM,
8094
                    'f.c_id = ? AND ' => intval($this->course_int_id),
8095
                    'f.lp_id = ?' => intval($this->lp_id),
8096
                ],
8097
            ],
8098
            'first'
8099
        );
8100
8101
        return $resultData['qty'] > 0;
8102
    }
8103
8104
    /**
8105
     * Get the forum for this learning path.
8106
     *
8107
     * @param int $sessionId
8108
     *
8109
     * @return array
8110
     */
8111
    public function getForum($sessionId = 0)
8112
    {
8113
        $repo = Container::getForumRepository();
8114
8115
        $course = api_get_course_entity();
8116
        $session = api_get_session_entity($sessionId);
8117
        $qb = $repo->getResourcesByCourse($course, $session);
8118
8119
        return $qb->getQuery()->getResult();
8120
    }
8121
8122
    /**
8123
     * Get the LP Final Item form.
8124
     *
8125
     * @throws Exception
8126
     *
8127
     *
8128
     * @return string
8129
     */
8130
    public function getFinalItemForm(): string
8131
    {
8132
        $finalItem = $this->getFinalItem();
8133
8134
        $lpId = (int) $this->lp_id;
8135
        $tableLpItem = Database::get_course_table(TABLE_LP_ITEM);
8136
8137
        $finalItemIid = 0;
8138
        $title = '';
8139
        $content = '';
8140
8141
        if ($finalItem) {
8142
            $finalItemIid = (int) $finalItem->get_id();
8143
            $title = (string) $finalItem->get_title();
8144
            $content = (string) $this->getSavedFinalItem();
8145
        } else {
8146
            $title = '';
8147
            $content = (string) $this->getFinalItemTemplate();
8148
        }
8149
8150
        // Read stored ref from THIS lp_item row
8151
        $storedRef = 0;
8152
        if ($finalItemIid > 0) {
8153
            $sql = "SELECT ref
8154
            FROM $tableLpItem
8155
            WHERE iid = $finalItemIid
8156
            LIMIT 1";
8157
            $res = Database::query($sql);
8158
            if ($res && Database::num_rows($res) > 0) {
8159
                $row = Database::fetch_array($res);
8160
                if (isset($row['ref']) && is_numeric($row['ref'])) {
8161
                    $storedRef = (int) $row['ref'];
8162
                }
8163
            }
8164
        }
8165
8166
        $editorConfig = [
8167
            'ToolbarSet' => 'LearningPathDocuments',
8168
            'Width' => '100%',
8169
            'Height' => '500',
8170
            'FullPage' => true,
8171
        ];
8172
8173
        // Keep build context params
8174
        $pathItem = isset($_REQUEST['path_item']) ? (int) $_REQUEST['path_item'] : 0;
8175
        $gradebook = isset($_REQUEST['gradebook']) ? (int) $_REQUEST['gradebook'] : null;
8176
        $origin = isset($_REQUEST['origin']) ? (string) $_REQUEST['origin'] : null;
8177
8178
        $qs = [
8179
            'action' => 'add_final_item',
8180
            'lp_id' => $lpId,
8181
        ];
8182
8183
        if ($pathItem > 0) {
8184
            $qs['path_item'] = $pathItem;
8185
        }
8186
        if (null !== $gradebook) {
8187
            $qs['gradebook'] = $gradebook;
8188
        }
8189
        if (null !== $origin && '' !== $origin) {
8190
            $qs['origin'] = Security::remove_XSS($origin);
8191
        }
8192
8193
        $actionUrl = 'lp_controller.php?'.api_get_cidreq().'&'.http_build_query($qs);
8194
8195
        $form = new FormValidator('final_item', 'POST', $actionUrl);
8196
8197
        // ---------- Form fields (order matters for UX) ----------
8198
        $form->addText('title', get_lang('Title'));
8199
8200
        $form->addHtml(
8201
            Display::return_message(
8202
                'Variables :<br><br> <b>((certificate))</b> <br> <b>((skill))</b>',
8203
                'normal',
8204
                false
8205
            )
8206
        );
8207
8208
        // Hidden that will ALWAYS be posted (even if the select is disabled/hidden)
8209
        $form->addHidden('final_item_ref', $storedRef > 0 ? (string) $storedRef : '');
8210
        // Do not send a POST field called "action" (it would override GET[action])
8211
        $form->addHidden('final_item_save', '1');
8212
8213
        $renderer = $form->defaultRenderer();
8214
        $renderer->setElementTemplate('&nbsp;{label}{element}', 'content_lp_certificate');
8215
8216
        $form->addHtmlEditor(
8217
            'content_lp_certificate',
8218
            null,
8219
            true,
8220
            false,
8221
            $editorConfig
8222
        );
8223
8224
        // Advanced settings AFTER the editor (as requested)
8225
        if (api_is_allowed_to_edit(null, true)) {
8226
            $form->addElement('advanced_settings', 'advanced_params', get_lang('Advanced settings'));
8227
            $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
8228
            GradebookUtils::load_gradebook_select_in_tool($form);
8229
            $form->addElement('html', '</div>');
8230
8231
            // Sync select -> hidden on change + submit
8232
            $form->addElement('html', '
8233
<script>
8234
(function () {
8235
  function findGradebookSelect() {
8236
    var sel = document.querySelector("select[name=\'selecteval\']");
8237
    if (sel) return sel;
8238
8239
    var container = document.getElementById("advanced_params_options");
8240
    if (!container) return null;
8241
    var selects = container.querySelectorAll("select");
8242
    if (!selects || !selects.length) return null;
8243
    return selects[0];
8244
  }
8245
8246
  function getStoredRef() {
8247
    var hidden = document.querySelector("input[name=\'final_item_ref\']");
8248
    if (!hidden) return "";
8249
    return (hidden.value || "").toString().trim();
8250
  }
8251
8252
  function applyStoredRefToSelect() {
8253
    var sel = findGradebookSelect();
8254
    if (!sel) return;
8255
8256
    var stored = getStoredRef();
8257
    if (!stored) return;
8258
8259
    var opt = sel.querySelector("option[value=\'" + stored.replace(/\'/g, "\\\'") + "\']");
8260
    if (opt) {
8261
      sel.value = stored;
8262
      try { sel.dispatchEvent(new Event("change", { bubbles: true })); } catch (e) {}
8263
    }
8264
  }
8265
8266
  function syncFinalRef() {
8267
    var hidden = document.querySelector("input[name=\'final_item_ref\']");
8268
    if (!hidden) return;
8269
8270
    var sel = findGradebookSelect();
8271
    if (!sel) return;
8272
8273
    try { sel.disabled = false; } catch (e) {}
8274
    hidden.value = sel.value || "";
8275
  }
8276
8277
  document.addEventListener("change", function (e) {
8278
    var sel = findGradebookSelect();
8279
    if (sel && e.target === sel) {
8280
      syncFinalRef();
8281
    }
8282
  });
8283
8284
  document.addEventListener("submit", function (e) {
8285
    var f = e.target;
8286
    if (!f || f.getAttribute("name") !== "final_item") return;
8287
    syncFinalRef();
8288
  }, true);
8289
8290
  function init() {
8291
    applyStoredRefToSelect();
8292
    syncFinalRef();
8293
  }
8294
8295
  document.addEventListener("DOMContentLoaded", init);
8296
  init();
8297
})();
8298
</script>
8299
');
8300
        }
8301
8302
        // Save button at the END (as requested)
8303
        $form->addButtonSave(get_lang('Save'));
8304
8305
        $form->setDefaults([
8306
            'title' => $title,
8307
            'content_lp_certificate' => $content,
8308
            'final_item_ref' => $storedRef > 0 ? (string) $storedRef : '',
8309
            'selecteval' => $storedRef > 0 ? $storedRef : null,
8310
        ]);
8311
8312
        if ($form->validate()) {
8313
            $values = $form->exportValues();
8314
8315
            $newTitle = trim((string) ($values['title'] ?? ''));
8316
            $newContent = (string) ($values['content_lp_certificate'] ?? '');
8317
8318
            // Read ref from hidden (most reliable)
8319
            $selectedRef = 0;
8320
            $rawRef = trim((string) ($values['final_item_ref'] ?? ''));
8321
            if ('' !== $rawRef && preg_match('/(\d+)/', $rawRef, $m)) {
8322
                $selectedRef = (int) $m[1];
8323
            }
8324
8325
            // Mark LP as modified
8326
            $this->set_modified_on();
8327
8328
            if (!$finalItem) {
8329
                $lastItemId = $this->getLastInFirstLevel();
8330
8331
                $documentId = $this->create_document(
8332
                    $this->course_info,
8333
                    $newContent,
8334
                    $newTitle,
8335
                    'html',
8336
                    0,
8337
                    0,
8338
                    'certificate'
8339
                );
8340
8341
                $lpItemRepo = Container::getLpItemRepository();
8342
                $root = $lpItemRepo->getRootItem($this->get_id());
8343
8344
                // Capture created item iid directly if add_item returns it
8345
                $created = $this->add_item(
8346
                    $root,
8347
                    $lastItemId,
8348
                    TOOL_LP_FINAL_ITEM,
8349
                    $documentId,
8350
                    $newTitle
8351
                );
8352
8353
                if (is_numeric($created)) {
8354
                    $finalItemIid = (int) $created;
8355
                } else {
8356
                    // Fallback: re-fetch iid
8357
                    $sqlIid = "SELECT iid
8358
                       FROM $tableLpItem
8359
                       WHERE lp_id = $lpId
8360
                         AND item_type = '".Database::escape_string(TOOL_LP_FINAL_ITEM)."'
8361
                       ORDER BY iid DESC
8362
                       LIMIT 1";
8363
                    $resIid = Database::query($sqlIid);
8364
                    if ($resIid && Database::num_rows($resIid) > 0) {
8365
                        $rowIid = Database::fetch_array($resIid);
8366
                        $finalItemIid = isset($rowIid['iid']) ? (int) $rowIid['iid'] : 0;
8367
                    }
8368
                }
8369
8370
                Display::addFlash(Display::return_message(get_lang('Added'), 'confirmation', false));
8371
            } else {
8372
                // Update underlying document (path stores the document id)
8373
                $documentId = isset($finalItem->path) ? (int) $finalItem->path : 0;
8374
8375
                if ($documentId > 0) {
8376
                    $em = Database::getManager();
8377
                    $document = $em->getRepository(CDocument::class)->find($documentId);
8378
8379
                    if ($document) {
8380
                        $documentRepo = Container::getDocumentRepository();
8381
8382
                        $documentRepo->setResourceName($document, $newTitle);
8383
8384
                        $updated = $documentRepo->updateResourceFileContent($document, $newContent);
8385
                        if (!$updated) {
8386
                            $node = $document->getResourceNode();
8387
                            if ($node && method_exists($node, 'setContent')) {
8388
                                $node->setContent($newContent);
8389
                            }
8390
                        }
8391
8392
                        $documentRepo->update($document, true);
8393
                    }
8394
                }
8395
8396
                // Update LP item title
8397
                if ($finalItemIid > 0) {
8398
                    $safeTitle = Database::escape_string($newTitle);
8399
                    Database::query("UPDATE $tableLpItem SET title = '$safeTitle' WHERE iid = $finalItemIid");
8400
                }
8401
8402
                Display::addFlash(Display::return_message(get_lang('Update successful'), 'confirmation', false));
8403
            }
8404
8405
            // Persist ref in c_lp_item.ref for THIS item
8406
            if (api_is_allowed_to_edit(null, true) && $finalItemIid > 0) {
8407
                if ($selectedRef > 0) {
8408
                    Database::query("UPDATE $tableLpItem SET ref = $selectedRef WHERE iid = $finalItemIid");
8409
                } else {
8410
                    Database::query("UPDATE $tableLpItem SET ref = NULL WHERE iid = $finalItemIid");
8411
                }
8412
            }
8413
8414
            // Redirect back to the final item form (same action), so the UI doesn't "disappear"
8415
            $redirectQs = [
8416
                'action' => 'add_final_item',
8417
                'lp_id' => $lpId,
8418
            ];
8419
            if ($pathItem > 0) {
8420
                $redirectQs['path_item'] = $pathItem;
8421
            }
8422
            if (null !== $gradebook) {
8423
                $redirectQs['gradebook'] = $gradebook;
8424
            }
8425
            if (null !== $origin && '' !== $origin) {
8426
                $redirectQs['origin'] = Security::remove_XSS($origin);
8427
            }
8428
8429
            \ChamiloSession::write('refresh', 1);
8430
            api_location('lp_controller.php?'.api_get_cidreq().'&'.http_build_query($redirectQs));
8431
        }
8432
8433
        return $form->returnForm();
8434
    }
8435
8436
    /**
8437
     * Check if the current lp item is first, both, last or none from lp list.
8438
     *
8439
     * @param int $currentItemId
8440
     *
8441
     * @return string
8442
     */
8443
    public function isFirstOrLastItem($currentItemId)
8444
    {
8445
        $lpItemId = [];
8446
        $typeListNotToVerify = self::getChapterTypes();
8447
8448
        // Using get_toc() function instead $this->items because returns the correct order of the items
8449
        foreach ($this->get_toc() as $item) {
8450
            if (!in_array($item['type'], $typeListNotToVerify)) {
8451
                $lpItemId[] = $item['id'];
8452
            }
8453
        }
8454
8455
        $lastLpItemIndex = count($lpItemId) - 1;
8456
        $position = array_search($currentItemId, $lpItemId);
8457
8458
        switch ($position) {
8459
            case 0:
8460
                if (!$lastLpItemIndex) {
8461
                    $answer = 'both';
8462
                    break;
8463
                }
8464
8465
                $answer = 'first';
8466
                break;
8467
            case $lastLpItemIndex:
8468
                $answer = 'last';
8469
                break;
8470
            default:
8471
                $answer = 'none';
8472
        }
8473
8474
        return $answer;
8475
    }
8476
8477
    /**
8478
     * Get whether this is a learning path with the accumulated SCORM time or not.
8479
     *
8480
     * @return int
8481
     */
8482
    public function getAccumulateScormTime()
8483
    {
8484
        return $this->accumulateScormTime;
8485
    }
8486
8487
    /**
8488
     * Returns an HTML-formatted link to a resource, to incorporate directly into
8489
     * the new learning path tool.
8490
     *
8491
     * The function is a big switch on tool type.
8492
     * In each case, we query the corresponding table for information and build the link
8493
     * with that information.
8494
     *
8495
     * @author Yannick Warnier <[email protected]> - rebranding based on
8496
     * previous work (display_addedresource_link_in_learnpath())
8497
     *
8498
     * @param int $course_id      Course code
8499
     * @param int $learningPathId The learning path ID (in lp table)
8500
     * @param int $id_in_path     the unique index in the items table
8501
     * @param int $lpViewId
8502
     *
8503
     * @return string
8504
     */
8505
    public static function rl_get_resource_link_for_learnpath(
8506
        $course_id,
8507
        $learningPathId,
8508
        $id_in_path,
8509
        $lpViewId
8510
    ) {
8511
        $session_id = api_get_session_id();
8512
8513
        $learningPathId = (int) $learningPathId;
8514
        $id_in_path = (int) $id_in_path;
8515
        $lpViewId = (int) $lpViewId;
8516
8517
        $em = Database::getManager();
8518
        $lpItemRepo = $em->getRepository(CLpItem::class);
8519
8520
        /** @var CLpItem $rowItem */
8521
        $rowItem = $lpItemRepo->findOneBy([
8522
            'lp' => $learningPathId,
8523
            'iid' => $id_in_path,
8524
        ]);
8525
        $type = $rowItem->getItemType();
8526
        $id = empty($rowItem->getPath()) ? '0' : $rowItem->getPath();
8527
        $main_dir_path = api_get_path(WEB_CODE_PATH);
8528
        $link = '';
8529
        $extraParams = api_get_cidreq(true, true, 'learnpath').'&sid='.$session_id;
8530
8531
        switch ($type) {
8532
            case 'dir':
8533
                return $main_dir_path.'lp/blank.php';
8534
            case TOOL_CALENDAR_EVENT:
8535
                return $main_dir_path.'calendar/agenda.php?agenda_id='.$id.'&'.$extraParams;
8536
            case TOOL_ANNOUNCEMENT:
8537
                return $main_dir_path.'announcements/announcements.php?ann_id='.$id.'&'.$extraParams;
8538
            case TOOL_LINK:
8539
                $linkInfo = Link::getLinkInfo($id);
8540
                if (isset($linkInfo['url'])) {
8541
                    return $linkInfo['url'];
8542
                }
8543
8544
                return '';
8545
            case TOOL_QUIZ:
8546
                if (empty($id)) {
8547
                    return '';
8548
                }
8549
8550
                // Get the lp_item_view with the highest view_count.
8551
                $learnpathItemViewResult = $em
8552
                    ->getRepository(CLpItemView::class)
8553
                    ->findBy(
8554
                        ['item' => $rowItem->getIid(), 'view' => $lpViewId],
8555
                        ['viewCount' => 'DESC'],
8556
                        1
8557
                    );
8558
                /** @var CLpItemView $learnpathItemViewData */
8559
                $learnpathItemViewData = current($learnpathItemViewResult);
8560
                $learnpathItemViewId = $learnpathItemViewData ? $learnpathItemViewData->getIid() : 0;
8561
8562
                return $main_dir_path.'exercise/overview.php?'.$extraParams.'&'
8563
                    .http_build_query([
8564
                        'lp_init' => 1,
8565
                        'learnpath_item_view_id' => $learnpathItemViewId,
8566
                        'learnpath_id' => $learningPathId,
8567
                        'learnpath_item_id' => $id_in_path,
8568
                        'exerciseId' => $id,
8569
                    ]);
8570
            case TOOL_HOTPOTATOES:
8571
                return '';
8572
            case TOOL_FORUM:
8573
                return $main_dir_path.'forum/viewforum.php?forum='.$id.'&lp=true&'.$extraParams;
8574
            case TOOL_THREAD:
8575
                // forum post
8576
                $tbl_topics = Database::get_course_table(TABLE_FORUM_THREAD);
8577
                if (empty($id)) {
8578
                    return '';
8579
                }
8580
                $sql = "SELECT * FROM $tbl_topics WHERE iid=$id";
8581
                $result = Database::query($sql);
8582
                $row = Database::fetch_array($result);
8583
8584
                return $main_dir_path.'forum/viewthread.php?thread='.$id.'&forum='.$row['forum_id'].'&lp=true&'
8585
                    .$extraParams;
8586
            case TOOL_POST:
8587
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8588
                $result = Database::query("SELECT * FROM $tbl_post WHERE post_id=$id");
8589
                $row = Database::fetch_array($result);
8590
8591
                return $main_dir_path.'forum/viewthread.php?post='.$id.'&thread='.$row['thread_id'].'&forum='
8592
                    .$row['forum_id'].'&lp=true&'.$extraParams;
8593
            case TOOL_READOUT_TEXT:
8594
                return api_get_path(WEB_CODE_PATH).
8595
                    'lp/readout_text.php?&id='.$id.'&lp_id='.$learningPathId.'&'.$extraParams;
8596
            case TOOL_DOCUMENT:
8597
            case 'video':
8598
                $repo = Container::getDocumentRepository();
8599
                $document = $repo->find($rowItem->getPath());
8600
                if ($document) {
8601
                    $params = [
8602
                        'cid' => $course_id,
8603
                        'sid' => $session_id,
8604
                    ];
8605
8606
                    return $repo->getResourceFileUrl($document, $params, UrlGeneratorInterface::ABSOLUTE_URL);
8607
                }
8608
8609
                return null;
8610
            case TOOL_LP_FINAL_ITEM:
8611
                return api_get_path(WEB_CODE_PATH).'lp/lp_final_item.php?&id='.$id.'&lp_id='.$learningPathId.'&'
8612
                    .$extraParams;
8613
            case 'assignments':
8614
                return $main_dir_path.'work/work.php?'.$extraParams;
8615
            case TOOL_DROPBOX:
8616
                return $main_dir_path.'dropbox/index.php?'.$extraParams;
8617
            case 'introduction_text': //DEPRECATED
8618
                return '';
8619
            case TOOL_COURSE_DESCRIPTION:
8620
                return $main_dir_path.'course_description?'.$extraParams;
8621
            case TOOL_GROUP:
8622
                return $main_dir_path.'group/group.php?'.$extraParams;
8623
            case TOOL_USER:
8624
                return $main_dir_path.'user/user.php?'.$extraParams;
8625
            case TOOL_STUDENTPUBLICATION:
8626
                $repo = Container::getStudentPublicationRepository();
8627
                $publication = $repo->find($rowItem->getPath());
8628
                if ($publication && $publication->hasResourceNode()) {
8629
                    $nodeId = $publication->getResourceNode()->getId();
8630
                    $assignmentId = $publication->getIid();
8631
8632
                    return api_get_path(WEB_PATH) .
8633
                        "resources/assignment/$nodeId/submission/$assignmentId?" .
8634
                        http_build_query([
8635
                            'cid' => $course_id,
8636
                            'sid' => $session_id,
8637
                            'gid' => 0,
8638
                            'origin' => 'learnpath',
8639
                            'isStudentView' => 'true',
8640
                        ]);
8641
                }
8642
                return '';
8643
            case TOOL_SURVEY:
8644
8645
                $surveyId = (int) $id;
8646
                $repo = Container::getSurveyRepository();
8647
                if (!empty($surveyId)) {
8648
                    /** @var CSurvey $survey */
8649
                    $survey = $repo->find($surveyId);
8650
                    $autoSurveyLink = SurveyUtil::generateFillSurveyLink(
8651
                        $survey,
8652
                        'auto',
8653
                        api_get_course_entity($course_id),
8654
                        $session_id
8655
                    );
8656
                    $lpParams = [
8657
                        'lp_id' => $learningPathId,
8658
                        'lp_item_id' => $id_in_path,
8659
                        'origin' => 'learnpath',
8660
                    ];
8661
8662
                    return $autoSurveyLink.'&'.http_build_query($lpParams).'&'.$extraParams;
8663
                }
8664
        }
8665
8666
        return $link;
8667
    }
8668
8669
    /**
8670
     * Checks if any forum items in a given learning path are from the base course.
8671
     */
8672
    public static function isForumFromBaseCourse(int $learningPathId): bool
8673
    {
8674
        $itemRepository = Container::getLpItemRepository();
8675
        $forumRepository = Container::getForumRepository();
8676
        $forums = $itemRepository->findItemsByLearningPathAndType($learningPathId, 'forum');
8677
8678
        /* @var CLpItem $forumItem */
8679
        foreach ($forums as $forumItem) {
8680
            $forumId = (int) $forumItem->getPath();
8681
            $forum = $forumRepository->find($forumId);
8682
8683
            if ($forum !== null) {
8684
                $forumSession = $forum->getFirstResourceLink()->getSession();
8685
                if ($forumSession === null) {
8686
                    return true;
8687
                }
8688
            }
8689
        }
8690
8691
        return false;
8692
    }
8693
8694
    /**
8695
     * Gets the name of a resource (generally used in learnpath when no name is provided).
8696
     *
8697
     * @author Yannick Warnier <[email protected]>
8698
     *
8699
     * @param string $course_code    Course code
8700
     * @param int    $learningPathId
8701
     * @param int    $id_in_path     The resource ID
8702
     *
8703
     * @return string
8704
     */
8705
    public static function rl_get_resource_name($course_code, $learningPathId, $id_in_path)
8706
    {
8707
        $_course = api_get_course_info($course_code);
8708
        if (empty($_course)) {
8709
            return '';
8710
        }
8711
        $course_id = $_course['real_id'];
8712
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
8713
        $learningPathId = (int) $learningPathId;
8714
        $id_in_path = (int) $id_in_path;
8715
8716
        $sql = "SELECT item_type, title, ref
8717
                FROM $tbl_lp_item
8718
                WHERE c_id = $course_id AND lp_id = $learningPathId AND iid = $id_in_path";
8719
        $res_item = Database::query($sql);
8720
8721
        if (Database::num_rows($res_item) < 1) {
8722
            return '';
8723
        }
8724
        $row_item = Database::fetch_array($res_item);
8725
        $type = strtolower($row_item['item_type']);
8726
        $id = $row_item['ref'];
8727
        $output = '';
8728
8729
        switch ($type) {
8730
            case TOOL_CALENDAR_EVENT:
8731
                $TABLEAGENDA = Database::get_course_table(TABLE_AGENDA);
8732
                $result = Database::query("SELECT * FROM $TABLEAGENDA WHERE c_id = $course_id AND id=$id");
8733
                $myrow = Database::fetch_array($result);
8734
                $output = $myrow['title'];
8735
                break;
8736
            case TOOL_ANNOUNCEMENT:
8737
                $tbl_announcement = Database::get_course_table(TABLE_ANNOUNCEMENT);
8738
                $result = Database::query("SELECT * FROM $tbl_announcement WHERE c_id = $course_id AND id=$id");
8739
                $myrow = Database::fetch_array($result);
8740
                $output = $myrow['title'];
8741
                break;
8742
            case TOOL_LINK:
8743
                // Doesn't take $target into account.
8744
                $TABLETOOLLINK = Database::get_course_table(TABLE_LINK);
8745
                $result = Database::query("SELECT * FROM $TABLETOOLLINK WHERE c_id = $course_id AND id=$id");
8746
                $myrow = Database::fetch_array($result);
8747
                $output = $myrow['title'];
8748
                break;
8749
            case TOOL_QUIZ:
8750
                $TBL_EXERCICES = Database::get_course_table(TABLE_QUIZ_TEST);
8751
                $result = Database::query("SELECT * FROM $TBL_EXERCICES WHERE c_id = $course_id AND id = $id");
8752
                $myrow = Database::fetch_array($result);
8753
                $output = $myrow['title'];
8754
                break;
8755
            case TOOL_FORUM:
8756
                $TBL_FORUMS = Database::get_course_table(TABLE_FORUM);
8757
                $result = Database::query("SELECT * FROM $TBL_FORUMS WHERE c_id = $course_id AND forum_id = $id");
8758
                $myrow = Database::fetch_array($result);
8759
                $output = $myrow['title'];
8760
                break;
8761
            case TOOL_THREAD:
8762
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8763
                // Grabbing the title of the post.
8764
                $sql_title = "SELECT * FROM $tbl_post WHERE c_id = $course_id AND post_id=".$id;
8765
                $result_title = Database::query($sql_title);
8766
                $myrow_title = Database::fetch_array($result_title);
8767
                $output = $myrow_title['title'];
8768
                break;
8769
            case TOOL_POST:
8770
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8771
                $sql = "SELECT * FROM $tbl_post p WHERE c_id = $course_id AND p.post_id = $id";
8772
                $result = Database::query($sql);
8773
                $post = Database::fetch_array($result);
8774
                $output = $post['title'];
8775
                break;
8776
            case 'dir':
8777
            case TOOL_DOCUMENT:
8778
            case 'video':
8779
                $title = $row_item['title'];
8780
                $output = '-';
8781
                if (!empty($title)) {
8782
                    $output = $title;
8783
                }
8784
                break;
8785
            case 'hotpotatoes':
8786
                $tbl_doc = Database::get_course_table(TABLE_DOCUMENT);
8787
                $result = Database::query("SELECT * FROM $tbl_doc WHERE c_id = $course_id AND iid = $id");
8788
                $myrow = Database::fetch_array($result);
8789
                $pathname = explode('/', $myrow['path']); // Making a correct name for the link.
8790
                $last = count($pathname) - 1; // Making a correct name for the link.
8791
                $filename = $pathname[$last]; // Making a correct name for the link.
8792
                $myrow['path'] = rawurlencode($myrow['path']);
8793
                $output = $filename;
8794
                break;
8795
        }
8796
8797
        return stripslashes($output);
8798
    }
8799
8800
    /**
8801
     * Get the parent names for the current item.
8802
     *
8803
     * @param int $newItemId Optional. The item ID
8804
     */
8805
    public function getCurrentItemParentNames($newItemId = 0): array
8806
    {
8807
        $newItemId = $newItemId ?: $this->get_current_item_id();
8808
        $return = [];
8809
        $item = $this->getItem($newItemId);
8810
8811
        $parent = null;
8812
        if ($item) {
8813
            $parent = $this->getItem($item->get_parent());
8814
        }
8815
8816
        while ($parent) {
8817
            $return[] = $parent->get_title();
8818
            $parent = $this->getItem($parent->get_parent());
8819
        }
8820
8821
        return array_reverse($return);
8822
    }
8823
8824
    /**
8825
     * Reads and process "lp_subscription_settings" setting.
8826
     *
8827
     * @return array
8828
     */
8829
    public static function getSubscriptionSettings()
8830
    {
8831
        $subscriptionSettings = api_get_setting('lp.lp_subscription_settings', true);
8832
        if (!is_array($subscriptionSettings)) {
8833
            // By default, allow both settings
8834
            $subscriptionSettings = [
8835
                'allow_add_users_to_lp' => true,
8836
                'allow_add_users_to_lp_category' => true,
8837
            ];
8838
        } else {
8839
            $subscriptionSettings = $subscriptionSettings['options'];
8840
        }
8841
8842
        return $subscriptionSettings;
8843
    }
8844
8845
    /**
8846
     * Exports a LP to a courseBuilder zip file. It adds the documents related to the LP.
8847
     */
8848
    public function exportToCourseBuildFormat()
8849
    {
8850
        if (!api_is_allowed_to_edit()) {
8851
            return false;
8852
        }
8853
8854
        $courseBuilder = new CourseBuilder();
8855
        $itemList = [];
8856
        /** @var learnpathItem $item */
8857
        foreach ($this->items as $item) {
8858
            $itemList[$item->get_type()][] = $item->get_path();
8859
        }
8860
8861
        if (empty($itemList)) {
8862
            return false;
8863
        }
8864
8865
        if (isset($itemList['document'])) {
8866
            // Get parents
8867
            foreach ($itemList['document'] as $documentId) {
8868
                $documentInfo = DocumentManager::get_document_data_by_id($documentId, api_get_course_id(), true);
8869
                if (!empty($documentInfo['parents'])) {
8870
                    foreach ($documentInfo['parents'] as $parentInfo) {
8871
                        if (in_array($parentInfo['iid'], $itemList['document'])) {
8872
                            continue;
8873
                        }
8874
                        $itemList['document'][] = $parentInfo['iid'];
8875
                    }
8876
                }
8877
            }
8878
8879
            $courseInfo = api_get_course_info();
8880
            foreach ($itemList['document'] as $documentId) {
8881
                $documentInfo = DocumentManager::get_document_data_by_id($documentId, api_get_course_id());
8882
                $items = DocumentManager::get_resources_from_source_html(
8883
                    $documentInfo['absolute_path'],
8884
                    true,
8885
                    TOOL_DOCUMENT
8886
                );
8887
8888
                if (!empty($items)) {
8889
                    foreach ($items as $item) {
8890
                        // Get information about source url
8891
                        $url = $item[0]; // url
8892
                        $scope = $item[1]; // scope (local, remote)
8893
                        $type = $item[2]; // type (rel, abs, url)
8894
8895
                        $origParseUrl = parse_url($url);
8896
                        $realOrigPath = isset($origParseUrl['path']) ? $origParseUrl['path'] : null;
8897
8898
                        if ('local' === $scope) {
8899
                            if ('abs' === $type || 'rel' === $type) {
8900
                                $documentFile = strstr($realOrigPath, 'document');
8901
                                if (false !== strpos($realOrigPath, $documentFile)) {
8902
                                    $documentFile = str_replace('document', '', $documentFile);
8903
                                    $itemDocumentId = DocumentManager::get_document_id($courseInfo, $documentFile);
8904
                                    // Document found! Add it to the list
8905
                                    if ($itemDocumentId) {
8906
                                        $itemList['document'][] = $itemDocumentId;
8907
                                    }
8908
                                }
8909
                            }
8910
                        }
8911
                    }
8912
                }
8913
            }
8914
8915
            $courseBuilder->build_documents(
8916
                api_get_session_id(),
8917
                $this->get_course_int_id(),
8918
                true,
8919
                $itemList['document']
8920
            );
8921
        }
8922
8923
        if (isset($itemList['quiz'])) {
8924
            $courseBuilder->build_quizzes(
8925
                api_get_session_id(),
8926
                $this->get_course_int_id(),
8927
                true,
8928
                $itemList['quiz']
8929
            );
8930
        }
8931
8932
        if (!empty($itemList['thread'])) {
8933
            $threadList = [];
8934
            $repo = Container::getForumThreadRepository();
8935
            foreach ($itemList['thread'] as $threadId) {
8936
                /** @var CForumThread $thread */
8937
                $thread = $repo->find($threadId);
8938
                if ($thread) {
8939
                    $itemList['forum'][] = $thread->getForum() ? $thread->getForum()->getIid() : 0;
8940
                    $threadList[] = $thread->getIid();
8941
                }
8942
            }
8943
8944
            if (!empty($threadList)) {
8945
                $courseBuilder->build_forum_topics(
8946
                    api_get_session_id(),
8947
                    $this->get_course_int_id(),
8948
                    null,
8949
                    $threadList
8950
                );
8951
            }
8952
        }
8953
8954
        $forumCategoryList = [];
8955
        if (isset($itemList['forum'])) {
8956
            foreach ($itemList['forum'] as $forumId) {
8957
                $forumInfo = get_forums($forumId);
8958
                $forumCategoryList[] = $forumInfo['forum_category'];
8959
            }
8960
        }
8961
8962
        if (!empty($forumCategoryList)) {
8963
            $courseBuilder->build_forum_category(
8964
                api_get_session_id(),
8965
                $this->get_course_int_id(),
8966
                true,
8967
                $forumCategoryList
8968
            );
8969
        }
8970
8971
        if (!empty($itemList['forum'])) {
8972
            $courseBuilder->build_forums(
8973
                api_get_session_id(),
8974
                $this->get_course_int_id(),
8975
                true,
8976
                $itemList['forum']
8977
            );
8978
        }
8979
8980
        if (isset($itemList['link'])) {
8981
            $courseBuilder->build_links(
8982
                api_get_session_id(),
8983
                $this->get_course_int_id(),
8984
                true,
8985
                $itemList['link']
8986
            );
8987
        }
8988
8989
        if (!empty($itemList['student_publication'])) {
8990
            $courseBuilder->build_works(
8991
                api_get_session_id(),
8992
                $this->get_course_int_id(),
8993
                true,
8994
                $itemList['student_publication']
8995
            );
8996
        }
8997
8998
        $courseBuilder->build_learnpaths(
8999
            api_get_session_id(),
9000
            $this->get_course_int_id(),
9001
            true,
9002
            [$this->get_id()],
9003
            false
9004
        );
9005
9006
        $courseBuilder->restoreDocumentsFromList();
9007
9008
        $zipFile = CourseArchiver::createBackup($courseBuilder->course);
9009
        $zipPath = CourseArchiver::getBackupDir().$zipFile;
9010
        $result = DocumentManager::file_send_for_download(
9011
            $zipPath,
9012
            true,
9013
            $this->get_name().'.zip'
9014
        );
9015
9016
        if ($result) {
9017
            api_not_allowed();
9018
        }
9019
9020
        return true;
9021
    }
9022
9023
    /**
9024
     * Get whether this is a learning path with the accumulated work time or not.
9025
     *
9026
     * @return int
9027
     */
9028
    public function getAccumulateWorkTime()
9029
    {
9030
        return (int) $this->accumulateWorkTime;
9031
    }
9032
9033
    /**
9034
     * Get whether this is a learning path with the accumulated work time or not.
9035
     *
9036
     * @return int
9037
     */
9038
    public function getAccumulateWorkTimeTotalCourse()
9039
    {
9040
        $table = Database::get_course_table(TABLE_LP_MAIN);
9041
        $sql = "SELECT SUM(accumulate_work_time) AS total
9042
                FROM $table
9043
                WHERE c_id = ".$this->course_int_id;
9044
        $result = Database::query($sql);
9045
        $row = Database::fetch_array($result);
9046
9047
        return (int) $row['total'];
9048
    }
9049
9050
    /**
9051
     * @param int $lpId
9052
     * @param int $courseId
9053
     *
9054
     * @return mixed
9055
     */
9056
    public static function getAccumulateWorkTimePrerequisite($lpId, $courseId)
9057
    {
9058
        $lpId = (int) $lpId;
9059
        $table = Database::get_course_table(TABLE_LP_MAIN);
9060
        $sql = "SELECT accumulate_work_time
9061
                FROM $table
9062
                WHERE iid = $lpId";
9063
        $result = Database::query($sql);
9064
        $row = Database::fetch_array($result);
9065
9066
        return $row['accumulate_work_time'];
9067
    }
9068
9069
    /**
9070
     * @param int $courseId
9071
     *
9072
     * @return int
9073
     */
9074
    public static function getAccumulateWorkTimeTotal($courseId)
9075
    {
9076
        $table = Database::get_course_table(TABLE_LP_MAIN);
9077
        $courseId = (int) $courseId;
9078
        $sql = "SELECT SUM(accumulate_work_time) AS total
9079
                FROM $table
9080
                WHERE c_id = $courseId";
9081
        $result = Database::query($sql);
9082
        $row = Database::fetch_array($result);
9083
9084
        return (int) $row['total'];
9085
    }
9086
9087
    /**
9088
     * In order to use the lp icon option you need to create the "lp_icon" LP extra field
9089
     * and put the images in.
9090
     */
9091
    public static function getIconSelect(): array
9092
    {
9093
        $theme = Container::$container->get(ThemeHelper::class)->getVisualTheme();
9094
        $filesystem = Container::$container->get('oneup_flysystem.themes_filesystem');
9095
9096
        if (!$filesystem->directoryExists("$theme/lp_icons")) {
9097
            return [];
9098
        }
9099
9100
        $icons = ['' => get_lang('Please select an option')];
9101
9102
        $iconFiles = $filesystem->listContents("$theme/lp_icons");
9103
        $allowedExtensions = ['image/jpeg', 'image/jpg', 'image/png'];
9104
9105
        foreach ($iconFiles as $iconFile) {
9106
            $mimeType = $filesystem->mimeType($iconFile->path());
9107
9108
            if (in_array($mimeType, $allowedExtensions)) {
9109
                $basename = basename($iconFile->path());
9110
                $icons[$basename] = $basename;
9111
            }
9112
        }
9113
9114
        return $icons;
9115
    }
9116
9117
    /**
9118
     * @param int $lpId
9119
     *
9120
     * @return string
9121
     */
9122
    public static function getSelectedIcon($lpId)
9123
    {
9124
        $extraFieldValue = new ExtraFieldValue('lp');
9125
        $lpIcon = $extraFieldValue->get_values_by_handler_and_field_variable($lpId, 'lp_icon');
9126
        $icon = '';
9127
        if (!empty($lpIcon) && isset($lpIcon['value'])) {
9128
            $icon = $lpIcon['value'];
9129
        }
9130
9131
        return $icon;
9132
    }
9133
9134
    public static function getSelectedIconHtml(int $lpId): string
9135
    {
9136
        $icon = self::getSelectedIcon($lpId);
9137
9138
        if (empty($icon)) {
9139
            return '';
9140
        }
9141
9142
        $path = Container::getThemeHelper()->getThemeAssetUrl("lp_icons/$icon");
9143
9144
        return Display::img($path);
9145
    }
9146
9147
    /**
9148
     * @param string $value
9149
     *
9150
     * @return string
9151
     */
9152
    public function cleanItemTitle($value)
9153
    {
9154
        $value = Security::remove_XSS(strip_tags($value));
9155
9156
        return $value;
9157
    }
9158
9159
    public function setItemTitle(FormValidator $form)
9160
    {
9161
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
9162
            $form->addHtmlEditor(
9163
                'title',
9164
                get_lang('Title'),
9165
                true,
9166
                false,
9167
                ['ToolbarSet' => 'TitleAsHtml', 'id' => uniqid('editor')]
9168
            );
9169
        } else {
9170
            $form->addText('title', get_lang('Title'), true, ['id' => 'idTitle', 'class' => 'learnpath_item_form']);
9171
            $form->applyFilter('title', 'trim');
9172
            $form->applyFilter('title', 'html_filter');
9173
        }
9174
    }
9175
9176
    /**
9177
     * @return array
9178
     */
9179
    public function getItemsForForm($addParentCondition = false)
9180
    {
9181
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
9182
9183
        $sql = "SELECT * FROM $tbl_lp_item
9184
                WHERE path <> 'root' AND lp_id = ".$this->lp_id;
9185
9186
        if ($addParentCondition) {
9187
            $sql .= ' AND parent_item_id IS NULL ';
9188
        }
9189
        $sql .= ' ORDER BY display_order ASC';
9190
9191
        $result = Database::query($sql);
9192
        $arrLP = [];
9193
        while ($row = Database::fetch_array($result)) {
9194
            $arrLP[] = [
9195
                'iid' => $row['iid'],
9196
                'id' => $row['iid'],
9197
                'item_type' => $row['item_type'],
9198
                'title' => $this->cleanItemTitle($row['title']),
9199
                'title_raw' => $row['title'],
9200
                'path' => $row['path'],
9201
                'description' => Security::remove_XSS($row['description']),
9202
                'parent_item_id' => $row['parent_item_id'],
9203
                'previous_item_id' => $row['previous_item_id'],
9204
                'next_item_id' => $row['next_item_id'],
9205
                'display_order' => $row['display_order'],
9206
                'max_score' => $row['max_score'],
9207
                'min_score' => $row['min_score'],
9208
                'mastery_score' => $row['mastery_score'],
9209
                'prerequisite' => $row['prerequisite'],
9210
                'max_time_allowed' => $row['max_time_allowed'],
9211
                'prerequisite_min_score' => $row['prerequisite_min_score'],
9212
                'prerequisite_max_score' => $row['prerequisite_max_score'],
9213
            ];
9214
        }
9215
9216
        return $arrLP;
9217
    }
9218
9219
    /**
9220
     * Gets whether this SCORM learning path has been marked to use the score
9221
     * as progress. Takes into account whether the learnpath matches (SCORM
9222
     * content + less than 2 items).
9223
     *
9224
     * @return bool True if the score should be used as progress, false otherwise
9225
     */
9226
    public function getUseScoreAsProgress()
9227
    {
9228
        // If not a SCORM, we don't care about the setting
9229
        if (2 != $this->get_type()) {
9230
            return false;
9231
        }
9232
        // If more than one step in the SCORM, we don't care about the setting
9233
        if ($this->get_total_items_count() > 1) {
9234
            return false;
9235
        }
9236
        $extraFieldValue = new ExtraFieldValue('lp');
9237
        $doUseScore = false;
9238
        $useScore = $extraFieldValue->get_values_by_handler_and_field_variable(
9239
            $this->get_id(),
9240
            'use_score_as_progress'
9241
        );
9242
        if (!empty($useScore) && isset($useScore['value'])) {
9243
            $doUseScore = $useScore['value'];
9244
        }
9245
9246
        return $doUseScore;
9247
    }
9248
9249
    /**
9250
     * Get the user identifier (user_id or username
9251
     * Depends on scorm_api_username_as_student_id in app/config/configuration.php.
9252
     *
9253
     * @return string User ID or username, depending on configuration setting
9254
     */
9255
    public static function getUserIdentifierForExternalServices()
9256
    {
9257
        $scormApiExtraFieldUseStudentId = api_get_setting('lp.scorm_api_extrafield_to_use_as_student_id');
9258
        $extraFieldValue = new ExtraFieldValue('user');
9259
        $extrafield = $extraFieldValue->get_values_by_handler_and_field_variable(
9260
            api_get_user_id(),
9261
            $scormApiExtraFieldUseStudentId
9262
        );
9263
        if (is_array($extrafield) && isset($extrafield['value'])) {
9264
            return $extrafield['value'];
9265
        } else {
9266
            if ('true' === $scormApiExtraFieldUseStudentId) {
9267
                return api_get_user_info(api_get_user_id())['username'];
9268
            } else {
9269
                return api_get_user_id();
9270
            }
9271
        }
9272
    }
9273
9274
    /**
9275
     * Save the new order for learning path items.
9276
     *
9277
     * @param array $orderList A associative array with id and parent_id keys.
9278
     */
9279
    public static function sortItemByOrderList(CLpItem $rootItem, array $orderList = [], $flush = true, $lpItemRepo = null, $em = null)
9280
    {
9281
        if (empty($orderList)) {
9282
            return true;
9283
        }
9284
        if (!isset($lpItemRepo)) {
9285
            $lpItemRepo = Container::getLpItemRepository();
9286
        }
9287
        if (!isset($em)) {
9288
            $em = Database::getManager();
9289
        }
9290
        $counter = 2;
9291
        $rootItem->setDisplayOrder(1);
9292
        $rootItem->setPreviousItemId(null);
9293
        $em->persist($rootItem);
9294
        if ($flush) {
9295
            $em->flush();
9296
        }
9297
9298
        foreach ($orderList as $item) {
9299
            $itemId = $item->id ?? 0;
9300
            if (empty($itemId)) {
9301
                continue;
9302
            }
9303
            $parentId = $item->parent_id ?? 0;
9304
            $parent = $rootItem;
9305
            if (!empty($parentId)) {
9306
                $parentExists = $lpItemRepo->find($parentId);
9307
                if (null !== $parentExists) {
9308
                    $parent = $parentExists;
9309
                }
9310
            }
9311
9312
            /** @var CLpItem $itemEntity */
9313
            $itemEntity = $lpItemRepo->find($itemId);
9314
            $itemEntity->setParent($parent);
9315
            $itemEntity->setPreviousItemId(null);
9316
            $itemEntity->setNextItemId(null);
9317
            $itemEntity->setDisplayOrder($counter);
9318
9319
            $em->persist($itemEntity);
9320
            if ($flush) {
9321
                $em->flush();
9322
            }
9323
            $counter++;
9324
        }
9325
9326
        $lpItemRepo->recoverNode($rootItem, 'displayOrder');
9327
        $em->persist($rootItem);
9328
        if ($flush) {
9329
            $em->flush();
9330
        }
9331
9332
        return true;
9333
    }
9334
9335
    public static function move(int $lpId, string $direction)
9336
    {
9337
        $em = Database::getManager();
9338
        /** @var CLp $lp */
9339
        $lp = Container::getLpRepository()->find($lpId);
9340
        if ($lp) {
9341
            $course = api_get_course_entity();
9342
            $session = api_get_session_entity();
9343
            $group = api_get_group_entity();
9344
9345
            $link = $lp->getResourceNode()->getResourceLinkByContext($course, $session, $group);
9346
9347
            if ($link) {
9348
                if ('down' === $direction) {
9349
                    $link->moveDownPosition();
9350
                }
9351
                if ('up' === $direction) {
9352
                    $link->moveUpPosition();
9353
                }
9354
9355
                $em->flush();
9356
            }
9357
        }
9358
    }
9359
9360
    /**
9361
     * Get the depth level of LP item.
9362
     *
9363
     * @param array $items
9364
     * @param int   $currentItemId
9365
     *
9366
     * @return int
9367
     */
9368
    private static function get_level_for_item($items, $currentItemId)
9369
    {
9370
        $parentItemId = 0;
9371
        if (isset($items[$currentItemId])) {
9372
            $parentItemId = $items[$currentItemId]->parent;
9373
        }
9374
9375
        if (0 == $parentItemId) {
9376
            return 0;
9377
        }
9378
9379
        return self::get_level_for_item($items, $parentItemId) + 1;
9380
    }
9381
9382
    /**
9383
     * Generate the link for a learnpath category as course tool.
9384
     *
9385
     * @param int $categoryId
9386
     *
9387
     * @return string
9388
     */
9389
    private static function getCategoryLinkForTool($categoryId)
9390
    {
9391
        $categoryId = (int) $categoryId;
9392
        return 'lp/lp_controller.php?'.api_get_cidreq().'&'
9393
            .http_build_query(
9394
                [
9395
                    'action' => 'view_category',
9396
                    'id' => $categoryId,
9397
                ]
9398
            );
9399
    }
9400
9401
    /**
9402
     * Check and obtain the lp final item if exist.
9403
     *
9404
     * @return learnpathItem
9405
     */
9406
    public function getFinalItem()
9407
    {
9408
        if (empty($this->items)) {
9409
            return null;
9410
        }
9411
9412
        foreach ($this->items as $item) {
9413
            if ('final_item' !== $item->type) {
9414
                continue;
9415
            }
9416
9417
            return $item;
9418
        }
9419
    }
9420
9421
    /**
9422
     * Get the LP Final Item Template.
9423
     *
9424
     * @return string
9425
     */
9426
    private function getFinalItemTemplate()
9427
    {
9428
        return file_get_contents(api_get_path(SYS_CODE_PATH).'lp/final_item_template/template.html');
9429
    }
9430
9431
    /**
9432
     * Get the LP Final Item Url.
9433
     *
9434
     * @return string
9435
     */
9436
    private function getSavedFinalItem(): string
9437
    {
9438
        $finalItem = $this->getFinalItem();
9439
9440
        if (!$finalItem || empty($finalItem->path)) {
9441
            return '';
9442
        }
9443
9444
        $documentId = (int) $finalItem->path;
9445
9446
        // Always load the real course document entity (avoid loading other resource types like Skill).
9447
        $em = Database::getManager();
9448
        /** @var CDocument|null $document */
9449
        $document = $em->getRepository(CDocument::class)->find($documentId);
9450
9451
        if (!$document) {
9452
            // Document was deleted or path is corrupted.
9453
            return '';
9454
        }
9455
9456
        $documentRepo = Container::getDocumentRepository();
9457
9458
        try {
9459
            return (string) $documentRepo->getResourceFileContent($document);
9460
        } catch (FileNotFoundException $e) {
9461
            // Fallback for cases where the resource has no file on disk (editable content, etc.)
9462
            $node = $document->getResourceNode();
9463
9464
            if ($node
9465
                && method_exists($node, 'hasEditableTextContent')
9466
                && $node->hasEditableTextContent()
9467
                && method_exists($node, 'getEditableTextContent')
9468
            ) {
9469
                return (string) $node->getEditableTextContent();
9470
            }
9471
9472
            return '';
9473
        } catch (\Throwable $e) {
9474
            return '';
9475
        }
9476
    }
9477
9478
    /**
9479
     * Recalculates the results for all exercises associated with the learning path (LP) for the given user.
9480
     */
9481
    public function recalculateResultsForLp(int $userId): void
9482
    {
9483
        $em = Database::getManager();
9484
        $lpItemRepo = $em->getRepository(CLpItem::class);
9485
        $lpItems = $lpItemRepo->findBy(['lp' => $this->lp_id]);
9486
9487
        if (empty($lpItems)) {
9488
            Display::addFlash(Display::return_message(get_lang('No item found'), 'error'));
9489
            return;
9490
        }
9491
9492
        $lpItemsById = [];
9493
        foreach ($lpItems as $item) {
9494
            $lpItemsById[$item->getIid()] = $item;
9495
        }
9496
9497
        $trackEExerciseRepo = $em->getRepository(TrackEExercise::class);
9498
        $trackExercises = $trackEExerciseRepo->createQueryBuilder('te')
9499
            ->where('te.origLpId = :lpId')
9500
            ->andWhere('te.user = :userId')
9501
            ->andWhere('te.origLpItemId IN (:lpItemIds)')
9502
            ->setParameter('lpId', $this->lp_id)
9503
            ->setParameter('userId', $userId)
9504
            ->setParameter('lpItemIds', array_keys($lpItemsById))
9505
            ->getQuery()
9506
            ->getResult();
9507
9508
        if (empty($trackExercises)) {
9509
            Display::addFlash(Display::return_message(get_lang('No test attempt found'), 'error'));
9510
            return;
9511
        }
9512
9513
        foreach ($trackExercises as $trackExercise) {
9514
            $exeId = $trackExercise->getExeId();
9515
            $lpItemId = $trackExercise->getOrigLpItemId();
9516
9517
            if (!isset($lpItemsById[$lpItemId])) {
9518
                continue;
9519
            }
9520
9521
            $lpItem = $lpItemsById[$lpItemId];
9522
            if ('quiz' !== $lpItem->getItemType()) {
9523
                continue;
9524
            }
9525
9526
            $quizId = (int) $lpItem->getPath();
9527
            $courseId = (int) $trackExercise->getCourse()->getId();
9528
            $updatedExercise = ExerciseLib::recalculateResult($exeId, $userId, $quizId, $courseId);
9529
            if ($updatedExercise instanceof TrackEExercise) {
9530
                Display::addFlash(Display::return_message(get_lang('Results recalculated'), 'success'));
9531
            } else {
9532
                Display::addFlash(Display::return_message(get_lang('Error recalculating results'), 'error'));
9533
            }
9534
        }
9535
    }
9536
9537
    /**
9538
     * Returns the video player HTML for a video-type document LP item.
9539
     *
9540
     * @param int $lpItemId
9541
     * @param string $autostart
9542
     *
9543
     * @return string
9544
     */
9545
    public function getVideoPlayer(CDocument $document, string $autostart = 'true'): string
9546
    {
9547
        $resourceNode = $document->getResourceNode();
9548
        $resourceFile = $resourceNode?->getFirstResourceFile();
9549
9550
        if (!$resourceNode || !$resourceFile) {
9551
            return '';
9552
        }
9553
9554
        $resourceNodeRepository = Container::getResourceNodeRepository();
9555
        $videoUrl = $resourceNodeRepository->getResourceFileUrl($resourceNode);
9556
9557
        if (empty($videoUrl)) {
9558
            return '';
9559
        }
9560
9561
        $fileName = $resourceFile->getTitle();
9562
        $ext = pathinfo($fileName, PATHINFO_EXTENSION);
9563
        $mimeType = $resourceFile->getMimeType() ?: 'video/mp4';
9564
        $autoplayAttr = ($autostart === 'true') ? 'autoplay muted playsinline' : '';
9565
9566
        $html = '';
9567
        $html .= '
9568
        <video id="lp-video" width="100%" height="auto" controls '.$autoplayAttr.'>
9569
            <source src="'.$videoUrl.'" type="$mimeType">
9570
        </video>';
9571
9572
        return $html;
9573
    }
9574
}
9575