learnpath::next()   A
last analyzed

Complexity

Conditions 5
Paths 16

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 16
nop 0
dl 0
loc 22
rs 9.4222
c 0
b 0
f 0
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\Routing\Generator\UrlGeneratorInterface;
38
39
/**
40
 * Class learnpath
41
 * This class defines the parent attributes and methods for Chamilo learnpaths
42
 * and SCORM learnpaths. It is used by the scorm class.
43
 *
44
 * @todo decouple class
45
 *
46
 * @author  Yannick Warnier <[email protected]>
47
 * @author  Julio Montoya   <[email protected]> Several improvements and fixes
48
 */
49
class learnpath
50
{
51
    public const MAX_LP_ITEM_TITLE_LENGTH = 36;
52
    public const STATUS_CSS_CLASS_NAME = [
53
        'not attempted' => 'scorm_not_attempted',
54
        'incomplete' => 'scorm_not_attempted',
55
        'failed' => 'scorm_failed',
56
        'completed' => 'scorm_completed',
57
        'passed' => 'scorm_completed',
58
        'succeeded' => 'scorm_completed',
59
        'browsed' => 'scorm_completed',
60
    ];
61
62
    public $attempt = 0; // The number for the current ID view.
63
    public $cc; // Course (code) this learnpath is located in. @todo change name for something more comprensible ...
64
    public $current; // Id of the current item the user is viewing.
65
    public $current_score; // The score of the current item.
66
    public $current_time_start; // The time the user loaded this resource (this does not mean he can see it yet).
67
    public $current_time_stop; // The time the user closed this resource.
68
    public $default_status = 'not attempted';
69
    public $encoding = 'UTF-8';
70
    public $error = '';
71
    public $force_commit = false; // For SCORM only- if true will send a scorm LMSCommit() request on each LMSSetValue()
72
    public $index; // The index of the active learnpath_item in $ordered_items array.
73
    /** @var learnpathItem[] */
74
    public $items = [];
75
    public $last; // item_id of last item viewed in the learning path.
76
    public $last_item_seen = 0; // In case we have already come in this lp, reuse the last item seen if authorized.
77
    public $license; // Which license this course has been given - not used yet on 20060522.
78
    public $lp_id; // DB iid for this learnpath.
79
    public $lp_view_id; // DB ID for lp_view
80
    public $maker; // Which maker has conceived the content (ENI, Articulate, ...).
81
    public $message = '';
82
    public $mode = 'embedded'; // Holds the video display mode (fullscreen or embedded).
83
    public $name; // Learnpath name (they generally have one).
84
    public $ordered_items = []; // List of the learnpath items in the order they are to be read.
85
    public $path = ''; // Path inside the scorm directory (if scorm).
86
    public $theme; // The current theme of the learning path.
87
    public $accumulateScormTime; // Flag to decide whether to accumulate SCORM time or not
88
    public $accumulateWorkTime; // The min time of learnpath
89
90
    // Tells if all the items of the learnpath can be tried again. Defaults to "no" (=1).
91
    public $prevent_reinit = 1;
92
93
    // Describes the mode of progress bar display.
94
    public $seriousgame_mode = 0;
95
    public $progress_bar_mode = '%';
96
97
    // Percentage progress as saved in the db.
98
    public $progress_db = 0;
99
    public $proximity; // Wether the content is distant or local or unknown.
100
    public $refs_list = []; //list of items by ref => db_id. Used only for prerequisites match.
101
    // !!!This array (refs_list) is built differently depending on the nature of the LP.
102
    // If SCORM, uses ref, if Chamilo, uses id to keep a unique value.
103
    public $type; //type of learnpath. Could be 'chamilo', 'scorm', 'scorm2004', 'aicc', ...
104
    // TODO: Check if this type variable is useful here (instead of just in the controller script).
105
    public $user_id; //ID of the user that is viewing/using the course
106
    public $update_queue = [];
107
    public $scorm_debug = 0;
108
    public $arrMenu = []; // Array for the menu items.
109
    public $debug = 0; // Logging level.
110
    public $lp_session_id = 0;
111
    public $lp_view_session_id = 0; // The specific view might be bound to a session.
112
    public $prerequisite = 0;
113
    public $use_max_score = 1; // 1 or 0
114
    public $subscribeUsers = 0; // Subscribe users or not
115
    public $created_on = '';
116
    public $modified_on = '';
117
    public $published_on = '';
118
    public $expired_on = '';
119
    public $ref;
120
    public $course_int_id;
121
    public $course_info;
122
    public $categoryId;
123
    public $scormUrl;
124
    public $entity;
125
    public $auto_forward_video = 1;
126
127
    public $js_lib;
128
    public $author;
129
    public $hide_toc_frame;
130
    public $max_ordered_items;
131
132
    public function __construct(CLp $entity = null, $course_info, $user_id)
133
    {
134
        $debug = $this->debug;
135
        $user_id = (int) $user_id;
136
        $this->encoding = api_get_system_encoding();
137
        $lp_id = 0;
138
        if (null !== $entity) {
139
            $lp_id = $entity->getIid();
140
        }
141
        $course_info = empty($course_info) ? api_get_course_info() : $course_info;
142
        $course_id = (int) $course_info['real_id'];
143
        $this->course_info = $course_info;
144
        $this->set_course_int_id($course_id);
145
        if (empty($lp_id) || empty($course_id)) {
146
            $this->error = "Parameter is empty: LpId:'$lp_id', courseId: '$lp_id'";
147
        } else {
148
            //$this->entity = $entity;
149
            $this->lp_id = $lp_id;
150
            $this->type = $entity->getLpType();
151
            $this->name = stripslashes($entity->getTitle());
152
            $this->proximity = $entity->getContentLocal();
153
            $this->theme = $entity->getTheme();
154
            $this->maker = $entity->getContentLocal();
155
            $this->prevent_reinit = $entity->getPreventReinit();
156
            $this->seriousgame_mode = $entity->getSeriousgameMode();
157
            $this->license = $entity->getContentLicense();
158
            $this->scorm_debug = $entity->getDebug();
159
            $this->js_lib = $entity->getJsLib();
160
            $this->path = $entity->getPath();
161
            $this->author = $entity->getAuthor();
162
            $this->hide_toc_frame = $entity->getHideTocFrame();
163
            //$this->lp_session_id = $entity->getSessionId();
164
            $this->use_max_score = $entity->getUseMaxScore();
165
            $this->subscribeUsers = $entity->getSubscribeUsers();
166
            $this->created_on = $entity->getCreatedOn()->format('Y-m-d H:i:s');
167
            $this->modified_on = $entity->getModifiedOn()->format('Y-m-d H:i:s');
168
            $this->ref = $entity->getRef();
169
            $this->auto_forward_video = $entity->getAutoForwardVideo();
170
            $this->categoryId = 0;
171
            if ($entity->getCategory()) {
172
                $this->categoryId = $entity->getCategory()->getIid();
173
            }
174
175
            if ($entity->hasAsset()) {
176
                $asset = $entity->getAsset();
177
                $this->scormUrl = Container::getAssetRepository()->getAssetUrl($asset).'/'.$entity->getPath().'/';
178
            }
179
180
            $this->accumulateScormTime = $entity->getAccumulateWorkTime();
181
182
            if (!empty($entity->getPublishedOn())) {
183
                $this->published_on = $entity->getPublishedOn()->format('Y-m-d H:i:s');
184
            }
185
186
            if (!empty($entity->getExpiredOn())) {
187
                $this->expired_on = $entity->getExpiredOn()->format('Y-m-d H:i:s');
188
            }
189
            if (2 == $this->type) {
190
                if (1 == $entity->getForceCommit()) {
191
                    $this->force_commit = true;
192
                }
193
            }
194
            $this->mode = $entity->getDefaultViewMod();
195
196
            // Check user ID.
197
            if (empty($user_id)) {
198
                $this->error = 'User ID is empty';
199
            } else {
200
                $this->user_id = $user_id;
201
            }
202
203
            // End of variables checking.
204
            $session_id = api_get_session_id();
205
            //  Get the session condition for learning paths of the base + session.
206
            $session = api_get_session_condition($session_id);
207
            // Now get the latest attempt from this user on this LP, if available, otherwise create a new one.
208
            $lp_table = Database::get_course_table(TABLE_LP_VIEW);
209
210
            // Selecting by view_count descending allows to get the highest view_count first.
211
            $sql = "SELECT * FROM $lp_table
212
                    WHERE
213
                        c_id = $course_id AND
214
                        lp_id = $lp_id AND
215
                        user_id = $user_id
216
                        $session
217
                    ORDER BY view_count DESC";
218
            $res = Database::query($sql);
219
220
            if (Database::num_rows($res) > 0) {
221
                $row = Database::fetch_array($res);
222
                $this->attempt = $row['view_count'];
223
                $this->lp_view_id = $row['iid'];
224
                $this->last_item_seen = $row['last_item'];
225
                $this->progress_db = $row['progress'];
226
                $this->lp_view_session_id = $row['session_id'];
227
            } elseif (!api_is_invitee()) {
228
                $this->attempt = 1;
229
                $params = [
230
                    'c_id' => $course_id,
231
                    'lp_id' => $lp_id,
232
                    'user_id' => $user_id,
233
                    'view_count' => 1,
234
                    //'session_id' => $session_id,
235
                    'last_item' => 0,
236
                ];
237
                if (!empty($session_id)) {
238
                    $params['session_id'] = $session_id;
239
                }
240
                $this->last_item_seen = 0;
241
                $this->lp_view_session_id = $session_id;
242
                $this->lp_view_id = Database::insert($lp_table, $params);
243
            }
244
245
            $criteria = new Criteria();
246
            $criteria
247
                ->where($criteria->expr()->neq('path', 'root'))
248
                ->orderBy(
249
                    [
250
                        'parent' => Criteria::ASC,
251
                        'displayOrder' => Criteria::ASC,
252
                    ]
253
                );
254
            $items = $entity->getItems()->matching($criteria);
255
            $lp_item_id_list = [];
256
            foreach ($items as $item) {
257
                $itemId = $item->getIid();
258
                $lp_item_id_list[] = $itemId;
259
260
                switch ($this->type) {
261
                    case CLp::AICC_TYPE:
262
                        $oItem = new aiccItem('db', $itemId, $course_id);
263
                        if (is_object($oItem)) {
264
                            $oItem->set_lp_view($this->lp_view_id);
265
                            $oItem->set_prevent_reinit($this->prevent_reinit);
266
                            // Don't use reference here as the next loop will make the pointed object change.
267
                            $this->items[$itemId] = $oItem;
268
                            $this->refs_list[$oItem->ref] = $itemId;
269
                        }
270
                        break;
271
                    case CLp::SCORM_TYPE:
272
                        $oItem = new scormItem('db', $itemId);
273
                        if (is_object($oItem)) {
274
                            $oItem->set_lp_view($this->lp_view_id);
275
                            $oItem->set_prevent_reinit($this->prevent_reinit);
276
                            // Don't use reference here as the next loop will make the pointed object change.
277
                            $this->items[$itemId] = $oItem;
278
                            $this->refs_list[$oItem->ref] = $itemId;
279
                        }
280
                        break;
281
                    case CLp::LP_TYPE:
282
                    default:
283
                        $oItem = new learnpathItem(null, $item);
284
                        if (is_object($oItem)) {
285
                            // Moved down to when we are sure the item_view exists.
286
                            //$oItem->set_lp_view($this->lp_view_id);
287
                            $oItem->set_prevent_reinit($this->prevent_reinit);
288
                            // Don't use reference here as the next loop will make the pointed object change.
289
                            $this->items[$itemId] = $oItem;
290
                            $this->refs_list[$itemId] = $itemId;
291
                        }
292
                        break;
293
                }
294
295
                // Setting the object level with variable $this->items[$i][parent]
296
                foreach ($this->items as $itemLPObject) {
297
                    $level = self::get_level_for_item($this->items, $itemLPObject->db_id);
298
                    $itemLPObject->level = $level;
299
                }
300
301
                // Setting the view in the item object.
302
                if (isset($this->items[$itemId]) && is_object($this->items[$itemId])) {
303
                    $this->items[$itemId]->set_lp_view($this->lp_view_id);
304
                    if (TOOL_HOTPOTATOES == $this->items[$itemId]->get_type()) {
305
                        $this->items[$itemId]->current_start_time = 0;
306
                        $this->items[$itemId]->current_stop_time = 0;
307
                    }
308
                }
309
            }
310
311
            if (!empty($lp_item_id_list)) {
312
                $lp_item_id_list_to_string = implode("','", $lp_item_id_list);
313
                if (!empty($lp_item_id_list_to_string)) {
314
                    // Get last viewing vars.
315
                    $itemViewTable = Database::get_course_table(TABLE_LP_ITEM_VIEW);
316
                    // This query should only return one or zero result.
317
                    $sql = "SELECT lp_item_id, status
318
                            FROM $itemViewTable
319
                            WHERE
320
                                lp_view_id = ".$this->get_view_id()." AND
321
                                lp_item_id IN ('".$lp_item_id_list_to_string."')
322
                            ORDER BY view_count DESC ";
323
                    $status_list = [];
324
                    $res = Database::query($sql);
325
                    while ($row = Database:: fetch_array($res)) {
326
                        $status_list[$row['lp_item_id']] = $row['status'];
327
                    }
328
329
                    foreach ($lp_item_id_list as $item_id) {
330
                        if (isset($status_list[$item_id])) {
331
                            $status = $status_list[$item_id];
332
333
                            if (is_object($this->items[$item_id])) {
334
                                $this->items[$item_id]->set_status($status);
335
                                if (empty($status)) {
336
                                    $this->items[$item_id]->set_status(
337
                                        $this->default_status
338
                                    );
339
                                }
340
                            }
341
                        } else {
342
                            if (!api_is_invitee()) {
343
                                if (isset($this->items[$item_id]) && is_object($this->items[$item_id])) {
344
                                    $this->items[$item_id]->set_status(
345
                                        $this->default_status
346
                                    );
347
                                }
348
349
                                if (!empty($this->lp_view_id)) {
350
                                    // Add that row to the lp_item_view table so that
351
                                    // we have something to show in the stats page.
352
                                    $params = [
353
                                        'lp_item_id' => $item_id,
354
                                        'lp_view_id' => $this->lp_view_id,
355
                                        'view_count' => 1,
356
                                        'status' => 'not attempted',
357
                                        'start_time' => time(),
358
                                        'total_time' => 0,
359
                                        'score' => 0,
360
                                    ];
361
                                    Database::insert($itemViewTable, $params);
362
363
                                    $this->items[$item_id]->set_lp_view(
364
                                        $this->lp_view_id
365
                                    );
366
                                }
367
                            }
368
                        }
369
                    }
370
                }
371
            }
372
373
            $this->ordered_items = self::get_flat_ordered_items_list($entity, null);
374
            $this->max_ordered_items = 0;
375
            foreach ($this->ordered_items as $index => $dummy) {
376
                if ($index > $this->max_ordered_items && !empty($dummy)) {
377
                    $this->max_ordered_items = $index;
378
                }
379
            }
380
            // TODO: Define the current item better.
381
            $this->first();
382
            if ($debug) {
383
                error_log('lp_view_session_id '.$this->lp_view_session_id);
384
                error_log('End of learnpath constructor for learnpath '.$this->get_id());
385
            }
386
        }
387
    }
388
389
    /**
390
     * @return int
391
     */
392
    public function get_course_int_id()
393
    {
394
        return $this->course_int_id ?? api_get_course_int_id();
395
    }
396
397
    /**
398
     * @param $course_id
399
     *
400
     * @return int
401
     */
402
    public function set_course_int_id($course_id)
403
    {
404
        return $this->course_int_id = (int) $course_id;
405
    }
406
407
    /**
408
     * Function rewritten based on old_add_item() from Yannick Warnier.
409
     * Due the fact that users can decide where the item should come, I had to overlook this function and
410
     * I found it better to rewrite it. Old function is still available.
411
     * Added also the possibility to add a description.
412
     *
413
     * @param CLpItem $parent
414
     * @param int     $previousId
415
     * @param string  $type
416
     * @param int     $id resource ID (ref)
417
     * @param string  $title
418
     * @param string  $description
419
     * @param int     $prerequisites
420
     * @param int     $maxTimeAllowed
421
     * @param int     $userId
422
     *
423
     * @return int
424
     */
425
    public function add_item(
426
        ?CLpItem $parent,
427
        $previousId,
428
        $type,
429
        $id,
430
        $title,
431
        $description = '',
432
        $prerequisites = 0,
433
        $maxTimeAllowed = 0
434
    ) {
435
        $type = empty($type) ? 'dir' : $type;
436
        $course_id = $this->course_info['real_id'];
437
        if (empty($course_id)) {
438
            // Sometimes Oogie doesn't catch the course info but sets $this->cc
439
            $this->course_info = api_get_course_info($this->cc);
440
            $course_id = $this->course_info['real_id'];
441
        }
442
        $id = (int) $id;
443
        $maxTimeAllowed = (int) $maxTimeAllowed;
444
        if (empty($maxTimeAllowed)) {
445
            $maxTimeAllowed = 0;
446
        }
447
        $maxScore = 100;
448
        if ('quiz' === $type && $id) {
449
            // Disabling the exercise if we add it inside a LP
450
            $exercise = new Exercise($course_id);
451
            $exercise->read($id);
452
            $maxScore = $exercise->getMaxScore();
453
454
            $exercise->disable();
455
            $exercise->save();
456
            $title = $exercise->get_formated_title();
457
        }
458
459
        $lpItem = (new CLpItem())
460
            ->setTitle($title)
461
            ->setDescription($description)
462
            ->setPath($id)
463
            ->setLp(api_get_lp_entity($this->get_id()))
464
            ->setItemType($type)
465
            ->setMaxScore($maxScore)
466
            ->setMaxTimeAllowed($maxTimeAllowed)
467
            ->setPrerequisite($prerequisites)
468
            //->setDisplayOrder($display_order + 1)
469
            //->setNextItemId((int) $next)
470
            //->setPreviousItemId($previous)
471
        ;
472
473
        if (!empty($parent))  {
474
            $lpItem->setParent($parent);
475
        }
476
        $em = Database::getManager();
477
        $em->persist($lpItem);
478
        $em->flush();
479
480
        $new_item_id = $lpItem->getIid();
481
        if ($new_item_id) {
482
            // @todo fix upload audio.
483
            // Upload audio.
484
            /*if (!empty($_FILES['mp3']['name'])) {
485
                // Create the audio folder if it does not exist yet.
486
                $filepath = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document/';
487
                if (!is_dir($filepath.'audio')) {
488
                    mkdir(
489
                        $filepath.'audio',
490
                        api_get_permissions_for_new_directories()
491
                    );
492
                    DocumentManager::addDocument(
493
                        $_course,
494
                        '/audio',
495
                        'folder',
496
                        0,
497
                        'audio',
498
                        '',
499
                        0,
500
                        true,
501
                        null,
502
                        $sessionId,
503
                        $userId
504
                    );
505
                }
506
507
                $file_path = handle_uploaded_document(
508
                    $_course,
509
                    $_FILES['mp3'],
510
                    api_get_path(SYS_COURSE_PATH).$_course['path'].'/document',
511
                    '/audio',
512
                    $userId,
513
                    '',
514
                    '',
515
                    '',
516
                    '',
517
                    false
518
                );
519
520
                // Getting the filename only.
521
                $file_components = explode('/', $file_path);
522
                $file = $file_components[count($file_components) - 1];
523
524
                // Store the mp3 file in the lp_item table.
525
                $sql = "UPDATE $tbl_lp_item SET
526
                          audio = '".Database::escape_string($file)."'
527
                        WHERE iid = '".intval($new_item_id)."'";
528
                Database::query($sql);
529
            }*/
530
        }
531
532
        return $new_item_id;
533
    }
534
535
    /**
536
     * Static admin function allowing addition of a learnpath to a course.
537
     *
538
     * @param string $courseCode
539
     * @param string $name
540
     * @param string $description
541
     * @param string $learnpath
542
     * @param string $origin
543
     * @param string $zipname       Zip file containing the learnpath or directory containing the learnpath
544
     * @param string $published_on
545
     * @param string $expired_on
546
     * @param int    $categoryId
547
     * @param int    $userId
548
     *
549
     * @return CLp
550
     */
551
    public static function add_lp(
552
        $courseCode,
553
        $name,
554
        $description = '',
555
        $learnpath = 'guess',
556
        $origin = 'zip',
557
        $zipname = '',
558
        $published_on = '',
559
        $expired_on = '',
560
        $categoryId = 0,
561
        $userId = 0
562
    ) {
563
        global $charset;
564
565
        if (!empty($courseCode)) {
566
            $courseInfo = api_get_course_info($courseCode);
567
            $course_id = $courseInfo['real_id'];
568
        } else {
569
            $course_id = api_get_course_int_id();
570
            $courseInfo = api_get_course_info();
571
        }
572
573
        $categoryId = (int) $categoryId;
574
575
        if (empty($published_on)) {
576
            $published_on = null;
577
        } else {
578
            $published_on = api_get_utc_datetime($published_on, true, true);
579
        }
580
581
        if (empty($expired_on)) {
582
            $expired_on = null;
583
        } else {
584
            $expired_on = api_get_utc_datetime($expired_on, true, true);
585
        }
586
587
        $description = Database::escape_string(api_htmlentities($description, ENT_QUOTES));
588
        $type = 1;
589
        switch ($learnpath) {
590
            case 'guess':
591
            case 'aicc':
592
                break;
593
            case 'dokeos':
594
            case 'chamilo':
595
                $type = 1;
596
                break;
597
        }
598
599
        $sessionEntity = api_get_session_entity();
600
        $courseEntity = api_get_course_entity($courseInfo['real_id']);
601
        $lp = null;
602
        switch ($origin) {
603
            case 'zip':
604
                // Check zip name string. If empty, we are currently creating a new Chamilo learnpath.
605
                break;
606
            case 'manual':
607
            default:
608
                /*$get_max = "SELECT MAX(display_order)
609
                            FROM $tbl_lp WHERE c_id = $course_id";
610
                $res_max = Database::query($get_max);
611
                if (Database::num_rows($res_max) < 1) {
612
                    $dsp = 1;
613
                } else {
614
                    $row = Database::fetch_array($res_max);
615
                    $dsp = $row[0] + 1;
616
                }*/
617
618
                $category = null;
619
                if (!empty($categoryId)) {
620
                    $category = Container::getLpCategoryRepository()->find($categoryId);
621
                }
622
623
                $lpRepo = Container::getLpRepository();
624
625
                $lp = (new CLp())
626
                    ->setLpType($type)
627
                    ->setTitle($name)
628
                    ->setDescription($description)
629
                    ->setCategory($category)
630
                    ->setPublishedOn($published_on)
631
                    ->setExpiredOn($expired_on)
632
                    ->setParent($courseEntity)
633
                    ->addCourseLink($courseEntity, $sessionEntity)
634
                ;
635
                $lpRepo->createLp($lp);
636
637
                break;
638
        }
639
640
        return $lp;
641
    }
642
643
    /**
644
     * Auto completes the parents of an item in case it's been completed or passed.
645
     *
646
     * @param int $item Optional ID of the item from which to look for parents
647
     */
648
    public function autocomplete_parents($item)
649
    {
650
        $debug = $this->debug;
651
652
        if (empty($item)) {
653
            $item = $this->current;
654
        }
655
656
        $currentItem = $this->getItem($item);
657
        if ($currentItem) {
658
            $parent_id = $currentItem->get_parent();
659
            $parent = $this->getItem($parent_id);
660
            if ($parent) {
661
                // if $item points to an object and there is a parent.
662
                if ($debug) {
663
                    error_log(
664
                        'Autocompleting parent of item '.$item.' '.
665
                        $currentItem->get_title().'" (item '.$parent_id.' "'.$parent->get_title().'") ',
666
                        0
667
                    );
668
                }
669
670
                // New experiment including failed and browsed in completed status.
671
                //$current_status = $currentItem->get_status();
672
                //if ($currentItem->is_done() || $current_status == 'browsed' || $current_status == 'failed') {
673
                // Fixes chapter auto complete
674
                if (true) {
675
                    // If the current item is completed or passes or succeeded.
676
                    $updateParentStatus = true;
677
                    if ($debug) {
678
                        error_log('Status of current item is alright');
679
                    }
680
681
                    foreach ($parent->get_children() as $childItemId) {
682
                        $childItem = $this->getItem($childItemId);
683
684
                        // If children was not set try to get the info
685
                        if (empty($childItem->db_item_view_id)) {
686
                            $childItem->set_lp_view($this->lp_view_id);
687
                        }
688
689
                        // Check all his brothers (parent's children) for completion status.
690
                        if ($childItemId != $item) {
691
                            if ($debug) {
692
                                error_log(
693
                                    'Looking at brother #'.$childItemId.' "'.$childItem->get_title().'", status is '.$childItem->get_status(),
694
                                    0
695
                                );
696
                            }
697
                            // Trying completing parents of failed and browsed items as well.
698
                            if ($childItem->status_is(
699
                                [
700
                                    'completed',
701
                                    'passed',
702
                                    'succeeded',
703
                                    'browsed',
704
                                    'failed',
705
                                ]
706
                            )
707
                            ) {
708
                                // Keep completion status to true.
709
                                continue;
710
                            } else {
711
                                if ($debug > 2) {
712
                                    error_log(
713
                                        '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,
714
                                        0
715
                                    );
716
                                }
717
                                $updateParentStatus = false;
718
                                break;
719
                            }
720
                        }
721
                    }
722
723
                    if ($updateParentStatus) {
724
                        // If all the children were completed:
725
                        $parent->set_status('completed');
726
                        $parent->save(false, $this->prerequisites_match($parent->get_id()));
727
                        // Force the status to "completed"
728
                        //$this->update_queue[$parent->get_id()] = $parent->get_status();
729
                        $this->update_queue[$parent->get_id()] = 'completed';
730
                        if ($debug) {
731
                            error_log(
732
                                'Added parent #'.$parent->get_id().' "'.$parent->get_title().'" to update queue status: completed '.
733
                                print_r($this->update_queue, 1),
734
                                0
735
                            );
736
                        }
737
                        // Recursive call.
738
                        $this->autocomplete_parents($parent->get_id());
739
                    }
740
                }
741
            } else {
742
                if ($debug) {
743
                    error_log("Parent #$parent_id does not exists");
744
                }
745
            }
746
        } else {
747
            if ($debug) {
748
                error_log("#$item is an item that doesn't have parents");
749
            }
750
        }
751
    }
752
753
    /**
754
     * Closes the current resource.
755
     *
756
     * Stops the timer
757
     * Saves into the database if required
758
     * Clears the current resource data from this object
759
     *
760
     * @return bool True on success, false on failure
761
     */
762
    public function close()
763
    {
764
        if (empty($this->lp_id)) {
765
            $this->error = 'Trying to close this learnpath but no ID is set';
766
767
            return false;
768
        }
769
        $this->current_time_stop = time();
770
        $this->ordered_items = [];
771
        $this->index = 0;
772
        unset($this->lp_id);
773
        //unset other stuff
774
        return true;
775
    }
776
777
    /**
778
     * Static admin function allowing removal of a learnpath.
779
     *
780
     * @param array  $courseInfo
781
     * @param int    $id         Learnpath ID
782
     * @param string $delete     Whether to delete data or keep it (default: 'keep', others: 'remove')
783
     *
784
     * @return bool True on success, false on failure (might change that to return number of elements deleted)
785
     */
786
    public function delete($courseInfo = null, $id = null, $delete = 'keep')
787
    {
788
        $course_id = api_get_course_int_id();
789
        if (!empty($courseInfo)) {
790
            $course_id = isset($courseInfo['real_id']) ? $courseInfo['real_id'] : $course_id;
791
        }
792
793
        // Prevent deleting a different LP than the current one if an explicit ID was passed
794
        if (!empty($id) && ($id != $this->lp_id)) {
795
            return false;
796
        }
797
798
        $course  = api_get_course_entity();
799
        $session = api_get_session_entity();
800
801
        /** @var CLp|null $lp */
802
        $lp = Container::getLpRepository()->find($this->lp_id);
803
804
        // Detach the asset to avoid FK constraint
805
        $asset = $lp ? $lp->getAsset() : null;
806
        if ($asset && $lp) {
807
            $lp->setAsset(null);
808
            $em = Database::getManager();
809
            $em->persist($lp);
810
            $em->flush();
811
        }
812
813
        // 2) Now delete the asset and its folder
814
        if ($asset) {
815
            Container::getAssetRepository()->delete($asset);
816
        }
817
818
        // Remove resource links (course/session context)
819
        Database::getManager()
820
            ->getRepository(ResourceLink::class)
821
            ->removeByResourceInContext($lp, $course, $session);
822
823
        // Remove from gradebook if present
824
        $link_info = GradebookUtils::isResourceInCourseGradebook(
825
            api_get_course_int_id(),
826
            4,
827
            $id,
828
            api_get_session_id()
829
        );
830
831
        if (!empty($link_info)) {
832
            GradebookUtils::remove_resource_from_course_gradebook($link_info['id']);
833
        }
834
835
        // Tracking event
836
        $trackRepo    = Container::$container->get(TrackEDefaultRepository::class);
837
        $resourceNode = $lp ? $lp->getResourceNode() : null;
838
        if ($resourceNode) {
839
            $trackRepo->registerResourceEvent(
840
                $resourceNode,
841
                'deletion',
842
                api_get_user_id(),
843
                api_get_course_int_id(),
844
                api_get_session_id()
845
            );
846
        }
847
848
        // Purge the SCORM ZIP registered under Documents/Learning paths (teacher-only folder)
849
        //    This keeps the course storage meter consistent after LP deletion.
850
        try {
851
            if ($lp && $course) {
852
                $em = Database::getManager();
853
                /** @var CDocumentRepository $docRepo */
854
                $docRepo = $em->getRepository(CDocument::class);
855
                $docRepo->purgeScormZip($course, $lp);
856
            }
857
        } catch (\Throwable $e) {
858
            // Do not block LP deletion if purge fails; just log the error.
859
            error_log('[learnpath::delete] Failed to purge SCORM ZIP from Documents: '.$e->getMessage());
860
        }
861
    }
862
863
    /**
864
     * Removes all the children of one item - dangerous!
865
     *
866
     * @param int $id Element ID of which children have to be removed
867
     *
868
     * @return int Total number of children removed
869
     */
870
    public function delete_children_items($id)
871
    {
872
        $course_id = $this->course_info['real_id'];
873
874
        $num = 0;
875
        $id = (int) $id;
876
        if (empty($id) || empty($course_id)) {
877
            return false;
878
        }
879
        $lp_item = Database::get_course_table(TABLE_LP_ITEM);
880
        $sql = "SELECT * FROM $lp_item
881
                WHERE parent_item_id = $id";
882
        $res = Database::query($sql);
883
        while ($row = Database::fetch_array($res)) {
884
            $num += $this->delete_children_items($row['iid']);
885
            $sql = "DELETE FROM $lp_item
886
                    WHERE iid = ".$row['iid'];
887
            Database::query($sql);
888
            $num++;
889
        }
890
891
        return $num;
892
    }
893
894
    /**
895
     * Removes an item from the current learnpath.
896
     *
897
     * @param int $id Elem ID (0 if first)
898
     *
899
     * @return int Number of elements moved
900
     *
901
     * @todo implement resource removal
902
     */
903
    public function delete_item($id)
904
    {
905
        $course_id = api_get_course_int_id();
906
        $id = (int) $id;
907
        // TODO: Implement the resource removal.
908
        if (empty($id) || empty($course_id)) {
909
            return false;
910
        }
911
912
        $repo = Container::getLpItemRepository();
913
        $item = $repo->find($id);
914
        if (null === $item) {
915
            return false;
916
        }
917
918
        $em = Database::getManager();
919
        $repo->removeFromTree($item);
920
        $em->flush();
921
        $lp_item = Database::get_course_table(TABLE_LP_ITEM);
922
923
        //Removing prerequisites since the item will not longer exist
924
        $sql_all = "UPDATE $lp_item SET prerequisite = ''
925
                    WHERE prerequisite = '$id'";
926
        Database::query($sql_all);
927
928
        $sql = "UPDATE $lp_item
929
                SET previous_item_id = ".$this->getLastInFirstLevel()."
930
                WHERE lp_id = {$this->lp_id} AND item_type = '".TOOL_LP_FINAL_ITEM."'";
931
        Database::query($sql);
932
933
        // Remove from search engine if enabled.
934
        if ('true' === api_get_setting('search_enabled')) {
935
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
936
            $sql = 'SELECT * FROM %s
937
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
938
                    LIMIT 1';
939
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp, $id);
940
            $res = Database::query($sql);
941
            if (Database::num_rows($res) > 0) {
942
                $row2 = Database::fetch_array($res);
943
                $di = new ChamiloIndexer();
944
                $di->remove_document($row2['search_did']);
945
            }
946
            $sql = 'DELETE FROM %s
947
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
948
                    LIMIT 1';
949
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp, $id);
950
            Database::query($sql);
951
        }
952
    }
953
954
    /**
955
     * Updates an item's content in place.
956
     *
957
     * @param int    $id               Element ID
958
     * @param int    $parent           Parent item ID
959
     * @param int    $previous         Previous item ID
960
     * @param string $title            Item title
961
     * @param string $description      Item description
962
     * @param string $prerequisites    Prerequisites (optional)
963
     * @param array  $audio            The array resulting of the $_FILES[mp3] element
964
     * @param int    $max_time_allowed
965
     * @param string $url
966
     *
967
     * @return bool True on success, false on error
968
     */
969
    public function edit_item(
970
        $id,
971
        $parent,
972
        $previous,
973
        $title,
974
        $description,
975
        $prerequisites = '0',
976
        $audio = [],
977
        $max_time_allowed = 0,
978
        $url = ''
979
    ) {
980
        $_course = api_get_course_info();
981
        $id = (int) $id;
982
983
        if (empty($id) || empty($_course)) {
984
            return false;
985
        }
986
        $repo = Container::getLpItemRepository();
987
        /** @var CLpItem $item */
988
        $item = $repo->find($id);
989
        if (null === $item) {
990
            return false;
991
        }
992
993
        $item
994
            ->setTitle($title)
995
            ->setDescription($description)
996
            ->setPrerequisite($prerequisites)
997
            ->setMaxTimeAllowed((int) $max_time_allowed)
998
        ;
999
1000
        $em = Database::getManager();
1001
        if (!empty($parent)) {
1002
            $parent = $repo->find($parent);
1003
            $item->setParent($parent);
1004
        } else {
1005
            $item->setParent(null);
1006
        }
1007
1008
        if (!empty($previous)) {
1009
            $previous = $repo->find($previous);
1010
            $repo->persistAsNextSiblingOf( $item, $previous);
1011
        } else {
1012
            $em->persist($item);
1013
        }
1014
1015
        $em->flush();
1016
1017
        if ('link' === $item->getItemType()) {
1018
            $link = new Link();
1019
            $linkId = $item->getPath();
1020
            $link->updateLink($linkId, $url);
1021
        }
1022
    }
1023
1024
    /**
1025
     * Updates an item's prereq in place.
1026
     *
1027
     * @param int    $id              Element ID
1028
     * @param string $prerequisite_id Prerequisite Element ID
1029
     * @param int    $minScore        Prerequisite min score
1030
     * @param int    $maxScore        Prerequisite max score
1031
     *
1032
     * @return bool True on success, false on error
1033
     */
1034
    public function edit_item_prereq($id, $prerequisite_id, $minScore = 0, $maxScore = 100)
1035
    {
1036
        $id = (int) $id;
1037
1038
        if (empty($id)) {
1039
            return false;
1040
        }
1041
        $prerequisite_id = (int) $prerequisite_id;
1042
1043
        if (empty($minScore) || $minScore < 0) {
1044
            $minScore = 0;
1045
        }
1046
1047
        if (empty($maxScore) || $maxScore < 0) {
1048
            $maxScore = 100;
1049
        }
1050
1051
        $minScore = (float) $minScore;
1052
        $maxScore = (float) $maxScore;
1053
1054
        if (empty($prerequisite_id)) {
1055
            $prerequisite_id = 'NULL';
1056
            $minScore = 0;
1057
            $maxScore = 100;
1058
        }
1059
1060
        $table = Database::get_course_table(TABLE_LP_ITEM);
1061
        $sql = " UPDATE $table
1062
                 SET
1063
                    prerequisite = $prerequisite_id ,
1064
                    prerequisite_min_score = $minScore ,
1065
                    prerequisite_max_score = $maxScore
1066
                 WHERE iid = $id";
1067
        Database::query($sql);
1068
1069
        return true;
1070
    }
1071
1072
    /**
1073
     * Get the specific prefix index terms of this learning path.
1074
     *
1075
     * @param string $prefix
1076
     *
1077
     * @return array Array of terms
1078
     */
1079
    public function get_common_index_terms_by_prefix($prefix)
1080
    {
1081
        $terms = get_specific_field_values_list_by_prefix(
1082
            $prefix,
1083
            $this->cc,
1084
            TOOL_LEARNPATH,
1085
            $this->lp_id
1086
        );
1087
        $prefix_terms = [];
1088
        if (!empty($terms)) {
1089
            foreach ($terms as $term) {
1090
                $prefix_terms[] = $term['value'];
1091
            }
1092
        }
1093
1094
        return $prefix_terms;
1095
    }
1096
1097
    /**
1098
     * Gets the number of items currently completed.
1099
     *
1100
     * @param bool Flag to determine the failed status is not considered progressed
1101
     *
1102
     * @return int The number of items currently completed
1103
     */
1104
    public function get_complete_items_count(bool $failedStatusException = false): int
1105
    {
1106
        $i = 0;
1107
        $completedStatusList = [
1108
            'completed',
1109
            'passed',
1110
            'succeeded',
1111
            'browsed',
1112
        ];
1113
1114
        if (!$failedStatusException) {
1115
            $completedStatusList[] = 'failed';
1116
        }
1117
1118
        foreach ($this->items as $id => $dummy) {
1119
            // Trying failed and browsed considered "progressed" as well.
1120
            if ($this->items[$id]->status_is($completedStatusList) &&
1121
                'dir' !== $this->items[$id]->get_type()
1122
            ) {
1123
                $i++;
1124
            }
1125
        }
1126
1127
        return $i;
1128
    }
1129
1130
    /**
1131
     * Gets the current item ID.
1132
     *
1133
     * @return int The current learnpath item id
1134
     */
1135
    public function get_current_item_id()
1136
    {
1137
        $current = 0;
1138
        if (!empty($this->current)) {
1139
            $current = (int) $this->current;
1140
        }
1141
1142
        return $current;
1143
    }
1144
1145
    /**
1146
     * Force to get the first learnpath item id.
1147
     *
1148
     * @return int The current learnpath item id
1149
     */
1150
    public function get_first_item_id()
1151
    {
1152
        $current = 0;
1153
        if (is_array($this->ordered_items)) {
1154
            $current = $this->ordered_items[0];
1155
        }
1156
1157
        return $current;
1158
    }
1159
1160
    /**
1161
     * Gets the total number of items available for viewing in this SCORM.
1162
     *
1163
     * @return int The total number of items
1164
     */
1165
    public function get_total_items_count()
1166
    {
1167
        return count($this->items);
1168
    }
1169
1170
    /**
1171
     * Gets the total number of items available for viewing in this SCORM but without chapters.
1172
     *
1173
     * @return int The total no-chapters number of items
1174
     */
1175
    public function getTotalItemsCountWithoutDirs()
1176
    {
1177
        $total = 0;
1178
        $typeListNotToCount = self::getChapterTypes();
1179
        foreach ($this->items as $temp2) {
1180
            if (!in_array($temp2->get_type(), $typeListNotToCount)) {
1181
                $total++;
1182
            }
1183
        }
1184
1185
        return $total;
1186
    }
1187
1188
    /**
1189
     *  Sets the first element URL.
1190
     */
1191
    public function first()
1192
    {
1193
        if ($this->debug > 0) {
1194
            error_log('In learnpath::first()', 0);
1195
            error_log('$this->last_item_seen '.$this->last_item_seen);
1196
        }
1197
1198
        // Test if the last_item_seen exists and is not a dir.
1199
        if (0 == count($this->ordered_items)) {
1200
            $this->index = 0;
1201
        }
1202
1203
        if (!empty($this->last_item_seen) &&
1204
            !empty($this->items[$this->last_item_seen]) &&
1205
            'dir' !== $this->items[$this->last_item_seen]->get_type()
1206
            //with this change (below) the LP will NOT go to the next item, it will take lp item we left
1207
            //&& !$this->items[$this->last_item_seen]->is_done()
1208
        ) {
1209
            if ($this->debug > 2) {
1210
                error_log(
1211
                    'In learnpath::first() - Last item seen is '.$this->last_item_seen.' of type '.
1212
                    $this->items[$this->last_item_seen]->get_type()
1213
                );
1214
            }
1215
            $index = -1;
1216
            foreach ($this->ordered_items as $myindex => $item_id) {
1217
                if ($item_id == $this->last_item_seen) {
1218
                    $index = $myindex;
1219
                    break;
1220
                }
1221
            }
1222
            if (-1 == $index) {
1223
                // Index hasn't changed, so item not found - panic (this shouldn't happen).
1224
                if ($this->debug > 2) {
1225
                    error_log('Last item ('.$this->last_item_seen.') was found in items but not in ordered_items, panic!', 0);
1226
                }
1227
1228
                return false;
1229
            } else {
1230
                $this->last = $this->last_item_seen;
1231
                $this->current = $this->last_item_seen;
1232
                $this->index = $index;
1233
            }
1234
        } else {
1235
            if ($this->debug > 2) {
1236
                error_log('In learnpath::first() - No last item seen', 0);
1237
            }
1238
            $index = 0;
1239
            // Loop through all ordered items and stop at the first item that is
1240
            // not a directory *and* that has not been completed yet.
1241
            while (!empty($this->ordered_items[$index]) &&
1242
                is_a($this->items[$this->ordered_items[$index]], 'learnpathItem') &&
1243
                (
1244
                    'dir' === $this->items[$this->ordered_items[$index]]->get_type() ||
1245
                    true === $this->items[$this->ordered_items[$index]]->is_done()
1246
                ) && $index < $this->max_ordered_items
1247
            ) {
1248
                $index++;
1249
            }
1250
1251
            $this->last = $this->current;
1252
            // current is
1253
            $this->current = isset($this->ordered_items[$index]) ? $this->ordered_items[$index] : null;
1254
            $this->index = $index;
1255
            if ($this->debug > 2) {
1256
                error_log('$index '.$index);
1257
                error_log('In learnpath::first() - No last item seen');
1258
                error_log('New last = '.$this->last.'('.$this->ordered_items[$index].')');
1259
            }
1260
        }
1261
        if ($this->debug > 2) {
1262
            error_log('In learnpath::first() - First item is '.$this->get_current_item_id());
1263
        }
1264
    }
1265
1266
    /**
1267
     * Gets the js library from the database.
1268
     *
1269
     * @return string The name of the javascript library to be used
1270
     */
1271
    public function get_js_lib()
1272
    {
1273
        $lib = '';
1274
        if (!empty($this->js_lib)) {
1275
            $lib = $this->js_lib;
1276
        }
1277
1278
        return $lib;
1279
    }
1280
1281
    /**
1282
     * Gets the learnpath database ID.
1283
     *
1284
     * @return int Learnpath ID in the lp table
1285
     */
1286
    public function get_id()
1287
    {
1288
        if (!empty($this->lp_id)) {
1289
            return (int) $this->lp_id;
1290
        }
1291
1292
        return 0;
1293
    }
1294
1295
    /**
1296
     * Gets the last element URL.
1297
     *
1298
     * @return string URL to load into the viewer
1299
     */
1300
    public function get_last()
1301
    {
1302
        // This is just in case the lesson doesn't cointain a valid scheme, just to avoid "Notices"
1303
        if (count($this->ordered_items) > 0) {
1304
            $this->index = count($this->ordered_items) - 1;
1305
1306
            return $this->ordered_items[$this->index];
1307
        }
1308
1309
        return false;
1310
    }
1311
1312
    /**
1313
     * Get the last element in the first level.
1314
     * Unlike learnpath::get_last this function doesn't consider the subsection' elements.
1315
     *
1316
     * @return mixed
1317
     */
1318
    public function getLastInFirstLevel()
1319
    {
1320
        try {
1321
            $lastId = Database::getManager()
1322
                ->createQuery('SELECT i.iid FROM ChamiloCourseBundle:CLpItem i
1323
                WHERE i.lp = :lp AND i.parent IS NULL AND i.itemType != :type ORDER BY i.displayOrder DESC')
1324
                ->setMaxResults(1)
1325
                ->setParameters(['lp' => $this->lp_id, 'type' => TOOL_LP_FINAL_ITEM])
1326
                ->getSingleScalarResult();
1327
1328
            return $lastId;
1329
        } catch (Exception $exception) {
1330
            return 0;
1331
        }
1332
    }
1333
1334
    /**
1335
     * Gets the navigation bar for the learnpath display screen.
1336
     *
1337
     * @param string $barId
1338
     *
1339
     * @return string The HTML string to use as a navigation bar
1340
     */
1341
    public function get_navigation_bar($barId = '')
1342
    {
1343
        if (empty($barId)) {
1344
            $barId = 'control-top';
1345
        }
1346
        $lpId = $this->lp_id;
1347
        $mycurrentitemid = $this->get_current_item_id();
1348
        $reportingText = get_lang('Reporting');
1349
        $previousText = get_lang('Previous');
1350
        $nextText = get_lang('Next');
1351
        $fullScreenText = get_lang('Back to normal screen');
1352
1353
        $settings = api_get_setting('lp.lp_view_settings', true);
1354
        $display = $settings['display'] ?? false;
1355
        $icon = Display::getMdiIcon('information');
1356
1357
        $reportingIcon = '
1358
            <a class="icon-toolbar"
1359
                id="stats_link"
1360
                href="lp_controller.php?action=stats&'.api_get_cidreq(true).'&lp_id='.$lpId.'"
1361
                onclick="window.parent.API.save_asset(); return true;"
1362
                target="content_name" title="'.$reportingText.'">
1363
                '.$icon.'<span class="sr-only">'.$reportingText.'</span>
1364
            </a>';
1365
1366
        if (!empty($display)) {
1367
            $showReporting = isset($display['show_reporting_icon']) ? $display['show_reporting_icon'] : true;
1368
            if (false === $showReporting) {
1369
                $reportingIcon = '';
1370
            }
1371
        }
1372
1373
        $hideArrows = false;
1374
        if (isset($settings['display']) && isset($settings['display']['hide_lp_arrow_navigation'])) {
1375
            $hideArrows = $settings['display']['hide_lp_arrow_navigation'];
1376
        }
1377
1378
        $previousIcon = '';
1379
        $nextIcon = '';
1380
        if (false === $hideArrows) {
1381
            $icon = Display::getMdiIcon('chevron-left');
1382
            $previousIcon = '
1383
                <button class="icon-toolbar" id="scorm-previous" type="button"
1384
                    onclick="switch_item('.$mycurrentitemid.',\'previous\');return false;" title="'.$previousText.'">
1385
                    '.$icon.'<span class="sr-only">'.$previousText.'</span>
1386
                </button>';
1387
1388
            $icon = Display::getMdiIcon('chevron-right');
1389
            $nextIcon = '
1390
                <button class="icon-toolbar" id="scorm-next" type="button"
1391
                    onclick="switch_item('.$mycurrentitemid.',\'next\');return false;" title="'.$nextText.'">
1392
                    '.$icon.'<span class="sr-only">'.$nextText.'</span>
1393
                </button>';
1394
        }
1395
1396
        if ('fullscreen' === $this->mode) {
1397
            $icon = Display::getMdiIcon('view-column');
1398
            $navbar = '
1399
                  <span id="'.$barId.'" class="buttons">
1400
                    '.$reportingIcon.'
1401
                    '.$previousIcon.'
1402
                    '.$nextIcon.'
1403
                    <a class="icon-toolbar" id="view-embedded"
1404
                        href="lp_controller.php?action=mode&mode=embedded" target="_top" title="'.$fullScreenText.'">
1405
                        '.$icon.'<span class="sr-only">'.$fullScreenText.'</span>
1406
                    </a>
1407
                  </span>';
1408
        } else {
1409
            $navbar = '
1410
                 <span id="'.$barId.'" class="buttons text-right">
1411
                    '.$reportingIcon.'
1412
                    '.$previousIcon.'
1413
                    '.$nextIcon.'
1414
                </span>';
1415
        }
1416
1417
        return $navbar;
1418
    }
1419
1420
    /**
1421
     * Gets the next resource in queue (url).
1422
     *
1423
     * @return string URL to load into the viewer
1424
     */
1425
    public function get_next_index()
1426
    {
1427
        // TODO
1428
        $index = $this->index;
1429
        $index++;
1430
        while (
1431
            !empty($this->ordered_items[$index]) && ('dir' == $this->items[$this->ordered_items[$index]]->get_type()) &&
1432
            $index < $this->max_ordered_items
1433
        ) {
1434
            $index++;
1435
            if ($index == $this->max_ordered_items) {
1436
                if ('dir' === $this->items[$this->ordered_items[$index]]->get_type()) {
1437
                    return $this->index;
1438
                }
1439
1440
                return $index;
1441
            }
1442
        }
1443
        if (empty($this->ordered_items[$index])) {
1444
            return $this->index;
1445
        }
1446
1447
        return $index;
1448
    }
1449
1450
    /**
1451
     * Gets item_id for the next element.
1452
     *
1453
     * @return int Next item (DB) ID
1454
     */
1455
    public function get_next_item_id()
1456
    {
1457
        $new_index = $this->get_next_index();
1458
        if (!empty($new_index)) {
1459
            if (isset($this->ordered_items[$new_index])) {
1460
                return $this->ordered_items[$new_index];
1461
            }
1462
        }
1463
1464
        return 0;
1465
    }
1466
1467
    /**
1468
     * Returns the package type ('scorm','aicc','scorm2004','ppt'...).
1469
     *
1470
     * Generally, the package provided is in the form of a zip file, so the function
1471
     * has been written to test a zip file. If not a zip, the function will return the
1472
     * default return value: ''
1473
     *
1474
     * @param string $filePath the path to the file
1475
     * @param string $file_name the original name of the file
1476
     *
1477
     * @return string 'scorm','aicc','scorm2004','error-empty-package'
1478
     *                if the package is empty, or '' if the package cannot be recognized
1479
     */
1480
    public static function getPackageType($filePath, $file_name)
1481
    {
1482
        // Get name of the zip file without the extension.
1483
        $file_info = pathinfo($file_name);
1484
        $extension = $file_info['extension']; // Extension only.
1485
        if (!empty($_POST['ppt2lp']) && !in_array(strtolower($extension), [
1486
                'dll',
1487
                'exe',
1488
            ])) {
1489
            return 'oogie';
1490
        }
1491
        if (!empty($_POST['woogie']) && !in_array(strtolower($extension), [
1492
                'dll',
1493
                'exe',
1494
            ])) {
1495
            return 'woogie';
1496
        }
1497
1498
        $zipFile = new ZipFile();
1499
        $zipFile->openFile($filePath);
1500
        $zipContentArray = $zipFile->getEntries();
1501
        $package_type = '';
1502
        $manifest = '';
1503
        $aicc_match_crs = 0;
1504
        $aicc_match_au = 0;
1505
        $aicc_match_des = 0;
1506
        $aicc_match_cst = 0;
1507
        $countItems = 0;
1508
        // The following loop should be stopped as soon as we found the right imsmanifest.xml (how to recognize it?).
1509
        if ($zipContentArray) {
1510
            $countItems = count($zipContentArray);
1511
            if ($countItems > 0) {
1512
                foreach ($zipContentArray as $thisContent) {
1513
                    $fileName = basename($thisContent->getName());
1514
                    if (preg_match('~.(php.*|phtml)$~i', $fileName)) {
1515
                        // New behaviour: Don't do anything. These files will be removed in scorm::import_package.
1516
                    } elseif (false !== stristr($fileName, 'imsmanifest.xml')) {
1517
                        $manifest = $fileName; // Just the relative directory inside scorm/
1518
                        $package_type = 'scorm';
1519
                        break; // Exit the foreach loop.
1520
                    } elseif (
1521
                        preg_match('/aicc\//i', $fileName) ||
1522
                        in_array(
1523
                            strtolower(pathinfo($fileName, PATHINFO_EXTENSION)),
1524
                            ['crs', 'au', 'des', 'cst']
1525
                        )
1526
                    ) {
1527
                        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
1528
                        switch ($ext) {
1529
                            case 'crs':
1530
                                $aicc_match_crs = 1;
1531
                                break;
1532
                            case 'au':
1533
                                $aicc_match_au = 1;
1534
                                break;
1535
                            case 'des':
1536
                                $aicc_match_des = 1;
1537
                                break;
1538
                            case 'cst':
1539
                                $aicc_match_cst = 1;
1540
                                break;
1541
                            default:
1542
                                break;
1543
                        }
1544
                        //break; // Don't exit the loop, because if we find an imsmanifest afterwards, we want it, not the AICC.
1545
                    } else {
1546
                        $package_type = '';
1547
                    }
1548
                }
1549
            }
1550
        }
1551
1552
        if (empty($package_type) && 4 == ($aicc_match_crs + $aicc_match_au + $aicc_match_des + $aicc_match_cst)) {
1553
            // If found an aicc directory... (!= false means it cannot be false (error) or 0 (no match)).
1554
            $package_type = 'aicc';
1555
        }
1556
1557
        // Try with chamilo course builder
1558
        if (empty($package_type)) {
1559
            // Sometimes users will try to upload an empty zip, or a zip with
1560
            // only a folder. Catch that and make the calling function aware.
1561
            // If the single file was the imsmanifest.xml, then $package_type
1562
            // would be 'scorm' and we wouldn't be here.
1563
            if ($countItems < 2) {
1564
                return 'error-empty-package';
1565
            }
1566
            $package_type = 'chamilo';
1567
        }
1568
1569
        return $package_type;
1570
    }
1571
1572
    /**
1573
     * Gets the previous resource in queue (url). Also initialises time values for this viewing.
1574
     *
1575
     * @return string URL to load into the viewer
1576
     */
1577
    public function get_previous_index()
1578
    {
1579
        $index = $this->index;
1580
        if (isset($this->ordered_items[$index - 1])) {
1581
            $index--;
1582
            while (isset($this->ordered_items[$index]) &&
1583
                ('dir' === $this->items[$this->ordered_items[$index]]->get_type())
1584
            ) {
1585
                $index--;
1586
                if ($index < 0) {
1587
                    return $this->index;
1588
                }
1589
            }
1590
        }
1591
1592
        return $index;
1593
    }
1594
1595
    /**
1596
     * Gets item_id for the next element.
1597
     *
1598
     * @return int Previous item (DB) ID
1599
     */
1600
    public function get_previous_item_id()
1601
    {
1602
        $index = $this->get_previous_index();
1603
1604
        return $this->ordered_items[$index];
1605
    }
1606
1607
    /**
1608
     * Returns the HTML necessary to print a mediaplayer block inside a page.
1609
     *
1610
     * @param int    $lpItemId
1611
     * @param string $autostart
1612
     *
1613
     * @return string The mediaplayer HTML
1614
     */
1615
    public function get_mediaplayer($lpItemId, $autostart = 'true')
1616
    {
1617
        $courseInfo = api_get_course_info();
1618
        $lpItemId = (int) $lpItemId;
1619
1620
        if (empty($courseInfo) || empty($lpItemId)) {
1621
            return '';
1622
        }
1623
        $item = $this->items[$lpItemId] ?? null;
1624
1625
        if (empty($item)) {
1626
            return '';
1627
        }
1628
1629
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
1630
        $tbl_lp_item_view = Database::get_course_table(TABLE_LP_ITEM_VIEW);
1631
        $itemViewId = (int) $item->db_item_view_id;
1632
1633
        // Getting all the information about the item.
1634
        $sql = "SELECT lp_view.status
1635
                FROM $tbl_lp_item as lpi
1636
                INNER JOIN $tbl_lp_item_view as lp_view
1637
                ON (lpi.iid = lp_view.lp_item_id)
1638
                WHERE
1639
                    lp_view.iid = $itemViewId AND
1640
                    lpi.iid = $lpItemId
1641
                ";
1642
        $result = Database::query($sql);
1643
        $row = Database::fetch_assoc($result);
1644
        $output = '';
1645
        $audio = $item->audio;
1646
1647
        if (!empty($audio)) {
1648
            $list = $_SESSION['oLP']->get_toc();
1649
1650
            switch ($item->get_type()) {
1651
                case 'quiz':
1652
                    $type_quiz = false;
1653
                    foreach ($list as $toc) {
1654
                        if ($toc['id'] == $_SESSION['oLP']->current) {
1655
                            $type_quiz = true;
1656
                        }
1657
                    }
1658
1659
                    if ($type_quiz) {
1660
                        if (1 == $_SESSION['oLP']->prevent_reinit) {
1661
                            $autostart_audio = 'completed' === $row['status'] ? 'false' : 'true';
1662
                        } else {
1663
                            $autostart_audio = $autostart;
1664
                        }
1665
                    }
1666
                    break;
1667
                case TOOL_READOUT_TEXT:
1668
                    $autostart_audio = 'false';
1669
                    break;
1670
                default:
1671
                    $autostart_audio = 'true';
1672
            }
1673
1674
            $file = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document'.$audio;
1675
            $url = api_get_path(WEB_COURSE_PATH).$courseInfo['path'].'/document'.$audio.'?'.api_get_cidreq();
1676
1677
            $player = Display::getMediaPlayer(
1678
                $file,
1679
                [
1680
                    'id' => 'lp_audio_media_player',
1681
                    'url' => $url,
1682
                    'autoplay' => $autostart_audio,
1683
                    'width' => '100%',
1684
                ]
1685
            );
1686
1687
            // The mp3 player.
1688
            $output = '<div id="container">';
1689
            $output .= $player;
1690
            $output .= '</div>';
1691
        }
1692
1693
        return $output;
1694
    }
1695
1696
    /**
1697
     * @param int    $studentId
1698
     * @param int    $prerequisite
1699
     * @param Course $course
1700
     * @param int    $sessionId
1701
     *
1702
     * @return bool
1703
     */
1704
    public static function isBlockedByPrerequisite(
1705
        $studentId,
1706
        $prerequisite,
1707
        Course $course,
1708
        $sessionId
1709
    ) {
1710
        $courseId = $course->getId();
1711
1712
        $allow = ('true' === api_get_setting('lp.allow_teachers_to_access_blocked_lp_by_prerequisite'));
1713
        if ($allow) {
1714
            if (api_is_allowed_to_edit() ||
1715
                api_is_platform_admin(true) ||
1716
                api_is_drh() ||
1717
                api_is_coach($sessionId, $courseId, false)
1718
            ) {
1719
                return false;
1720
            }
1721
        }
1722
1723
        $isBlocked = false;
1724
        if (!empty($prerequisite)) {
1725
            $progress = self::getProgress(
1726
                $prerequisite,
1727
                $studentId,
1728
                $courseId,
1729
                $sessionId
1730
            );
1731
            if ($progress < 100) {
1732
                $isBlocked = true;
1733
            }
1734
1735
            if (Tracking::minimumTimeAvailable($sessionId, $courseId)) {
1736
                // Block if it does not exceed minimum time
1737
                // Minimum time (in minutes) to pass the learning path
1738
                $accumulateWorkTime = self::getAccumulateWorkTimePrerequisite($prerequisite, $courseId);
1739
1740
                if ($accumulateWorkTime > 0) {
1741
                    // Total time in course (sum of times in learning paths from course)
1742
                    $accumulateWorkTimeTotal = self::getAccumulateWorkTimeTotal($courseId);
1743
1744
                    // Connect with the plugin_licences_course_session table
1745
                    // which indicates what percentage of the time applies
1746
                    // Minimum connection percentage
1747
                    $perc = 100;
1748
                    // Time from the course
1749
                    $tc = $accumulateWorkTimeTotal;
1750
1751
                    // Percentage of the learning paths
1752
                    $pl = $accumulateWorkTime / $accumulateWorkTimeTotal;
1753
                    // Minimum time for each learning path
1754
                    $accumulateWorkTime = ($pl * $tc * $perc / 100);
1755
1756
                    // Spent time (in seconds) so far in the learning path
1757
                    $lpTimeList = Tracking::getCalculateTime($studentId, $courseId, $sessionId);
1758
                    $lpTime = isset($lpTimeList[TOOL_LEARNPATH][$prerequisite]) ? $lpTimeList[TOOL_LEARNPATH][$prerequisite] : 0;
1759
1760
                    if ($lpTime < ($accumulateWorkTime * 60)) {
1761
                        $isBlocked = true;
1762
                    }
1763
                }
1764
            }
1765
        }
1766
1767
        return $isBlocked;
1768
    }
1769
1770
    /**
1771
     * Checks if the learning path is visible for student after the progress
1772
     * of its prerequisite is completed, considering the time availability and
1773
     * the LP visibility.
1774
     */
1775
    public static function is_lp_visible_for_student(CLp $lp, $student_id, Course $course, SessionEntity $session = null): bool
1776
    {
1777
        $sessionId = $session ? $session->getId() : 0;
1778
        $courseId = $course->getId();
1779
        $visibility = $lp->isVisible($course, $session);
1780
1781
        // If the item was deleted.
1782
        if (false === $visibility) {
1783
            return false;
1784
        }
1785
1786
        $now = time();
1787
        if ($lp->hasCategory()) {
1788
            $category = $lp->getCategory();
1789
1790
            if (false === self::categoryIsVisibleForStudent(
1791
                    $category,
1792
                    api_get_user_entity($student_id),
1793
                    $course,
1794
                    $session
1795
                )) {
1796
                return false;
1797
            }
1798
1799
            $prerequisite = $lp->getPrerequisite();
1800
            $is_visible = true;
1801
1802
            $isBlocked = self::isBlockedByPrerequisite(
1803
                $student_id,
1804
                $prerequisite,
1805
                $course,
1806
                $sessionId
1807
            );
1808
1809
            if ($isBlocked) {
1810
                $is_visible = false;
1811
            }
1812
1813
            // Also check the time availability of the LP
1814
            if ($is_visible) {
1815
                // Adding visibility restrictions
1816
                if (null !== $lp->getPublishedOn()) {
1817
                    if ($now < $lp->getPublishedOn()->getTimestamp()) {
1818
                        $is_visible = false;
1819
                    }
1820
                }
1821
                // Blocking empty start times see BT#2800
1822
                global $_custom;
1823
                if (isset($_custom['lps_hidden_when_no_start_date']) &&
1824
                    $_custom['lps_hidden_when_no_start_date']
1825
                ) {
1826
                    if (null !== $lp->getPublishedOn()) {
1827
                        $is_visible = false;
1828
                    }
1829
                }
1830
1831
                if (null !== $lp->getExpiredOn()) {
1832
                    if ($now > $lp->getExpiredOn()->getTimestamp()) {
1833
                        $is_visible = false;
1834
                    }
1835
                }
1836
            }
1837
1838
            if ($is_visible) {
1839
                $subscriptionSettings = self::getSubscriptionSettings();
1840
1841
                // Check if the subscription users/group to a LP is ON
1842
                if (1 == $lp->getSubscribeUsers() &&
1843
                    true === $subscriptionSettings['allow_add_users_to_lp']
1844
                ) {
1845
                    // Try group
1846
                    $is_visible = false;
1847
                    // Checking only the user visibility
1848
                    $userVisibility = self::isUserSubscribedToLp($lp, $student_id, $course, $session);
1849
1850
                    if (true === $userVisibility) {
1851
                        return true;
1852
                    }
1853
1854
                    // Try with groups
1855
                    $groupVisibility = self::isGroupSubscribedToLp($lp, $student_id, $course, $session);
1856
                    if (true === $groupVisibility) {
1857
                        return true;
1858
                    }
1859
                }
1860
            }
1861
1862
            return $is_visible;
1863
        } else {
1864
1865
            $is_visible = true;
1866
            $subscriptionSettings = self::getSubscriptionSettings();
1867
            // Check if the subscription users/group to a LP is ON
1868
            if (1 == $lp->getSubscribeUsers() &&
1869
                true === $subscriptionSettings['allow_add_users_to_lp']
1870
            ) {
1871
                $is_visible = false;
1872
                $userVisibility = self::isUserSubscribedToLp($lp, $student_id, $course, $session);
1873
1874
                if (true === $userVisibility) {
1875
                    return true;
1876
                }
1877
1878
                // Try with groups
1879
                $groupVisibility = self::isGroupSubscribedToLp($lp, $student_id, $course, $session);
1880
                if (true === $groupVisibility) {
1881
                    return true;
1882
                }
1883
            }
1884
1885
            return $is_visible;
1886
        }
1887
1888
        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...
1889
    }
1890
1891
    public static function isGroupSubscribedToLp(
1892
        CLp $lp,
1893
        int $studentId,
1894
        Course $course,
1895
        SessionEntity $session = null
1896
    ): bool {
1897
1898
        // Subscribed groups to a LP
1899
        $links = $lp->getResourceNode()->getResourceLinks();
1900
        $selectedChoices = [];
1901
        foreach ($links as $link) {
1902
            if (null !== $link->getGroup()) {
1903
                $selectedChoices[] = $link->getGroup()->getIid();
1904
            }
1905
        }
1906
1907
        $isVisible = false;
1908
        $userGroups = GroupManager::getAllGroupPerUserSubscription($studentId, $course->getId());
1909
        if (!empty($userGroups)) {
1910
            foreach ($userGroups as $groupInfo) {
1911
                $groupId = $groupInfo['iid'];
1912
                if (in_array($groupId, $selectedChoices)) {
1913
                    $isVisible = true;
1914
                    break;
1915
                }
1916
            }
1917
        }
1918
1919
        return $isVisible;
1920
    }
1921
1922
    public static function isUserSubscribedToLp(
1923
        CLp $lp,
1924
        int $studentId,
1925
        Course $course,
1926
        SessionEntity $session = null
1927
    ): bool {
1928
1929
        $isVisible = true;
1930
        $em = Database::getManager();
1931
1932
        /** @var CLpRelUserRepository $cLpRelUserRepo */
1933
        $cLpRelUserRepo = $em->getRepository(CLpRelUser::class);
1934
1935
        // Getting subscribed users to a LP.
1936
        $subscribedUsersInLp = $cLpRelUserRepo->getUsersSubscribedToItem(
1937
            $lp,
1938
            $course,
1939
            $session
1940
        );
1941
1942
        $selectedChoices = [];
1943
        foreach ($subscribedUsersInLp as $users) {
1944
            /** @var \Chamilo\CourseBundle\Entity\CLpRelUser $users */
1945
            $selectedChoices[] = $users->getUser()->getId();
1946
        }
1947
1948
        if (!api_is_allowed_to_edit() && !in_array($studentId, $selectedChoices)) {
1949
            $isVisible = false;
1950
        }
1951
1952
        return $isVisible;
1953
    }
1954
1955
    /**
1956
     * @param int $lpId
1957
     * @param int $userId
1958
     * @param int $courseId
1959
     * @param int $sessionId
1960
     *
1961
     * @return int
1962
     */
1963
    public static function getProgress($lpId, $userId, $courseId, $sessionId = 0)
1964
    {
1965
        $lpId = (int) $lpId;
1966
        $userId = (int) $userId;
1967
        $courseId = (int) $courseId;
1968
        $sessionId = (int) $sessionId;
1969
1970
        $sessionCondition = api_get_session_condition($sessionId);
1971
        $table = Database::get_course_table(TABLE_LP_VIEW);
1972
        $sql = "SELECT progress FROM $table
1973
                WHERE
1974
                    c_id = $courseId AND
1975
                    lp_id = $lpId AND
1976
                    user_id = $userId $sessionCondition ";
1977
        $res = Database::query($sql);
1978
1979
        $progress = 0;
1980
        if (Database::num_rows($res) > 0) {
1981
            $row = Database::fetch_array($res);
1982
            $progress = (int) $row['progress'];
1983
        }
1984
1985
        return $progress;
1986
    }
1987
1988
    /**
1989
     * @param array $lpList
1990
     * @param int   $userId
1991
     * @param int   $courseId
1992
     * @param int   $sessionId
1993
     *
1994
     * @return array
1995
     */
1996
    public static function getProgressFromLpList($lpList, $userId, $courseId, $sessionId = 0)
1997
    {
1998
        $lpList = array_map('intval', $lpList);
1999
        if (empty($lpList)) {
2000
            return [];
2001
        }
2002
2003
        $lpList = implode("','", $lpList);
2004
2005
        $userId = (int) $userId;
2006
        $courseId = (int) $courseId;
2007
        $sessionId = (int) $sessionId;
2008
2009
        $sessionCondition = api_get_session_condition($sessionId);
2010
        $table = Database::get_course_table(TABLE_LP_VIEW);
2011
        $sql = "SELECT lp_id, progress FROM $table
2012
                WHERE
2013
                    c_id = $courseId AND
2014
                    lp_id IN ('".$lpList."') AND
2015
                    user_id = $userId $sessionCondition ";
2016
        $res = Database::query($sql);
2017
2018
        if (Database::num_rows($res) > 0) {
2019
            $list = [];
2020
            while ($row = Database::fetch_array($res)) {
2021
                $list[$row['lp_id']] = $row['progress'];
2022
            }
2023
2024
            return $list;
2025
        }
2026
2027
        return [];
2028
    }
2029
2030
    /**
2031
     * Displays a progress bar
2032
     * completed so far.
2033
     *
2034
     * @param int    $percentage Progress value to display
2035
     * @param string $text_add   Text to display near the progress value
2036
     *
2037
     * @return string HTML string containing the progress bar
2038
     */
2039
    public static function get_progress_bar($percentage = -1, $text_add = '')
2040
    {
2041
        $text = $percentage.$text_add;
2042
2043
        return '<div class="p-progressbar p-progressbar-determinate"
2044
            role="progressbar" aria-valuenow="'.$percentage.'" aria-valuemin="0" aria-valuemax="100">
2045
            <div id="progress_bar_value" class="p-progressbar-value" style="width: '.$text.';">
2046
                 <div class="p-progressbar-label">'.$text.'</div>
2047
            </div>
2048
        </div>';
2049
    }
2050
2051
    /**
2052
     * @param string $mode can be '%' or 'abs'
2053
     *                     otherwise this value will be used $this->progress_bar_mode
2054
     *
2055
     * @return string
2056
     */
2057
    public function getProgressBar($mode = null)
2058
    {
2059
        [$percentage, $text_add] = $this->get_progress_bar_text($mode);
2060
2061
        return self::get_progress_bar($percentage, $text_add);
2062
    }
2063
2064
    /**
2065
     * Gets the progress bar info to display inside the progress bar.
2066
     * Also used by scorm_api.php.
2067
     *
2068
     * @param string $mode Mode of display (can be '%' or 'abs').abs means
2069
     *                     we display a number of completed elements per total elements
2070
     * @param int    $add  Additional steps to fake as completed
2071
     *
2072
     * @return array Percentage or number and symbol (% or /xx)
2073
     */
2074
    public function get_progress_bar_text($mode = '', $add = 0)
2075
    {
2076
        if (empty($mode)) {
2077
            $mode = $this->progress_bar_mode;
2078
        }
2079
        $text = '';
2080
        $percentage = 0;
2081
        // If the option to use the score as progress is set for this learning
2082
        // path, then the rules are completely different: we assume only one
2083
        // item exists and the progress of the LP depends on the score
2084
        $scoreAsProgressSetting = ('true' === api_get_setting('lp.lp_score_as_progress_enable'));
2085
        if (true === $scoreAsProgressSetting) {
2086
            $scoreAsProgress = $this->getUseScoreAsProgress();
2087
            if ($scoreAsProgress) {
2088
                // Get single item's score
2089
                $itemId = $this->get_current_item_id();
2090
                $item = $this->getItem($itemId);
2091
                $score = $item->get_score();
2092
                $maxScore = $item->get_max();
2093
                if ($mode = '%') {
2094
                    if (!empty($maxScore)) {
2095
                        $percentage = ((float) $score / (float) $maxScore) * 100;
2096
                    }
2097
                    $percentage = number_format($percentage, 0);
2098
                    $text = '%';
2099
                } else {
2100
                    $percentage = $score;
2101
                    $text = '/'.$maxScore;
2102
                }
2103
2104
                return [$percentage, $text];
2105
            }
2106
        }
2107
        // otherwise just continue the normal processing of progress
2108
        $total_items = $this->getTotalItemsCountWithoutDirs();
2109
        $completeItems = $this->get_complete_items_count();
2110
        if (0 != $add) {
2111
            $completeItems += $add;
2112
        }
2113
        if ($completeItems > $total_items) {
2114
            $completeItems = $total_items;
2115
        }
2116
        if ('%' === $mode) {
2117
            if ($total_items > 0) {
2118
                $percentage = ((float) $completeItems / (float) $total_items) * 100;
2119
            }
2120
            $percentage = number_format($percentage, 0);
2121
            $text = '%';
2122
        } elseif ('abs' === $mode) {
2123
            $percentage = $completeItems;
2124
            $text = '/'.$total_items;
2125
        }
2126
2127
        return [
2128
            $percentage,
2129
            $text,
2130
        ];
2131
    }
2132
2133
    /**
2134
     * Gets the progress bar mode.
2135
     *
2136
     * @return string The progress bar mode attribute
2137
     */
2138
    public function get_progress_bar_mode()
2139
    {
2140
        if (!empty($this->progress_bar_mode)) {
2141
            return $this->progress_bar_mode;
2142
        }
2143
2144
        return '%';
2145
    }
2146
2147
    /**
2148
     * Gets the learnpath theme (remote or local).
2149
     *
2150
     * @return string Learnpath theme
2151
     */
2152
    public function get_theme()
2153
    {
2154
        if (!empty($this->theme)) {
2155
            return $this->theme;
2156
        }
2157
2158
        return '';
2159
    }
2160
2161
    /**
2162
     * Gets the learnpath session id.
2163
     *
2164
     * @return int
2165
     */
2166
    public function get_lp_session_id()
2167
    {
2168
        $lp = Container::getLpRepository()->find($this->lp_id);
2169
        if ($lp) {
2170
            /* @var ResourceNode $resourceNode */
2171
            $resourceNode = $lp->getResourceNode();
2172
            if ($resourceNode) {
0 ignored issues
show
introduced by
$resourceNode is of type Chamilo\CoreBundle\Entity\ResourceNode, thus it always evaluated to true.
Loading history...
2173
                $link = $resourceNode->getResourceLinks()->first();
2174
                if ($link && $link->getSession()) {
2175
2176
                    return (int) $link->getSession()->getId();
2177
                }
2178
            }
2179
        }
2180
2181
        return 0;
2182
    }
2183
2184
    /**
2185
     * Generate a new prerequisites string for a given item. If this item was a sco and
2186
     * its prerequisites were strings (instead of IDs), then transform those strings into
2187
     * IDs, knowing that SCORM IDs are kept in the "ref" field of the lp_item table.
2188
     * Prefix all item IDs that end-up in the prerequisites string by "ITEM_" to use the
2189
     * same rule as the scormExport() method.
2190
     *
2191
     * @param int $item_id Item ID
2192
     *
2193
     * @return string Prerequisites string ready for the export as SCORM
2194
     */
2195
    public function get_scorm_prereq_string($item_id)
2196
    {
2197
        if ($this->debug > 0) {
2198
            error_log('In learnpath::get_scorm_prereq_string()');
2199
        }
2200
        if (!is_object($this->items[$item_id])) {
2201
            return false;
2202
        }
2203
        /** @var learnpathItem $oItem */
2204
        $oItem = $this->items[$item_id];
2205
        $prereq = $oItem->get_prereq_string();
2206
2207
        if (empty($prereq)) {
2208
            return '';
2209
        }
2210
        if (preg_match('/^\d+$/', $prereq) &&
2211
            isset($this->items[$prereq]) &&
2212
            is_object($this->items[$prereq])
2213
        ) {
2214
            // If the prerequisite is a simple integer ID and this ID exists as an item ID,
2215
            // then simply return it (with the ITEM_ prefix).
2216
            //return 'ITEM_' . $prereq;
2217
            return $this->items[$prereq]->ref;
2218
        } else {
2219
            if (isset($this->refs_list[$prereq])) {
2220
                // It's a simple string item from which the ID can be found in the refs list,
2221
                // so we can transform it directly to an ID for export.
2222
                return $this->items[$this->refs_list[$prereq]]->ref;
2223
            } elseif (isset($this->refs_list['ITEM_'.$prereq])) {
2224
                return $this->items[$this->refs_list['ITEM_'.$prereq]]->ref;
2225
            } else {
2226
                // The last case, if it's a complex form, then find all the IDs (SCORM strings)
2227
                // and replace them, one by one, by the internal IDs (chamilo db)
2228
                // TODO: Modify the '*' replacement to replace the multiplier in front of it
2229
                // by a space as well.
2230
                $find = [
2231
                    '&',
2232
                    '|',
2233
                    '~',
2234
                    '=',
2235
                    '<>',
2236
                    '{',
2237
                    '}',
2238
                    '*',
2239
                    '(',
2240
                    ')',
2241
                ];
2242
                $replace = [
2243
                    ' ',
2244
                    ' ',
2245
                    ' ',
2246
                    ' ',
2247
                    ' ',
2248
                    ' ',
2249
                    ' ',
2250
                    ' ',
2251
                    ' ',
2252
                    ' ',
2253
                ];
2254
                $prereq_mod = str_replace($find, $replace, $prereq);
2255
                $ids = explode(' ', $prereq_mod);
2256
                foreach ($ids as $id) {
2257
                    $id = trim($id);
2258
                    if (isset($this->refs_list[$id])) {
2259
                        $prereq = preg_replace(
2260
                            '/[^a-zA-Z_0-9]('.$id.')[^a-zA-Z_0-9]/',
2261
                            'ITEM_'.$this->refs_list[$id],
2262
                            $prereq
2263
                        );
2264
                    }
2265
                }
2266
2267
                return $prereq;
2268
            }
2269
        }
2270
    }
2271
2272
    /**
2273
     * Returns the XML DOM document's node.
2274
     *
2275
     * @param resource $children Reference to a list of objects to search for the given ITEM_*
2276
     * @param string   $id       The identifier to look for
2277
     *
2278
     * @return mixed The reference to the element found with that identifier. False if not found
2279
     */
2280
    public function get_scorm_xml_node(&$children, $id)
2281
    {
2282
        for ($i = 0; $i < $children->length; $i++) {
2283
            $item_temp = $children->item($i);
2284
            if ('item' === $item_temp->nodeName) {
2285
                if ($item_temp->getAttribute('identifier') == $id) {
2286
                    return $item_temp;
2287
                }
2288
            }
2289
            $subchildren = $item_temp->childNodes;
2290
            if ($subchildren && $subchildren->length > 0) {
2291
                $val = $this->get_scorm_xml_node($subchildren, $id);
2292
                if (is_object($val)) {
2293
                    return $val;
2294
                }
2295
            }
2296
        }
2297
2298
        return false;
2299
    }
2300
2301
    /**
2302
     * Gets the status list for all LP's items.
2303
     *
2304
     * @return array Array of [index] => [item ID => current status]
2305
     */
2306
    public function get_items_status_list()
2307
    {
2308
        $list = [];
2309
        foreach ($this->ordered_items as $item_id) {
2310
            $list[] = [
2311
                $item_id => $this->items[$item_id]->get_status(),
2312
            ];
2313
        }
2314
2315
        return $list;
2316
    }
2317
2318
    /**
2319
     * Return the number of interactions for the given learnpath Item View ID.
2320
     * This method can be used as static.
2321
     *
2322
     * @param int $lp_iv_id  Item View ID
2323
     * @param int $course_id course id
2324
     *
2325
     * @return int
2326
     */
2327
    public static function get_interactions_count_from_db($lp_iv_id, $course_id)
2328
    {
2329
        $table = Database::get_course_table(TABLE_LP_IV_INTERACTION);
2330
        $lp_iv_id = (int) $lp_iv_id;
2331
        $course_id = (int) $course_id;
2332
2333
        $sql = "SELECT count(*) FROM $table
2334
                WHERE c_id = $course_id AND lp_iv_id = $lp_iv_id";
2335
        $res = Database::query($sql);
2336
        $num = 0;
2337
        if (Database::num_rows($res)) {
2338
            $row = Database::fetch_array($res);
2339
            $num = $row[0];
2340
        }
2341
2342
        return $num;
2343
    }
2344
2345
    /**
2346
     * Return the interactions as an array for the given lp_iv_id.
2347
     * This method can be used as static.
2348
     *
2349
     * @param int $lp_iv_id Learnpath Item View ID
2350
     *
2351
     * @return array
2352
     *
2353
     * @todo    Transcode labels instead of switching to HTML (which requires to know the encoding of the LP)
2354
     */
2355
    public static function get_iv_interactions_array($lp_iv_id, $course_id = 0)
2356
    {
2357
        $course_id = empty($course_id) ? api_get_course_int_id() : (int) $course_id;
2358
        $list = [];
2359
        $table = Database::get_course_table(TABLE_LP_IV_INTERACTION);
2360
        $lp_iv_id = (int) $lp_iv_id;
2361
2362
        if (empty($lp_iv_id) || empty($course_id)) {
2363
            return [];
2364
        }
2365
2366
        $sql = "SELECT * FROM $table
2367
                WHERE c_id = ".$course_id." AND lp_iv_id = $lp_iv_id
2368
                ORDER BY order_id ASC";
2369
        $res = Database::query($sql);
2370
        $num = Database::num_rows($res);
2371
        if ($num > 0) {
2372
            $list[] = [
2373
                'order_id' => api_htmlentities(get_lang('Order'), ENT_QUOTES),
2374
                'id' => api_htmlentities(get_lang('Interaction ID'), ENT_QUOTES),
2375
                'type' => api_htmlentities(get_lang('Type'), ENT_QUOTES),
2376
                'time' => api_htmlentities(get_lang('Time (finished at...)'), ENT_QUOTES),
2377
                'correct_responses' => api_htmlentities(get_lang('Correct answers'), ENT_QUOTES),
2378
                'student_response' => api_htmlentities(get_lang('Learner answers'), ENT_QUOTES),
2379
                'result' => api_htmlentities(get_lang('Result'), ENT_QUOTES),
2380
                'latency' => api_htmlentities(get_lang('Time spent'), ENT_QUOTES),
2381
                'student_response_formatted' => '',
2382
            ];
2383
            while ($row = Database::fetch_array($res)) {
2384
                $studentResponseFormatted = urldecode($row['student_response']);
2385
                $content_student_response = explode('__|', $studentResponseFormatted);
2386
                if (count($content_student_response) > 0) {
2387
                    if (count($content_student_response) >= 3) {
2388
                        // Pop the element off the end of array.
2389
                        array_pop($content_student_response);
2390
                    }
2391
                    $studentResponseFormatted = implode(',', $content_student_response);
2392
                }
2393
2394
                $list[] = [
2395
                    'order_id' => $row['order_id'] + 1,
2396
                    'id' => urldecode($row['interaction_id']), //urldecode because they often have %2F or stuff like that
2397
                    'type' => $row['interaction_type'],
2398
                    'time' => $row['completion_time'],
2399
                    'correct_responses' => '', // Hide correct responses from students.
2400
                    'student_response' => $row['student_response'],
2401
                    'result' => $row['result'],
2402
                    'latency' => $row['latency'],
2403
                    'student_response_formatted' => $studentResponseFormatted,
2404
                ];
2405
            }
2406
        }
2407
2408
        return $list;
2409
    }
2410
2411
    /**
2412
     * Return the number of objectives for the given learnpath Item View ID.
2413
     * This method can be used as static.
2414
     *
2415
     * @param int $lp_iv_id  Item View ID
2416
     * @param int $course_id Course ID
2417
     *
2418
     * @return int Number of objectives
2419
     */
2420
    public static function get_objectives_count_from_db($lp_iv_id, $course_id)
2421
    {
2422
        $table = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
2423
        $course_id = (int) $course_id;
2424
        $lp_iv_id = (int) $lp_iv_id;
2425
        $sql = "SELECT count(*) FROM $table
2426
                WHERE c_id = $course_id AND lp_iv_id = $lp_iv_id";
2427
        //@todo seems that this always returns 0
2428
        $res = Database::query($sql);
2429
        $num = 0;
2430
        if (Database::num_rows($res)) {
2431
            $row = Database::fetch_array($res);
2432
            $num = $row[0];
2433
        }
2434
2435
        return $num;
2436
    }
2437
2438
    /**
2439
     * Return the objectives as an array for the given lp_iv_id.
2440
     * This method can be used as static.
2441
     *
2442
     * @param int $lpItemViewId Learnpath Item View ID
2443
     * @param int $course_id
2444
     *
2445
     * @return array
2446
     *
2447
     * @todo    Translate labels
2448
     */
2449
    public static function get_iv_objectives_array($lpItemViewId = 0, $course_id = 0)
2450
    {
2451
        $course_id = empty($course_id) ? api_get_course_int_id() : (int) $course_id;
2452
        $lpItemViewId = (int) $lpItemViewId;
2453
2454
        if (empty($course_id) || empty($lpItemViewId)) {
2455
            return [];
2456
        }
2457
2458
        $table = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
2459
        $sql = "SELECT * FROM $table
2460
                WHERE c_id = $course_id AND lp_iv_id = $lpItemViewId
2461
                ORDER BY order_id ASC";
2462
        $res = Database::query($sql);
2463
        $num = Database::num_rows($res);
2464
        $list = [];
2465
        if ($num > 0) {
2466
            $list[] = [
2467
                'order_id' => api_htmlentities(get_lang('Order'), ENT_QUOTES),
2468
                'objective_id' => api_htmlentities(get_lang('Objective ID'), ENT_QUOTES),
2469
                'score_raw' => api_htmlentities(get_lang('Objective raw score'), ENT_QUOTES),
2470
                'score_max' => api_htmlentities(get_lang('Objective max score'), ENT_QUOTES),
2471
                'score_min' => api_htmlentities(get_lang('Objective min score'), ENT_QUOTES),
2472
                'status' => api_htmlentities(get_lang('Objective status'), ENT_QUOTES),
2473
            ];
2474
            while ($row = Database::fetch_array($res)) {
2475
                $list[] = [
2476
                    'order_id' => $row['order_id'] + 1,
2477
                    'objective_id' => urldecode($row['objective_id']), // urldecode() because they often have %2F
2478
                    'score_raw' => $row['score_raw'],
2479
                    'score_max' => $row['score_max'],
2480
                    'score_min' => $row['score_min'],
2481
                    'status' => $row['status'],
2482
                ];
2483
            }
2484
        }
2485
2486
        return $list;
2487
    }
2488
2489
    /**
2490
     * Generate and return the table of contents for this learnpath. The (flat) table returned can be
2491
     * used by get_html_toc() to be ready to display.
2492
     */
2493
    public function get_toc(): array
2494
    {
2495
        $toc = [];
2496
        foreach ($this->ordered_items as $item_id) {
2497
            // TODO: Change this link generation and use new function instead.
2498
            $toc[] = [
2499
                'id' => $item_id,
2500
                'title' => $this->items[$item_id]->get_title(),
2501
                'status' => $this->items[$item_id]->get_status(false),
2502
                'status_class' => self::getStatusCSSClassName($this->items[$item_id]->get_status(false)),
2503
                'level' => $this->items[$item_id]->get_level(),
2504
                'type' => $this->items[$item_id]->get_type(),
2505
                'description' => $this->items[$item_id]->get_description(),
2506
                'path' => $this->items[$item_id]->get_path(),
2507
                'parent' => $this->items[$item_id]->get_parent(),
2508
            ];
2509
        }
2510
2511
        return $toc;
2512
    }
2513
2514
    /**
2515
     * Returns the CSS class name associated with a given item status.
2516
     *
2517
     * @param $status string an item status
2518
     *
2519
     * @return string CSS class name
2520
     */
2521
    public static function getStatusCSSClassName($status)
2522
    {
2523
        if (array_key_exists($status, self::STATUS_CSS_CLASS_NAME)) {
2524
            return self::STATUS_CSS_CLASS_NAME[$status];
2525
        }
2526
2527
        return '';
2528
    }
2529
2530
    /**
2531
     * Generate and return the table of contents for this learnpath. The JS
2532
     * table returned is used inside of scorm_api.php.
2533
     *
2534
     * @param string $varname
2535
     *
2536
     * @return string A JS array variable construction
2537
     */
2538
    public function get_items_details_as_js($varname = 'olms.lms_item_types')
2539
    {
2540
        $toc = $varname.' = new Array();';
2541
        foreach ($this->ordered_items as $item_id) {
2542
            $toc .= $varname."['i$item_id'] = '".$this->items[$item_id]->get_type()."';";
2543
        }
2544
2545
        return $toc;
2546
    }
2547
2548
    /**
2549
     * Gets the learning path type.
2550
     *
2551
     * @param bool $get_name Return the name? If false, return the ID. Default is false.
2552
     *
2553
     * @return mixed Type ID or name, depending on the parameter
2554
     */
2555
    public function get_type($get_name = false)
2556
    {
2557
        $res = false;
2558
        if (!empty($this->type) && (!$get_name)) {
2559
            $res = $this->type;
2560
        }
2561
2562
        return $res;
2563
    }
2564
2565
    /**
2566
     * Gets the learning path type as static method.
2567
     *
2568
     * @param int $lp_id
2569
     *
2570
     * @return mixed Type ID or name, depending on the parameter
2571
     */
2572
    public static function get_type_static($lp_id = 0)
2573
    {
2574
        $tbl_lp = Database::get_course_table(TABLE_LP_MAIN);
2575
        $lp_id = (int) $lp_id;
2576
        $sql = "SELECT lp_type FROM $tbl_lp
2577
                WHERE iid = $lp_id";
2578
        $res = Database::query($sql);
2579
        if (false === $res) {
2580
            return null;
2581
        }
2582
        if (Database::num_rows($res) <= 0) {
2583
            return null;
2584
        }
2585
        $row = Database::fetch_array($res);
2586
2587
        return $row['lp_type'];
2588
    }
2589
2590
    /**
2591
     * Gets a flat list of item IDs ordered for display (level by level ordered by order_display)
2592
     * This method can be used as abstract and is recursive.
2593
     *
2594
     * @param CLp $lp
2595
     * @param int $parent    Parent ID of the items to look for
2596
     *
2597
     * @return array Ordered list of item IDs (empty array on error)
2598
     */
2599
    public static function get_flat_ordered_items_list(CLp $lp, $parent = 0, $withExportFlag = false)
2600
    {
2601
        $parent = (int) $parent;
2602
        $lpItemRepo = Container::getLpItemRepository();
2603
        if (empty($parent)) {
2604
            $rootItem = $lpItemRepo->getRootItem($lp->getIid());
2605
            if (null !== $rootItem) {
2606
                $parent = $rootItem->getIid();
2607
            }
2608
        }
2609
2610
        if (empty($parent)) {
2611
            return [];
2612
        }
2613
2614
        $criteria = new Criteria();
2615
        $criteria
2616
            ->where($criteria->expr()->neq('path', 'root'))
2617
            ->orderBy(['displayOrder' => Criteria::ASC]);
2618
        $items = $lp->getItems()->matching($criteria);
2619
        $items = $items->filter(function (CLpItem $element) use ($parent) {
2620
            if ('root' === $element->getPath()) {
2621
                return false;
2622
            }
2623
            if (null !== $element->getParent()) {
2624
                return $element->getParent()->getIid() === $parent;
2625
            }
2626
            return false;
2627
        });
2628
2629
        if (!$withExportFlag) {
2630
            $ids = [];
2631
            foreach ($items as $item) {
2632
                $itemId = $item->getIid();
2633
                $ids[] = $itemId;
2634
                $subIds = self::get_flat_ordered_items_list($lp, $itemId, false);
2635
                foreach ($subIds as $subId) {
2636
                    $ids[] = $subId;
2637
                }
2638
            }
2639
            return $ids;
2640
        }
2641
2642
        $list = [];
2643
        foreach ($items as $item) {
2644
            $itemId = $item->getIid();
2645
            $list[] = [
2646
                'iid'            => $itemId,
2647
                'export_allowed' => $item->isExportAllowed() ? 1 : 0,
2648
            ];
2649
            $subList = self::get_flat_ordered_items_list($lp, $itemId, true);
2650
            foreach ($subList as $subEntry) {
2651
                $list[] = $subEntry;
2652
            }
2653
        }
2654
2655
        return $list;
2656
    }
2657
2658
    public static function getChapterTypes(): array
2659
    {
2660
        return [
2661
            'dir',
2662
        ];
2663
    }
2664
2665
    /**
2666
     * Uses the table generated by get_toc() and returns an HTML-formatted string ready to display.
2667
     *
2668
     * @return array HTML TOC ready to display
2669
     */
2670
    public function getListArrayToc()
2671
    {
2672
        $lpItemRepo = Container::getLpItemRepository();
2673
        $itemRoot = $lpItemRepo->getRootItem($this->get_id());
2674
        $options = [
2675
            'decorate' => false,
2676
        ];
2677
2678
        return $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
2679
    }
2680
2681
    /**
2682
     * Returns an HTML-formatted string ready to display with teacher buttons
2683
     * in LP view menu.
2684
     *
2685
     * @return string HTML TOC ready to display
2686
     */
2687
    public function get_teacher_toc_buttons()
2688
    {
2689
        $isAllow = api_is_allowed_to_edit(null, true, false, false);
2690
        $hideIcons = api_get_configuration_value('hide_teacher_icons_lp');
2691
        $html = '';
2692
        if ($isAllow && false == $hideIcons) {
2693
            if ($this->get_lp_session_id() == api_get_session_id()) {
2694
                $html .= '<div id="actions_lp" class="actions_lp"><hr>';
2695
                $html .= '<div class="flex flex-wrap gap-1 justify-center">';
2696
                $html .= "<a
2697
                    class='btn btn-sm btn--plain'
2698
                    href='lp_controller.php?".api_get_cidreq()."&action=add_item&type=step&lp_id=".$this->lp_id."&isStudentView=false'
2699
                    target='_parent'>".
2700
                    Display::getMdiIcon('pencil').get_lang('Edit')."</a>";
2701
                $html .= '<a
2702
                    class="btn btn-sm btn--plain"
2703
                    href="lp_controller.php?'.api_get_cidreq()."&action=edit&lp_id=".$this->lp_id.'&isStudentView=false">'.
2704
                    Display::getMdiIcon('hammer-wrench').get_lang('Settings').'</a>';
2705
                $html .= '</div>';
2706
                $html .= '</div>';
2707
            }
2708
        }
2709
2710
        return $html;
2711
    }
2712
2713
    /**
2714
     * Gets the learnpath name/title.
2715
     *
2716
     * @return string Learnpath name/title
2717
     */
2718
    public function get_name()
2719
    {
2720
        if (!empty($this->name)) {
2721
            return $this->name;
2722
        }
2723
2724
        return 'N/A';
2725
    }
2726
2727
    /**
2728
     * @return string
2729
     */
2730
    public function getNameNoTags()
2731
    {
2732
        return strip_tags($this->get_name());
2733
    }
2734
2735
    /**
2736
     * Gets a link to the resource from the present location, depending on item ID.
2737
     *
2738
     * @param string $type         Type of link expected
2739
     * @param int    $item_id      Learnpath item ID
2740
     * @param bool   $provided_toc
2741
     *
2742
     * @return string $provided_toc Link to the lp_item resource
2743
     */
2744
    public function get_link($type = 'http', $item_id = 0, $provided_toc = false)
2745
    {
2746
        $course_id = $this->get_course_int_id();
2747
        $item_id = (int) $item_id;
2748
2749
        if (empty($item_id)) {
2750
            $item_id = $this->get_current_item_id();
2751
2752
            if (empty($item_id)) {
2753
                //still empty, this means there was no item_id given and we are not in an object context or
2754
                //the object property is empty, return empty link
2755
                $this->first();
2756
2757
                return '';
2758
            }
2759
        }
2760
2761
        $file = '';
2762
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
2763
        $lp_item_table = Database::get_course_table(TABLE_LP_ITEM);
2764
        $lp_item_view_table = Database::get_course_table(TABLE_LP_ITEM_VIEW);
2765
2766
        $sql = "SELECT
2767
                    l.lp_type as ltype,
2768
                    l.path as lpath,
2769
                    li.item_type as litype,
2770
                    li.path as lipath,
2771
                    li.parameters as liparams
2772
        		FROM $lp_table l
2773
                INNER JOIN $lp_item_table li
2774
                ON (li.lp_id = l.iid)
2775
        		WHERE
2776
        		    li.iid = $item_id
2777
        		";
2778
        $res = Database::query($sql);
2779
        if (Database::num_rows($res) > 0) {
2780
            $row = Database::fetch_array($res);
2781
            $lp_type = $row['ltype'];
2782
            $lp_path = $row['lpath'];
2783
            $lp_item_type = $row['litype'];
2784
            $lp_item_path = $row['lipath'];
2785
            $lp_item_params = $row['liparams'];
2786
            if (empty($lp_item_params) && false !== strpos($lp_item_path, '?')) {
2787
                [$lp_item_path, $lp_item_params] = explode('?', $lp_item_path);
2788
            }
2789
            //$sys_course_path = api_get_path(SYS_COURSE_PATH).api_get_course_path();
2790
            if ('http' === $type) {
2791
                //web path
2792
                //$course_path = api_get_path(WEB_COURSE_PATH).api_get_course_path();
2793
            } else {
2794
                //$course_path = $sys_course_path; //system path
2795
            }
2796
2797
            // Fixed issue BT#1272 - If the item type is a Chamilo Item (quiz, link, etc),
2798
            // then change the lp type to thread it as a normal Chamilo LP not a SCO.
2799
            if (in_array(
2800
                $lp_item_type,
2801
                ['quiz', 'document', 'final_item', 'link', 'forum', 'thread', 'student_publication', 'survey']
2802
            )
2803
            ) {
2804
                $lp_type = CLp::LP_TYPE;
2805
            }
2806
2807
            // Now go through the specific cases to get the end of the path
2808
            // @todo Use constants instead of int values.
2809
            switch ($lp_type) {
2810
                case CLp::LP_TYPE:
2811
                    $file = self::rl_get_resource_link_for_learnpath(
2812
                        $course_id,
2813
                        $this->get_id(),
2814
                        $item_id,
2815
                        $this->get_view_id()
2816
                    );
2817
                    switch ($lp_item_type) {
2818
                        case 'document':
2819
                            // Shows a button to download the file instead of just downloading the file directly.
2820
                            $documentPathInfo = pathinfo($file);
2821
                            if (isset($documentPathInfo['extension'])) {
2822
                                $parsed = parse_url($documentPathInfo['extension']);
2823
                                if (isset($parsed['path'])) {
2824
                                    $extension = $parsed['path'];
2825
                                    $extensionsToDownload = [
2826
                                        'zip',
2827
                                        'ppt',
2828
                                        'pptx',
2829
                                        'ods',
2830
                                        'xlsx',
2831
                                        'xls',
2832
                                        'csv',
2833
                                        'doc',
2834
                                        'docx',
2835
                                        'dot',
2836
                                    ];
2837
2838
                                    if (in_array($extension, $extensionsToDownload)) {
2839
                                        $file = api_get_path(WEB_CODE_PATH).
2840
                                            'lp/embed.php?type=download&source=file&lp_item_id='.$item_id.'&'.api_get_cidreq();
2841
                                    }
2842
                                }
2843
                            }
2844
                            break;
2845
                        case 'dir':
2846
                            $file = 'lp_content.php?type=dir';
2847
                            break;
2848
                        case 'link':
2849
                            if (Link::is_youtube_link($file)) {
2850
                                $src = Link::get_youtube_video_id($file);
2851
                                $file = api_get_path(WEB_CODE_PATH).'lp/embed.php?type=youtube&source='.$src;
2852
                            } elseif (Link::isVimeoLink($file)) {
2853
                                $src = Link::getVimeoLinkId($file);
2854
                                $file = api_get_path(WEB_CODE_PATH).'lp/embed.php?type=vimeo&source='.$src;
2855
                            } else {
2856
                                // If the current site is HTTPS and the link is
2857
                                // HTTP, browsers will refuse opening the link
2858
                                $urlId = api_get_current_access_url_id();
2859
                                $url = api_get_access_url($urlId, false);
2860
                                $protocol = substr($url['url'], 0, 5);
2861
                                if ('https' === $protocol) {
2862
                                    $linkProtocol = substr($file, 0, 5);
2863
                                    if ('http:' === $linkProtocol) {
2864
                                        //this is the special intervention case
2865
                                        $file = api_get_path(WEB_CODE_PATH).
2866
                                            'lp/embed.php?type=nonhttps&source='.urlencode($file);
2867
                                    }
2868
                                }
2869
                            }
2870
                            break;
2871
                        case 'quiz':
2872
                            // Check how much attempts of a exercise exits in lp
2873
                            $lp_item_id = $this->get_current_item_id();
2874
                            $lp_view_id = $this->get_view_id();
2875
2876
                            $prevent_reinit = null;
2877
                            if (isset($this->items[$this->current])) {
2878
                                $prevent_reinit = $this->items[$this->current]->get_prevent_reinit();
2879
                            }
2880
2881
                            if (empty($provided_toc)) {
2882
                                $list = $this->get_toc();
2883
                            } else {
2884
                                $list = $provided_toc;
2885
                            }
2886
2887
                            $type_quiz = false;
2888
                            foreach ($list as $toc) {
2889
                                if ($toc['id'] == $lp_item_id && 'quiz' === $toc['type']) {
2890
                                    $type_quiz = true;
2891
                                }
2892
                            }
2893
2894
                            if ($type_quiz) {
2895
                                $lp_item_id = (int) $lp_item_id;
2896
                                $lp_view_id = (int) $lp_view_id;
2897
                                $sql = "SELECT count(*) FROM $lp_item_view_table
2898
                                        WHERE
2899
                                            lp_item_id='".$lp_item_id."' AND
2900
                                            lp_view_id ='".$lp_view_id."' AND
2901
                                            status='completed'";
2902
                                $result = Database::query($sql);
2903
                                $row_count = Database:: fetch_row($result);
2904
                                $count_item_view = (int) $row_count[0];
2905
                                $not_multiple_attempt = 0;
2906
                                if (1 === $prevent_reinit && $count_item_view > 0) {
2907
                                    $not_multiple_attempt = 1;
2908
                                }
2909
                                $file .= '&not_multiple_attempt='.$not_multiple_attempt;
2910
                            }
2911
                            break;
2912
                    }
2913
2914
                    $tmp_array = explode('/', $file);
2915
                    $document_name = $tmp_array[count($tmp_array) - 1];
2916
                    if (strpos($document_name, '_DELETED_')) {
2917
                        $file = 'blank.php?error=document_deleted';
2918
                    }
2919
                    break;
2920
                case CLp::SCORM_TYPE:
2921
                    if ('dir' !== $lp_item_type) {
2922
                        // Quite complex here:
2923
                        // We want to make sure 'http://' (and similar) links can
2924
                        // be loaded as is (withouth the Chamilo path in front) but
2925
                        // some contents use this form: resource.htm?resource=http://blablabla
2926
                        // which means we have to find a protocol at the path's start, otherwise
2927
                        // it should not be considered as an external URL.
2928
                        // if ($this->prerequisites_match($item_id)) {
2929
                        if (0 != preg_match('#^[a-zA-Z]{2,5}://#', $lp_item_path)) {
2930
                            if ($this->debug > 2) {
2931
                                error_log('In learnpath::get_link() '.__LINE__.' - Found match for protocol in '.$lp_item_path, 0);
2932
                            }
2933
                            // Distant url, return as is.
2934
                            $file = $lp_item_path;
2935
                        } else {
2936
                            if ($this->debug > 2) {
2937
                                error_log('In learnpath::get_link() '.__LINE__.' - No starting protocol in '.$lp_item_path);
2938
                            }
2939
                            // Prevent getting untranslatable urls.
2940
                            $lp_item_path = preg_replace('/%2F/', '/', $lp_item_path);
2941
                            $lp_item_path = preg_replace('/%3A/', ':', $lp_item_path);
2942
2943
                            /*$asset = $this->getEntity()->getAsset();
2944
                            $folder = Container::getAssetRepository()->getFolder($asset);
2945
                            $hasFile = Container::getAssetRepository()->getFileSystem()->has($folder.$lp_item_path);
2946
                            $file = null;
2947
                            if ($hasFile) {
2948
                                $file = Container::getAssetRepository()->getAssetUrl($asset).'/'.$lp_item_path;
2949
                            }*/
2950
                            $file = $this->scormUrl.$lp_item_path;
2951
2952
                            // Prepare the path.
2953
                            /*$file = $course_path.'/scorm/'.$lp_path.'/'.$lp_item_path;
2954
                            // TODO: Fix this for urls with protocol header.
2955
                            $file = str_replace('//', '/', $file);
2956
                            $file = str_replace(':/', '://', $file);
2957
                            if ('/' === substr($lp_path, -1)) {
2958
                                $lp_path = substr($lp_path, 0, -1);
2959
                            }*/
2960
                            /*if (!$hasFile) {
2961
                                // if file not found.
2962
                                $decoded = html_entity_decode($lp_item_path);
2963
                                [$decoded] = explode('?', $decoded);
2964
                                if (!is_file(realpath($sys_course_path.'/scorm/'.$lp_path.'/'.$decoded))) {
2965
                                    $file = self::rl_get_resource_link_for_learnpath(
2966
                                        $course_id,
2967
                                        $this->get_id(),
2968
                                        $item_id,
2969
                                        $this->get_view_id()
2970
                                    );
2971
                                    if (empty($file)) {
2972
                                        $file = 'blank.php?error=document_not_found';
2973
                                    } else {
2974
                                        $tmp_array = explode('/', $file);
2975
                                        $document_name = $tmp_array[count($tmp_array) - 1];
2976
                                        if (strpos($document_name, '_DELETED_')) {
2977
                                            $file = 'blank.php?error=document_deleted';
2978
                                        } else {
2979
                                            $file = 'blank.php?error=document_not_found';
2980
                                        }
2981
                                    }
2982
                                } else {
2983
                                    $file = $course_path.'/scorm/'.$lp_path.'/'.$decoded;
2984
                                }
2985
                            }*/
2986
                        }
2987
2988
                        // We want to use parameters if they were defined in the imsmanifest
2989
                        if (false === strpos($file, 'blank.php')) {
2990
                            $lp_item_params = ltrim($lp_item_params, '?');
2991
                            $file .= (false === strstr($file, '?') ? '?' : '').$lp_item_params;
2992
                        }
2993
                    } else {
2994
                        $file = 'lp_content.php?type=dir';
2995
                    }
2996
                    break;
2997
                case CLp::AICC_TYPE:
2998
                    // Formatting AICC HACP append URL.
2999
                    $aicc_append = '?aicc_sid='.
3000
                        urlencode(session_id()).'&aicc_url='.urlencode(api_get_path(WEB_CODE_PATH).'lp/aicc_hacp.php').'&';
3001
                    if (!empty($lp_item_params)) {
3002
                        $aicc_append .= $lp_item_params.'&';
3003
                    }
3004
                    if ('dir' !== $lp_item_type) {
3005
                        // Quite complex here:
3006
                        // We want to make sure 'http://' (and similar) links can
3007
                        // be loaded as is (withouth the Chamilo path in front) but
3008
                        // some contents use this form: resource.htm?resource=http://blablabla
3009
                        // which means we have to find a protocol at the path's start, otherwise
3010
                        // it should not be considered as an external URL.
3011
                        if (0 != preg_match('#^[a-zA-Z]{2,5}://#', $lp_item_path)) {
3012
                            if ($this->debug > 2) {
3013
                                error_log('In learnpath::get_link() '.__LINE__.' - Found match for protocol in '.$lp_item_path, 0);
3014
                            }
3015
                            // Distant url, return as is.
3016
                            $file = $lp_item_path;
3017
                            // Enabled and modified by Ivan Tcholakov, 16-OCT-2008.
3018
                            /*
3019
                            if (stristr($file,'<servername>') !== false) {
3020
                                $file = str_replace('<servername>', $course_path.'/scorm/'.$lp_path.'/', $lp_item_path);
3021
                            }
3022
                            */
3023
                            if (false !== stripos($file, '<servername>')) {
3024
                                //$file = str_replace('<servername>',$course_path.'/scorm/'.$lp_path.'/',$lp_item_path);
3025
                                $web_course_path = str_replace('https://', '', str_replace('http://', '', $course_path));
3026
                                $file = str_replace('<servername>', $web_course_path.'/scorm/'.$lp_path, $lp_item_path);
3027
                            }
3028
3029
                            $file .= $aicc_append;
3030
                        } else {
3031
                            if ($this->debug > 2) {
3032
                                error_log('In learnpath::get_link() '.__LINE__.' - No starting protocol in '.$lp_item_path, 0);
3033
                            }
3034
                            // Prevent getting untranslatable urls.
3035
                            $lp_item_path = preg_replace('/%2F/', '/', $lp_item_path);
3036
                            $lp_item_path = preg_replace('/%3A/', ':', $lp_item_path);
3037
                            // Prepare the path - lp_path might be unusable because it includes the "aicc" subdir name.
3038
                            $file = $course_path.'/scorm/'.$lp_path.'/'.$lp_item_path;
3039
                            // TODO: Fix this for urls with protocol header.
3040
                            $file = str_replace('//', '/', $file);
3041
                            $file = str_replace(':/', '://', $file);
3042
                            $file .= $aicc_append;
3043
                        }
3044
                    } else {
3045
                        $file = 'lp_content.php?type=dir';
3046
                    }
3047
                    break;
3048
                case 4:
3049
                default:
3050
                    break;
3051
            }
3052
            // Replace &amp; by & because &amp; will break URL with params
3053
            $file = !empty($file) ? str_replace('&amp;', '&', $file) : '';
3054
        }
3055
        if ($this->debug > 2) {
3056
            error_log('In learnpath::get_link() - returning "'.$file.'" from get_link', 0);
3057
        }
3058
3059
        return $file;
3060
    }
3061
3062
    /**
3063
     * Gets the latest usable view or generate a new one.
3064
     *
3065
     * @param int $attempt_num Optional attempt number. If none given, takes the highest from the lp_view table
3066
     * @param int $userId      The user ID, as $this->get_user_id() is not always available
3067
     *
3068
     * @return int DB lp_view id
3069
     */
3070
    public function get_view($attempt_num = 0, $userId = null)
3071
    {
3072
        $search = '';
3073
        $attempt_num = (int) $attempt_num;
3074
        // Use $attempt_num to enable multi-views management (disabled so far).
3075
        if (!empty($attempt_num)) {
3076
            $search = 'AND view_count = '.$attempt_num;
3077
        }
3078
3079
        $course_id = api_get_course_int_id();
3080
        $sessionId = api_get_session_id();
3081
3082
        // Check user ID.
3083
        if (empty($userId)) {
3084
            if (empty($this->get_user_id())) {
3085
                $this->error = 'User ID is empty in learnpath::get_view()';
3086
3087
                return null;
3088
            } else {
3089
                $userId = $this->get_user_id();
3090
            }
3091
        }
3092
        $sessionCondition = api_get_session_condition($sessionId);
3093
3094
        // When missing $attempt_num, search for a unique lp_view record for this lp and user.
3095
        $table = Database::get_course_table(TABLE_LP_VIEW);
3096
        $sql = "SELECT iid FROM $table
3097
        		WHERE
3098
        		    c_id = $course_id AND
3099
        		    lp_id = ".$this->get_id()." AND
3100
        		    user_id = ".$userId."
3101
        		    $sessionCondition
3102
        		    $search
3103
                ORDER BY view_count DESC";
3104
        $res = Database::query($sql);
3105
        if (Database::num_rows($res) > 0) {
3106
            $row = Database::fetch_array($res);
3107
            $this->lp_view_id = $row['iid'];
3108
        } elseif (!api_is_invitee()) {
3109
            $params = [
3110
                'c_id' => $course_id,
3111
                'lp_id' => $this->get_id(),
3112
                'user_id' => $this->get_user_id(),
3113
                'view_count' => 1,
3114
                'last_item' => 0,
3115
            ];
3116
            if (!empty($sessionId)) {
3117
                $params['session_id']  = $sessionId;
3118
            }
3119
            $this->lp_view_id = Database::insert($table, $params);
3120
        }
3121
3122
        return $this->lp_view_id;
3123
    }
3124
3125
    /**
3126
     * Gets the current view id.
3127
     *
3128
     * @return int View ID (from lp_view)
3129
     */
3130
    public function get_view_id()
3131
    {
3132
        if (!empty($this->lp_view_id)) {
3133
            return (int) $this->lp_view_id;
3134
        }
3135
3136
        return 0;
3137
    }
3138
3139
    /**
3140
     * Gets the update queue.
3141
     *
3142
     * @return array Array containing IDs of items to be updated by JavaScript
3143
     */
3144
    public function get_update_queue()
3145
    {
3146
        return $this->update_queue;
3147
    }
3148
3149
    /**
3150
     * Gets the user ID.
3151
     *
3152
     * @return int User ID
3153
     */
3154
    public function get_user_id()
3155
    {
3156
        if (!empty($this->user_id)) {
3157
            return (int) $this->user_id;
3158
        }
3159
3160
        return false;
3161
    }
3162
3163
    /**
3164
     * Checks if any of the items has an audio element attached.
3165
     *
3166
     * @return bool True or false
3167
     */
3168
    public function has_audio()
3169
    {
3170
        $has = false;
3171
        foreach ($this->items as $i => $item) {
3172
            if (!empty($this->items[$i]->audio)) {
3173
                $has = true;
3174
                break;
3175
            }
3176
        }
3177
3178
        return $has;
3179
    }
3180
3181
    /**
3182
     * Updates learnpath attributes to point to the next element
3183
     * The last part is similar to set_current_item but processing the other way around.
3184
     */
3185
    public function next()
3186
    {
3187
        if ($this->debug > 0) {
3188
            error_log('In learnpath::next()', 0);
3189
        }
3190
        $this->last = $this->get_current_item_id();
3191
        $this->items[$this->last]->save(
3192
            false,
3193
            $this->prerequisites_match($this->last)
3194
        );
3195
        $this->autocomplete_parents($this->last);
3196
        $new_index = $this->get_next_index();
3197
        if ($this->debug > 2) {
3198
            error_log('New index: '.$new_index, 0);
3199
        }
3200
        $this->index = $new_index;
3201
        if ($this->debug > 2) {
3202
            error_log('Now having orderedlist['.$new_index.'] = '.$this->ordered_items[$new_index], 0);
3203
        }
3204
        $this->current = $this->ordered_items[$new_index];
3205
        if ($this->debug > 2) {
3206
            error_log('new item id is '.$this->current.'-'.$this->get_current_item_id(), 0);
3207
        }
3208
    }
3209
3210
    /**
3211
     * Open a resource = initialise all local variables relative to this resource. Depending on the child
3212
     * class, this might be redefined to allow several behaviours depending on the document type.
3213
     *
3214
     * @param int $id Resource ID
3215
     */
3216
    public function open($id)
3217
    {
3218
        // TODO:
3219
        // set the current resource attribute to this resource
3220
        // switch on element type (redefine in child class?)
3221
        // set status for this item to "opened"
3222
        // start timer
3223
        // initialise score
3224
        $this->index = 0; //or = the last item seen (see $this->last)
3225
    }
3226
3227
    /**
3228
     * Check that all prerequisites are fulfilled. Returns true and an
3229
     * empty string on success, returns false
3230
     * and the prerequisite string on error.
3231
     * This function is based on the rules for aicc_script language as
3232
     * described in the SCORM 1.2 CAM documentation page 108.
3233
     *
3234
     * @param int $itemId Optional item ID. If none given, uses the current open item.
3235
     *
3236
     * @return bool true if prerequisites are matched, false otherwise - Empty string if true returned, prerequisites
3237
     *              string otherwise
3238
     */
3239
    public function prerequisites_match($itemId = null)
3240
    {
3241
        $allow = ('true' === api_get_setting('lp.allow_teachers_to_access_blocked_lp_by_prerequisite'));
3242
        if ($allow) {
3243
            if (api_is_allowed_to_edit() ||
3244
                api_is_platform_admin(true) ||
3245
                api_is_drh() ||
3246
                api_is_coach(api_get_session_id(), api_get_course_int_id())
3247
            ) {
3248
                return true;
3249
            }
3250
        }
3251
3252
        $debug = $this->debug;
3253
        if ($debug > 0) {
3254
            error_log('In learnpath::prerequisites_match()');
3255
        }
3256
3257
        if (empty($itemId)) {
3258
            $itemId = $this->current;
3259
        }
3260
3261
        $currentItem = $this->getItem($itemId);
3262
3263
        if ($currentItem) {
3264
            if (2 == $this->type) {
3265
                // Getting prereq from scorm
3266
                $prereq_string = $this->get_scorm_prereq_string($itemId);
3267
            } else {
3268
                $prereq_string = $currentItem->get_prereq_string();
3269
            }
3270
3271
            if (empty($prereq_string)) {
3272
                if ($debug > 0) {
3273
                    error_log('Found prereq_string is empty return true');
3274
                }
3275
3276
                return true;
3277
            }
3278
3279
            // Clean spaces.
3280
            $prereq_string = str_replace(' ', '', $prereq_string);
3281
            if ($debug > 0) {
3282
                error_log('Found prereq_string: '.$prereq_string, 0);
3283
            }
3284
3285
            // Now send to the parse_prereq() function that will check this component's prerequisites.
3286
            $result = $currentItem->parse_prereq(
3287
                $prereq_string,
3288
                $this->items,
3289
                $this->refs_list,
3290
                $this->get_user_id()
3291
            );
3292
3293
            if (false === $result) {
3294
                $this->set_error_msg($currentItem->prereq_alert);
3295
            }
3296
        } else {
3297
            $result = true;
3298
            if ($debug > 1) {
3299
                error_log('$this->items['.$itemId.'] was not an object', 0);
3300
            }
3301
        }
3302
3303
        if ($debug > 1) {
3304
            error_log('End of prerequisites_match(). Error message is now '.$this->error, 0);
3305
        }
3306
3307
        return $result;
3308
    }
3309
3310
    /**
3311
     * Updates learnpath attributes to point to the previous element
3312
     * The last part is similar to set_current_item but processing the other way around.
3313
     */
3314
    public function previous()
3315
    {
3316
        $this->last = $this->get_current_item_id();
3317
        $this->items[$this->last]->save(
3318
            false,
3319
            $this->prerequisites_match($this->last)
3320
        );
3321
        $this->autocomplete_parents($this->last);
3322
        $new_index = $this->get_previous_index();
3323
        $this->index = $new_index;
3324
        $this->current = $this->ordered_items[$new_index];
3325
    }
3326
3327
    /**
3328
     * Publishes a learnpath. This basically means show or hide the learnpath
3329
     * to normal users.
3330
     * Can be used as abstract.
3331
     *
3332
     * @param int $id         Learnpath ID
3333
     * @param int $visibility New visibility (1 = visible/published, 0= invisible/draft)
3334
     *
3335
     * @return bool
3336
     */
3337
    public static function toggleVisibility($id, $visibility = 1)
3338
    {
3339
        $repo = Container::getLpRepository();
3340
        $lp = $repo->find($id);
3341
3342
        if (!$lp) {
3343
            return false;
3344
        }
3345
3346
        $visibility = (int) $visibility;
3347
3348
        $course = api_get_course_entity();
3349
        $session = api_get_session_entity();
3350
3351
        if (1 === $visibility) {
3352
            $repo->setVisibilityPublished($lp, $course, $session);
3353
        } else {
3354
            $repo->setVisibilityDraft($lp, $course, $session);
3355
        }
3356
3357
        return true;
3358
    }
3359
3360
    /**
3361
     * Publishes a learnpath category.
3362
     * This basically means show or hide the learnpath category to normal users.
3363
     *
3364
     * @param int $id
3365
     * @param int $visibility
3366
     *
3367
     * @return bool
3368
     */
3369
    public static function toggleCategoryVisibility($id, $visibility = 1)
3370
    {
3371
        $repo = Container::getLpCategoryRepository();
3372
        $resource = $repo->find($id);
3373
3374
        if (!$resource) {
3375
            return false;
3376
        }
3377
3378
        $visibility = (int) $visibility;
3379
3380
        $course = api_get_course_entity();
3381
        $session = api_get_session_entity();
3382
3383
        if (1 === $visibility) {
3384
            $repo->setVisibilityPublished($resource, $course, $session);
3385
        } else {
3386
            $repo->setVisibilityDraft($resource, $course, $session);
3387
            self::toggleCategoryPublish($id, 0);
3388
        }
3389
3390
        return false;
3391
    }
3392
3393
    /**
3394
     * Publishes a learnpath. This basically means show or hide the learnpath
3395
     * on the course homepage.
3396
     *
3397
     * @param int    $id            Learnpath id
3398
     * @param string $setVisibility New visibility (v/i - visible/invisible)
3399
     *
3400
     * @return bool
3401
     */
3402
    public static function togglePublish($id, $setVisibility = 'v')
3403
    {
3404
        $addShortcut = false;
3405
        if ('v' === $setVisibility) {
3406
            $addShortcut = true;
3407
        }
3408
        $repo = Container::getLpRepository();
3409
        /** @var CLp|null $lp */
3410
        $lp = $repo->find($id);
3411
        if (null === $lp) {
3412
            return false;
3413
        }
3414
        $repoShortcut = Container::getShortcutRepository();
3415
        if ($addShortcut) {
3416
            $repoShortcut->addShortCut($lp, api_get_user_entity(), api_get_course_entity(), api_get_session_entity());
3417
        } else {
3418
            $repoShortcut->removeShortCut($lp);
3419
        }
3420
3421
        return true;
3422
    }
3423
3424
    /**
3425
     * Show or hide the learnpath category on the course homepage.
3426
     *
3427
     * @param int $id
3428
     * @param int $setVisibility
3429
     *
3430
     * @return bool
3431
     */
3432
    public static function toggleCategoryPublish($id, $setVisibility = 1)
3433
    {
3434
        $setVisibility = (int) $setVisibility;
3435
        $addShortcut = false;
3436
        if (1 === $setVisibility) {
3437
            $addShortcut = true;
3438
        }
3439
3440
        $repo = Container::getLpCategoryRepository();
3441
        /** @var CLpCategory|null $lp */
3442
        $category = $repo->find($id);
3443
3444
        if (null === $category) {
3445
            return false;
3446
        }
3447
3448
        $repoShortcut = Container::getShortcutRepository();
3449
        if ($addShortcut) {
3450
            $courseEntity = api_get_course_entity(api_get_course_int_id());
3451
            $repoShortcut->addShortCut($category, api_get_user_entity(), $courseEntity, api_get_session_entity());
3452
        } else {
3453
            $repoShortcut->removeShortCut($category);
3454
        }
3455
3456
        return true;
3457
    }
3458
3459
    /**
3460
     * Check if the learnpath category is visible for a user.
3461
     *
3462
     * @return bool
3463
     */
3464
    public static function categoryIsVisibleForStudent(
3465
        CLpCategory $category,
3466
        User $user,
3467
        Course $course,
3468
        SessionEntity $session = null
3469
    ) {
3470
        $isAllowedToEdit = api_is_allowed_to_edit(null, true);
3471
3472
        if ($isAllowedToEdit) {
3473
            return true;
3474
        }
3475
3476
        $categoryVisibility = $category->isVisible($course, $session);
3477
3478
        if (!$categoryVisibility) {
3479
            return false;
3480
        }
3481
3482
        $subscriptionSettings = self::getSubscriptionSettings();
3483
3484
        if (false === $subscriptionSettings['allow_add_users_to_lp_category']) {
3485
            return true;
3486
        }
3487
3488
        $noUserSubscribed = false;
3489
        $noGroupSubscribed = true;
3490
        $users = $category->getUsers();
3491
        if (empty($users) || !$users->count()) {
3492
            $noUserSubscribed = true;
3493
        } elseif ($category->hasUserAdded($user)) {
3494
            return true;
3495
        }
3496
3497
        //$groups = GroupManager::getAllGroupPerUserSubscription($user->getId());
3498
3499
        return $noGroupSubscribed && $noUserSubscribed;
3500
    }
3501
3502
    /**
3503
     * Check if a learnpath category is published as course tool.
3504
     *
3505
     * @param int $courseId
3506
     *
3507
     * @return bool
3508
     */
3509
    public static function categoryIsPublished(CLpCategory $category, $courseId)
3510
    {
3511
        return false;
3512
        $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...
3513
        $em = Database::getManager();
3514
3515
        $tools = $em
3516
            ->createQuery("
3517
                SELECT t FROM ChamiloCourseBundle:CTool t
3518
                WHERE t.course = :course AND
3519
                    t.name = :name AND
3520
                    t.image LIKE 'lp_category.%' AND
3521
                    t.link LIKE :link
3522
            ")
3523
            ->setParameters([
3524
                'course' => $courseId,
3525
                'name' => strip_tags($category->getTitle()),
3526
                'link' => "$link%",
3527
            ])
3528
            ->getResult();
3529
3530
        /** @var CTool $tool */
3531
        $tool = current($tools);
3532
3533
        return $tool ? $tool->getVisibility() : false;
3534
    }
3535
3536
    /**
3537
     * Restart the whole learnpath. Return the URL of the first element.
3538
     * Make sure the results are saved with anoter method. This method should probably be redefined in children classes.
3539
     * To use a similar method  statically, use the create_new_attempt() method.
3540
     *
3541
     * @return bool
3542
     */
3543
    public function restart()
3544
    {
3545
        if ($this->debug > 0) {
3546
            error_log('In learnpath::restart()', 0);
3547
        }
3548
        // TODO
3549
        // Call autosave method to save the current progress.
3550
        //$this->index = 0;
3551
        if (api_is_invitee()) {
3552
            return false;
3553
        }
3554
        $session_id = api_get_session_id();
3555
        $course_id = api_get_course_int_id();
3556
        $lp_view_table = Database::get_course_table(TABLE_LP_VIEW);
3557
        $sql = "INSERT INTO $lp_view_table (c_id, lp_id, user_id, view_count, session_id)
3558
                VALUES ($course_id, ".$this->lp_id.",".$this->get_user_id().",".($this->attempt + 1).", $session_id)";
3559
        if ($this->debug > 2) {
3560
            error_log('Inserting new lp_view for restart: '.$sql, 0);
3561
        }
3562
        Database::query($sql);
3563
        $view_id = Database::insert_id();
3564
3565
        if ($view_id) {
3566
            $this->lp_view_id = $view_id;
3567
            $this->attempt = $this->attempt + 1;
3568
        } else {
3569
            $this->error = 'Could not insert into item_view table...';
3570
3571
            return false;
3572
        }
3573
        $this->autocomplete_parents($this->current);
3574
        foreach ($this->items as $index => $dummy) {
3575
            $this->items[$index]->restart();
3576
            $this->items[$index]->set_lp_view($this->lp_view_id);
3577
        }
3578
        $this->first();
3579
3580
        return true;
3581
    }
3582
3583
    /**
3584
     * Saves the current item.
3585
     *
3586
     * @return bool
3587
     */
3588
    public function save_current()
3589
    {
3590
        $debug = $this->debug;
3591
        // TODO: Do a better check on the index pointing to the right item (it is supposed to be working
3592
        // on $ordered_items[] but not sure it's always safe to use with $items[]).
3593
        if ($debug) {
3594
            error_log('save_current() saving item '.$this->current, 0);
3595
            error_log(''.print_r($this->items, true), 0);
3596
        }
3597
        if (isset($this->items[$this->current]) &&
3598
            is_object($this->items[$this->current])
3599
        ) {
3600
            if ($debug) {
3601
                error_log('Before save last_scorm_session_time: '.$this->items[$this->current]->getLastScormSessionTime());
3602
            }
3603
3604
            $res = $this->items[$this->current]->save(
3605
                false,
3606
                $this->prerequisites_match($this->current)
3607
            );
3608
            $this->autocomplete_parents($this->current);
3609
            $status = $this->items[$this->current]->get_status();
3610
            $this->update_queue[$this->current] = $status;
3611
3612
            if ($debug) {
3613
                error_log('After save last_scorm_session_time: '.$this->items[$this->current]->getLastScormSessionTime());
3614
            }
3615
3616
            return $res;
3617
        }
3618
3619
        return false;
3620
    }
3621
3622
    /**
3623
     * Saves the given item.
3624
     *
3625
     * @param int  $item_id      Optional (will take from $_REQUEST if null)
3626
     * @param bool $from_outside Save from url params (true) or from current attributes (false). Default true
3627
     *
3628
     * @return bool
3629
     */
3630
    public function save_item($item_id = null, $from_outside = true)
3631
    {
3632
        $debug = $this->debug;
3633
        if ($debug) {
3634
            error_log('In learnpath::save_item('.$item_id.','.intval($from_outside).')', 0);
3635
        }
3636
        // TODO: Do a better check on the index pointing to the right item (it is supposed to be working
3637
        // on $ordered_items[] but not sure it's always safe to use with $items[]).
3638
        if (empty($item_id)) {
3639
            $item_id = (int) $_REQUEST['id'];
3640
        }
3641
3642
        if (empty($item_id)) {
3643
            $item_id = $this->get_current_item_id();
3644
        }
3645
        if (isset($this->items[$item_id]) &&
3646
            is_object($this->items[$item_id])
3647
        ) {
3648
            if ($debug) {
3649
                error_log('Object exists');
3650
            }
3651
3652
            // Saving the item.
3653
            $res = $this->items[$item_id]->save(
3654
                $from_outside,
3655
                $this->prerequisites_match($item_id)
3656
            );
3657
3658
            if ($debug) {
3659
                error_log('update_queue before:');
3660
                error_log(print_r($this->update_queue, 1));
3661
            }
3662
            $this->autocomplete_parents($item_id);
3663
3664
            $status = $this->items[$item_id]->get_status();
3665
            $this->update_queue[$item_id] = $status;
3666
3667
            if ($debug) {
3668
                error_log('get_status(): '.$status);
3669
                error_log('update_queue after:');
3670
                error_log(print_r($this->update_queue, 1));
3671
            }
3672
3673
            return $res;
3674
        }
3675
3676
        return false;
3677
    }
3678
3679
    /**
3680
     * Saves the last item seen's ID only in case.
3681
     */
3682
    public function save_last()
3683
    {
3684
        $course_id = api_get_course_int_id();
3685
        $debug = $this->debug;
3686
        if ($debug) {
3687
            error_log('In learnpath::save_last()', 0);
3688
        }
3689
        $session_condition = api_get_session_condition(
3690
            api_get_session_id(),
3691
            true,
3692
            false
3693
        );
3694
        $table = Database::get_course_table(TABLE_LP_VIEW);
3695
3696
        $userId = $this->get_user_id();
3697
        if (empty($userId)) {
3698
            $userId = api_get_user_id();
3699
            if ($debug) {
3700
                error_log('$this->get_user_id() was empty, used api_get_user_id() instead in '.__FILE__.' line '.__LINE__);
3701
            }
3702
        }
3703
        if (isset($this->current) && !api_is_invitee()) {
3704
            if ($debug) {
3705
                error_log('Saving current item ('.$this->current.') for later review', 0);
3706
            }
3707
            $sql = "UPDATE $table SET
3708
                        last_item = ".$this->get_current_item_id()."
3709
                    WHERE
3710
                        c_id = $course_id AND
3711
                        lp_id = ".$this->get_id()." AND
3712
                        user_id = ".$userId." ".$session_condition;
3713
3714
            if ($debug) {
3715
                error_log('Saving last item seen : '.$sql, 0);
3716
            }
3717
            Database::query($sql);
3718
        }
3719
3720
        if (!api_is_invitee()) {
3721
            // Save progress.
3722
            [$progress] = $this->get_progress_bar_text('%');
3723
            $scoreAsProgressSetting = ('true' === api_get_setting('lp.lp_score_as_progress_enable'));
3724
            $scoreAsProgress = $this->getUseScoreAsProgress();
3725
            if ($scoreAsProgress && $scoreAsProgressSetting && (null === $score || empty($score) || -1 == $score)) {
3726
                if ($debug) {
3727
                    error_log("Return false: Dont save score: $score");
3728
                    error_log("progress: $progress");
3729
                }
3730
3731
                return false;
3732
            }
3733
3734
            if ($scoreAsProgress && $scoreAsProgressSetting) {
3735
                $storedProgress = self::getProgress(
3736
                    $this->get_id(),
3737
                    $userId,
3738
                    $course_id,
3739
                    $this->get_lp_session_id()
3740
                );
3741
3742
                // Check if the stored progress is higher than the new value
3743
                if ($storedProgress >= $progress) {
3744
                    if ($debug) {
3745
                        error_log("Return false: New progress value is lower than stored value - Current value: $storedProgress - New value: $progress [lp ".$this->get_id()." - user ".$userId."]");
3746
                    }
3747
3748
                    return false;
3749
                }
3750
            }
3751
            if ($progress >= 0 && $progress <= 100) {
3752
                $progress = (int) $progress;
3753
                $sql = "UPDATE $table SET
3754
                            progress = $progress
3755
                        WHERE
3756
                            c_id = $course_id AND
3757
                            lp_id = ".$this->get_id()." AND
3758
                            user_id = ".$userId." ".$session_condition;
3759
                // Ignore errors as some tables might not have the progress field just yet.
3760
                Database::query($sql);
3761
                $this->progress_db = $progress;
3762
3763
                if (100 == $progress) {
3764
                    Container::getEventDispatcher()->dispatch(
3765
                        new LearningPathEndedEvent(['lp_view_id' => $this->lp_view_id]),
3766
                        Events::LP_ENDED
3767
                    );
3768
                }
3769
            }
3770
        }
3771
    }
3772
3773
    /**
3774
     * Sets the current item ID (checks if valid and authorized first).
3775
     *
3776
     * @param int $item_id New item ID. If not given or not authorized, defaults to current
3777
     */
3778
    public function set_current_item($item_id = null)
3779
    {
3780
        $debug = $this->debug;
3781
        if ($debug) {
3782
            error_log('In learnpath::set_current_item('.$item_id.')', 0);
3783
        }
3784
        if (empty($item_id)) {
3785
            if ($debug) {
3786
                error_log('No new current item given, ignore...', 0);
3787
            }
3788
            // Do nothing.
3789
        } else {
3790
            if ($debug) {
3791
                error_log('New current item given is '.$item_id.'...', 0);
3792
            }
3793
            if (is_numeric($item_id)) {
3794
                $item_id = (int) $item_id;
3795
                // TODO: Check in database here.
3796
                $this->last = $this->current;
3797
                $this->current = $item_id;
3798
                // TODO: Update $this->index as well.
3799
                foreach ($this->ordered_items as $index => $item) {
3800
                    if ($item == $this->current) {
3801
                        $this->index = $index;
3802
                        break;
3803
                    }
3804
                }
3805
                if ($debug) {
3806
                    error_log('set_current_item('.$item_id.') done. Index is now : '.$this->index);
3807
                }
3808
            } else {
3809
                if ($debug) {
3810
                    error_log('set_current_item('.$item_id.') failed. Not a numeric value: ');
3811
                }
3812
            }
3813
        }
3814
    }
3815
3816
    /**
3817
     * Set index specified prefix terms for all items in this path.
3818
     *
3819
     * @param string $terms_string Comma-separated list of terms
3820
     * @param string $prefix       Xapian term prefix
3821
     *
3822
     * @return bool False on error, true otherwise
3823
     */
3824
    public function set_terms_by_prefix($terms_string, $prefix)
3825
    {
3826
        $course_id = api_get_course_int_id();
3827
        if ('true' !== api_get_setting('search_enabled')) {
3828
            return false;
3829
        }
3830
3831
        if (!extension_loaded('xapian')) {
3832
            return false;
3833
        }
3834
3835
        $terms_string = trim($terms_string);
3836
        $terms = explode(',', $terms_string);
3837
        array_walk($terms, 'trim_value');
3838
        $stored_terms = $this->get_common_index_terms_by_prefix($prefix);
3839
3840
        // Don't do anything if no change, verify only at DB, not the search engine.
3841
        if ((0 == count(array_diff($terms, $stored_terms))) && (0 == count(array_diff($stored_terms, $terms)))) {
3842
            return false;
3843
        }
3844
3845
        require_once 'xapian.php'; // TODO: Try catch every xapian use or make wrappers on API.
3846
        require_once api_get_path(LIBRARY_PATH).'search/xapian/XapianQuery.php';
3847
3848
        $items_table = Database::get_course_table(TABLE_LP_ITEM);
3849
        // TODO: Make query secure agains XSS : use member attr instead of post var.
3850
        $lp_id = (int) $_POST['lp_id'];
3851
        $sql = "SELECT * FROM $items_table WHERE c_id = $course_id AND lp_id = $lp_id";
3852
        $result = Database::query($sql);
3853
        $di = new ChamiloIndexer();
3854
3855
        while ($lp_item = Database::fetch_array($result)) {
3856
            // Get search_did.
3857
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
3858
            $sql = 'SELECT * FROM %s
3859
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
3860
                    LIMIT 1';
3861
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp_id, $lp_item['id']);
3862
3863
            //echo $sql; echo '<br>';
3864
            $res = Database::query($sql);
3865
            if (Database::num_rows($res) > 0) {
3866
                $se_ref = Database::fetch_array($res);
3867
                // Compare terms.
3868
                $doc = $di->get_document($se_ref['search_did']);
3869
                $xapian_terms = xapian_get_doc_terms($doc, $prefix);
3870
                $xterms = [];
3871
                foreach ($xapian_terms as $xapian_term) {
3872
                    $xterms[] = substr($xapian_term['name'], 1);
3873
                }
3874
3875
                $dterms = $terms;
3876
                $missing_terms = array_diff($dterms, $xterms);
3877
                $deprecated_terms = array_diff($xterms, $dterms);
3878
3879
                // Save it to search engine.
3880
                foreach ($missing_terms as $term) {
3881
                    $doc->add_term($prefix.$term, 1);
3882
                }
3883
                foreach ($deprecated_terms as $term) {
3884
                    $doc->remove_term($prefix.$term);
3885
                }
3886
                $di->getDb()->replace_document((int) $se_ref['search_did'], $doc);
3887
                $di->getDb()->flush();
3888
            }
3889
        }
3890
3891
        return true;
3892
    }
3893
3894
    /**
3895
     * Sets the previous item ID to a given ID. Generally, this should be set to the previous 'current' item.
3896
     *
3897
     * @param int $id DB ID of the item
3898
     */
3899
    public function set_previous_item($id)
3900
    {
3901
        if ($this->debug > 0) {
3902
            error_log('In learnpath::set_previous_item()', 0);
3903
        }
3904
        $this->last = $id;
3905
    }
3906
3907
    /**
3908
     * Sets and saves the expired_on date.
3909
     *
3910
     * @return bool Returns true if author's name is not empty
3911
     */
3912
    public function set_modified_on()
3913
    {
3914
        $this->modified_on = api_get_utc_datetime();
3915
        $table = Database::get_course_table(TABLE_LP_MAIN);
3916
        $lp_id = $this->get_id();
3917
        $sql = "UPDATE $table SET modified_on = '".$this->modified_on."'
3918
                WHERE iid = $lp_id";
3919
        Database::query($sql);
3920
3921
        return true;
3922
    }
3923
3924
    /**
3925
     * Sets the object's error message.
3926
     *
3927
     * @param string $error Error message. If empty, reinits the error string
3928
     */
3929
    public function set_error_msg($error = '')
3930
    {
3931
        if ($this->debug > 0) {
3932
            error_log('In learnpath::set_error_msg()', 0);
3933
        }
3934
        if (empty($error)) {
3935
            $this->error = '';
3936
        } else {
3937
            $this->error .= $error;
3938
        }
3939
    }
3940
3941
    /**
3942
     * Launches the current item if not 'sco'
3943
     * (starts timer and make sure there is a record ready in the DB).
3944
     *
3945
     * @param bool $allow_new_attempt Whether to allow a new attempt or not
3946
     *
3947
     * @return bool
3948
     */
3949
    public function start_current_item($allow_new_attempt = false)
3950
    {
3951
        $debug = $this->debug;
3952
        if ($debug) {
3953
            error_log('In learnpath::start_current_item()');
3954
            error_log('current: '.$this->current);
3955
        }
3956
        if (0 != $this->current && isset($this->items[$this->current]) &&
3957
            is_object($this->items[$this->current])
3958
        ) {
3959
            $type = $this->get_type();
3960
            $item_type = $this->items[$this->current]->get_type();
3961
            if ($debug) {
3962
                error_log('item type: '.$item_type);
3963
                error_log('lp type: '.$type);
3964
            }
3965
            if ((2 == $type && 'sco' !== $item_type) ||
3966
                (3 == $type && 'au' !== $item_type) ||
3967
                (1 == $type && TOOL_QUIZ != $item_type && TOOL_HOTPOTATOES != $item_type)
3968
            ) {
3969
                $this->items[$this->current]->open($allow_new_attempt);
3970
                $this->autocomplete_parents($this->current);
3971
                $prereq_check = $this->prerequisites_match($this->current);
3972
                if ($debug) {
3973
                    error_log('start_current_item will save item with prereq: '.$prereq_check);
3974
                }
3975
                $this->items[$this->current]->save(false, $prereq_check);
3976
            }
3977
            // If sco, then it is supposed to have been updated by some other call.
3978
            if ('sco' === $item_type) {
3979
                $this->items[$this->current]->restart();
3980
            }
3981
        }
3982
        if ($debug) {
3983
            error_log('lp_view_session_id');
3984
            error_log($this->lp_view_session_id);
3985
            error_log('api session id');
3986
            error_log(api_get_session_id());
3987
            error_log('End of learnpath::start_current_item()');
3988
        }
3989
3990
        return true;
3991
    }
3992
3993
    /**
3994
     * Stops the processing and counters for the old item (as held in $this->last).
3995
     *
3996
     * @return bool True/False
3997
     */
3998
    public function stop_previous_item()
3999
    {
4000
        $debug = $this->debug;
4001
        if ($debug) {
4002
            error_log('In learnpath::stop_previous_item()', 0);
4003
        }
4004
4005
        if (0 != $this->last && $this->last != $this->current &&
4006
            isset($this->items[$this->last]) && is_object($this->items[$this->last])
4007
        ) {
4008
            if ($debug) {
4009
                error_log('In learnpath::stop_previous_item() - '.$this->last.' is object');
4010
            }
4011
            switch ($this->get_type()) {
4012
                case '3':
4013
                    if ('au' != $this->items[$this->last]->get_type()) {
4014
                        if ($debug) {
4015
                            error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 3 is <> au');
4016
                        }
4017
                        $this->items[$this->last]->close();
4018
                    } else {
4019
                        if ($debug) {
4020
                            error_log('In learnpath::stop_previous_item() - Item is an AU, saving is managed by AICC signals');
4021
                        }
4022
                    }
4023
                    break;
4024
                case '2':
4025
                    if ('sco' != $this->items[$this->last]->get_type()) {
4026
                        if ($debug) {
4027
                            error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 2 is <> sco');
4028
                        }
4029
                        $this->items[$this->last]->close();
4030
                    } else {
4031
                        if ($debug) {
4032
                            error_log('In learnpath::stop_previous_item() - Item is a SCO, saving is managed by SCO signals');
4033
                        }
4034
                    }
4035
                    break;
4036
                case '1':
4037
                default:
4038
                    if ($debug) {
4039
                        error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 1 is asset');
4040
                    }
4041
                    $this->items[$this->last]->close();
4042
                    break;
4043
            }
4044
        } else {
4045
            if ($debug) {
4046
                error_log('In learnpath::stop_previous_item() - No previous element found, ignoring...');
4047
            }
4048
4049
            return false;
4050
        }
4051
4052
        return true;
4053
    }
4054
4055
    /**
4056
     * Updates the default view mode from fullscreen to embedded and inversely.
4057
     *
4058
     * @return string The current default view mode ('fullscreen' or 'embedded')
4059
     */
4060
    public function update_default_view_mode()
4061
    {
4062
        $table = Database::get_course_table(TABLE_LP_MAIN);
4063
        $sql = "SELECT * FROM $table
4064
                WHERE iid = ".$this->get_id();
4065
        $res = Database::query($sql);
4066
        if (Database::num_rows($res) > 0) {
4067
            $row = Database::fetch_array($res);
4068
            $default_view_mode = $row['default_view_mod'];
4069
            $view_mode = $default_view_mode;
4070
            switch ($default_view_mode) {
4071
                case 'fullscreen': // default with popup
4072
                    $view_mode = 'embedded';
4073
                    break;
4074
                case 'embedded': // default view with left menu
4075
                    $view_mode = 'embedframe';
4076
                    break;
4077
                case 'embedframe': //folded menu
4078
                    $view_mode = 'impress';
4079
                    break;
4080
                case 'impress':
4081
                    $view_mode = 'fullscreen';
4082
                    break;
4083
            }
4084
            $sql = "UPDATE $table SET default_view_mod = '$view_mode'
4085
                    WHERE iid = ".$this->get_id();
4086
            Database::query($sql);
4087
            $this->mode = $view_mode;
4088
4089
            return $view_mode;
4090
        }
4091
4092
        return -1;
4093
    }
4094
4095
    /**
4096
     * Updates the default behaviour about auto-commiting SCORM updates.
4097
     *
4098
     * @return bool True if auto-commit has been set to 'on', false otherwise
4099
     */
4100
    public function update_default_scorm_commit()
4101
    {
4102
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
4103
        $sql = "SELECT * FROM $lp_table
4104
                WHERE iid = ".$this->get_id();
4105
        $res = Database::query($sql);
4106
        if (Database::num_rows($res) > 0) {
4107
            $row = Database::fetch_array($res);
4108
            $force = $row['force_commit'];
4109
            if (1 == $force) {
4110
                $force = 0;
4111
                $force_return = false;
4112
            } elseif (0 == $force) {
4113
                $force = 1;
4114
                $force_return = true;
4115
            }
4116
            $sql = "UPDATE $lp_table SET force_commit = $force
4117
                    WHERE iid = ".$this->get_id();
4118
            Database::query($sql);
4119
            $this->force_commit = $force_return;
4120
4121
            return $force_return;
4122
        }
4123
4124
        return -1;
4125
    }
4126
4127
    /**
4128
     * Updates the order of learning paths (goes through all of them by order and fills the gaps).
4129
     *
4130
     * @return bool True on success, false on failure
4131
     */
4132
    public function update_display_order()
4133
    {
4134
        return;
4135
        $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...
4136
        $table = Database::get_course_table(TABLE_LP_MAIN);
4137
        $sql = "SELECT * FROM $table
4138
                WHERE c_id = $course_id
4139
                ORDER BY display_order";
4140
        $res = Database::query($sql);
4141
        if (false === $res) {
4142
            return false;
4143
        }
4144
4145
        $num = Database::num_rows($res);
4146
        // First check the order is correct, globally (might be wrong because
4147
        // of versions < 1.8.4).
4148
        if ($num > 0) {
4149
            $i = 1;
4150
            while ($row = Database::fetch_array($res)) {
4151
                if ($row['display_order'] != $i) {
4152
                    // If we find a gap in the order, we need to fix it.
4153
                    $sql = "UPDATE $table SET display_order = $i
4154
                            WHERE iid = ".$row['iid'];
4155
                    Database::query($sql);
4156
                }
4157
                $i++;
4158
            }
4159
        }
4160
4161
        return true;
4162
    }
4163
4164
    /**
4165
     * Updates the "prevent_reinit" value that enables control on reinitialising items on second view.
4166
     *
4167
     * @return bool True if prevent_reinit has been set to 'on', false otherwise (or 1 or 0 in this case)
4168
     */
4169
    public function update_reinit()
4170
    {
4171
        $force = $this->prevent_reinit;
4172
        if (1 == $force) {
4173
            $force = 0;
4174
        } elseif (0 == $force) {
4175
            $force = 1;
4176
        }
4177
4178
        $table = Database::get_course_table(TABLE_LP_MAIN);
4179
        $sql = "UPDATE $table SET prevent_reinit = $force
4180
                WHERE iid = ".$this->get_id();
4181
        Database::query($sql);
4182
        $this->prevent_reinit = $force;
4183
4184
        return $force;
4185
    }
4186
4187
    /**
4188
     * Determine the attempt_mode thanks to prevent_reinit and seriousgame_mode db flag.
4189
     *
4190
     * @return string 'single', 'multi' or 'seriousgame'
4191
     *
4192
     * @author ndiechburg <[email protected]>
4193
     */
4194
    public function get_attempt_mode()
4195
    {
4196
        //Set default value for seriousgame_mode
4197
        if (!isset($this->seriousgame_mode)) {
4198
            $this->seriousgame_mode = 0;
4199
        }
4200
        // Set default value for prevent_reinit
4201
        if (!isset($this->prevent_reinit)) {
4202
            $this->prevent_reinit = 1;
4203
        }
4204
        if (1 == $this->seriousgame_mode && 1 == $this->prevent_reinit) {
4205
            return 'seriousgame';
4206
        }
4207
        if (0 == $this->seriousgame_mode && 1 == $this->prevent_reinit) {
4208
            return 'single';
4209
        }
4210
        if (0 == $this->seriousgame_mode && 0 == $this->prevent_reinit) {
4211
            return 'multiple';
4212
        }
4213
4214
        return 'single';
4215
    }
4216
4217
    /**
4218
     * Register the attempt mode into db thanks to flags prevent_reinit and seriousgame_mode flags.
4219
     *
4220
     * @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...
4221
     *
4222
     * @return bool
4223
     *
4224
     * @author ndiechburg <[email protected]>
4225
     */
4226
    public function set_attempt_mode($mode)
4227
    {
4228
        switch ($mode) {
4229
            case 'seriousgame':
4230
                $sg_mode = 1;
4231
                $prevent_reinit = 1;
4232
                break;
4233
            case 'single':
4234
                $sg_mode = 0;
4235
                $prevent_reinit = 1;
4236
                break;
4237
            case 'multiple':
4238
                $sg_mode = 0;
4239
                $prevent_reinit = 0;
4240
                break;
4241
            default:
4242
                $sg_mode = 0;
4243
                $prevent_reinit = 0;
4244
                break;
4245
        }
4246
        $this->prevent_reinit = $prevent_reinit;
4247
        $this->seriousgame_mode = $sg_mode;
4248
        $table = Database::get_course_table(TABLE_LP_MAIN);
4249
        $sql = "UPDATE $table SET
4250
                prevent_reinit = $prevent_reinit ,
4251
                seriousgame_mode = $sg_mode
4252
                WHERE iid = ".$this->get_id();
4253
        $res = Database::query($sql);
4254
        if ($res) {
4255
            return true;
4256
        } else {
4257
            return false;
4258
        }
4259
    }
4260
4261
    /**
4262
     * Switch between multiple attempt, single attempt or serious_game mode (only for scorm).
4263
     *
4264
     * @author ndiechburg <[email protected]>
4265
     */
4266
    public function switch_attempt_mode()
4267
    {
4268
        $mode = $this->get_attempt_mode();
4269
        switch ($mode) {
4270
            case 'single':
4271
                $next_mode = 'multiple';
4272
                break;
4273
            case 'multiple':
4274
                $next_mode = 'seriousgame';
4275
                break;
4276
            case 'seriousgame':
4277
            default:
4278
                $next_mode = 'single';
4279
                break;
4280
        }
4281
        $this->set_attempt_mode($next_mode);
4282
    }
4283
4284
    /**
4285
     * Switch the lp in ktm mode. This is a special scorm mode with unique attempt
4286
     * but possibility to do again a completed item.
4287
     *
4288
     * @return bool true if seriousgame_mode has been set to 1, false otherwise
4289
     *
4290
     * @author ndiechburg <[email protected]>
4291
     */
4292
    public function set_seriousgame_mode()
4293
    {
4294
        $table = Database::get_course_table(TABLE_LP_MAIN);
4295
        $force = $this->seriousgame_mode;
4296
        if (1 == $force) {
4297
            $force = 0;
4298
        } elseif (0 == $force) {
4299
            $force = 1;
4300
        }
4301
        $sql = "UPDATE $table SET seriousgame_mode = $force
4302
                WHERE iid = ".$this->get_id();
4303
        Database::query($sql);
4304
        $this->seriousgame_mode = $force;
4305
4306
        return $force;
4307
    }
4308
4309
    /**
4310
     * Updates the "scorm_debug" value that shows or hide the debug window.
4311
     *
4312
     * @return bool True if scorm_debug has been set to 'on', false otherwise (or 1 or 0 in this case)
4313
     */
4314
    public function update_scorm_debug()
4315
    {
4316
        $table = Database::get_course_table(TABLE_LP_MAIN);
4317
        $force = $this->scorm_debug;
4318
        if (1 == $force) {
4319
            $force = 0;
4320
        } elseif (0 == $force) {
4321
            $force = 1;
4322
        }
4323
        $sql = "UPDATE $table SET debug = $force
4324
                WHERE iid = ".$this->get_id();
4325
        Database::query($sql);
4326
        $this->scorm_debug = $force;
4327
4328
        return $force;
4329
    }
4330
4331
    /**
4332
     * Function that creates a html list of learning path items so that we can add audio files to them.
4333
     *
4334
     * @author Kevin Van Den Haute
4335
     *
4336
     * @return string
4337
     */
4338
    public function overview()
4339
    {
4340
        $return = '';
4341
        $update_audio = $_GET['updateaudio'] ?? null;
4342
4343
        // we need to start a form when we want to update all the mp3 files
4344
        if ('true' == $update_audio) {
4345
            $return .= '<form action="'.api_get_self().'?'.api_get_cidreq().'&updateaudio='.Security::remove_XSS(
4346
                    $_GET['updateaudio']
4347
                ).'&action='.Security::remove_XSS(
4348
                    $_GET['action']
4349
                ).'&lp_id='.$_SESSION['oLP']->lp_id.'" method="post" enctype="multipart/form-data" name="updatemp3" id="updatemp3">';
4350
        }
4351
        $return .= '<div id="message"></div>';
4352
        if (0 == count($this->items)) {
4353
            $return .= Display::return_message(
4354
                get_lang(
4355
                    'You should add some items to your learning path, otherwise you won\'t be able to attach audio files to them'
4356
                ),
4357
                'normal'
4358
            );
4359
        } else {
4360
            $return_audio = '<table class="table table-hover table-striped data_table">';
4361
            $return_audio .= '<tr>';
4362
            $return_audio .= '<th width="40%">'.get_lang('Title').'</th>';
4363
            $return_audio .= '<th>'.get_lang('Audio').'</th>';
4364
            $return_audio .= '</tr>';
4365
4366
            if ('true' != $update_audio) {
4367
                /*$return .= '<div class="col-md-12">';
4368
                $return .= self::return_new_tree($update_audio);
4369
                $return .= '</div>';*/
4370
                $return .= Display::div(
4371
                    Display::url(get_lang('Save'), '#', ['id' => 'listSubmit', 'class' => 'btn btn--primary']),
4372
                    ['style' => 'float:left; margin-top:15px;width:100%']
4373
                );
4374
            } else {
4375
                //$return_audio .= self::return_new_tree($update_audio);
4376
                $return .= $return_audio.'</table>';
4377
            }
4378
4379
            // We need to close the form when we are updating the mp3 files.
4380
            if ('true' == $update_audio) {
4381
                $return .= '<div class="footer-audio">';
4382
                $return .= Display::button(
4383
                    'save_audio',
4384
                    '<em class="fa fa-file-audio-o"></em> '.get_lang('Save audio and organization'),
4385
                    ['class' => 'btn btn--primary', 'type' => 'submit']
4386
                );
4387
                $return .= '</div>';
4388
            }
4389
        }
4390
4391
        // We need to close the form when we are updating the mp3 files.
4392
        if ('true' === $update_audio && isset($this->arrMenu) && 0 != count($this->arrMenu)) {
4393
            $return .= '</form>';
4394
        }
4395
4396
        return $return;
4397
    }
4398
4399
    public function showBuildSideBar($updateAudio = false, $dropElementHere = false, $type = null)
4400
    {
4401
        $sureToDelete = trim(get_lang('Are you sure to delete'));
4402
        $ajax_url = api_get_path(WEB_AJAX_PATH).'lp.ajax.php?lp_id='.$this->get_id().'&'.api_get_cidreq();
4403
4404
        $content = '
4405
    <script>
4406
    $(function() {
4407
        function enforceFinalAtEndDOM() {
4408
            var $root = $("#lp_item_list");
4409
            $root.find("li.final-item").each(function() {
4410
                $root.append(this);
4411
            });
4412
        }
4413
4414
        function refreshTree() {
4415
            var params = "&a=get_lp_item_tree";
4416
            $.get(
4417
                "'.$ajax_url.'",
4418
                params,
4419
                function(result) {
4420
                    $("#lp_item_list").html(result);
4421
                    enforceFinalAtEndDOM();
4422
                    nestedSortable();
4423
                }
4424
            );
4425
        }
4426
4427
        const nestedQuery = ".nested-sortable";
4428
        const identifier  = "id";
4429
        const root        = document.getElementById("lp_item_list");
4430
4431
        function serialize(sortable) {
4432
            var out    = [];
4433
            var finals = [];
4434
            var children = [].slice.call(sortable.children);
4435
4436
            for (var i = 0; i < children.length; i++) {
4437
                var li = children[i];
4438
                if (!li || !li.dataset) continue;
4439
4440
                var id = li.dataset[identifier];
4441
                if (!id) continue;
4442
4443
                var isFinal =
4444
                    li.classList.contains("final-item") ||
4445
                    li.dataset.type === "final_item" ||
4446
                    li.dataset.fixed === "final";
4447
4448
                var parentLi = $(li).closest("ul.nested-sortable").closest("li")[0];
4449
                var parentId = (parentLi && parentLi.dataset) ? parentLi.dataset[identifier] : null;
4450
                var rec = { id: id, parent_id: isFinal ? null : parentId };
4451
                var nested = li.querySelector(nestedQuery);
4452
                if (nested && !isFinal) {
4453
                    out = out.concat( serialize(nested) );
4454
                }
4455
4456
                (isFinal ? finals : out).push(rec);
4457
            }
4458
4459
            return out.concat(finals);
4460
        }
4461
4462
        function nestedSortable() {
4463
            let lists = document.getElementsByClassName("nested-sortable");
4464
            Array.prototype.forEach.call(lists, function(ul) {
4465
                Sortable.create(ul, {
4466
                    group: "nested",
4467
                    put: ["nested-sortable", ".lp_resource", ".nested-source"],
4468
                    animation: 150,
4469
                    swapThreshold: 0.65,
4470
                    dataIdAttr: "data-id",
4471
                    filter: ".disable_drag",
4472
                    onMove: function (evt) {
4473
                        var t = evt.dragged && evt.dragged.dataset ? evt.dragged.dataset.type : null;
4474
                        if (t === "final_item") return false;
4475
                    },
4476
                    onEnd: function(evt) {
4477
                        if (evt.item && (evt.item.classList.contains("final-item") || evt.item.dataset.type === "final_item")) {
4478
                            enforceFinalAtEndDOM();
4479
                            return;
4480
                        }
4481
                        enforceFinalAtEndDOM();
4482
4483
                        let list  = serialize(root);
4484
                        let order = "&a=update_lp_item_order&new_order=" + JSON.stringify(list);
4485
4486
                        $.get(
4487
                            "'.$ajax_url.'",
4488
                            order,
4489
                            function(reponse) {
4490
                                $("#message").html(reponse);
4491
                                refreshTree();
4492
                            }
4493
                        );
4494
                    },
4495
                });
4496
            });
4497
        }
4498
4499
        nestedSortable();
4500
4501
        let resources = document.getElementsByClassName("lp_resource");
4502
        Array.prototype.forEach.call(resources, function(resource) {
4503
            Sortable.create(resource, {
4504
                group: {
4505
                    name: "nested",
4506
                    pull: true,
4507
                    put: false
4508
                },
4509
                sort: false,
4510
                filter: ".disable_drag",
4511
                animation: 150,
4512
                fallbackOnBody: true,
4513
                swapThreshold: 0.65,
4514
                dataIdAttr: "data-id",
4515
                onRemove: function(evt) {
4516
                    var itemEl   = evt.item;
4517
                    var newIndex = evt.newIndex;
4518
                    var id       = $(itemEl).attr("id");
4519
                    var parentId = $(itemEl).parent().parent().attr("id");
4520
                    var type     = $(itemEl).find(".link_with_id").attr("data_type");
4521
                    var title    = $(itemEl).find(".link_with_id").text();
4522
4523
                    let previousId = 0;
4524
                    if (0 !== newIndex) {
4525
                        previousId = $(itemEl).prev().attr("id");
4526
                    }
4527
4528
                    var params = {
4529
                        "a": "add_lp_item",
4530
                        "id": id,
4531
                        "parent_id": parentId,
4532
                        "previous_id": previousId,
4533
                        "type": type,
4534
                        "title" : title
4535
                    };
4536
4537
                    $.ajax({
4538
                        type: "GET",
4539
                        url: "'.$ajax_url.'",
4540
                        data: params,
4541
                        success: function(itemId) {
4542
                            $(itemEl).attr("id", itemId);
4543
                            $(itemEl).attr("data-id", itemId);
4544
4545
                            enforceFinalAtEndDOM();
4546
4547
                            let list = serialize(root);
4548
                            let listInString = JSON.stringify(list) || "[]";
4549
                            let order = "&a=update_lp_item_order&new_order=" + listInString;
4550
4551
                            $.get(
4552
                                "'.$ajax_url.'",
4553
                                order,
4554
                                function(reponse) {
4555
                                    $("#message").html(reponse);
4556
                                    refreshTree();
4557
                                }
4558
                            );
4559
                        }
4560
                    });
4561
                }
4562
            });
4563
        });
4564
    });
4565
    </script>';
4566
4567
        $content .= "
4568
    <script>
4569
        function confirmation(name) {
4570
            return confirm('$sureToDelete ' + name);
4571
        }
4572
        function refreshTree() {
4573
            var params = '&a=get_lp_item_tree';
4574
            $.get(
4575
                '".$ajax_url."',
4576
                params,
4577
                function(result) {
4578
                    $('#lp_item_list').html(result);
4579
                }
4580
            );
4581
        }
4582
4583
        $(function () {
4584
            expandColumnToggle('#hide_bar_template', { selector: '#lp_sidebar' }, { selector: '#doc_form' });
4585
4586
            $('.lp-btn-associate-forum').on('click', function (e) {
4587
                var ok = confirm('".get_lang('This action will associate a forum thread to this learning path item. Do you want to proceed?')."');
4588
                if (!ok) e.preventDefault();
4589
            });
4590
4591
            $('.lp-btn-dissociate-forum').on('click', function (e) {
4592
                var ok = confirm('".get_lang('This action will dissociate the forum thread of this learning path item. Do you want to proceed?')."');
4593
                if (!ok) e.preventDefault();
4594
            });
4595
4596
            $('#frmModel').hide();
4597
        });
4598
4599
        function deleteItem(event) {
4600
            var id = $(event).attr('data-id');
4601
            var title = $(event).attr('data-title');
4602
            var params = '&a=delete_item&id=' + id;
4603
            if (confirmation(title)) {
4604
                $.get(
4605
                    '".$ajax_url."',
4606
                    params,
4607
                    function(result) {
4608
                        refreshTree();
4609
                    }
4610
                );
4611
            }
4612
        }
4613
    </script>";
4614
4615
        $content .= $this->return_new_tree($updateAudio, $dropElementHere);
4616
        $documentId = isset($_GET['path_item']) ? (int) $_GET['path_item'] : 0;
4617
4618
        $repo = Container::getDocumentRepository();
4619
        $document = $repo->find($documentId);
4620
        if ($document) {
4621
            // Show the template list
4622
            $content .= '<div id="frmModel" class="scrollbar-inner lp-add-item"></div>';
4623
        }
4624
4625
        // Show the template list.
4626
        if (('document' === $type || 'step' === $type) && !isset($_GET['file'])) {
4627
            // Show the template list.
4628
            $content .= '<div id="frmModel" class="scrollbar-inner lp-add-item"></div>';
4629
        }
4630
4631
        return $content;
4632
    }
4633
4634
    /**
4635
     * @param bool  $updateAudio
4636
     * @param bool   $dropElement
4637
     *
4638
     * @return string
4639
     */
4640
    public function return_new_tree($updateAudio = false, $dropElement = false)
4641
    {
4642
        $list = $this->getBuildTree(false, $dropElement);
4643
        $return = Display::panelCollapse(
4644
            $this->name,
4645
            $list,
4646
            'scorm-list',
4647
            null,
4648
            'scorm-list-accordion',
4649
            'scorm-list-collapse'
4650
        );
4651
4652
        if ($updateAudio) {
4653
            //$return = $result['return_audio'];
4654
        }
4655
4656
        return $return;
4657
    }
4658
4659
    public function getBuildTree($noWrapper = false, $dropElement = false): string
4660
    {
4661
        $mainUrl = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?'.api_get_cidreq();
4662
        $upIcon = Display::getMdiIcon('arrow-up-bold', 'ch-tool-icon', '', 16, get_lang('Up'));
4663
        $disableUpIcon = Display::getMdiIcon('arrow-up-bold', 'ch-tool-icon-disabled', '', 16, get_lang('Up'));
4664
        $downIcon = Display::getMdiIcon('arrow-down-bold', 'ch-tool-icon', '', 16, get_lang('Down'));
4665
        $previewImage = Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', '', 16, get_lang('Preview'));
4666
4667
        $lpItemRepo = Container::getLpItemRepository();
4668
        $itemRoot = $lpItemRepo->getRootItem($this->get_id());
4669
4670
        $options = [
4671
            'decorate' => true,
4672
            'rootOpen' => function($tree) use ($noWrapper) {
4673
                if ($tree[0]['lvl'] === 1) {
4674
                    if ($noWrapper) {
4675
                        return '';
4676
                    }
4677
                    return '<ul id="lp_item_list" class="list-group nested-sortable">';
4678
                }
4679
4680
                return '<ul class="list-group nested-sortable">';
4681
            },
4682
            'rootClose' => function($tree) use ($noWrapper, $dropElement)  {
4683
                if ($tree[0]['lvl'] === 1) {
4684
                    if ($dropElement) {
4685
                        //return Display::return_message(get_lang('Drag and drop an element here'));
4686
                        //return $this->getDropElementHtml();
4687
                    }
4688
                    if ($noWrapper) {
4689
                        return '';
4690
                    }
4691
                }
4692
4693
                return '</ul>';
4694
            },
4695
            'childOpen' => function($child) {
4696
                $id   = $child['iid'];
4697
                $type = $child['itemType'] ?? ($child['item_type'] ?? '');
4698
                $isFinal = (TOOL_LP_FINAL_ITEM === $type);
4699
                $extraClass = $isFinal ? ' final-item disable_drag' : '';
4700
                $extraAttr  = $isFinal ? ' data-fixed="final"' : '';
4701
4702
                return '<li
4703
                    id="'.$id.'"
4704
                    data-id="'.$id.'"
4705
                    data-type="'.$type.'"
4706
                    '.$extraAttr.'
4707
                    class="flex flex-col list-group-item nested-'.$child['lvl'].$extraClass.'">';
4708
            },
4709
            'childClose' => '',
4710
            'nodeDecorator' => function ($node) use ($mainUrl, $previewImage, $upIcon, $downIcon) {
4711
                $fullTitle = $node['title'];
4712
                //$title = cut($fullTitle, self::MAX_LP_ITEM_TITLE_LENGTH);
4713
                $title = $fullTitle;
4714
                $itemId = $node['iid'];
4715
                $type = $node['itemType'];
4716
                $lpId = $this->get_id();
4717
4718
                $moveIcon = '';
4719
                if (TOOL_LP_FINAL_ITEM !== $type) {
4720
                    $moveIcon .= '<a class="moved" href="#">';
4721
                    $moveIcon .= Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
4722
                    $moveIcon .= '</a>';
4723
                }
4724
4725
                $iconName = str_replace(' ', '', $type);
4726
                $icon = '';
4727
                switch ($iconName) {
4728
                    case 'category':
4729
                    case 'chapter':
4730
                    case 'folder':
4731
                    case 'dir':
4732
                        $icon = Display::getMdiIcon(ObjectIcon::CHAPTER, 'ch-tool-icon', '', ICON_SIZE_TINY);
4733
                        break;
4734
                    default:
4735
                        $icon = Display::getMdiIcon(ObjectIcon::SINGLE_ELEMENT, 'ch-tool-icon', '', ICON_SIZE_TINY);
4736
                        break;
4737
                }
4738
4739
                $urlPreviewLink = $mainUrl.'&action=view_item&mode=preview_document&id='.$itemId.'&lp_id='.$lpId;
4740
                $previewIcon = Display::url(
4741
                    $previewImage,
4742
                    $urlPreviewLink,
4743
                    [
4744
                        'target' => '_blank',
4745
                        'class' => 'btn btn--plain',
4746
                        'data-title' => $title,
4747
                        'title' => $title,
4748
                    ]
4749
                );
4750
                $url = $mainUrl.'&view=build&id='.$itemId.'&lp_id='.$lpId;
4751
4752
                $preRequisitesIcon = Display::url(
4753
                    Display::getMdiIcon('graph', 'ch-tool-icon', '', 16, get_lang('Prerequisites')),
4754
                    $url.'&action=edit_item_prereq',
4755
                    ['class' => '']
4756
                );
4757
4758
                $editIcon = '<a
4759
                    href="'.$mainUrl.'&action=edit_item&view=build&id='.$itemId.'&lp_id='.$lpId.'&path_item='.$node['path'].'"
4760
                    class=""
4761
                    >';
4762
                $editIcon .= Display::getMdiIcon('pencil', 'ch-tool-icon', '', 16, get_lang('Edit section description/name'));
4763
                $editIcon .= '</a>';
4764
                $orderIcons = '';
4765
                /*if ('final_item' !== $type) {
4766
                    $orderIcons = Display::url(
4767
                        $upIcon,
4768
                        'javascript:void(0)',
4769
                        ['class' => 'btn btn--plain order_items', 'data-dir' => 'up', 'data-id' => $itemId]
4770
                    );
4771
                    $orderIcons .= Display::url(
4772
                        $downIcon,
4773
                        'javascript:void(0)',
4774
                        ['class' => 'btn btn--plain order_items', 'data-dir' => 'down', 'data-id' => $itemId]
4775
                    );
4776
                }*/
4777
4778
                $deleteIcon = ' <a
4779
                    data-id = '.$itemId.'
4780
                    data-title = \''.addslashes($title).'\'
4781
                    href="javascript:void(0);"
4782
                    onclick="return deleteItem(this);"
4783
                    class="">';
4784
                $deleteIcon .= Display::getMdiIcon('delete', 'ch-tool-icon', '', 16, get_lang('Delete section'));
4785
                $deleteIcon .= '</a>';
4786
                $extra = '';
4787
4788
                if ('dir' === $type && empty($node['__children'])) {
4789
                    $level = $node['lvl'] + 1;
4790
                    $extra = '<ul class="list-group nested-sortable">
4791
                                <li class="list-group-item list-group-item-empty nested-'.$level.'"></li>
4792
                              </ul>';
4793
                }
4794
4795
                $buttons = Display::tag(
4796
                    'div',
4797
                    "<div class=\"btn-group btn-group-sm\">
4798
                                $editIcon
4799
                                $preRequisitesIcon
4800
                                $orderIcons
4801
                                $deleteIcon
4802
                               </div>",
4803
                    ['class' => 'btn-toolbar button_actions']
4804
                );
4805
4806
                return
4807
                    "<div class='flex flex-row'> $moveIcon  $icon <span class='mx-1'>$title </span></div>
4808
                    $extra
4809
                    $buttons
4810
                    "
4811
                    ;
4812
            },
4813
        ];
4814
4815
        $tree = $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
4816
4817
        if (empty($tree) && $dropElement) {
4818
            return $this->getDropElementHtml($noWrapper);
4819
        }
4820
4821
        return $tree;
4822
    }
4823
4824
    public function getDropElementHtml($noWrapper = false)
4825
    {
4826
        $li = '<li class="list-group-item">'.
4827
            Display::return_message(get_lang('Drag and drop an element here')).
4828
            '</li>';
4829
        if ($noWrapper) {
4830
            return $li;
4831
        }
4832
4833
        return
4834
            '<ul id="lp_item_list" class="list-group nested-sortable">
4835
            '.$li.'
4836
            </ul>';
4837
    }
4838
4839
    /**
4840
     * This function builds the action menu.
4841
     *
4842
     * @param bool   $returnString           Optional
4843
     * @param bool   $showRequirementButtons Optional. Allow show the requirements button
4844
     * @param bool   $isConfigPage           Optional. If is the config page, show the edit button
4845
     * @param bool   $allowExpand            Optional. Allow show the expand/contract button
4846
     * @param string $action
4847
     * @param array  $extraField
4848
     *
4849
     * @return string
4850
     */
4851
    public function build_action_menu(
4852
        $returnString = false,
4853
        $showRequirementButtons = true,
4854
        $isConfigPage = false,
4855
        $allowExpand = true,
4856
        $action = '',
4857
        $extraField = []
4858
    ) {
4859
        $actionsRight = '';
4860
        $lpId = $this->lp_id;
4861
        if (!isset($extraField['backTo']) && empty($extraField['backTo'])) {
4862
            $back = Display::url(
4863
                Display::getMdiIcon('arrow-left-bold-box', 'ch-tool-icon', '', 32, get_lang('Back to learning paths')),
4864
                'lp_controller.php?'.api_get_cidreq()
4865
            );
4866
        } else {
4867
            $back = Display::url(
4868
                Display::getMdiIcon('arrow-left-bold-box', 'ch-tool-icon', '', 32, get_lang('Back')),
4869
                $extraField['backTo']
4870
            );
4871
        }
4872
4873
        /*if ($backToBuild) {
4874
            $back = Display::url(
4875
                Display::getMdiIcon('arrow-left-bold-box', 'ch-tool-icon', null, 32, get_lang('Go back')),
4876
                "lp_controller.php?action=add_item&type=step&lp_id=$lpId&".api_get_cidreq()
4877
            );
4878
        }*/
4879
4880
        $actionsLeft = $back;
4881
4882
        $actionsLeft .= Display::url(
4883
            Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', '', 32, get_lang('Preview')),
4884
            'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4885
                'action' => 'view',
4886
                'lp_id' => $lpId,
4887
                'isStudentView' => 'true',
4888
            ])
4889
        );
4890
4891
        /*$actionsLeft .= Display::url(
4892
            Display::getMdiIcon('music-note-plus', 'ch-tool-icon', null, 32, get_lang('Add audio')),
4893
            'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4894
                'action' => 'admin_view',
4895
                'lp_id' => $lpId,
4896
                'updateaudio' => 'true',
4897
            ])
4898
        );*/
4899
4900
        $subscriptionSettings = self::getSubscriptionSettings();
4901
4902
        $request = api_request_uri();
4903
        if (false === strpos($request, 'edit')) {
4904
            $actionsLeft .= Display::url(
4905
                Display::getMdiIcon('hammer-wrench', 'ch-tool-icon', '', 32, get_lang('Course settings')),
4906
                'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4907
                    'action' => 'edit',
4908
                    'lp_id' => $lpId,
4909
                ])
4910
            );
4911
        }
4912
4913
        if ((false === strpos($request, 'build') &&
4914
            false === strpos($request, 'add_item')) ||
4915
            in_array($action, ['add_audio'], true)
4916
        ) {
4917
            $actionsLeft .= Display::url(
4918
                Display::getMdiIcon('pencil', 'ch-tool-icon', '', 32, get_lang('Edit')),
4919
                'lp_controller.php?'.http_build_query([
4920
                    'action' => 'build',
4921
                    'lp_id' => $lpId,
4922
                ]).'&'.api_get_cidreq()
4923
            );
4924
        }
4925
4926
        if (false === strpos(api_get_self(), 'lp_subscribe_users.php')) {
4927
            if (1 == $this->subscribeUsers &&
4928
                $subscriptionSettings['allow_add_users_to_lp']) {
4929
                $actionsLeft .= Display::url(
4930
                    Display::getMdiIcon('account-multiple-plus', 'ch-tool-icon', '', 32, get_lang('Subscribe users to learning path')),
4931
                    api_get_path(WEB_CODE_PATH)."lp/lp_subscribe_users.php?lp_id=$lpId&".api_get_cidreq()
4932
                );
4933
            }
4934
        }
4935
4936
        if ($allowExpand) {
4937
            /*$actionsLeft .= Display::url(
4938
                Display::getMdiIcon('arrow-expand-all', 'ch-tool-icon', null, 32, get_lang('Expand')).
4939
                Display::getMdiIcon('arrow-collapse-all', 'ch-tool-icon', null, 32, get_lang('Collapse')),
4940
                '#',
4941
                ['role' => 'button', 'id' => 'hide_bar_template']
4942
            );*/
4943
        }
4944
4945
        if ($showRequirementButtons) {
4946
            $buttons = [
4947
                [
4948
                    'title' => get_lang('Set previous step as prerequisite for each step'),
4949
                    'href' => 'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4950
                        'action' => 'set_previous_step_as_prerequisite',
4951
                        'lp_id' => $lpId,
4952
                    ]),
4953
                ],
4954
                [
4955
                    'title' => get_lang('Clear all prerequisites'),
4956
                    'href' => 'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4957
                        'action' => 'clear_prerequisites',
4958
                        'lp_id' => $lpId,
4959
                    ]),
4960
                ],
4961
            ];
4962
            $actionsRight = Display::groupButtonWithDropDown(
4963
                get_lang('Prerequisites options'),
4964
                $buttons,
4965
                true
4966
            );
4967
        }
4968
4969
        if (api_is_platform_admin() && isset($extraField['authorlp'])) {
4970
            $actionsLeft .= Display::url(
4971
                Display::getMdiIcon('account-multiple-plus', 'ch-tool-icon', '', 32, get_lang('Author')),
4972
                'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4973
                    'action' => 'author_view',
4974
                    'lp_id' => $lpId,
4975
                ])
4976
            );
4977
        }
4978
4979
        $toolbar = Display::toolbarAction('actions-lp-controller', [$actionsLeft, $actionsRight]);
4980
4981
        if ($returnString) {
4982
            return $toolbar;
4983
        }
4984
4985
        echo $toolbar;
4986
    }
4987
4988
    /**
4989
     * Creates the default learning path folder.
4990
     *
4991
     * @param array $course
4992
     * @param int   $creatorId
4993
     *
4994
     * @return CDocument
4995
     */
4996
    public static function generate_learning_path_folder($course, $creatorId = 0)
4997
    {
4998
        // Creating learning_path folder
4999
        $dir = 'learning_path';
5000
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
5001
5002
        return create_unexisting_directory(
5003
            $course,
5004
            $creatorId,
5005
            0,
5006
            null,
5007
            0,
5008
            '',
5009
            $dir,
5010
            get_lang('Learning paths'),
5011
            0
5012
        );
5013
    }
5014
5015
    /**
5016
     * @param array  $course
5017
     * @param string $lp_name
5018
     * @param int    $creatorId
5019
     *
5020
     * @return CDocument
5021
     */
5022
    public function generate_lp_folder($course, $lp_name = '', $creatorId = 0)
5023
    {
5024
        $filepath = '';
5025
        $dir = '/learning_path/';
5026
5027
        if (empty($lp_name)) {
5028
            $lp_name = $this->name;
5029
        }
5030
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
5031
        $parent = self::generate_learning_path_folder($course, $creatorId);
5032
5033
        // Limits title size
5034
        $title = api_substr(api_replace_dangerous_char($lp_name), 0, 80);
5035
        $dir = $dir.$title;
5036
5037
        // Creating LP folder
5038
        $folder = null;
5039
        if ($parent) {
5040
            $folder = create_unexisting_directory(
5041
                $course,
5042
                $creatorId,
5043
                0,
5044
                0,
5045
                0,
5046
                $filepath,
5047
                $dir,
5048
                $lp_name,
5049
                '',
5050
                false,
5051
                false,
5052
                $parent
5053
            );
5054
        }
5055
5056
        return $folder;
5057
    }
5058
5059
    /**
5060
     * Create a new document //still needs some finetuning.
5061
     *
5062
     * @param array  $courseInfo
5063
     * @param string $content
5064
     * @param string $title
5065
     * @param string $extension
5066
     * @param int    $parentId
5067
     * @param int    $creatorId  creator id
5068
     *
5069
     * @return int
5070
     */
5071
    public function create_document(
5072
        $courseInfo,
5073
        $content = '',
5074
        $title = '',
5075
        $extension = 'html',
5076
        $parentId = 0,
5077
        $creatorId = 0,
5078
        $docFiletype = 'file'
5079
    ) {
5080
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
5081
        $sessionId = api_get_session_id();
5082
5083
        // Generates folder
5084
        $this->generate_lp_folder($courseInfo);
5085
        // stripslashes() before calling api_replace_dangerous_char() because $_POST['title']
5086
        // is already escaped twice when it gets here.
5087
        $originalTitle = !empty($title) ? $title : $_POST['title'];
5088
        if (!empty($title)) {
5089
            $title = api_replace_dangerous_char(stripslashes($title));
5090
        } else {
5091
            $title = api_replace_dangerous_char(stripslashes($_POST['title']));
5092
        }
5093
5094
        $title = disable_dangerous_file($title);
5095
        $filename = $title;
5096
        $tmp_filename = "$filename.$extension";
5097
        /*$i = 0;
5098
        while (file_exists($filepath.$tmp_filename.'.'.$extension)) {
5099
            $tmp_filename = $filename.'_'.++$i;
5100
        }*/
5101
        $filename = $tmp_filename.'.'.$extension;
5102
5103
        if ('html' === $extension) {
5104
            $content = stripslashes($content);
5105
            $content = str_replace(
5106
                api_get_path(WEB_COURSE_PATH),
5107
                api_get_path(REL_PATH).'courses/',
5108
                $content
5109
            );
5110
            $content = str_replace(
5111
                '</body>',
5112
                '<style type="text/css">body{}</style></body>',
5113
                $content
5114
            );
5115
        }
5116
5117
        $ext = strtolower((string) $extension);
5118
        if (in_array($ext, ['html','htm'], true)) {
5119
            $docFiletype = 'html';
5120
        }
5121
5122
        $document = DocumentManager::addDocument(
5123
            $courseInfo,
5124
            null,
5125
            $docFiletype,
5126
            '',
5127
            $tmp_filename,
5128
            '',
5129
            0, //readonly
5130
            true,
5131
            null,
5132
            $sessionId,
5133
            $creatorId,
5134
            false,
5135
            $content,
5136
            $parentId
5137
        );
5138
5139
        if ($document && in_array($ext, ['html','htm'], true)) {
5140
            $em = Database::getManager();
5141
            $docRepo = Container::getDocumentRepository();
5142
            $docEntity = $docRepo->find($document->getIid());
5143
            if ($docEntity && $docEntity->hasResourceNode()) {
5144
                $rf = $docEntity->getResourceNode()->getResourceFiles()->first();
5145
                if ($rf && $rf->getMimeType() !== 'text/html') {
5146
                    $rf->setMimeType('text/html');
5147
                    $em->flush();
5148
                }
5149
            }
5150
        }
5151
5152
        $document_id = $document->getIid();
5153
        if ($document_id) {
5154
            $new_comment = isset($_POST['comment']) ? trim($_POST['comment']) : '';
5155
            $new_title = $originalTitle;
5156
5157
            if ($new_comment || $new_title) {
5158
                $tbl_doc = Database::get_course_table(TABLE_DOCUMENT);
5159
                $ct = '';
5160
                if ($new_comment) {
5161
                    $ct .= ", comment='".Database::escape_string($new_comment)."'";
5162
                }
5163
                if ($new_title) {
5164
                    $ct .= ", title='".Database::escape_string($new_title)."' ";
5165
                }
5166
5167
                $sql = "UPDATE $tbl_doc SET ".substr($ct, 1)."
5168
                        WHERE iid = $document_id ";
5169
                Database::query($sql);
5170
            }
5171
        }
5172
5173
        return $document_id;
5174
    }
5175
5176
    /**
5177
     * Edit a document based on $_POST and $_GET parameters 'dir' and 'path'.
5178
     */
5179
    public function edit_document()
5180
    {
5181
        $repo = Container::getDocumentRepository();
5182
        if (isset($_REQUEST['document_id']) && !empty($_REQUEST['document_id'])) {
5183
            $id = (int) $_REQUEST['document_id'];
5184
            /** @var CDocument $document */
5185
            $document = $repo->find($id);
5186
            if ($document->getResourceNode()->hasEditableTextContent()) {
5187
                $repo->updateResourceFileContent($document, $_REQUEST['content_lp']);
5188
            }
5189
            $document->setTitle($_REQUEST['title']);
5190
            $repo->update($document);
5191
        }
5192
    }
5193
5194
    /**
5195
     * Displays the selected item, with a panel for manipulating the item.
5196
     *
5197
     * @param CLpItem $lpItem
5198
     * @param string  $msg
5199
     * @param bool    $show_actions
5200
     *
5201
     * @return string
5202
     */
5203
    public function display_item($lpItem, $msg = null, $show_actions = true)
5204
    {
5205
        $course_id = api_get_course_int_id();
5206
        $return = '';
5207
5208
        if (null === $lpItem) {
5209
            return '';
5210
        }
5211
        $item_id = $lpItem->getIid();
5212
        $itemType = $lpItem->getItemType();
5213
        $lpId = $lpItem->getLp()->getIid();
5214
        $path = $lpItem->getPath();
5215
5216
        Session::write('parent_item_id', 'dir' === $itemType ? $item_id : 0);
5217
5218
        // Prevents wrong parent selection for document, see Bug#1251.
5219
        if ('dir' !== $itemType) {
5220
            Session::write('parent_item_id', $lpItem->getParentItemId());
5221
        }
5222
5223
        if ($show_actions) {
5224
            $return .= $this->displayItemMenu($lpItem);
5225
        }
5226
        $return .= '<div style="padding:10px;">';
5227
5228
        if ('' != $msg) {
5229
            $return .= $msg;
5230
        }
5231
5232
        $return .= '<h3>'.$lpItem->getTitle().'</h3>';
5233
5234
        switch ($itemType) {
5235
            case TOOL_THREAD:
5236
                $link = $this->rl_get_resource_link_for_learnpath(
5237
                    $course_id,
5238
                    $lpId,
5239
                    $item_id,
5240
                    0
5241
                );
5242
                $return .= Display::url(
5243
                    get_lang('Go to thread'),
5244
                    $link,
5245
                    ['class' => 'btn btn--primary']
5246
                );
5247
                break;
5248
            case TOOL_FORUM:
5249
                $return .= Display::url(
5250
                    get_lang('Go to the forum'),
5251
                    api_get_path(WEB_CODE_PATH).'forum/viewforum.php?'.api_get_cidreq().'&forum='.$path,
5252
                    ['class' => 'btn btn--primary']
5253
                );
5254
                break;
5255
            case TOOL_QUIZ:
5256
                if (!empty($path)) {
5257
                    $exercise = new Exercise();
5258
                    $exercise->read($path);
5259
                    $return .= $exercise->description.'<br />';
5260
                    $return .= Display::url(
5261
                        get_lang('Go to exercise'),
5262
                        api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$exercise->id,
5263
                        ['class' => 'btn btn--primary']
5264
                    );
5265
                }
5266
                break;
5267
            case TOOL_LP_FINAL_ITEM:
5268
                $return .= $this->getSavedFinalItem();
5269
                break;
5270
            case TOOL_DOCUMENT:
5271
            case 'video':
5272
            case TOOL_READOUT_TEXT:
5273
                $repo = Container::getDocumentRepository();
5274
                /** @var CDocument $document */
5275
                $document = $repo->find($lpItem->getPath());
5276
                $return .= $this->display_document($document, true, true);
5277
                break;
5278
        }
5279
        $return .= '</div>';
5280
5281
        return $return;
5282
    }
5283
5284
    /**
5285
     * Shows the needed forms for editing a specific item.
5286
     *
5287
     * @param CLpItem $lpItem
5288
     *
5289
     * @throws Exception
5290
     *
5291
     *
5292
     * @return string
5293
     */
5294
    public function display_edit_item($lpItem, $excludeExtraFields = [])
5295
    {
5296
        $return = '';
5297
        if (empty($lpItem)) {
5298
            return '';
5299
        }
5300
        $itemType = $lpItem->getItemType();
5301
        $path = $lpItem->getPath();
5302
5303
        switch ($itemType) {
5304
            case 'dir':
5305
            case 'asset':
5306
            case 'sco':
5307
                if (isset($_GET['view']) && 'build' === $_GET['view']) {
5308
                    $return .= $this->displayItemMenu($lpItem);
5309
                    $return .= $this->display_item_form($lpItem, 'edit');
5310
                } else {
5311
                    $return .= $this->display_item_form($lpItem, 'edit_item');
5312
                }
5313
                break;
5314
            case TOOL_LP_FINAL_ITEM:
5315
            case TOOL_DOCUMENT:
5316
            case 'video':
5317
            case TOOL_READOUT_TEXT:
5318
                $return .= $this->displayItemMenu($lpItem);
5319
                $return .= $this->displayDocumentForm('edit', $lpItem);
5320
                break;
5321
            case TOOL_LINK:
5322
                $link = null;
5323
                if (!empty($path)) {
5324
                    $repo = Container::getLinkRepository();
5325
                    $link = $repo->find($path);
5326
                }
5327
                $return .= $this->displayItemMenu($lpItem);
5328
                $return .= $this->display_link_form('edit', $lpItem, $link);
5329
5330
                break;
5331
            case TOOL_QUIZ:
5332
                if (!empty($path)) {
5333
                    $repo = Container::getQuizRepository();
5334
                    $resource = $repo->find($path);
5335
                }
5336
                $return .= $this->displayItemMenu($lpItem);
5337
                $return .= $this->display_quiz_form('edit', $lpItem, $resource);
5338
                break;
5339
            case TOOL_STUDENTPUBLICATION:
5340
                if (!empty($path)) {
5341
                    $repo = Container::getStudentPublicationRepository();
5342
                    $resource = $repo->find($path);
5343
                }
5344
                $return .= $this->displayItemMenu($lpItem);
5345
                $return .= $this->display_student_publication_form('edit', $lpItem, $resource);
5346
                break;
5347
            case TOOL_FORUM:
5348
                if (!empty($path)) {
5349
                    $repo = Container::getForumRepository();
5350
                    $resource = $repo->find($path);
5351
                }
5352
                $return .= $this->displayItemMenu($lpItem);
5353
                $return .= $this->display_forum_form('edit', $lpItem, $resource);
5354
                break;
5355
            case TOOL_THREAD:
5356
                if (!empty($path)) {
5357
                    $repo = Container::getForumPostRepository();
5358
                    $resource = $repo->find($path);
5359
                }
5360
                $return .= $this->displayItemMenu($lpItem);
5361
                $return .= $this->display_thread_form('edit', $lpItem, $resource);
5362
                break;
5363
        }
5364
5365
        return $return;
5366
    }
5367
5368
    /**
5369
     * Function that displays a list with al the resources that
5370
     * could be added to the learning path.
5371
     *
5372
     * @throws Exception
5373
     */
5374
    public function displayResources(): string
5375
    {
5376
        // Get all the docs.
5377
        $documents = $this->get_documents(true);
5378
5379
        // Get all the exercises.
5380
        $exercises = $this->get_exercises();
5381
5382
        // Get all the links.
5383
        $links = $this->get_links();
5384
5385
        // Get all the student publications.
5386
        $works = $this->get_student_publications();
5387
5388
        // Get all the forums.
5389
        $forums = $this->get_forums();
5390
5391
        // Get all surveys
5392
        $surveys = $this->getSurveys();
5393
5394
        // Get the final item form (see BT#11048) .
5395
        $finish = $this->getFinalItemForm();
5396
        $size = ICON_SIZE_MEDIUM; //ICON_SIZE_BIG
5397
        $headers = [
5398
            Display::getMdiIcon('bookshelf', 'ch-tool-icon-gradient', '', 64, get_lang('Documents')),
5399
            Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon-gradient', '', 64, get_lang('Tests')),
5400
            Display::getMdiIcon('file-link', 'ch-tool-icon-gradient', '', 64, get_lang('Links')),
5401
            Display::getMdiIcon('inbox-full', 'ch-tool-icon-gradient', '', 64, get_lang('Assignments')),
5402
            Display::getMdiIcon('comment-quote', 'ch-tool-icon-gradient', '', 64, get_lang('Forums')),
5403
            Display::getMdiIcon('bookmark-multiple', 'ch-tool-icon-gradient', '', 64, get_lang('Add section')),
5404
            Display::getMdiIcon('form-dropdown', 'ch-tool-icon-gradient', '', 64, get_lang('Create survey')),
5405
            Display::getMdiIcon('certificate', 'ch-tool-icon-gradient', '', 64, get_lang('Certificate')),
5406
        ];
5407
        $content = '';
5408
        /*$content = Display::return_message(
5409
            get_lang('Click on the [Learner view] button to see your learning path'),
5410
            'normal'
5411
        );*/
5412
        $section = $this->displayNewSectionForm();
5413
        $selected = isset($_REQUEST['lp_build_selected']) ? (int) $_REQUEST['lp_build_selected'] : 0;
5414
5415
        return Display::tabs(
5416
            $headers,
5417
            [
5418
                $documents,
5419
                $exercises,
5420
                $links,
5421
                $works,
5422
                $forums,
5423
                $section,
5424
                $surveys,
5425
                $finish,
5426
            ],
5427
            'resource_tab',
5428
            ['class' => 'lp-resource-tabs'],
5429
            [],
5430
            $selected
5431
        );
5432
    }
5433
5434
    /**
5435
     * Returns the extension of a document.
5436
     *
5437
     * @param string $filename
5438
     *
5439
     * @return string Extension (part after the last dot)
5440
     */
5441
    public function get_extension($filename)
5442
    {
5443
        $explode = explode('.', $filename);
5444
5445
        return $explode[count($explode) - 1];
5446
    }
5447
5448
    /**
5449
     * @return string
5450
     */
5451
    public function getCurrentBuildingModeURL()
5452
    {
5453
        $pathItem = isset($_GET['path_item']) ? (int) $_GET['path_item'] : '';
5454
        $action = isset($_GET['action']) ? Security::remove_XSS($_GET['action']) : '';
5455
        $id = isset($_GET['id']) ? (int) $_GET['id'] : '';
5456
        $view = isset($_GET['view']) ? Security::remove_XSS($_GET['view']) : '';
5457
5458
        $currentUrl = api_get_self().'?'.api_get_cidreq().
5459
            '&action='.$action.'&lp_id='.$this->lp_id.'&path_item='.$pathItem.'&view='.$view.'&id='.$id;
5460
5461
        return $currentUrl;
5462
    }
5463
5464
    /**
5465
     * Displays a document by id.
5466
     *
5467
     * @param CDocument $document
5468
     * @param bool      $show_title
5469
     * @param bool      $iframe
5470
     * @param bool      $edit_link
5471
     *
5472
     * @return string
5473
     */
5474
    public function display_document($document, $show_title = false, $iframe = true, $edit_link = false)
5475
    {
5476
        $return = '';
5477
        if (!$document) {
5478
            return '';
5479
        }
5480
5481
        $repo = Container::getDocumentRepository();
5482
5483
        // TODO: Add a path filter.
5484
        if ($iframe) {
5485
            $url = $repo->getResourceFileUrl($document);
5486
5487
            $return .= '<iframe
5488
                id="learnpath_preview_frame"
5489
                frameborder="0"
5490
                height="400"
5491
                width="100%"
5492
                scrolling="auto"
5493
                src="'.$url.'"></iframe>';
5494
        } else {
5495
            $return = $repo->getResourceFileContent($document);
5496
        }
5497
5498
        return $return;
5499
    }
5500
5501
    /**
5502
     * Return HTML form to add/edit a link item.
5503
     *
5504
     * @param string  $action (add/edit)
5505
     * @param CLpItem $lpItem
5506
     * @param CLink   $link
5507
     *
5508
     * @throws Exception
5509
     *
5510
     *
5511
     * @return string HTML form
5512
     */
5513
    public function display_link_form($action, $lpItem, $link)
5514
    {
5515
        $item_url = '';
5516
        if ($link) {
5517
            $item_url = stripslashes($link->getUrl());
5518
        }
5519
        $form = new FormValidator(
5520
            'edit_link',
5521
            'POST',
5522
            $this->getCurrentBuildingModeURL()
5523
        );
5524
5525
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5526
5527
        $urlAttributes = ['class' => 'learnpath_item_form'];
5528
        $urlAttributes['disabled'] = 'disabled';
5529
        $form->addElement('url', 'url', get_lang('URL'), $urlAttributes);
5530
        $form->setDefault('url', $item_url);
5531
5532
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5533
5534
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
5535
    }
5536
5537
    /**
5538
     * Return HTML form to add/edit a quiz.
5539
     *
5540
     * @param string  $action   Action (add/edit)
5541
     * @param CLpItem $lpItem   Item ID if already exists
5542
     * @param CQuiz   $exercise Extra information (quiz ID if integer)
5543
     *
5544
     * @throws Exception
5545
     *
5546
     * @return string HTML form
5547
     */
5548
    public function display_quiz_form($action, $lpItem, $exercise)
5549
    {
5550
        $form = new FormValidator(
5551
            'quiz_form',
5552
            'POST',
5553
            $this->getCurrentBuildingModeURL()
5554
        );
5555
5556
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5557
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5558
5559
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
5560
    }
5561
5562
    /**
5563
     * Return the form to display the forum edit/add option.
5564
     *
5565
     * @param CLpItem $lpItem
5566
     *
5567
     * @throws Exception
5568
     *
5569
     * @return string HTML form
5570
     */
5571
    public function display_forum_form($action, $lpItem, $resource)
5572
    {
5573
        $form = new FormValidator(
5574
            'forum_form',
5575
            'POST',
5576
            $this->getCurrentBuildingModeURL()
5577
        );
5578
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5579
5580
        if ('add' === $action) {
5581
            $form->addButtonSave(get_lang('Add forum to course'), 'submit_button');
5582
        } else {
5583
            $form->addButtonSave(get_lang('Edit the current forum'), 'submit_button');
5584
        }
5585
5586
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
5587
    }
5588
5589
    /**
5590
     * Return HTML form to add/edit forum threads.
5591
     *
5592
     * @param string  $action
5593
     * @param CLpItem $lpItem
5594
     * @param string  $resource
5595
     *
5596
     * @throws Exception
5597
     *
5598
     * @return string HTML form
5599
     */
5600
    public function display_thread_form($action, $lpItem, $resource)
5601
    {
5602
        $form = new FormValidator(
5603
            'thread_form',
5604
            'POST',
5605
            $this->getCurrentBuildingModeURL()
5606
        );
5607
5608
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
5609
5610
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5611
5612
        return $form->returnForm();
5613
    }
5614
5615
    /**
5616
     * Return the HTML form to display an item (generally a dir item).
5617
     *
5618
     * @param CLpItem $lpItem
5619
     * @param string  $action
5620
     *
5621
     * @throws Exception
5622
     *
5623
     *
5624
     * @return string HTML form
5625
     */
5626
    public function display_item_form(
5627
        $lpItem,
5628
        $action = 'add_item'
5629
    ) {
5630
        $item_type = $lpItem->getItemType();
5631
5632
        $url = api_get_self().'?'.api_get_cidreq().'&action='.$action.'&type='.$item_type.'&lp_id='.$this->lp_id;
5633
5634
        $form = new FormValidator('form_'.$item_type, 'POST', $url);
5635
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
5636
5637
        $form->addButtonSave(get_lang('Save section'), 'submit_button');
5638
5639
        return $form->returnForm();
5640
    }
5641
5642
    /**
5643
     * Return HTML form to add/edit a student publication (work).
5644
     *
5645
     * @param string              $action
5646
     * @param CStudentPublication $resource
5647
     *
5648
     * @throws Exception
5649
     *
5650
     * @return string HTML form
5651
     */
5652
    public function display_student_publication_form($action, CLpItem $lpItem, $resource)
5653
    {
5654
        $form = new FormValidator('frm_student_publication', 'post', '#');
5655
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
5656
5657
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5658
5659
        $return = '<div class="sectioncomment">';
5660
        $return .= $form->returnForm();
5661
        $return .= '</div>';
5662
5663
        return $return;
5664
    }
5665
5666
    public function displayNewSectionForm()
5667
    {
5668
        $action = 'add_item';
5669
        $item_type = 'dir';
5670
5671
        $lpItem = (new CLpItem())
5672
            ->setTitle('')
5673
            ->setItemType('dir')
5674
        ;
5675
5676
        $url = api_get_self().'?'.api_get_cidreq().'&action='.$action.'&type='.$item_type.'&lp_id='.$this->lp_id;
5677
5678
        $form = new FormValidator('form_'.$item_type, 'POST', $url);
5679
        LearnPathItemForm::setForm($form, 'add', $this, $lpItem);
5680
5681
        $form->addButtonSave(get_lang('Save section'), 'submit_button');
5682
        $form->addElement('hidden', 'type', 'dir');
5683
5684
        return $form->returnForm();
5685
    }
5686
5687
    /**
5688
     * Returns the form to update or create a document.
5689
     *
5690
     * @param string  $action (add/edit)
5691
     * @param CLpItem $lpItem
5692
     *
5693
     *
5694
     * @throws Exception
5695
     *
5696
     * @return string HTML form
5697
     */
5698
    public function displayDocumentForm($action = 'add', $lpItem = null)
5699
    {
5700
        $courseInfo = api_get_course_info();
5701
5702
        $form = new FormValidator(
5703
            'form',
5704
            'POST',
5705
            $this->getCurrentBuildingModeURL(),
5706
            '',
5707
            ['enctype' => 'multipart/form-data']
5708
        );
5709
5710
        $data = $this->generate_lp_folder($courseInfo);
5711
5712
        if (null !== $lpItem) {
5713
            LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5714
        }
5715
5716
        switch ($action) {
5717
            case 'add':
5718
                $folders = DocumentManager::get_all_document_folders($courseInfo, 0, true);
5719
                DocumentManager::build_directory_selector(
5720
                    $folders,
5721
                    '',
5722
                    [],
5723
                    true,
5724
                    $form,
5725
                    'directory_parent_id'
5726
                );
5727
                if ($data) {
5728
                    $form->setDefaults(['directory_parent_id' => $data->getIid()]);
5729
                }
5730
                break;
5731
        }
5732
5733
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5734
5735
        $script = '<script>
5736
document.addEventListener("DOMContentLoaded", function () {
5737
    var form = document.getElementById("form") || document.forms["form"];
5738
    if (!form) return;
5739
    form.addEventListener("submit", function (e) {
5740
        var btn = form.querySelector("button[name=submit_button], input[name=submit_button]");
5741
        if (!btn) return;
5742
        // async disable to avoid interfering with possible sync handlers triggered on submit
5743
        setTimeout(function () {
5744
            try {
5745
                btn.disabled = true;
5746
                if (btn.classList) btn.classList.add("disabled");
5747
            } catch (err) {
5748
                // silent
5749
            }
5750
        }, 0);
5751
    }, { once: true }); // once: true to disable only on the first submit
5752
});
5753
</script>';
5754
5755
        return $form->returnForm() . $script;
5756
    }
5757
5758
    /**
5759
     * @param array  $courseInfo
5760
     * @param string $content
5761
     * @param string $title
5762
     * @param int    $parentId
5763
     *
5764
     * @return int
5765
     */
5766
    public function createReadOutText($courseInfo, $content = '', $title = '', $parentId = 0)
5767
    {
5768
        $creatorId = api_get_user_id();
5769
        $sessionId = api_get_session_id();
5770
5771
        // Generates folder
5772
        $result = $this->generate_lp_folder($courseInfo);
5773
        $dir = $result['dir'];
5774
5775
        if (empty($parentId) || '/' === $parentId) {
5776
            $postDir = isset($_POST['dir']) ? $_POST['dir'] : $dir;
5777
            $dir = isset($_GET['dir']) ? $_GET['dir'] : $postDir; // Please, do not modify this dirname formatting.
5778
5779
            if ('/' === $parentId) {
5780
                $dir = '/';
5781
            }
5782
5783
            // Please, do not modify this dirname formatting.
5784
            if (strstr($dir, '..')) {
5785
                $dir = '/';
5786
            }
5787
5788
            if (!empty($dir[0]) && '.' == $dir[0]) {
5789
                $dir = substr($dir, 1);
5790
            }
5791
            if (!empty($dir[0]) && '/' != $dir[0]) {
5792
                $dir = '/'.$dir;
5793
            }
5794
            if (isset($dir[strlen($dir) - 1]) && '/' != $dir[strlen($dir) - 1]) {
5795
                $dir .= '/';
5796
            }
5797
        } else {
5798
            $parentInfo = DocumentManager::get_document_data_by_id(
5799
                $parentId,
5800
                $courseInfo['code']
5801
            );
5802
            if (!empty($parentInfo)) {
5803
                $dir = $parentInfo['path'].'/';
5804
            }
5805
        }
5806
5807
        $filepath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/'.$dir;
5808
5809
        if (!is_dir($filepath)) {
5810
            $dir = '/';
5811
            $filepath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/'.$dir;
5812
        }
5813
5814
        $originalTitle = !empty($title) ? $title : $_POST['title'];
5815
5816
        if (!empty($title)) {
5817
            $title = api_replace_dangerous_char(stripslashes($title));
5818
        } else {
5819
            $title = api_replace_dangerous_char(stripslashes($_POST['title']));
5820
        }
5821
5822
        $title = disable_dangerous_file($title);
5823
        $filename = $title;
5824
        $content = !empty($content) ? $content : $_POST['content_lp'];
5825
        $tmpFileName = $filename;
5826
5827
        $i = 0;
5828
        while (file_exists($filepath.$tmpFileName.'.html')) {
5829
            $tmpFileName = $filename.'_'.++$i;
5830
        }
5831
5832
        $filename = $tmpFileName.'.html';
5833
        $content = stripslashes($content);
5834
5835
        if (file_exists($filepath.$filename)) {
5836
            return 0;
5837
        }
5838
5839
        $putContent = file_put_contents($filepath.$filename, $content);
5840
5841
        if (false === $putContent) {
5842
            return 0;
5843
        }
5844
5845
        $fileSize = filesize($filepath.$filename);
5846
        $saveFilePath = $dir.$filename;
5847
5848
        $document = DocumentManager::addDocument(
5849
            $courseInfo,
5850
            $saveFilePath,
5851
            'file',
5852
            $fileSize,
5853
            $tmpFileName,
5854
            '',
5855
            0, //readonly
5856
            true,
5857
            null,
5858
            $sessionId,
5859
            $creatorId
5860
        );
5861
5862
        $documentId = $document->getIid();
5863
5864
        if (!$document) {
5865
            return 0;
5866
        }
5867
5868
        $newComment = isset($_POST['comment']) ? trim($_POST['comment']) : '';
5869
        $newTitle = $originalTitle;
5870
5871
        if ($newComment || $newTitle) {
5872
            $em = Database::getManager();
5873
5874
            if ($newComment) {
5875
                $document->setComment($newComment);
5876
            }
5877
5878
            if ($newTitle) {
5879
                $document->setTitle($newTitle);
5880
            }
5881
5882
            $em->persist($document);
5883
            $em->flush();
5884
        }
5885
5886
        return $documentId;
5887
    }
5888
5889
    /**
5890
     * Displays the menu for manipulating a step.
5891
     *
5892
     * @return string
5893
     */
5894
    public function displayItemMenu(CLpItem $lpItem)
5895
    {
5896
        $item_id = $lpItem->getIid();
5897
        $audio = $lpItem->getAudio();
5898
        $itemType = $lpItem->getItemType();
5899
        $path = $lpItem->getPath();
5900
5901
        $return = '';
5902
        $audio_player = null;
5903
        // We display an audio player if needed.
5904
        if (!empty($audio)) {
5905
            /*$webAudioPath = '../..'.api_get_path(REL_COURSE_PATH).$_course['path'].'/document/audio/'.$row['audio'];
5906
            $audio_player .= '<div class="lp_mediaplayer" id="container">'
5907
                .'<audio src="'.$webAudioPath.'" controls>'
5908
                .'</div><br>';*/
5909
        }
5910
5911
        $url = api_get_self().'?'.api_get_cidreq().'&view=build&id='.$item_id.'&lp_id='.$this->lp_id;
5912
5913
        if (TOOL_LP_FINAL_ITEM !== $itemType) {
5914
            $return .= Display::url(
5915
                Display::getMdiIcon('pencil', 'ch-tool-icon', null, 22, get_lang('Edit')),
5916
                $url.'&action=edit_item&path_item='.$path
5917
            );
5918
5919
            /*$return .= Display::url(
5920
                Display::getMdiIcon('arrow-right-bold', 'ch-tool-icon', null, 22, get_lang('Move')),
5921
                $url.'&action=move_item'
5922
            );*/
5923
        }
5924
5925
        // Commented for now as prerequisites cannot be added to chapters.
5926
        if ('dir' !== $itemType) {
5927
            $return .= Display::url(
5928
                Display::getMdiIcon('graph', 'ch-tool-icon', null, 22, get_lang('Prerequisites')),
5929
                $url.'&action=edit_item_prereq'
5930
            );
5931
        }
5932
        $return .= Display::url(
5933
            Display::getMdiIcon('delete', 'ch-tool-icon', null, 22, get_lang('Delete')),
5934
            $url.'&action=delete_item'
5935
        );
5936
5937
        /*if (in_array($itemType, [TOOL_DOCUMENT, TOOL_LP_FINAL_ITEM, TOOL_HOTPOTATOES])) {
5938
            $documentData = DocumentManager::get_document_data_by_id($path, $course_code);
5939
            if (empty($documentData)) {
5940
                // Try with iid
5941
                $table = Database::get_course_table(TABLE_DOCUMENT);
5942
                $sql = "SELECT path FROM $table
5943
                        WHERE
5944
                              c_id = ".api_get_course_int_id()." AND
5945
                              iid = ".$path." AND
5946
                              path NOT LIKE '%_DELETED_%'";
5947
                $result = Database::query($sql);
5948
                $documentData = Database::fetch_array($result);
5949
                if ($documentData) {
5950
                    $documentData['absolute_path_from_document'] = '/document'.$documentData['path'];
5951
                }
5952
            }
5953
            if (isset($documentData['absolute_path_from_document'])) {
5954
                $return .= get_lang('File').': '.$documentData['absolute_path_from_document'];
5955
            }
5956
        }*/
5957
5958
        if (!empty($audio_player)) {
5959
            $return .= $audio_player;
5960
        }
5961
5962
        return Display::toolbarAction('lp_item', [$return]);
5963
    }
5964
5965
    /**
5966
     * Creates the javascript needed for filling up the checkboxes without page reload.
5967
     *
5968
     * @return string
5969
     */
5970
    public function get_js_dropdown_array()
5971
    {
5972
        $return = 'var child_name = new Array();'."\n";
5973
        $return .= 'var child_value = new Array();'."\n\n";
5974
        $return .= 'child_name[0] = new Array();'."\n";
5975
        $return .= 'child_value[0] = new Array();'."\n\n";
5976
5977
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
5978
        $sql = "SELECT * FROM ".$tbl_lp_item."
5979
                WHERE
5980
                    lp_id = ".$this->lp_id." AND
5981
                    parent_item_id = 0
5982
                ORDER BY display_order ASC";
5983
        Database::query($sql);
5984
        $i = 0;
5985
5986
        $list = $this->getItemsForForm(true);
5987
5988
        foreach ($list as $row_zero) {
5989
            if (TOOL_LP_FINAL_ITEM !== $row_zero['item_type']) {
5990
                if (TOOL_QUIZ == $row_zero['item_type']) {
5991
                    $row_zero['title'] = Exercise::get_formated_title_variable($row_zero['title']);
5992
                }
5993
                $js_var = json_encode(get_lang('After').' '.$row_zero['title']);
5994
                $return .= 'child_name[0]['.$i.'] = '.$js_var.' ;'."\n";
5995
                $return .= 'child_value[0]['.$i++.'] = "'.$row_zero['iid'].'";'."\n";
5996
            }
5997
        }
5998
5999
        $return .= "\n";
6000
        $sql = "SELECT * FROM $tbl_lp_item
6001
                WHERE lp_id = ".$this->lp_id;
6002
        $res = Database::query($sql);
6003
        while ($row = Database::fetch_array($res)) {
6004
            $sql_parent = "SELECT * FROM ".$tbl_lp_item."
6005
                           WHERE
6006
                                parent_item_id = ".$row['iid']."
6007
                           ORDER BY display_order ASC";
6008
            $res_parent = Database::query($sql_parent);
6009
            $i = 0;
6010
            $return .= 'child_name['.$row['iid'].'] = new Array();'."\n";
6011
            $return .= 'child_value['.$row['iid'].'] = new Array();'."\n\n";
6012
6013
            while ($row_parent = Database::fetch_array($res_parent)) {
6014
                $js_var = json_encode(get_lang('After').' '.$this->cleanItemTitle($row_parent['title']));
6015
                $return .= 'child_name['.$row['iid'].']['.$i.'] =   '.$js_var.' ;'."\n";
6016
                $return .= 'child_value['.$row['iid'].']['.$i++.'] = "'.$row_parent['iid'].'";'."\n";
6017
            }
6018
            $return .= "\n";
6019
        }
6020
6021
        $return .= "
6022
            function load_cbo(id) {
6023
                if (!id) {
6024
                    return false;
6025
                }
6026
6027
                var cbo = document.getElementById('previous');
6028
                if (cbo) {
6029
                    for(var i = cbo.length - 1; i > 0; i--) {
6030
                        cbo.options[i] = null;
6031
                    }
6032
                    var k=0;
6033
                    for (var i = 1; i <= child_name[id].length; i++){
6034
                        var option = new Option(child_name[id][i - 1], child_value[id][i - 1]);
6035
                        option.style.paddingLeft = '40px';
6036
                        cbo.options[i] = option;
6037
                        k = i;
6038
                    }
6039
                    cbo.options[k].selected = true;
6040
                }
6041
6042
                //$('#previous').selectpicker('refresh');
6043
            }";
6044
6045
        return $return;
6046
    }
6047
6048
    /**
6049
     * Display the form to allow moving an item.
6050
     *
6051
     * @param CLpItem $lpItem
6052
     *
6053
     * @throws Exception
6054
     *
6055
     *
6056
     * @return string HTML form
6057
     */
6058
    public function display_move_item($lpItem)
6059
    {
6060
        $return = '';
6061
        $path = $lpItem->getPath();
6062
6063
        if ($lpItem) {
6064
            $itemType = $lpItem->getItemType();
6065
            switch ($itemType) {
6066
                case 'dir':
6067
                case 'asset':
6068
                    $return .= $this->displayItemMenu($lpItem);
6069
                    $return .= $this->display_item_form(
6070
                        $lpItem,
6071
                        get_lang('Move the current section'),
6072
                        'move',
6073
                        $row
6074
                    );
6075
                    break;
6076
                case TOOL_DOCUMENT:
6077
                case 'video':
6078
                    $return .= $this->displayItemMenu($lpItem);
6079
                    $return .= $this->displayDocumentForm('move', $lpItem);
6080
                    break;
6081
                case TOOL_LINK:
6082
                    $link = null;
6083
                    if (!empty($path)) {
6084
                        $repo = Container::getLinkRepository();
6085
                        $link = $repo->find($path);
6086
                    }
6087
                    $return .= $this->displayItemMenu($lpItem);
6088
                    $return .= $this->display_link_form('move', $lpItem, $link);
6089
                    break;
6090
                case TOOL_HOTPOTATOES:
6091
                    $return .= $this->displayItemMenu($lpItem);
6092
                    $return .= $this->display_link_form('move', $lpItem, $row);
6093
                    break;
6094
                case TOOL_QUIZ:
6095
                    $return .= $this->displayItemMenu($lpItem);
6096
                    $return .= $this->display_quiz_form('move', $lpItem, $row);
6097
                    break;
6098
                case TOOL_STUDENTPUBLICATION:
6099
                    $return .= $this->displayItemMenu($lpItem);
6100
                    $return .= $this->display_student_publication_form('move', $lpItem, $row);
6101
                    break;
6102
                case TOOL_FORUM:
6103
                    $return .= $this->displayItemMenu($lpItem);
6104
                    $return .= $this->display_forum_form('move', $lpItem, $row);
6105
                    break;
6106
                case TOOL_THREAD:
6107
                    $return .= $this->displayItemMenu($lpItem);
6108
                    $return .= $this->display_forum_form('move', $lpItem, $row);
6109
                    break;
6110
            }
6111
        }
6112
6113
        return $return;
6114
    }
6115
6116
    /**
6117
     * Return HTML form to allow prerequisites selection.
6118
     *
6119
     * @todo use FormValidator
6120
     *
6121
     * @return string HTML form
6122
     */
6123
    public function displayItemPrerequisitesForm(CLpItem $lpItem)
6124
    {
6125
        $courseId = api_get_course_int_id();
6126
        $preRequisiteId = $lpItem->getPrerequisite();
6127
        $itemId = $lpItem->getIid();
6128
6129
        $return = Display::page_header(get_lang('Add/edit prerequisites').' '.$lpItem->getTitle());
6130
6131
        $return .= '<form method="POST">';
6132
        $return .= '<div class="table-responsive">';
6133
        $return .= '<table class="table table-hover">';
6134
        $return .= '<thead>';
6135
        $return .= '<tr>';
6136
        $return .= '<th>'.get_lang('Prerequisites').'</th>';
6137
        $return .= '<th width="140">'.get_lang('minimum').'</th>';
6138
        $return .= '<th width="140">'.get_lang('maximum').'</th>';
6139
        $return .= '</tr>';
6140
        $return .= '</thead>';
6141
6142
        // Adding the none option to the prerequisites see http://www.chamilo.org/es/node/146
6143
        $return .= '<tbody>';
6144
        $return .= '<tr>';
6145
        $return .= '<td colspan="3">';
6146
        $return .= '<div class="radio learnpath"><label for="idnone">';
6147
        $return .= '<input checked="checked" id="idnone" name="prerequisites" type="radio" />';
6148
        $return .= get_lang('none').'</label>';
6149
        $return .= '</div>';
6150
        $return .= '</tr>';
6151
6152
        // @todo use entitites
6153
        $tblLpItem = Database::get_course_table(TABLE_LP_ITEM);
6154
        $sql = "SELECT * FROM $tblLpItem
6155
                WHERE lp_id = ".$this->lp_id;
6156
        $result = Database::query($sql);
6157
6158
        $selectedMinScore = [];
6159
        $selectedMaxScore = [];
6160
        $masteryScore = [];
6161
        while ($row = Database::fetch_array($result)) {
6162
            if ($row['iid'] == $itemId) {
6163
                $selectedMinScore[$row['prerequisite']] = $row['prerequisite_min_score'];
6164
                $selectedMaxScore[$row['prerequisite']] = $row['prerequisite_max_score'];
6165
            }
6166
            $masteryScore[$row['iid']] = $row['mastery_score'];
6167
        }
6168
6169
        $displayOrder = $lpItem->getDisplayOrder();
6170
        $lpItemRepo = Container::getLpItemRepository();
6171
        $itemRoot = $lpItemRepo->getRootItem($this->get_id());
6172
        $em = Database::getManager();
6173
6174
        $currentItemId = $itemId;
6175
        $options = [
6176
            'decorate' => true,
6177
            'rootOpen' => function () {
6178
                return '';
6179
            },
6180
            'rootClose' => function () {
6181
                return '';
6182
            },
6183
            'childOpen' => function () {
6184
                return '';
6185
            },
6186
            'childClose' => '',
6187
            'nodeDecorator' => function ($item) use (
6188
                $currentItemId,
6189
                $preRequisiteId,
6190
                $courseId,
6191
                $selectedMaxScore,
6192
                $selectedMinScore,
6193
                $displayOrder,
6194
                $lpItemRepo,
6195
                $em
6196
            ) {
6197
                $itemId = $item['iid'];
6198
                $type = $item['itemType'];
6199
                $iconName = str_replace(' ', '', $type);
6200
                switch ($iconName) {
6201
                    case 'category':
6202
                    case 'chapter':
6203
                    case 'folder':
6204
                    case 'dir':
6205
                        $icon = Display::getMdiIcon(ObjectIcon::CHAPTER, 'ch-tool-icon', '', ICON_SIZE_TINY);
6206
                        break;
6207
                    default:
6208
                        $icon = Display::getMdiIcon(ObjectIcon::SINGLE_ELEMENT, 'ch-tool-icon', '', ICON_SIZE_TINY);
6209
                        break;
6210
                }
6211
6212
                if ($itemId == $currentItemId) {
6213
                    return '';
6214
                }
6215
6216
                if ($displayOrder < $item['displayOrder']) {
6217
                    return '';
6218
                }
6219
6220
                $selectedMaxScoreValue = isset($selectedMaxScore[$itemId]) ? $selectedMaxScore[$itemId] : $item['maxScore'];
6221
                $selectedMinScoreValue = $selectedMinScore[$itemId] ?? 0;
6222
                $masteryScoreAsMinValue = $masteryScore[$itemId] ?? 0;
6223
6224
                $return = '<tr>';
6225
                $return .= '<td '.((TOOL_QUIZ != $type && TOOL_HOTPOTATOES != $type) ? ' colspan="3"' : '').'>';
6226
                $return .= '<div style="margin-left:'.($item['lvl'] * 20).'px;" class="radio learnpath">';
6227
                $return .= '<label for="id'.$itemId.'">';
6228
6229
                $checked = '';
6230
                if (null !== $preRequisiteId) {
6231
                    $checked = in_array($preRequisiteId, [$itemId, $item['ref']]) ? ' checked="checked" ' : '';
6232
                }
6233
6234
                $disabled = 'dir' === $type ? ' disabled="disabled" ' : '';
6235
6236
                $return .= '<input
6237
                    '.$checked.' '.$disabled.'
6238
                    id="id'.$itemId.'"
6239
                    name="prerequisites"
6240
                    type="radio"
6241
                    value="'.$itemId.'" />';
6242
6243
                $return .= $icon.'&nbsp;&nbsp;'.$item['title'].'</label>';
6244
                $return .= '</div>';
6245
                $return .= '</td>';
6246
6247
                if (TOOL_QUIZ == $type) {
6248
                    // let's update max_score Tests information depending of the Tests Advanced properties
6249
                    $exercise = new Exercise($courseId);
6250
                    /** @var CLpItem $itemEntity */
6251
                    $itemEntity = $lpItemRepo->find($itemId);
6252
                    $exercise->read($item['path']);
6253
                    $itemEntity->setMaxScore($exercise->getMaxScore());
6254
                    $em->persist($itemEntity);
6255
                    $em->flush($itemEntity);
6256
6257
                    $item['maxScore'] = $exercise->getMaxScore();
6258
6259
                    if (empty($selectedMinScoreValue) && !empty($masteryScoreAsMinValue)) {
6260
                        // Backwards compatibility with 1.9.x use mastery_score as min value
6261
                        $selectedMinScoreValue = $masteryScoreAsMinValue;
6262
                    }
6263
                    $return .= '<td>';
6264
                    $return .= '<input
6265
                        class="form-control"
6266
                        size="4" maxlength="3"
6267
                        name="min_'.$itemId.'"
6268
                        type="number"
6269
                        min="0"
6270
                        step="any"
6271
                        max="'.$item['maxScore'].'"
6272
                        value="'.$selectedMinScoreValue.'"
6273
                    />';
6274
                    $return .= '</td>';
6275
                    $return .= '<td>';
6276
                    $return .= '<input
6277
                        class="form-control"
6278
                        size="4"
6279
                        maxlength="3"
6280
                        name="max_'.$itemId.'"
6281
                        type="number"
6282
                        min="0"
6283
                        step="any"
6284
                        max="'.$item['maxScore'].'"
6285
                        value="'.$selectedMaxScoreValue.'"
6286
                    />';
6287
                        $return .= '</td>';
6288
                    }
6289
6290
                if (TOOL_HOTPOTATOES == $type) {
6291
                    $return .= '<td>';
6292
                    $return .= '<input
6293
                        size="4"
6294
                        maxlength="3"
6295
                        name="min_'.$itemId.'"
6296
                        type="number"
6297
                        min="0"
6298
                        step="any"
6299
                        max="'.$item['maxScore'].'"
6300
                        value="'.$selectedMinScoreValue.'"
6301
                    />';
6302
                        $return .= '</td>';
6303
                        $return .= '<td>';
6304
                        $return .= '<input
6305
                        size="4"
6306
                        maxlength="3"
6307
                        name="max_'.$itemId.'"
6308
                        type="number"
6309
                        min="0"
6310
                        step="any"
6311
                        max="'.$item['maxScore'].'"
6312
                        value="'.$selectedMaxScoreValue.'"
6313
                    />';
6314
                    $return .= '</td>';
6315
                }
6316
                $return .= '</tr>';
6317
6318
                return $return;
6319
            },
6320
        ];
6321
6322
        $tree = $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
6323
        $return .= $tree;
6324
        $return .= '</tbody>';
6325
        $return .= '</table>';
6326
        $return .= '</div>';
6327
        $return .= '<div class="form-group">';
6328
        $return .= '<button class="btn btn--primary" name="submit_button" type="submit">'.
6329
            get_lang('Save prerequisites settings').'</button>';
6330
        $return .= '</form>';
6331
6332
        return $return;
6333
    }
6334
6335
    /**
6336
     * Return HTML list to allow prerequisites selection for lp.
6337
     */
6338
    public function display_lp_prerequisites_list(FormValidator $form)
6339
    {
6340
        $lp_id = $this->lp_id;
6341
        $lp = api_get_lp_entity($lp_id);
6342
        $prerequisiteId = $lp->getPrerequisite();
6343
6344
        $repo = Container::getLpRepository();
6345
        $qb = $repo->findAllByCourse(api_get_course_entity(), api_get_session_entity());
6346
        /** @var CLp[] $lps */
6347
        $lps = $qb->getQuery()->getResult();
6348
6349
        //$session_id = api_get_session_id();
6350
        /*$session_condition = api_get_session_condition($session_id, true, true);
6351
        $sql = "SELECT * FROM $tbl_lp
6352
                WHERE c_id = $course_id $session_condition
6353
                ORDER BY display_order ";
6354
        $rs = Database::query($sql);*/
6355
6356
        $items = [get_lang('none')];
6357
        foreach ($lps as $lp) {
6358
            $myLpId = $lp->getIid();
6359
            if ($myLpId == $lp_id) {
6360
                continue;
6361
            }
6362
            $items[$myLpId] = $lp->getTitle();
6363
            /*$return .= '<option
6364
                value="'.$myLpId.'" '.(($myLpId == $prerequisiteId) ? ' selected ' : '').'>'.
6365
                $lp->getName().
6366
                '</option>';*/
6367
        }
6368
6369
        $select = $form->addSelect('prerequisites', get_lang('Prerequisites'), $items);
6370
        $select->setSelected($prerequisiteId);
6371
    }
6372
6373
    /**
6374
     * Creates a list with all the documents in it.
6375
     *
6376
     * @param bool $showInvisibleFiles
6377
     *
6378
     * @throws Exception
6379
     *
6380
     *
6381
     * @return string
6382
     */
6383
    public function get_documents($showInvisibleFiles = false)
6384
    {
6385
        $sessionId = api_get_session_id();
6386
        $documentTree = DocumentManager::get_document_preview(
6387
            api_get_course_entity(),
6388
            $this->lp_id,
6389
            null,
6390
            $sessionId,
6391
            true,
6392
            null,
6393
            null,
6394
            $showInvisibleFiles,
6395
            false,
6396
            false,
6397
            true,
6398
            false,
6399
            [],
6400
            [],
6401
            ['file', 'folder'],
6402
            true
6403
        );
6404
6405
        $form = new FormValidator(
6406
            'form_upload',
6407
            'POST',
6408
            $this->getCurrentBuildingModeURL(),
6409
            '',
6410
            ['enctype' => 'multipart/form-data']
6411
        );
6412
6413
        $folders = DocumentManager::get_all_document_folders(
6414
            api_get_course_info(),
6415
            0,
6416
            true
6417
        );
6418
6419
        $folder = $this->generate_lp_folder(api_get_course_info());
6420
6421
        DocumentManager::build_directory_selector(
6422
            $folders,
6423
            $folder->getIid(),
6424
            [],
6425
            true,
6426
            $form,
6427
            'directory_parent_id'
6428
        );
6429
6430
        $group = [
6431
            $form->createElement(
6432
                'radio',
6433
                'if_exists',
6434
                get_lang('If file exists:'),
6435
                get_lang('Do nothing'),
6436
                'nothing'
6437
            ),
6438
            $form->createElement(
6439
                'radio',
6440
                'if_exists',
6441
                null,
6442
                get_lang('Overwrite the existing file'),
6443
                'overwrite'
6444
            ),
6445
            $form->createElement(
6446
                'radio',
6447
                'if_exists',
6448
                null,
6449
                get_lang('Rename the uploaded file if it exists'),
6450
                'rename'
6451
            ),
6452
        ];
6453
        $form->addGroup($group, null, get_lang('If file exists:'));
6454
6455
        $fileExistsOption = api_get_setting('document.document_if_file_exists_option');
6456
        $defaultFileExistsOption = 'rename';
6457
        if (!empty($fileExistsOption)) {
6458
            $defaultFileExistsOption = $fileExistsOption;
6459
        }
6460
        $form->setDefaults(['if_exists' => $defaultFileExistsOption]);
6461
6462
        // Check box options
6463
        $form->addCheckBox(
6464
            'unzip',
6465
            get_lang('Options'),
6466
            get_lang('Uncompress zip')
6467
        );
6468
6469
        $url = api_get_path(WEB_AJAX_PATH).'document.ajax.php?'.api_get_cidreq().'&a=upload_file&curdirpath=';
6470
        $form->addMultipleUpload($url);
6471
6472
        $lpItem = (new CLpItem())
6473
            ->setTitle('')
6474
            ->setItemType(TOOL_DOCUMENT)
6475
        ;
6476
6477
        // Original document form (editor + fields).
6478
        $newForm = $this->displayDocumentForm('add', $lpItem);
6479
6480
        // New templates panel (left side).
6481
        $templatesPanel = $this->renderDocumentTemplatesPanel();
6482
6483
        if (!empty($templatesPanel)) {
6484
            // Two-column layout: templates on the left, form on the right.
6485
            $new = '
6486
            <div class="lp-document-create flex flex-col lg:flex-row gap-4">
6487
                <div class="w-full lg:w-1/3">
6488
                    '.$templatesPanel.'
6489
                </div>
6490
                <div class="w-full lg:w-2/3">
6491
                    '.$newForm.'
6492
                </div>
6493
            </div>
6494
        ';
6495
        } else {
6496
            // Fallback: only the form, as before.
6497
            $new = $newForm;
6498
        }
6499
6500
        $videosTree = $this->get_videos();
6501
        $headers = [
6502
            get_lang('Files'),
6503
            get_lang('Videos'),
6504
            get_lang('Create a new document'),
6505
            get_lang('Upload'),
6506
        ];
6507
6508
        return Display::tabs(
6509
            $headers,
6510
            [$documentTree, $videosTree, $new, $form->returnForm()],
6511
            'subtab',
6512
            ['class' => 'mt-3 lp-subtabs']
6513
        );
6514
    }
6515
6516
    /**
6517
     * Render system and course templates panel for LP document editor.
6518
     */
6519
    protected function renderDocumentTemplatesPanel(): string
6520
    {
6521
        $courseId = api_get_course_int_id();
6522
        if (empty($courseId)) {
6523
            return '';
6524
        }
6525
6526
        try {
6527
            $router = Container::getRouter();
6528
            $url = $router->generate('all-templates', ['courseId' => $courseId]);
6529
        } catch (\Throwable $e) {
6530
            return '';
6531
        }
6532
6533
        if (empty($url)) {
6534
            return '';
6535
        }
6536
6537
        $url = Security::remove_XSS($url);
6538
6539
        $html  = '<div id="lp-doc-template-panel"';
6540
        $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"';
6541
        $html .= ' data-templates-url="'.$url.'">';
6542
        $html .= '<div class="flex items-center justify-between mb-3">';
6543
        $html .= '<span class="font-semibold">'.get_lang('Templates').'</span>';
6544
        $html .= '</div>';
6545
        $html .= '<div id="lp-doc-template-list" class="space-y-2 max-h-96 overflow-y-auto"></div>';
6546
        $html .= '</div>';
6547
6548
        $html .= <<<'JS'
6549
        <script>
6550
        (function () {
6551
            var panel = document.getElementById('lp-doc-template-panel');
6552
            if (!panel) { return; }
6553
6554
            var listEl = document.getElementById('lp-doc-template-list');
6555
            var url = panel.getAttribute('data-templates-url');
6556
            if (!url || !listEl) { return; }
6557
6558
            function encodeContent(html) {
6559
                var value = html || '';
6560
                try {
6561
                    return btoa(unescape(encodeURIComponent(value)));
6562
                } catch (e) {
6563
                    return btoa(value);
6564
                }
6565
            }
6566
6567
            function decodeContent(encoded) {
6568
                if (!encoded) { return ''; }
6569
                try {
6570
                    return decodeURIComponent(escape(atob(encoded)));
6571
                } catch (e) {
6572
                    return atob(encoded);
6573
                }
6574
            }
6575
6576
            // Try to find the main HTML editor used in LP document form
6577
            function getCkEditorInstance() {
6578
                if (typeof CKEDITOR === 'undefined' || !CKEDITOR.instances) {
6579
                    return null;
6580
                }
6581
6582
                // Prefer common field names
6583
                var preferred = ['content_lp', 'content', 'content[content]'];
6584
                for (var i = 0; i < preferred.length; i++) {
6585
                    var key = preferred[i];
6586
                    if (CKEDITOR.instances[key]) {
6587
                        return CKEDITOR.instances[key];
6588
                    }
6589
                }
6590
6591
                // Fallback: first instance found
6592
                for (var id in CKEDITOR.instances) {
6593
                    if (Object.prototype.hasOwnProperty.call(CKEDITOR.instances, id)) {
6594
                        return CKEDITOR.instances[id];
6595
                    }
6596
                }
6597
6598
                return null;
6599
            }
6600
6601
            function applyTemplate(encoded) {
6602
                var content = decodeContent(encoded);
6603
6604
                // CKEditor
6605
                var ck = getCkEditorInstance();
6606
                if (ck && ck.setData) {
6607
                    ck.setData(content);
6608
                    return;
6609
                }
6610
6611
                // TinyMCE (just in case)
6612
                if (typeof tinyMCE !== 'undefined') {
6613
                    if (tinyMCE.activeEditor && tinyMCE.activeEditor.setContent) {
6614
                        tinyMCE.activeEditor.setContent(content);
6615
                        return;
6616
                    }
6617
                    if (tinyMCE.editors && tinyMCE.editors.length && tinyMCE.editors[0].setContent) {
6618
                        tinyMCE.editors[0].setContent(content);
6619
                        return;
6620
                    }
6621
                }
6622
6623
                // FCKeditor fallback
6624
                if (typeof FCKeditorAPI !== 'undefined' && FCKeditorAPI.GetInstance) {
6625
                    var fckNames = ['content_lp', 'content'];
6626
                    for (var i = 0; i < fckNames.length; i++) {
6627
                        var inst = FCKeditorAPI.GetInstance(fckNames[i]);
6628
                        if (inst && inst.SetHTML) {
6629
                            inst.SetHTML(content);
6630
                            return;
6631
                        }
6632
                    }
6633
                }
6634
6635
                // Plain textarea fallback
6636
                var txt =
6637
                    document.getElementById('content_lp') ||
6638
                    document.getElementById('content') ||
6639
                    document.querySelector('textarea[name="content_lp"], textarea[name="content"]');
6640
6641
                if (txt) {
6642
                    txt.value = content;
6643
                } else {
6644
                    console.warn(
6645
                        'LP document templates: no editor instance or textarea found to apply template content.'
6646
                    );
6647
                }
6648
            }
6649
6650
            function findTemplateButton(target) {
6651
                while (target && target !== panel) {
6652
                    if (target.classList && target.classList.contains('lp-template-pill')) {
6653
                        return target;
6654
                    }
6655
                    target = target.parentNode;
6656
                }
6657
                return null;
6658
            }
6659
6660
            fetch(url, { credentials: 'same-origin' })
6661
                .then(function (response) {
6662
                    return response.json();
6663
                })
6664
                .then(function (data) {
6665
                    if (!Array.isArray(data)) {
6666
                        return;
6667
                    }
6668
6669
                    if (data.length === 0) {
6670
                        listEl.classList.add('text-gray-50', 'text-xs');
6671
                        listEl.textContent = 'No templates found';
6672
                        return;
6673
                    }
6674
6675
                    data.forEach(function (tpl) {
6676
                        // Try different property names for content
6677
                        var rawContent = tpl.content || tpl.contentFile || tpl.html || '';
6678
                        var encoded = encodeContent(rawContent);
6679
6680
                        var btn = document.createElement('button');
6681
                        btn.type = 'button';
6682
                        btn.className =
6683
                            'lp-template-pill w-full text-left px-3 py-2 mb-2 rounded-lg border border-gray-25 ' +
6684
                            'bg-white text-xs text-gray-90 hover:border-primary hover:bg-support-1 hover:text-primary ' +
6685
                            'transition flex flex-col';
6686
                        btn.setAttribute('data-template', encoded);
6687
6688
                        if (tpl.image) {
6689
                            var img = document.createElement('img');
6690
                            img.src = tpl.image;
6691
                            img.alt = tpl.title || '';
6692
                            img.className = 'mb-2 rounded border border-gray-25 max-h-20 w-full object-cover';
6693
                            btn.appendChild(img);
6694
                        }
6695
6696
                        var span = document.createElement('span');
6697
                        span.className = 'truncate';
6698
                        span.textContent = tpl.title || '';
6699
                        btn.appendChild(span);
6700
6701
                        listEl.appendChild(btn);
6702
                    });
6703
                })
6704
                .catch(function (error) {
6705
                    console.error('Unable to load document templates for LP editor', error);
6706
                });
6707
6708
            panel.addEventListener('click', function (event) {
6709
                var button = findTemplateButton(event.target);
6710
                if (!button) {
6711
                    return;
6712
                }
6713
                var encoded = button.getAttribute('data-template');
6714
                if (!encoded) {
6715
                    return;
6716
                }
6717
                applyTemplate(encoded);
6718
            });
6719
        })();
6720
        </script>
6721
        JS;
6722
6723
        return $html;
6724
    }
6725
6726
6727
    public function get_videos()
6728
    {
6729
        $sessionId = api_get_session_id();
6730
6731
        $documentTree = DocumentManager::get_document_preview(
6732
            api_get_course_entity(),
6733
            $this->lp_id,
6734
            null,
6735
            $sessionId,
6736
            true,
6737
            null,
6738
            null,
6739
            false,
6740
            false,
6741
            false,
6742
            true,
6743
            false,
6744
            [],
6745
            [],
6746
            'video',
6747
            true
6748
        );
6749
6750
        return $documentTree ?: get_lang('No video found');
6751
    }
6752
6753
    /**
6754
     * Creates a list with all the exercises (quiz) in it.
6755
     *
6756
     * @return string
6757
     */
6758
    public function get_exercises()
6759
    {
6760
        $course_id = api_get_course_int_id();
6761
        $session_id = api_get_session_id();
6762
        $setting = 'true' === api_get_setting('lp.show_invisible_exercise_in_lp_toc');
6763
6764
        $active = 2;
6765
        if ($setting) {
6766
            $active = 1;
6767
        }
6768
        $keyword = $_REQUEST['keyword'] ?? null;
6769
        $categoryId = $_REQUEST['category_id'] ?? null;
6770
        $course = api_get_course_entity($course_id);
6771
        $session = api_get_session_entity($session_id);
6772
6773
        $qb = Container::getQuizRepository()->findAllByCourse($course, $session, $keyword, $active, false, $categoryId);
6774
        /** @var CQuiz[] $exercises */
6775
        $exercises = $qb->getQuery()->getResult();
6776
6777
        $return = '<ul class="mt-2 bg-white list-group list-group-flush border border-gray-25 rounded lp_resource">';
6778
        $return .= '<li class="list-group-item lp_resource_element border-gray-25 disable_drag">';
6779
        $return .= Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, 32, get_lang('New test'));
6780
        $return .= '<a
6781
            href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise_admin.php?'.api_get_cidreq().'&lp_id='.$this->lp_id.'">'.
6782
            get_lang('New test').'</a>';
6783
        $return .= '</li>';
6784
6785
        $previewIcon = Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview'));
6786
        $quizIcon = Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, 16, get_lang('Test'));
6787
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6788
        $exerciseUrl = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq();
6789
        foreach ($exercises as $exercise) {
6790
            $exerciseId = $exercise->getIid();
6791
            $title = strip_tags(api_html_entity_decode($exercise->getTitle()));
6792
            $visibility = $exercise->isVisible($course, $session);
6793
6794
            $link = Display::url(
6795
                $previewIcon,
6796
                $exerciseUrl.'&exerciseId='.$exerciseId,
6797
                ['target' => '_blank']
6798
            );
6799
            $return .= '<li
6800
                class="list-group-item lp_resource_element border-gray-25"
6801
                id="'.$exerciseId.'"
6802
                data-id="'.$exerciseId.'"
6803
                title="'.$title.'">';
6804
            $return .= Display::url($moveIcon, '#', ['class' => 'moved']);
6805
            $return .= $quizIcon;
6806
            $sessionStar = '';
6807
            $return .= Display::url(
6808
                Security::remove_XSS(cut($title, 80)).$link.$sessionStar,
6809
                api_get_self().'?'.
6810
                    api_get_cidreq().'&action=add_item&type='.TOOL_QUIZ.'&file='.$exerciseId.'&lp_id='.$this->lp_id,
6811
                [
6812
                    'class' => false === $visibility ? 'moved text-muted ' : 'moved link_with_id',
6813
                    'data_type' => 'quiz',
6814
                    'data-id' => $exerciseId,
6815
                ]
6816
            );
6817
            $return .= '</li>';
6818
        }
6819
6820
        $return .= '</ul>';
6821
6822
        return $return;
6823
    }
6824
6825
    /**
6826
     * Creates a list with all the links in it.
6827
     *
6828
     * @return string
6829
     */
6830
    public function get_links()
6831
    {
6832
        $sessionId = api_get_session_id();
6833
        $repo = Container::getLinkRepository();
6834
6835
        $course = api_get_course_entity();
6836
        $session = api_get_session_entity($sessionId);
6837
        $qb = $repo->getResourcesByCourse($course, $session);
6838
        /** @var CLink[] $links */
6839
        $links = $qb->getQuery()->getResult();
6840
6841
        $selfUrl = api_get_self();
6842
        $courseIdReq = api_get_cidreq();
6843
        $userInfo = api_get_user_info();
6844
6845
        $moveEverywhereIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6846
6847
        $categorizedLinks = [];
6848
        $categories = [];
6849
6850
        foreach ($links as $link) {
6851
            $categoryId = null !== $link->getCategory() ? $link->getCategory()->getIid() : 0;
6852
            if (empty($categoryId)) {
6853
                $categories[0] = get_lang('Uncategorized');
6854
            } else {
6855
                $category = $link->getCategory();
6856
                $categories[$categoryId] = $category->getTitle();
6857
            }
6858
            $categorizedLinks[$categoryId][$link->getIid()] = $link;
6859
        }
6860
6861
        $linksHtmlCode =
6862
            '<script>
6863
            function toggle_tool(tool, id) {
6864
                if(document.getElementById(tool+"_"+id+"_content").style.display == "none"){
6865
                    document.getElementById(tool+"_"+id+"_content").style.display = "block";
6866
                    document.getElementById(tool+"_"+id+"_opener").src = "'.Display::returnIconPath('remove.gif').'";
6867
                } else {
6868
                    document.getElementById(tool+"_"+id+"_content").style.display = "none";
6869
                    document.getElementById(tool+"_"+id+"_opener").src = "'.Display::returnIconPath('add.png').'";
6870
                }
6871
            }
6872
        </script>
6873
6874
        <ul class="mt-2 bg-white list-group lp_resource">
6875
            <li class="list-group-item lp_resource_element border-gray-25 disable_drag ">
6876
                '.Display::getMdiIcon(ObjectIcon::LINK, 'ch-tool-icon', null, ICON_SIZE_SMALL).'
6877
                <a
6878
                href="'.api_get_path(WEB_CODE_PATH).'link/link.php?'.$courseIdReq.'&action=addlink&lp_id='.$this->lp_id.'"
6879
                title="'.get_lang('Add a link').'">'.
6880
                get_lang('Add a link').'
6881
                </a>
6882
            </li>';
6883
        $linkIcon = Display::getMdiIcon('file-link', 'ch-tool-icon', null, 16, get_lang('Link'));
6884
        foreach ($categorizedLinks as $categoryId => $links) {
6885
            $linkNodes = null;
6886
            /** @var CLink $link */
6887
            foreach ($links as $key => $link) {
6888
                $title = $link->getTitle();
6889
                $id = $link->getIid();
6890
                $linkUrl = Display::url(
6891
                    Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6892
                    api_get_path(WEB_CODE_PATH).'link/link_goto.php?'.api_get_cidreq().'&link_id='.$key,
6893
                    ['target' => '_blank']
6894
                );
6895
6896
                if ($link->isVisible($course, $session)) {
6897
                    $sessionStar = '';
6898
                    $url = $selfUrl.'?'.$courseIdReq.'&action=add_item&type='.TOOL_LINK.'&file='.$key.'&lp_id='.$this->lp_id;
6899
                    $link = Display::url(
6900
                        Security::remove_XSS($title).$sessionStar.$linkUrl,
6901
                        $url,
6902
                        [
6903
                            'class' => 'moved link_with_id',
6904
                            'data-id' => $key,
6905
                            'data_type' => TOOL_LINK,
6906
                            'title' => $title,
6907
                        ]
6908
                    );
6909
                    $linkNodes .=
6910
                        "<li
6911
                            class='list-group-item border-gray-25 lp_resource_element'
6912
                            id= $id
6913
                            data-id= $id
6914
                            >
6915
                         <a class='moved' href='#'>
6916
                            $moveEverywhereIcon
6917
                        </a>
6918
                        $linkIcon $link
6919
                        </li>";
6920
                }
6921
            }
6922
            $linksHtmlCode .=
6923
                '<li class="list-group-item border-gray-25 disable_drag">
6924
                    <a style="cursor:hand" onclick="javascript: toggle_tool(\''.TOOL_LINK.'\','.$categoryId.')" >
6925
                        <img src="'.Display::returnIconPath('add.png').'" id="'.TOOL_LINK.'_'.$categoryId.'_opener"
6926
                        align="absbottom" />
6927
                    </a>
6928
                    <span style="vertical-align:middle">'.Security::remove_XSS($categories[$categoryId]).'</span>
6929
                </li>
6930
            '.
6931
                $linkNodes.
6932
            '';
6933
        }
6934
        $linksHtmlCode .= '</ul>';
6935
6936
        return $linksHtmlCode;
6937
    }
6938
6939
    /**
6940
     * Creates a list with all the student publications in it.
6941
     *
6942
     * @return string
6943
     */
6944
    public function get_student_publications()
6945
    {
6946
        $return = '<ul class="mt-2 bg-white list-group list-group-flush border border-gray-25 rounded lp_resource">';
6947
        $return .= '<li class="list-group-item border-gray-25 lp_resource_element">';
6948
        $works = getWorkListTeacher(0, 100, null, null, null);
6949
        if (!empty($works)) {
6950
            $icon = Display::getMdiIcon('inbox-full', 'ch-tool-icon',null, 16, get_lang('Assignments'));
6951
            foreach ($works as $work) {
6952
                $workId = $work['iid'];
6953
                $link = Display::url(
6954
                    Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6955
                    api_get_path(WEB_CODE_PATH).'work/work_list_all.php?'.api_get_cidreq().'&id='.$workId,
6956
                    ['target' => '_blank']
6957
                );
6958
6959
                $return .= '<li
6960
                    class="list-group-item border-gray-25 lp_resource_element"
6961
                    id="'.$workId.'"
6962
                    data-id="'.$workId.'"
6963
                    >';
6964
                $return .= '<a class="moved" href="#">';
6965
                $return .= Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6966
                $return .= '</a> ';
6967
6968
                $return .= $icon;
6969
                $return .= Display::url(
6970
                    Security::remove_XSS(cut(strip_tags($work['title']), 80)).' '.$link,
6971
                    api_get_self().'?'.
6972
                    api_get_cidreq().'&action=add_item&type='.TOOL_STUDENTPUBLICATION.'&file='.$work['iid'].'&lp_id='.$this->lp_id,
6973
                    [
6974
                        'class' => 'moved link_with_id',
6975
                        'data-id' => $work['iid'],
6976
                        'data_type' => TOOL_STUDENTPUBLICATION,
6977
                        'title' => Security::remove_XSS(cut(strip_tags($work['title']), 80)),
6978
                    ]
6979
                );
6980
                $return .= '</li>';
6981
            }
6982
        }
6983
6984
        $return .= '</ul>';
6985
6986
        return $return;
6987
    }
6988
6989
    /**
6990
     * Creates a list with all the forums in it.
6991
     *
6992
     * @return string
6993
     */
6994
    public function get_forums()
6995
    {
6996
        $forumCategories = get_forum_categories();
6997
        $forumsInNoCategory = get_forums_in_category(0);
6998
        if (!empty($forumsInNoCategory)) {
6999
            $forumCategories = array_merge(
7000
                $forumCategories,
7001
                [
7002
                    [
7003
                        'cat_id' => 0,
7004
                        'session_id' => 0,
7005
                        'visibility' => 1,
7006
                        'cat_comment' => null,
7007
                    ],
7008
                ]
7009
            );
7010
        }
7011
7012
        $a_forums = [];
7013
        $courseEntity = api_get_course_entity(api_get_course_int_id());
7014
        $sessionEntity = api_get_session_entity(api_get_session_id());
7015
7016
        foreach ($forumCategories as $forumCategory) {
7017
            // The forums in this category.
7018
            $forumsInCategory = get_forums_in_category($forumCategory->getIid());
7019
            if (!empty($forumsInCategory)) {
7020
                foreach ($forumsInCategory as $forum) {
7021
                    if ($forum->isVisible($courseEntity, $sessionEntity)) {
7022
                        $a_forums[] = $forum;
7023
                    }
7024
                }
7025
            }
7026
        }
7027
7028
        $return = '<ul class="mt-2 bg-white list-group list-group-flush border border-gray-25 rounded lp_resource">';
7029
7030
        // First add link
7031
        $return .= '<li class="list-group-item border-gray-25 lp_resource_element disable_drag">';
7032
        $return .= Display::getMdiIcon('comment-quote	', 'ch-tool-icon', null, 32, get_lang('Create a new forum'));
7033
        $return .= Display::url(
7034
            get_lang('Create a new forum'),
7035
            api_get_path(WEB_CODE_PATH).'forum/index.php?'.api_get_cidreq().'&'.http_build_query([
7036
                'action' => 'add',
7037
                'content' => 'forum',
7038
                'lp_id' => $this->lp_id,
7039
            ]),
7040
            ['title' => get_lang('Create a new forum')]
7041
        );
7042
        $return .= '</li>';
7043
7044
        $return .= '<script>
7045
            function toggle_forum(forum_id) {
7046
                if (document.getElementById("forum_"+forum_id+"_content").style.display == "none") {
7047
                    document.getElementById("forum_"+forum_id+"_content").style.display = "block";
7048
                    document.getElementById("forum_"+forum_id+"_opener").src = "'.Display::returnIconPath('remove.gif').'";
7049
                } else {
7050
                    document.getElementById("forum_"+forum_id+"_content").style.display = "none";
7051
                    document.getElementById("forum_"+forum_id+"_opener").src = "'.Display::returnIconPath('add.png').'";
7052
                }
7053
            }
7054
        </script>';
7055
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
7056
        $userRights = api_is_allowed_to_edit(false, true);
7057
        foreach ($a_forums as $forum) {
7058
            $forumSession = $forum->getFirstResourceLink()->getSession();
7059
            $isForumSession = (null !== $forumSession);
7060
            $forumId = $forum->getIid();
7061
            $title = Security::remove_XSS($forum->getTitle());
7062
            $link = Display::url(
7063
                Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
7064
                api_get_path(WEB_CODE_PATH).'forum/viewforum.php?'.api_get_cidreq().'&forum='.$forumId,
7065
                ['target' => '_blank']
7066
            );
7067
7068
            $return .= '<li
7069
                    class="list-group-item border-gray-25 lp_resource_element"
7070
                    id="'.$forumId.'"
7071
                    data-id="'.$forumId.'"
7072
                    >';
7073
            $return .= '<a class="moved" href="#">';
7074
            $return .= $moveIcon;
7075
            $return .= ' </a>';
7076
            $return .= Display::getMdiIcon('comment-quote', 'ch-tool-icon', null, 16, get_lang('Forum'));
7077
7078
            $moveLink = Display::url(
7079
                $title,
7080
                api_get_self().'?'.
7081
                api_get_cidreq().'&action=add_item&type='.TOOL_FORUM.'&forum_id='.$forumId.'&lp_id='.$this->lp_id,
7082
                [
7083
                    'class' => 'moved link_with_id',
7084
                    'data-id' => $forumId,
7085
                    'data_type' => TOOL_FORUM,
7086
                    'title' => $title,
7087
                    'style' => 'vertical-align:middle',
7088
                ]
7089
            );
7090
            $return .= '<a onclick="javascript:toggle_forum('.$forumId.');" style="cursor:hand; vertical-align:middle">
7091
                    <img
7092
                        src="'.Display::returnIconPath('add.png').'"
7093
                        id="forum_'.$forumId.'_opener" align="absbottom"
7094
                     />
7095
                </a>
7096
                '.$moveLink;
7097
            $return .= '</li>';
7098
7099
            $return .= '<div style="display:none" id="forum_'.$forumId.'_content">';
7100
            $threads = get_threads($forumId);
7101
            if (is_array($threads)) {
7102
                foreach ($threads as $thread) {
7103
                    $threadId = $thread->getIid();
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).
7107
                        'forum/viewthread.php?'.api_get_cidreq().'&forum='.$forumId.'&thread='.$threadId,
7108
                        ['target' => '_blank']
7109
                    );
7110
7111
                    $return .= '<li
7112
                        class="list-group-item border-gray-25 lp_resource_element"
7113
                      id="'.$threadId.'"
7114
                        data-id="'.$threadId.'"
7115
                    >';
7116
                    $return .= '&nbsp;<a class="moved" href="#">';
7117
                    $return .= $moveIcon;
7118
                    $return .= ' </a>';
7119
                    $return .= Display::getMdiIcon('format-quote-open', 'ch-tool-icon', null, 16, get_lang('Thread'));
7120
                    $return .= '<a
7121
                        class="moved link_with_id"
7122
                        data-id="'.$threadId.'"
7123
                        data_type="'.TOOL_THREAD.'"
7124
                        title="'.$thread->getTitle().'"
7125
                        href="'.api_get_self().'?'.api_get_cidreq().'&action=add_item&type='.TOOL_THREAD.'&thread_id='.$threadId.'&lp_id='.$this->lp_id.'"
7126
                        >'.
7127
                        Security::remove_XSS($thread->getTitle()).' '.$link.'</a>';
7128
                    $return .= '</li>';
7129
                }
7130
            }
7131
            $return .= '</div>';
7132
        }
7133
        $return .= '</ul>';
7134
7135
        return $return;
7136
    }
7137
7138
    /**
7139
     * Creates a list with all the surveys in it.
7140
     *
7141
     * @return string
7142
     */
7143
    public function getSurveys()
7144
    {
7145
        $return = '<ul class="mt-2 bg-white list-group list-group-flush border border-gray-25 rounded lp_resource">';
7146
7147
        // First add link
7148
        $return .= '<li class="list-group-item border-gray-25 lp_resource_element disable_drag">';
7149
        $return .= Display::getMdiIcon('clipboard-question-outline', 'ch-tool-icon', null, 32, get_lang('Create survey'));
7150
        $return .= Display::url(
7151
            get_lang('Create survey'),
7152
            api_get_path(WEB_CODE_PATH).'survey/create_new_survey.php?'.api_get_cidreq().'&'.http_build_query([
7153
                'action' => 'add',
7154
                'lp_id' => $this->lp_id,
7155
            ]),
7156
            ['title' => get_lang('Create survey')]
7157
        );
7158
        $return .= '</li>';
7159
7160
        $surveys = SurveyManager::get_surveys(api_get_course_id(), api_get_session_id());
7161
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
7162
7163
        foreach ($surveys as $survey) {
7164
            if (!empty($survey['iid'])) {
7165
                $surveyTitle = strip_tags($survey['title']);
7166
                $return .= '<li class="list-group-item border-gray-25 lp_resource_element" id="'.$survey['iid'].'" data-id="'.$survey['iid'].'">';
7167
                $return .= '<a class="moved" href="#">';
7168
                $return .= $moveIcon;
7169
                $return .= ' </a>';
7170
                $return .= Display::getMdiIcon('poll', 'ch-tool-icon', null, 16, get_lang('Survey'));
7171
                $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>';
7172
                $return .= '</li>';
7173
            }
7174
        }
7175
7176
        $return .= '</ul>';
7177
7178
        return $return;
7179
    }
7180
7181
    /**
7182
     * Temp function to be moved in main_api or the best place around for this.
7183
     * Creates a file path if it doesn't exist.
7184
     *
7185
     * @param string $path
7186
     */
7187
    public function create_path($path)
7188
    {
7189
        $path_bits = explode('/', dirname($path));
7190
7191
        // IS_WINDOWS_OS has been defined in main_api.lib.php
7192
        $path_built = IS_WINDOWS_OS ? '' : '/';
7193
        foreach ($path_bits as $bit) {
7194
            if (!empty($bit)) {
7195
                $new_path = $path_built.$bit;
7196
                if (is_dir($new_path)) {
7197
                    $path_built = $new_path.'/';
7198
                } else {
7199
                    mkdir($new_path, api_get_permissions_for_new_directories());
7200
                    $path_built = $new_path.'/';
7201
                }
7202
            }
7203
        }
7204
    }
7205
7206
    /**
7207
     * @param int    $lp_id
7208
     * @param string $status
7209
     */
7210
    public function set_autolaunch($lp_id, $status)
7211
    {
7212
        $status = (int) $status;
7213
        $em = Database::getManager();
7214
        $repo = Container::getLpRepository();
7215
7216
        $session = api_get_session_entity();
7217
        $course = api_get_course_entity();
7218
7219
        $qb = $repo->getResourcesByCourse($course, $session);
7220
        $lps = $qb->getQuery()->getResult();
7221
7222
        foreach ($lps as $lp) {
7223
            $lp->setAutoLaunch(0);
7224
            $em->persist($lp);
7225
        }
7226
7227
        $em->flush();
7228
7229
        if ($status === 1) {
7230
            $lp = $repo->find($lp_id);
7231
            if ($lp) {
7232
                $lp->setAutolaunch(1);
7233
                $em->persist($lp);
7234
            }
7235
            $em->flush();
7236
        }
7237
    }
7238
7239
    /**
7240
     * Gets previous_item_id for the next element of the lp_item table.
7241
     *
7242
     * @author Isaac flores paz
7243
     *
7244
     * @return int Previous item ID
7245
     */
7246
    public function select_previous_item_id()
7247
    {
7248
        $course_id = api_get_course_int_id();
7249
        $table_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7250
7251
        // Get the max order of the items
7252
        $sql = "SELECT max(display_order) AS display_order FROM $table_lp_item
7253
                WHERE c_id = $course_id AND lp_id = ".$this->lp_id;
7254
        $rs_max_order = Database::query($sql);
7255
        $row_max_order = Database::fetch_object($rs_max_order);
7256
        $max_order = $row_max_order->display_order;
7257
        // Get the previous item ID
7258
        $sql = "SELECT iid as previous FROM $table_lp_item
7259
                WHERE
7260
                    c_id = $course_id AND
7261
                    lp_id = ".$this->lp_id." AND
7262
                    display_order = '$max_order' ";
7263
        $rs_max = Database::query($sql);
7264
        $row_max = Database::fetch_object($rs_max);
7265
7266
        // Return the previous item ID
7267
        return $row_max->previous;
7268
    }
7269
7270
    /**
7271
     * Copies an LP.
7272
     */
7273
    public function copy()
7274
    {
7275
        // Course builder
7276
        $cb = new CourseBuilder();
7277
7278
        //Setting tools that will be copied
7279
        $cb->set_tools_to_build(['learnpaths']);
7280
7281
        //Setting elements that will be copied
7282
        $cb->set_tools_specific_id_list(
7283
            ['learnpaths' => [$this->lp_id]]
7284
        );
7285
7286
        $course = $cb->build();
7287
7288
        //Course restorer
7289
        $course_restorer = new CourseRestorer($course);
7290
        $course_restorer->set_add_text_in_items(true);
7291
        $course_restorer->set_tool_copy_settings(
7292
            ['learnpaths' => ['reset_dates' => true]]
7293
        );
7294
        $course_restorer->restore(
7295
            api_get_course_id(),
7296
            api_get_session_id(),
7297
            false,
7298
            false
7299
        );
7300
    }
7301
7302
    public static function getQuotaInfo(string $localFilePath): array
7303
    {
7304
        $post_max_raw   = ini_get('post_max_size');
7305
        $post_max_bytes = (int) rtrim($post_max_raw, 'MG') * (str_ends_with($post_max_raw,'G') ? 1024**3 : 1024**2);
7306
        $upload_max_raw = ini_get('upload_max_filesize');
7307
        $upload_max_bytes = (int) rtrim($upload_max_raw, 'MG') * (str_ends_with($upload_max_raw,'G') ? 1024**3 : 1024**2);
7308
7309
        $em     = Database::getManager();
7310
        $course = api_get_course_entity(api_get_course_int_id());
7311
7312
        $nodes  = Container::getResourceNodeRepository()->findByResourceTypeAndCourse('file', $course);
7313
        $root   = null;
7314
        foreach ($nodes as $n) {
7315
            if ($n->getParent() === null) {
7316
                $root = $n; break;
7317
            }
7318
        }
7319
        $docsSize = $root
7320
            ? Container::getDocumentRepository()->getFolderSize($root, $course)
7321
            : 0;
7322
7323
        $assetRepo = Container::getAssetRepository();
7324
        $fs        = $assetRepo->getFileSystem();
7325
        $scormSize = 0;
7326
        foreach (Container::getLpRepository()->findScormByCourse($course) as $lp) {
7327
            $asset = $lp->getAsset();
7328
            if (!$asset) {
7329
                continue;
7330
            }
7331
7332
            // Path may point to an extracted folder or a .zip file
7333
            $path = $assetRepo->getFolder($asset);
7334
            if (!$path) {
7335
                continue;
7336
            }
7337
7338
            try {
7339
                if ($fs->directoryExists($path)) {
7340
                    // Extracted SCORM folder
7341
                    $scormSize += self::getFolderSize($path);
7342
                    continue;
7343
                }
7344
                if ($fs->fileExists($path)) {
7345
                    // SCORM .zip file
7346
                    $scormSize += (int) $fs->fileSize($path);
7347
                    continue;
7348
                }
7349
7350
                // Local filesystem fallbacks
7351
                if (@is_dir($path)) {
7352
                    $scormSize += self::getFolderSize($path);
7353
                    continue;
7354
                }
7355
                if (@is_file($path)) {
7356
                    $size = @filesize($path);
7357
                    if ($size !== false) {
7358
                        $scormSize += (int) $size;
7359
                        continue;
7360
                    }
7361
                }
7362
7363
                // Only log when we truly cannot resolve the size
7364
                error_log('[Learnpath::getQuotaInfo] Unable to resolve SCORM size (path not found or unreadable): '.$path);
7365
            } catch (\Throwable $e) {
7366
                error_log('[Learnpath::getQuotaInfo] Exception while resolving SCORM size for path '.$path.' - '.$e->getMessage());
7367
            }
7368
        }
7369
7370
        $uploadedSize = filesize($localFilePath);
7371
        $existingTotal = $docsSize + $scormSize;
7372
        $combined = $existingTotal + $uploadedSize;
7373
7374
        $quotaMb = DocumentManager::get_course_quota();
7375
        $quotaBytes = $quotaMb * 1024 * 1024;
7376
7377
        return [
7378
            'post_max'      => $post_max_bytes,
7379
            'upload_max'    => $upload_max_bytes,
7380
            'docs_size'     => $docsSize,
7381
            'scorm_size'    => $scormSize,
7382
            'existing_total'=> $existingTotal,
7383
            'uploaded_size' => $uploadedSize,
7384
            'combined'      => $combined,
7385
            'quota_bytes'   => $quotaBytes,
7386
        ];
7387
    }
7388
7389
    /**
7390
     * Verify document size.
7391
     */
7392
    public static function verify_document_size(string $localFilePath): bool
7393
    {
7394
        $info = self::getQuotaInfo($localFilePath);
7395
        if ($info['uploaded_size'] > $info['post_max']
7396
            || $info['uploaded_size'] > $info['upload_max']
7397
            || $info['combined']    > $info['quota_bytes']
7398
        ) {
7399
            Container::getSession()->set('quota_info', $info);
7400
            return true;
7401
        }
7402
7403
        return false;
7404
    }
7405
7406
    private static function getFolderSize(string $path): int
7407
    {
7408
        $size     = 0;
7409
        $iterator = new \RecursiveIteratorIterator(
7410
            new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
7411
        );
7412
        foreach ($iterator as $file) {
7413
            if ($file->isFile()) {
7414
                $size += $file->getSize();
7415
            }
7416
        }
7417
        return $size;
7418
    }
7419
7420
    /**
7421
     * Clear LP prerequisites.
7422
     */
7423
    public function clearPrerequisites()
7424
    {
7425
        $course_id = $this->get_course_int_id();
7426
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7427
        $lp_id = $this->get_id();
7428
        // Cleaning prerequisites
7429
        $sql = "UPDATE $tbl_lp_item SET prerequisite = ''
7430
                WHERE lp_id = $lp_id";
7431
        Database::query($sql);
7432
7433
        // Cleaning mastery score for exercises
7434
        $sql = "UPDATE $tbl_lp_item SET mastery_score = ''
7435
                WHERE lp_id = $lp_id AND item_type = 'quiz'";
7436
        Database::query($sql);
7437
    }
7438
7439
    public function set_previous_step_as_prerequisite_for_all_items()
7440
    {
7441
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7442
        $course_id = $this->get_course_int_id();
7443
        $lp_id = $this->get_id();
7444
7445
        if (!empty($this->items)) {
7446
            $previous_item_id = null;
7447
            $previous_item_max = 0;
7448
            $previous_item_type = null;
7449
            $last_item_not_dir = null;
7450
            $last_item_not_dir_type = null;
7451
            $last_item_not_dir_max = null;
7452
7453
            foreach ($this->ordered_items as $itemId) {
7454
                $item = $this->getItem($itemId);
7455
                // if there was a previous item... (otherwise jump to set it)
7456
                if (!empty($previous_item_id)) {
7457
                    $current_item_id = $item->get_id(); //save current id
7458
                    if ('dir' != $item->get_type()) {
7459
                        // Current item is not a folder, so it qualifies to get a prerequisites
7460
                        if ('quiz' == $last_item_not_dir_type) {
7461
                            // if previous is quiz, mark its max score as default score to be achieved
7462
                            $sql = "UPDATE $tbl_lp_item SET mastery_score = '$last_item_not_dir_max'
7463
                                    WHERE c_id = $course_id AND lp_id = $lp_id AND iid = $last_item_not_dir";
7464
                            Database::query($sql);
7465
                        }
7466
                        // now simply update the prerequisite to set it to the last non-chapter item
7467
                        $sql = "UPDATE $tbl_lp_item SET prerequisite = '$last_item_not_dir'
7468
                                WHERE lp_id = $lp_id AND iid = $current_item_id";
7469
                        Database::query($sql);
7470
                        // record item as 'non-chapter' reference
7471
                        $last_item_not_dir = $item->get_id();
7472
                        $last_item_not_dir_type = $item->get_type();
7473
                        $last_item_not_dir_max = $item->get_max();
7474
                    }
7475
                } else {
7476
                    if ('dir' != $item->get_type()) {
7477
                        // Current item is not a folder (but it is the first item) so record as last "non-chapter" item
7478
                        $last_item_not_dir = $item->get_id();
7479
                        $last_item_not_dir_type = $item->get_type();
7480
                        $last_item_not_dir_max = $item->get_max();
7481
                    }
7482
                }
7483
                // Saving the item as "previous item" for the next loop
7484
                $previous_item_id = $item->get_id();
7485
                $previous_item_max = $item->get_max();
7486
                $previous_item_type = $item->get_type();
7487
            }
7488
        }
7489
    }
7490
7491
    /**
7492
     * @param array $params
7493
     *
7494
     * @return int
7495
     */
7496
    public static function createCategory($params)
7497
    {
7498
        $courseEntity = api_get_course_entity(api_get_course_int_id());
7499
7500
        $item = new CLpCategory();
7501
        $item
7502
            ->setTitle($params['name'])
7503
            ->setParent($courseEntity)
7504
            ->addCourseLink($courseEntity, api_get_session_entity())
7505
        ;
7506
7507
        $repo = Container::getLpCategoryRepository();
7508
        $repo->create($item);
7509
7510
        return $item->getIid();
7511
    }
7512
7513
    /**
7514
     * @param array $params
7515
     */
7516
    public static function updateCategory($params)
7517
    {
7518
        $em = Database::getManager();
7519
        /** @var CLpCategory $item */
7520
        $item = $em->find(CLpCategory::class, $params['id']);
7521
        if ($item) {
7522
            $item->setTitle($params['name']);
7523
            $em->persist($item);
7524
            $em->flush();
7525
        }
7526
    }
7527
7528
    public static function moveUpCategory(int $id): void
7529
    {
7530
        $em = Database::getManager();
7531
        /** @var CLpCategory $item */
7532
        $item = $em->find(CLpCategory::class, $id);
7533
        if ($item) {
7534
            $course = api_get_course_entity();
7535
            $session = api_get_session_entity();
7536
7537
            $link = $item->resourceNode->getResourceLinkByContext($course, $session);
7538
7539
            if ($link) {
7540
                $link->moveUpPosition();
7541
7542
                $em->flush();
7543
            }
7544
        }
7545
    }
7546
7547
    public static function moveDownCategory(int $id): void
7548
    {
7549
        $em = Database::getManager();
7550
        /** @var CLpCategory $item */
7551
        $item = $em->find(CLpCategory::class, $id);
7552
        if ($item) {
7553
            $course = api_get_course_entity();
7554
            $session = api_get_session_entity();
7555
7556
            $link = $item->resourceNode->getResourceLinkByContext($course, $session);
7557
7558
            if ($link) {
7559
                $link->moveDownPosition();
7560
7561
                $em->flush();
7562
            }
7563
        }
7564
    }
7565
7566
    /**
7567
     * @param int $courseId
7568
     *
7569
     * @return int
7570
     */
7571
    public static function getCountCategories($courseId)
7572
    {
7573
        if (empty($courseId)) {
7574
            return 0;
7575
        }
7576
        $repo = Container::getLpCategoryRepository();
7577
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId));
7578
        $qb->addSelect('count(resource)');
7579
7580
        return (int) $qb->getQuery()->getSingleScalarResult();
7581
    }
7582
7583
    /**
7584
     * @param int $courseId
7585
     *
7586
     * @return CLpCategory[]
7587
     */
7588
    public static function getCategories($courseId)
7589
    {
7590
        // Using doctrine extensions
7591
        $repo = Container::getLpCategoryRepository();
7592
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId), api_get_session_entity(), null, null, true, true);
7593
7594
        return $qb->getQuery()->getResult();
7595
    }
7596
7597
    public static function getCategorySessionId($id)
7598
    {
7599
        if ('true' !== api_get_setting('lp.allow_session_lp_category')) {
7600
            return 0;
7601
        }
7602
7603
        $repo = Container::getLpCategoryRepository();
7604
        /** @var CLpCategory $category */
7605
        $category = $repo->find($id);
7606
7607
        $sessionId = 0;
7608
        $link = $category->getFirstResourceLink();
7609
        if ($link && $link->getSession()) {
7610
            $sessionId = (int) $link->getSession()->getId();
7611
        }
7612
7613
        return $sessionId;
7614
    }
7615
7616
    public static function deleteCategory(int $id): bool
7617
    {
7618
        $repo = Container::getLpCategoryRepository();
7619
        /** @var CLpCategory $category */
7620
        $category = $repo->find($id);
7621
        if ($category) {
7622
            $em = Database::getManager();
7623
            $lps = $category->getLps();
7624
7625
            foreach ($lps as $lp) {
7626
                $lp->setCategory(null);
7627
                $em->persist($lp);
7628
            }
7629
7630
            $course = api_get_course_entity();
7631
            $session = api_get_session_entity();
7632
7633
            $em->getRepository(ResourceLink::class)->removeByResourceInContext($category, $course, $session);
7634
7635
            return true;
7636
        }
7637
7638
        return false;
7639
    }
7640
7641
    /**
7642
     * @param int  $courseId
7643
     * @param bool $addSelectOption
7644
     *
7645
     * @return array
7646
     */
7647
    public static function getCategoryFromCourseIntoSelect($courseId, $addSelectOption = false)
7648
    {
7649
        $repo = Container::getLpCategoryRepository();
7650
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId), api_get_session_entity());
7651
        $items = $qb->getQuery()->getResult();
7652
7653
        $cats = [];
7654
        if ($addSelectOption) {
7655
            $cats = [get_lang('Select a category')];
7656
        }
7657
7658
        if (!empty($items)) {
7659
            foreach ($items as $cat) {
7660
                $cats[$cat->getIid()] = $cat->getTitle();
7661
            }
7662
        }
7663
7664
        return $cats;
7665
    }
7666
7667
    /**
7668
     * @param int   $courseId
7669
     * @param int   $lpId
7670
     * @param int   $user_id
7671
     *
7672
     * @return learnpath
7673
     */
7674
    public static function getLpFromSession(int $courseId, int $lpId, int $user_id)
7675
    {
7676
        $debug = 0;
7677
        $learnPath = null;
7678
        $lpObject = Session::read('lpobject');
7679
7680
        $repo = Container::getLpRepository();
7681
        $lp = $repo->find($lpId);
7682
        if (null !== $lpObject) {
7683
            /** @var learnpath $learnPath */
7684
            $learnPath = UnserializeApi::unserialize('lp', $lpObject);
7685
            $learnPath->entity = $lp;
7686
            if ($debug) {
7687
                error_log('getLpFromSession: unserialize');
7688
                error_log('------getLpFromSession------');
7689
                error_log('------unserialize------');
7690
                error_log("lp_view_session_id: ".$learnPath->lp_view_session_id);
7691
                error_log("api_get_sessionid: ".api_get_session_id());
7692
            }
7693
        }
7694
7695
        if (!is_object($learnPath)) {
7696
            $learnPath = new learnpath($lp, api_get_course_info_by_id($courseId), $user_id);
7697
            if ($debug) {
7698
                error_log('------getLpFromSession------');
7699
                error_log('getLpFromSession: create new learnpath');
7700
                error_log("create new LP with $courseId - $lpId - $user_id");
7701
                error_log("lp_view_session_id: ".$learnPath->lp_view_session_id);
7702
                error_log("api_get_sessionid: ".api_get_session_id());
7703
            }
7704
        }
7705
7706
        return $learnPath;
7707
    }
7708
7709
    /**
7710
     * @param int $itemId
7711
     *
7712
     * @return learnpathItem|false
7713
     */
7714
    public function getItem($itemId)
7715
    {
7716
        if (isset($this->items[$itemId]) && is_object($this->items[$itemId])) {
7717
            return $this->items[$itemId];
7718
        }
7719
7720
        return false;
7721
    }
7722
7723
    /**
7724
     * @return int
7725
     */
7726
    public function getCurrentAttempt()
7727
    {
7728
        $attempt = $this->getItem($this->get_current_item_id());
7729
        if ($attempt) {
7730
            return $attempt->get_attempt_id();
7731
        }
7732
7733
        return 0;
7734
    }
7735
7736
    /**
7737
     * @return int
7738
     */
7739
    public function getCategoryId()
7740
    {
7741
        return (int) $this->categoryId;
7742
    }
7743
7744
    /**
7745
     * Get whether this is a learning path with the possibility to subscribe
7746
     * users or not.
7747
     *
7748
     * @return int
7749
     */
7750
    public function getSubscribeUsers()
7751
    {
7752
        return $this->subscribeUsers;
7753
    }
7754
7755
    /**
7756
     * Calculate the count of stars for a user in this LP
7757
     * This calculation is based on the following rules:
7758
     * - the student gets one star when he gets to 50% of the learning path
7759
     * - the student gets a second star when the average score of all tests inside the learning path >= 50%
7760
     * - the student gets a third star when the average score of all tests inside the learning path >= 80%
7761
     * - the student gets the final star when the score for the *last* test is >= 80%.
7762
     *
7763
     * @param int $sessionId Optional. The session ID
7764
     *
7765
     * @return int The count of stars
7766
     */
7767
    public function getCalculateStars($sessionId = 0)
7768
    {
7769
        $stars = 0;
7770
        $progress = self::getProgress(
7771
            $this->lp_id,
7772
            $this->user_id,
7773
            $this->course_int_id,
7774
            $sessionId
7775
        );
7776
7777
        if ($progress >= 50) {
7778
            $stars++;
7779
        }
7780
7781
        // Calculate stars chapters evaluation
7782
        $exercisesItems = $this->getExercisesItems();
7783
7784
        if (!empty($exercisesItems)) {
7785
            $totalResult = 0;
7786
7787
            foreach ($exercisesItems as $exerciseItem) {
7788
                $exerciseResultInfo = Event::getExerciseResultsByUser(
7789
                    $this->user_id,
7790
                    $exerciseItem->path,
7791
                    $this->course_int_id,
7792
                    $sessionId,
7793
                    $this->lp_id,
7794
                    $exerciseItem->db_id
7795
                );
7796
7797
                $exerciseResultInfo = end($exerciseResultInfo);
7798
7799
                if (!$exerciseResultInfo) {
7800
                    continue;
7801
                }
7802
7803
                if (!empty($exerciseResultInfo['max_score'])) {
7804
                    $exerciseResult = $exerciseResultInfo['score'] * 100 / $exerciseResultInfo['max_score'];
7805
                } else {
7806
                    $exerciseResult = 0;
7807
                }
7808
                $totalResult += $exerciseResult;
7809
            }
7810
7811
            $totalExerciseAverage = $totalResult / (count($exercisesItems) > 0 ? count($exercisesItems) : 1);
7812
7813
            if ($totalExerciseAverage >= 50) {
7814
                $stars++;
7815
            }
7816
7817
            if ($totalExerciseAverage >= 80) {
7818
                $stars++;
7819
            }
7820
        }
7821
7822
        // Calculate star for final evaluation
7823
        $finalEvaluationItem = $this->getFinalEvaluationItem();
7824
7825
        if (!empty($finalEvaluationItem)) {
7826
            $evaluationResultInfo = Event::getExerciseResultsByUser(
7827
                $this->user_id,
7828
                $finalEvaluationItem->path,
7829
                $this->course_int_id,
7830
                $sessionId,
7831
                $this->lp_id,
7832
                $finalEvaluationItem->db_id
7833
            );
7834
7835
            $evaluationResultInfo = end($evaluationResultInfo);
7836
7837
            if ($evaluationResultInfo) {
7838
                $evaluationResult = $evaluationResultInfo['score'] * 100 / $evaluationResultInfo['max_score'];
7839
                if ($evaluationResult >= 80) {
7840
                    $stars++;
7841
                }
7842
            }
7843
        }
7844
7845
        return $stars;
7846
    }
7847
7848
    /**
7849
     * Get the items of exercise type.
7850
     *
7851
     * @return array The items. Otherwise return false
7852
     */
7853
    public function getExercisesItems()
7854
    {
7855
        $exercises = [];
7856
        foreach ($this->items as $item) {
7857
            if ('quiz' !== $item->type) {
7858
                continue;
7859
            }
7860
            $exercises[] = $item;
7861
        }
7862
7863
        array_pop($exercises);
7864
7865
        return $exercises;
7866
    }
7867
7868
    /**
7869
     * Get the item of exercise type (evaluation type).
7870
     *
7871
     * @return array The final evaluation. Otherwise return false
7872
     */
7873
    public function getFinalEvaluationItem()
7874
    {
7875
        $exercises = [];
7876
        foreach ($this->items as $item) {
7877
            if (TOOL_QUIZ !== $item->type) {
7878
                continue;
7879
            }
7880
7881
            $exercises[] = $item;
7882
        }
7883
7884
        return array_pop($exercises);
7885
    }
7886
7887
    /**
7888
     * Calculate the total points achieved for the current user in this learning path.
7889
     *
7890
     * @param int $sessionId Optional. The session Id
7891
     *
7892
     * @return int
7893
     */
7894
    public function getCalculateScore($sessionId = 0)
7895
    {
7896
        // Calculate stars chapters evaluation
7897
        $exercisesItems = $this->getExercisesItems();
7898
        $finalEvaluationItem = $this->getFinalEvaluationItem();
7899
        $totalExercisesResult = 0;
7900
        $totalEvaluationResult = 0;
7901
7902
        if (false !== $exercisesItems) {
7903
            foreach ($exercisesItems as $exerciseItem) {
7904
                $exerciseResultInfo = Event::getExerciseResultsByUser(
7905
                    $this->user_id,
7906
                    $exerciseItem->path,
7907
                    $this->course_int_id,
7908
                    $sessionId,
7909
                    $this->lp_id,
7910
                    $exerciseItem->db_id
7911
                );
7912
7913
                $exerciseResultInfo = end($exerciseResultInfo);
7914
7915
                if (!$exerciseResultInfo) {
7916
                    continue;
7917
                }
7918
7919
                $totalExercisesResult += $exerciseResultInfo['score'];
7920
            }
7921
        }
7922
7923
        if (!empty($finalEvaluationItem)) {
7924
            $evaluationResultInfo = Event::getExerciseResultsByUser(
7925
                $this->user_id,
7926
                $finalEvaluationItem->path,
7927
                $this->course_int_id,
7928
                $sessionId,
7929
                $this->lp_id,
7930
                $finalEvaluationItem->db_id
7931
            );
7932
7933
            $evaluationResultInfo = end($evaluationResultInfo);
7934
7935
            if ($evaluationResultInfo) {
7936
                $totalEvaluationResult += $evaluationResultInfo['score'];
7937
            }
7938
        }
7939
7940
        return $totalExercisesResult + $totalEvaluationResult;
7941
    }
7942
7943
    /**
7944
     * Check if URL is not allowed to be show in a iframe.
7945
     *
7946
     * @param string $src
7947
     *
7948
     * @return string
7949
     */
7950
    public function fixBlockedLinks($src)
7951
    {
7952
        $urlInfo = parse_url($src);
7953
7954
        $platformProtocol = 'https';
7955
        if (false === strpos(api_get_path(WEB_CODE_PATH), 'https')) {
7956
            $platformProtocol = 'http';
7957
        }
7958
7959
        $protocolFixApplied = false;
7960
        //Scheme validation to avoid "Notices" when the lesson doesn't contain a valid scheme
7961
        $scheme = isset($urlInfo['scheme']) ? $urlInfo['scheme'] : null;
7962
        $host = isset($urlInfo['host']) ? $urlInfo['host'] : null;
7963
7964
        if ($platformProtocol != $scheme) {
7965
            Session::write('x_frame_source', $src);
7966
            $src = 'blank.php?error=x_frames_options';
7967
            $protocolFixApplied = true;
7968
        }
7969
7970
        if (false == $protocolFixApplied) {
7971
            if (false === strpos(api_get_path(WEB_PATH), $host)) {
7972
                // Check X-Frame-Options
7973
                $ch = curl_init();
7974
                $options = [
7975
                    CURLOPT_URL => $src,
7976
                    CURLOPT_RETURNTRANSFER => true,
7977
                    CURLOPT_HEADER => true,
7978
                    CURLOPT_FOLLOWLOCATION => true,
7979
                    CURLOPT_ENCODING => "",
7980
                    CURLOPT_AUTOREFERER => true,
7981
                    CURLOPT_CONNECTTIMEOUT => 120,
7982
                    CURLOPT_TIMEOUT => 120,
7983
                    CURLOPT_MAXREDIRS => 10,
7984
                ];
7985
7986
                $proxySettings = api_get_setting('security.proxy_settings', true);
7987
                if (!empty($proxySettings) &&
7988
                    isset($proxySettings['curl_setopt_array'])
7989
                ) {
7990
                    $options[CURLOPT_PROXY] = $proxySettings['curl_setopt_array']['CURLOPT_PROXY'];
7991
                    $options[CURLOPT_PROXYPORT] = $proxySettings['curl_setopt_array']['CURLOPT_PROXYPORT'];
7992
                }
7993
7994
                curl_setopt_array($ch, $options);
7995
                $response = curl_exec($ch);
7996
                $httpCode = curl_getinfo($ch);
7997
                $headers = substr($response, 0, $httpCode['header_size']);
7998
7999
                $error = false;
8000
                if (stripos($headers, 'X-Frame-Options: DENY') > -1
8001
                    //|| stripos($headers, 'X-Frame-Options: SAMEORIGIN') > -1
8002
                ) {
8003
                    $error = true;
8004
                }
8005
8006
                if ($error) {
8007
                    Session::write('x_frame_source', $src);
8008
                    $src = 'blank.php?error=x_frames_options';
8009
                }
8010
            }
8011
        }
8012
8013
        return $src;
8014
    }
8015
8016
    /**
8017
     * Check if this LP has a created forum in the basis course.
8018
     *
8019
     * @deprecated
8020
     *
8021
     * @return bool
8022
     */
8023
    public function lpHasForum()
8024
    {
8025
        $forumTable = Database::get_course_table(TABLE_FORUM);
8026
        $itemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
8027
8028
        $fakeFrom = "
8029
            $forumTable f
8030
            INNER JOIN $itemProperty ip
8031
            ON (f.forum_id = ip.ref AND f.c_id = ip.c_id)
8032
        ";
8033
8034
        $resultData = Database::select(
8035
            'COUNT(f.iid) AS qty',
8036
            $fakeFrom,
8037
            [
8038
                'where' => [
8039
                    'ip.visibility != ? AND ' => 2,
8040
                    'ip.tool = ? AND ' => TOOL_FORUM,
8041
                    'f.c_id = ? AND ' => intval($this->course_int_id),
8042
                    'f.lp_id = ?' => intval($this->lp_id),
8043
                ],
8044
            ],
8045
            'first'
8046
        );
8047
8048
        return $resultData['qty'] > 0;
8049
    }
8050
8051
    /**
8052
     * Get the forum for this learning path.
8053
     *
8054
     * @param int $sessionId
8055
     *
8056
     * @return array
8057
     */
8058
    public function getForum($sessionId = 0)
8059
    {
8060
        $repo = Container::getForumRepository();
8061
8062
        $course = api_get_course_entity();
8063
        $session = api_get_session_entity($sessionId);
8064
        $qb = $repo->getResourcesByCourse($course, $session);
8065
8066
        return $qb->getQuery()->getResult();
8067
    }
8068
8069
    /**
8070
     * Get the LP Final Item form.
8071
     *
8072
     * @throws Exception
8073
     *
8074
     *
8075
     * @return string
8076
     */
8077
    public function getFinalItemForm()
8078
    {
8079
        $finalItem = $this->getFinalItem();
8080
        $title = '';
8081
8082
        if ($finalItem) {
8083
            $title = $finalItem->get_title();
8084
            $buttonText = get_lang('Save');
8085
            $content = $this->getSavedFinalItem();
8086
        } else {
8087
            $buttonText = get_lang('Add this document to the course');
8088
            $content = $this->getFinalItemTemplate();
8089
        }
8090
8091
        $editorConfig = [
8092
            'ToolbarSet' => 'LearningPathDocuments',
8093
            'Width' => '100%',
8094
            'Height' => '500',
8095
            'FullPage' => true,
8096
        ];
8097
8098
        $url = api_get_self().'?'.api_get_cidreq().'&'.http_build_query([
8099
            'type' => 'document',
8100
            'lp_id' => $this->lp_id,
8101
        ]);
8102
8103
        $form = new FormValidator('final_item', 'POST', $url);
8104
        $form->addText('title', get_lang('Title'));
8105
        $form->addButtonSave($buttonText);
8106
        $form->addHtml(
8107
            Display::return_message(
8108
                'Variables :<br><br> <b>((certificate))</b> <br> <b>((skill))</b>',
8109
                'normal',
8110
                false
8111
            )
8112
        );
8113
8114
        $renderer = $form->defaultRenderer();
8115
        $renderer->setElementTemplate('&nbsp;{label}{element}', 'content_lp_certificate');
8116
8117
        $form->addHtmlEditor(
8118
            'content_lp_certificate',
8119
            null,
8120
            true,
8121
            false,
8122
            $editorConfig
8123
        );
8124
        $form->addHidden('action', 'add_final_item');
8125
        $form->addHidden('path', Session::read('pathItem'));
8126
        $form->addHidden('previous', $this->get_last());
8127
        $form->setDefaults(
8128
            ['title' => $title, 'content_lp_certificate' => $content]
8129
        );
8130
8131
        if ($form->validate()) {
8132
            $values = $form->exportValues();
8133
            $lastItemId = $this->getLastInFirstLevel();
8134
8135
            if (!$finalItem) {
8136
                $documentId = $this->create_document(
8137
                    $this->course_info,
8138
                    $values['content_lp_certificate'],
8139
                    $values['title'],
8140
                    'html',
8141
                    0,
8142
                    0,
8143
                    'certificate'
8144
                );
8145
8146
                $lpItemRepo = Container::getLpItemRepository();
8147
                $root       = $lpItemRepo->getRootItem($this->get_id());
8148
8149
                $this->add_item(
8150
                    $root,
8151
                    $lastItemId,
8152
                    TOOL_LP_FINAL_ITEM,
8153
                    $documentId,
8154
                    $values['title']
8155
                );
8156
8157
                Display::addFlash(
8158
                    Display::return_message(get_lang('Added'))
8159
                );
8160
            } else {
8161
                $this->edit_document();
8162
            }
8163
        }
8164
8165
        return $form->returnForm();
8166
    }
8167
8168
    /**
8169
     * Check if the current lp item is first, both, last or none from lp list.
8170
     *
8171
     * @param int $currentItemId
8172
     *
8173
     * @return string
8174
     */
8175
    public function isFirstOrLastItem($currentItemId)
8176
    {
8177
        $lpItemId = [];
8178
        $typeListNotToVerify = self::getChapterTypes();
8179
8180
        // Using get_toc() function instead $this->items because returns the correct order of the items
8181
        foreach ($this->get_toc() as $item) {
8182
            if (!in_array($item['type'], $typeListNotToVerify)) {
8183
                $lpItemId[] = $item['id'];
8184
            }
8185
        }
8186
8187
        $lastLpItemIndex = count($lpItemId) - 1;
8188
        $position = array_search($currentItemId, $lpItemId);
8189
8190
        switch ($position) {
8191
            case 0:
8192
                if (!$lastLpItemIndex) {
8193
                    $answer = 'both';
8194
                    break;
8195
                }
8196
8197
                $answer = 'first';
8198
                break;
8199
            case $lastLpItemIndex:
8200
                $answer = 'last';
8201
                break;
8202
            default:
8203
                $answer = 'none';
8204
        }
8205
8206
        return $answer;
8207
    }
8208
8209
    /**
8210
     * Get whether this is a learning path with the accumulated SCORM time or not.
8211
     *
8212
     * @return int
8213
     */
8214
    public function getAccumulateScormTime()
8215
    {
8216
        return $this->accumulateScormTime;
8217
    }
8218
8219
    /**
8220
     * Returns an HTML-formatted link to a resource, to incorporate directly into
8221
     * the new learning path tool.
8222
     *
8223
     * The function is a big switch on tool type.
8224
     * In each case, we query the corresponding table for information and build the link
8225
     * with that information.
8226
     *
8227
     * @author Yannick Warnier <[email protected]> - rebranding based on
8228
     * previous work (display_addedresource_link_in_learnpath())
8229
     *
8230
     * @param int $course_id      Course code
8231
     * @param int $learningPathId The learning path ID (in lp table)
8232
     * @param int $id_in_path     the unique index in the items table
8233
     * @param int $lpViewId
8234
     *
8235
     * @return string
8236
     */
8237
    public static function rl_get_resource_link_for_learnpath(
8238
        $course_id,
8239
        $learningPathId,
8240
        $id_in_path,
8241
        $lpViewId
8242
    ) {
8243
        $session_id = api_get_session_id();
8244
8245
        $learningPathId = (int) $learningPathId;
8246
        $id_in_path = (int) $id_in_path;
8247
        $lpViewId = (int) $lpViewId;
8248
8249
        $em = Database::getManager();
8250
        $lpItemRepo = $em->getRepository(CLpItem::class);
8251
8252
        /** @var CLpItem $rowItem */
8253
        $rowItem = $lpItemRepo->findOneBy([
8254
            'lp' => $learningPathId,
8255
            'iid' => $id_in_path,
8256
        ]);
8257
        $type = $rowItem->getItemType();
8258
        $id = empty($rowItem->getPath()) ? '0' : $rowItem->getPath();
8259
        $main_dir_path = api_get_path(WEB_CODE_PATH);
8260
        $link = '';
8261
        $extraParams = api_get_cidreq(true, true, 'learnpath').'&sid='.$session_id;
8262
8263
        switch ($type) {
8264
            case 'dir':
8265
                return $main_dir_path.'lp/blank.php';
8266
            case TOOL_CALENDAR_EVENT:
8267
                return $main_dir_path.'calendar/agenda.php?agenda_id='.$id.'&'.$extraParams;
8268
            case TOOL_ANNOUNCEMENT:
8269
                return $main_dir_path.'announcements/announcements.php?ann_id='.$id.'&'.$extraParams;
8270
            case TOOL_LINK:
8271
                $linkInfo = Link::getLinkInfo($id);
8272
                if (isset($linkInfo['url'])) {
8273
                    return $linkInfo['url'];
8274
                }
8275
8276
                return '';
8277
            case TOOL_QUIZ:
8278
                if (empty($id)) {
8279
                    return '';
8280
                }
8281
8282
                // Get the lp_item_view with the highest view_count.
8283
                $learnpathItemViewResult = $em
8284
                    ->getRepository(CLpItemView::class)
8285
                    ->findBy(
8286
                        ['item' => $rowItem->getIid(), 'view' => $lpViewId],
8287
                        ['viewCount' => 'DESC'],
8288
                        1
8289
                    );
8290
                /** @var CLpItemView $learnpathItemViewData */
8291
                $learnpathItemViewData = current($learnpathItemViewResult);
8292
                $learnpathItemViewId = $learnpathItemViewData ? $learnpathItemViewData->getIid() : 0;
8293
8294
                return $main_dir_path.'exercise/overview.php?'.$extraParams.'&'
8295
                    .http_build_query([
8296
                        'lp_init' => 1,
8297
                        'learnpath_item_view_id' => $learnpathItemViewId,
8298
                        'learnpath_id' => $learningPathId,
8299
                        'learnpath_item_id' => $id_in_path,
8300
                        'exerciseId' => $id,
8301
                    ]);
8302
            case TOOL_HOTPOTATOES:
8303
                return '';
8304
            case TOOL_FORUM:
8305
                return $main_dir_path.'forum/viewforum.php?forum='.$id.'&lp=true&'.$extraParams;
8306
            case TOOL_THREAD:
8307
                // forum post
8308
                $tbl_topics = Database::get_course_table(TABLE_FORUM_THREAD);
8309
                if (empty($id)) {
8310
                    return '';
8311
                }
8312
                $sql = "SELECT * FROM $tbl_topics WHERE iid=$id";
8313
                $result = Database::query($sql);
8314
                $row = Database::fetch_array($result);
8315
8316
                return $main_dir_path.'forum/viewthread.php?thread='.$id.'&forum='.$row['forum_id'].'&lp=true&'
8317
                    .$extraParams;
8318
            case TOOL_POST:
8319
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8320
                $result = Database::query("SELECT * FROM $tbl_post WHERE post_id=$id");
8321
                $row = Database::fetch_array($result);
8322
8323
                return $main_dir_path.'forum/viewthread.php?post='.$id.'&thread='.$row['thread_id'].'&forum='
8324
                    .$row['forum_id'].'&lp=true&'.$extraParams;
8325
            case TOOL_READOUT_TEXT:
8326
                return api_get_path(WEB_CODE_PATH).
8327
                    'lp/readout_text.php?&id='.$id.'&lp_id='.$learningPathId.'&'.$extraParams;
8328
            case TOOL_DOCUMENT:
8329
            case 'video':
8330
                $repo = Container::getDocumentRepository();
8331
                $document = $repo->find($rowItem->getPath());
8332
                if ($document) {
8333
                    $params = [
8334
                        'cid' => $course_id,
8335
                        'sid' => $session_id,
8336
                    ];
8337
8338
                    return $repo->getResourceFileUrl($document, $params, UrlGeneratorInterface::ABSOLUTE_URL);
8339
                }
8340
8341
                return null;
8342
            case TOOL_LP_FINAL_ITEM:
8343
                return api_get_path(WEB_CODE_PATH).'lp/lp_final_item.php?&id='.$id.'&lp_id='.$learningPathId.'&'
8344
                    .$extraParams;
8345
            case 'assignments':
8346
                return $main_dir_path.'work/work.php?'.$extraParams;
8347
            case TOOL_DROPBOX:
8348
                return $main_dir_path.'dropbox/index.php?'.$extraParams;
8349
            case 'introduction_text': //DEPRECATED
8350
                return '';
8351
            case TOOL_COURSE_DESCRIPTION:
8352
                return $main_dir_path.'course_description?'.$extraParams;
8353
            case TOOL_GROUP:
8354
                return $main_dir_path.'group/group.php?'.$extraParams;
8355
            case TOOL_USER:
8356
                return $main_dir_path.'user/user.php?'.$extraParams;
8357
            case TOOL_STUDENTPUBLICATION:
8358
                $repo = Container::getStudentPublicationRepository();
8359
                $publication = $repo->find($rowItem->getPath());
8360
                if ($publication && $publication->hasResourceNode()) {
8361
                    $nodeId = $publication->getResourceNode()->getId();
8362
                    $assignmentId = $publication->getIid();
8363
8364
                    return api_get_path(WEB_PATH) .
8365
                        "resources/assignment/$nodeId/submission/$assignmentId?" .
8366
                        http_build_query([
8367
                            'cid' => $course_id,
8368
                            'sid' => $session_id,
8369
                            'gid' => 0,
8370
                            'origin' => 'learnpath',
8371
                            'isStudentView' => 'true',
8372
                        ]);
8373
                }
8374
                return '';
8375
            case TOOL_SURVEY:
8376
8377
                $surveyId = (int) $id;
8378
                $repo = Container::getSurveyRepository();
8379
                if (!empty($surveyId)) {
8380
                    /** @var CSurvey $survey */
8381
                    $survey = $repo->find($surveyId);
8382
                    $autoSurveyLink = SurveyUtil::generateFillSurveyLink(
8383
                        $survey,
8384
                        'auto',
8385
                        api_get_course_entity($course_id),
8386
                        $session_id
8387
                    );
8388
                    $lpParams = [
8389
                        'lp_id' => $learningPathId,
8390
                        'lp_item_id' => $id_in_path,
8391
                        'origin' => 'learnpath',
8392
                    ];
8393
8394
                    return $autoSurveyLink.'&'.http_build_query($lpParams).'&'.$extraParams;
8395
                }
8396
        }
8397
8398
        return $link;
8399
    }
8400
8401
    /**
8402
     * Checks if any forum items in a given learning path are from the base course.
8403
     */
8404
    public static function isForumFromBaseCourse(int $learningPathId): bool
8405
    {
8406
        $itemRepository = Container::getLpItemRepository();
8407
        $forumRepository = Container::getForumRepository();
8408
        $forums = $itemRepository->findItemsByLearningPathAndType($learningPathId, 'forum');
8409
8410
        /* @var CLpItem $forumItem */
8411
        foreach ($forums as $forumItem) {
8412
            $forumId = (int) $forumItem->getPath();
8413
            $forum = $forumRepository->find($forumId);
8414
8415
            if ($forum !== null) {
8416
                $forumSession = $forum->getFirstResourceLink()->getSession();
8417
                if ($forumSession === null) {
8418
                    return true;
8419
                }
8420
            }
8421
        }
8422
8423
        return false;
8424
    }
8425
8426
    /**
8427
     * Gets the name of a resource (generally used in learnpath when no name is provided).
8428
     *
8429
     * @author Yannick Warnier <[email protected]>
8430
     *
8431
     * @param string $course_code    Course code
8432
     * @param int    $learningPathId
8433
     * @param int    $id_in_path     The resource ID
8434
     *
8435
     * @return string
8436
     */
8437
    public static function rl_get_resource_name($course_code, $learningPathId, $id_in_path)
8438
    {
8439
        $_course = api_get_course_info($course_code);
8440
        if (empty($_course)) {
8441
            return '';
8442
        }
8443
        $course_id = $_course['real_id'];
8444
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
8445
        $learningPathId = (int) $learningPathId;
8446
        $id_in_path = (int) $id_in_path;
8447
8448
        $sql = "SELECT item_type, title, ref
8449
                FROM $tbl_lp_item
8450
                WHERE c_id = $course_id AND lp_id = $learningPathId AND iid = $id_in_path";
8451
        $res_item = Database::query($sql);
8452
8453
        if (Database::num_rows($res_item) < 1) {
8454
            return '';
8455
        }
8456
        $row_item = Database::fetch_array($res_item);
8457
        $type = strtolower($row_item['item_type']);
8458
        $id = $row_item['ref'];
8459
        $output = '';
8460
8461
        switch ($type) {
8462
            case TOOL_CALENDAR_EVENT:
8463
                $TABLEAGENDA = Database::get_course_table(TABLE_AGENDA);
8464
                $result = Database::query("SELECT * FROM $TABLEAGENDA WHERE c_id = $course_id AND id=$id");
8465
                $myrow = Database::fetch_array($result);
8466
                $output = $myrow['title'];
8467
                break;
8468
            case TOOL_ANNOUNCEMENT:
8469
                $tbl_announcement = Database::get_course_table(TABLE_ANNOUNCEMENT);
8470
                $result = Database::query("SELECT * FROM $tbl_announcement WHERE c_id = $course_id AND id=$id");
8471
                $myrow = Database::fetch_array($result);
8472
                $output = $myrow['title'];
8473
                break;
8474
            case TOOL_LINK:
8475
                // Doesn't take $target into account.
8476
                $TABLETOOLLINK = Database::get_course_table(TABLE_LINK);
8477
                $result = Database::query("SELECT * FROM $TABLETOOLLINK WHERE c_id = $course_id AND id=$id");
8478
                $myrow = Database::fetch_array($result);
8479
                $output = $myrow['title'];
8480
                break;
8481
            case TOOL_QUIZ:
8482
                $TBL_EXERCICES = Database::get_course_table(TABLE_QUIZ_TEST);
8483
                $result = Database::query("SELECT * FROM $TBL_EXERCICES WHERE c_id = $course_id AND id = $id");
8484
                $myrow = Database::fetch_array($result);
8485
                $output = $myrow['title'];
8486
                break;
8487
            case TOOL_FORUM:
8488
                $TBL_FORUMS = Database::get_course_table(TABLE_FORUM);
8489
                $result = Database::query("SELECT * FROM $TBL_FORUMS WHERE c_id = $course_id AND forum_id = $id");
8490
                $myrow = Database::fetch_array($result);
8491
                $output = $myrow['title'];
8492
                break;
8493
            case TOOL_THREAD:
8494
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8495
                // Grabbing the title of the post.
8496
                $sql_title = "SELECT * FROM $tbl_post WHERE c_id = $course_id AND post_id=".$id;
8497
                $result_title = Database::query($sql_title);
8498
                $myrow_title = Database::fetch_array($result_title);
8499
                $output = $myrow_title['title'];
8500
                break;
8501
            case TOOL_POST:
8502
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8503
                $sql = "SELECT * FROM $tbl_post p WHERE c_id = $course_id AND p.post_id = $id";
8504
                $result = Database::query($sql);
8505
                $post = Database::fetch_array($result);
8506
                $output = $post['title'];
8507
                break;
8508
            case 'dir':
8509
            case TOOL_DOCUMENT:
8510
            case 'video':
8511
                $title = $row_item['title'];
8512
                $output = '-';
8513
                if (!empty($title)) {
8514
                    $output = $title;
8515
                }
8516
                break;
8517
            case 'hotpotatoes':
8518
                $tbl_doc = Database::get_course_table(TABLE_DOCUMENT);
8519
                $result = Database::query("SELECT * FROM $tbl_doc WHERE c_id = $course_id AND iid = $id");
8520
                $myrow = Database::fetch_array($result);
8521
                $pathname = explode('/', $myrow['path']); // Making a correct name for the link.
8522
                $last = count($pathname) - 1; // Making a correct name for the link.
8523
                $filename = $pathname[$last]; // Making a correct name for the link.
8524
                $myrow['path'] = rawurlencode($myrow['path']);
8525
                $output = $filename;
8526
                break;
8527
        }
8528
8529
        return stripslashes($output);
8530
    }
8531
8532
    /**
8533
     * Get the parent names for the current item.
8534
     *
8535
     * @param int $newItemId Optional. The item ID
8536
     */
8537
    public function getCurrentItemParentNames($newItemId = 0): array
8538
    {
8539
        $newItemId = $newItemId ?: $this->get_current_item_id();
8540
        $return = [];
8541
        $item = $this->getItem($newItemId);
8542
8543
        $parent = null;
8544
        if ($item) {
8545
            $parent = $this->getItem($item->get_parent());
8546
        }
8547
8548
        while ($parent) {
8549
            $return[] = $parent->get_title();
8550
            $parent = $this->getItem($parent->get_parent());
8551
        }
8552
8553
        return array_reverse($return);
8554
    }
8555
8556
    /**
8557
     * Reads and process "lp_subscription_settings" setting.
8558
     *
8559
     * @return array
8560
     */
8561
    public static function getSubscriptionSettings()
8562
    {
8563
        $subscriptionSettings = api_get_setting('lp.lp_subscription_settings', true);
8564
        if (!is_array($subscriptionSettings)) {
8565
            // By default, allow both settings
8566
            $subscriptionSettings = [
8567
                'allow_add_users_to_lp' => true,
8568
                'allow_add_users_to_lp_category' => true,
8569
            ];
8570
        } else {
8571
            $subscriptionSettings = $subscriptionSettings['options'];
8572
        }
8573
8574
        return $subscriptionSettings;
8575
    }
8576
8577
    /**
8578
     * Exports a LP to a courseBuilder zip file. It adds the documents related to the LP.
8579
     */
8580
    public function exportToCourseBuildFormat()
8581
    {
8582
        if (!api_is_allowed_to_edit()) {
8583
            return false;
8584
        }
8585
8586
        $courseBuilder = new CourseBuilder();
8587
        $itemList = [];
8588
        /** @var learnpathItem $item */
8589
        foreach ($this->items as $item) {
8590
            $itemList[$item->get_type()][] = $item->get_path();
8591
        }
8592
8593
        if (empty($itemList)) {
8594
            return false;
8595
        }
8596
8597
        if (isset($itemList['document'])) {
8598
            // Get parents
8599
            foreach ($itemList['document'] as $documentId) {
8600
                $documentInfo = DocumentManager::get_document_data_by_id($documentId, api_get_course_id(), true);
8601
                if (!empty($documentInfo['parents'])) {
8602
                    foreach ($documentInfo['parents'] as $parentInfo) {
8603
                        if (in_array($parentInfo['iid'], $itemList['document'])) {
8604
                            continue;
8605
                        }
8606
                        $itemList['document'][] = $parentInfo['iid'];
8607
                    }
8608
                }
8609
            }
8610
8611
            $courseInfo = api_get_course_info();
8612
            foreach ($itemList['document'] as $documentId) {
8613
                $documentInfo = DocumentManager::get_document_data_by_id($documentId, api_get_course_id());
8614
                $items = DocumentManager::get_resources_from_source_html(
8615
                    $documentInfo['absolute_path'],
8616
                    true,
8617
                    TOOL_DOCUMENT
8618
                );
8619
8620
                if (!empty($items)) {
8621
                    foreach ($items as $item) {
8622
                        // Get information about source url
8623
                        $url = $item[0]; // url
8624
                        $scope = $item[1]; // scope (local, remote)
8625
                        $type = $item[2]; // type (rel, abs, url)
8626
8627
                        $origParseUrl = parse_url($url);
8628
                        $realOrigPath = isset($origParseUrl['path']) ? $origParseUrl['path'] : null;
8629
8630
                        if ('local' === $scope) {
8631
                            if ('abs' === $type || 'rel' === $type) {
8632
                                $documentFile = strstr($realOrigPath, 'document');
8633
                                if (false !== strpos($realOrigPath, $documentFile)) {
8634
                                    $documentFile = str_replace('document', '', $documentFile);
8635
                                    $itemDocumentId = DocumentManager::get_document_id($courseInfo, $documentFile);
8636
                                    // Document found! Add it to the list
8637
                                    if ($itemDocumentId) {
8638
                                        $itemList['document'][] = $itemDocumentId;
8639
                                    }
8640
                                }
8641
                            }
8642
                        }
8643
                    }
8644
                }
8645
            }
8646
8647
            $courseBuilder->build_documents(
8648
                api_get_session_id(),
8649
                $this->get_course_int_id(),
8650
                true,
8651
                $itemList['document']
8652
            );
8653
        }
8654
8655
        if (isset($itemList['quiz'])) {
8656
            $courseBuilder->build_quizzes(
8657
                api_get_session_id(),
8658
                $this->get_course_int_id(),
8659
                true,
8660
                $itemList['quiz']
8661
            );
8662
        }
8663
8664
        if (!empty($itemList['thread'])) {
8665
            $threadList = [];
8666
            $repo = Container::getForumThreadRepository();
8667
            foreach ($itemList['thread'] as $threadId) {
8668
                /** @var CForumThread $thread */
8669
                $thread = $repo->find($threadId);
8670
                if ($thread) {
8671
                    $itemList['forum'][] = $thread->getForum() ? $thread->getForum()->getIid() : 0;
8672
                    $threadList[] = $thread->getIid();
8673
                }
8674
            }
8675
8676
            if (!empty($threadList)) {
8677
                $courseBuilder->build_forum_topics(
8678
                    api_get_session_id(),
8679
                    $this->get_course_int_id(),
8680
                    null,
8681
                    $threadList
8682
                );
8683
            }
8684
        }
8685
8686
        $forumCategoryList = [];
8687
        if (isset($itemList['forum'])) {
8688
            foreach ($itemList['forum'] as $forumId) {
8689
                $forumInfo = get_forums($forumId);
8690
                $forumCategoryList[] = $forumInfo['forum_category'];
8691
            }
8692
        }
8693
8694
        if (!empty($forumCategoryList)) {
8695
            $courseBuilder->build_forum_category(
8696
                api_get_session_id(),
8697
                $this->get_course_int_id(),
8698
                true,
8699
                $forumCategoryList
8700
            );
8701
        }
8702
8703
        if (!empty($itemList['forum'])) {
8704
            $courseBuilder->build_forums(
8705
                api_get_session_id(),
8706
                $this->get_course_int_id(),
8707
                true,
8708
                $itemList['forum']
8709
            );
8710
        }
8711
8712
        if (isset($itemList['link'])) {
8713
            $courseBuilder->build_links(
8714
                api_get_session_id(),
8715
                $this->get_course_int_id(),
8716
                true,
8717
                $itemList['link']
8718
            );
8719
        }
8720
8721
        if (!empty($itemList['student_publication'])) {
8722
            $courseBuilder->build_works(
8723
                api_get_session_id(),
8724
                $this->get_course_int_id(),
8725
                true,
8726
                $itemList['student_publication']
8727
            );
8728
        }
8729
8730
        $courseBuilder->build_learnpaths(
8731
            api_get_session_id(),
8732
            $this->get_course_int_id(),
8733
            true,
8734
            [$this->get_id()],
8735
            false
8736
        );
8737
8738
        $courseBuilder->restoreDocumentsFromList();
8739
8740
        $zipFile = CourseArchiver::createBackup($courseBuilder->course);
8741
        $zipPath = CourseArchiver::getBackupDir().$zipFile;
8742
        $result = DocumentManager::file_send_for_download(
8743
            $zipPath,
8744
            true,
8745
            $this->get_name().'.zip'
8746
        );
8747
8748
        if ($result) {
8749
            api_not_allowed();
8750
        }
8751
8752
        return true;
8753
    }
8754
8755
    /**
8756
     * Get whether this is a learning path with the accumulated work time or not.
8757
     *
8758
     * @return int
8759
     */
8760
    public function getAccumulateWorkTime()
8761
    {
8762
        return (int) $this->accumulateWorkTime;
8763
    }
8764
8765
    /**
8766
     * Get whether this is a learning path with the accumulated work time or not.
8767
     *
8768
     * @return int
8769
     */
8770
    public function getAccumulateWorkTimeTotalCourse()
8771
    {
8772
        $table = Database::get_course_table(TABLE_LP_MAIN);
8773
        $sql = "SELECT SUM(accumulate_work_time) AS total
8774
                FROM $table
8775
                WHERE c_id = ".$this->course_int_id;
8776
        $result = Database::query($sql);
8777
        $row = Database::fetch_array($result);
8778
8779
        return (int) $row['total'];
8780
    }
8781
8782
    /**
8783
     * @param int $lpId
8784
     * @param int $courseId
8785
     *
8786
     * @return mixed
8787
     */
8788
    public static function getAccumulateWorkTimePrerequisite($lpId, $courseId)
8789
    {
8790
        $lpId = (int) $lpId;
8791
        $table = Database::get_course_table(TABLE_LP_MAIN);
8792
        $sql = "SELECT accumulate_work_time
8793
                FROM $table
8794
                WHERE iid = $lpId";
8795
        $result = Database::query($sql);
8796
        $row = Database::fetch_array($result);
8797
8798
        return $row['accumulate_work_time'];
8799
    }
8800
8801
    /**
8802
     * @param int $courseId
8803
     *
8804
     * @return int
8805
     */
8806
    public static function getAccumulateWorkTimeTotal($courseId)
8807
    {
8808
        $table = Database::get_course_table(TABLE_LP_MAIN);
8809
        $courseId = (int) $courseId;
8810
        $sql = "SELECT SUM(accumulate_work_time) AS total
8811
                FROM $table
8812
                WHERE c_id = $courseId";
8813
        $result = Database::query($sql);
8814
        $row = Database::fetch_array($result);
8815
8816
        return (int) $row['total'];
8817
    }
8818
8819
    /**
8820
     * In order to use the lp icon option you need to create the "lp_icon" LP extra field
8821
     * and put the images in.
8822
     */
8823
    public static function getIconSelect(): array
8824
    {
8825
        $theme = Container::$container->get(ThemeHelper::class)->getVisualTheme();
8826
        $filesystem = Container::$container->get('oneup_flysystem.themes_filesystem');
8827
8828
        if (!$filesystem->directoryExists("$theme/lp_icons")) {
8829
            return [];
8830
        }
8831
8832
        $icons = ['' => get_lang('Please select an option')];
8833
8834
        $iconFiles = $filesystem->listContents("$theme/lp_icons");
8835
        $allowedExtensions = ['image/jpeg', 'image/jpg', 'image/png'];
8836
8837
        foreach ($iconFiles as $iconFile) {
8838
            $mimeType = $filesystem->mimeType($iconFile->path());
8839
8840
            if (in_array($mimeType, $allowedExtensions)) {
8841
                $basename = basename($iconFile->path());
8842
                $icons[$basename] = $basename;
8843
            }
8844
        }
8845
8846
        return $icons;
8847
    }
8848
8849
    /**
8850
     * @param int $lpId
8851
     *
8852
     * @return string
8853
     */
8854
    public static function getSelectedIcon($lpId)
8855
    {
8856
        $extraFieldValue = new ExtraFieldValue('lp');
8857
        $lpIcon = $extraFieldValue->get_values_by_handler_and_field_variable($lpId, 'lp_icon');
8858
        $icon = '';
8859
        if (!empty($lpIcon) && isset($lpIcon['value'])) {
8860
            $icon = $lpIcon['value'];
8861
        }
8862
8863
        return $icon;
8864
    }
8865
8866
    public static function getSelectedIconHtml(int $lpId): string
8867
    {
8868
        $icon = self::getSelectedIcon($lpId);
8869
8870
        if (empty($icon)) {
8871
            return '';
8872
        }
8873
8874
        $path = Container::getThemeHelper()->getThemeAssetUrl("lp_icons/$icon");
8875
8876
        return Display::img($path);
8877
    }
8878
8879
    /**
8880
     * @param string $value
8881
     *
8882
     * @return string
8883
     */
8884
    public function cleanItemTitle($value)
8885
    {
8886
        $value = Security::remove_XSS(strip_tags($value));
8887
8888
        return $value;
8889
    }
8890
8891
    public function setItemTitle(FormValidator $form)
8892
    {
8893
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
8894
            $form->addHtmlEditor(
8895
                'title',
8896
                get_lang('Title'),
8897
                true,
8898
                false,
8899
                ['ToolbarSet' => 'TitleAsHtml', 'id' => uniqid('editor')]
8900
            );
8901
        } else {
8902
            $form->addText('title', get_lang('Title'), true, ['id' => 'idTitle', 'class' => 'learnpath_item_form']);
8903
            $form->applyFilter('title', 'trim');
8904
            $form->applyFilter('title', 'html_filter');
8905
        }
8906
    }
8907
8908
    /**
8909
     * @return array
8910
     */
8911
    public function getItemsForForm($addParentCondition = false)
8912
    {
8913
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
8914
8915
        $sql = "SELECT * FROM $tbl_lp_item
8916
                WHERE path <> 'root' AND lp_id = ".$this->lp_id;
8917
8918
        if ($addParentCondition) {
8919
            $sql .= ' AND parent_item_id IS NULL ';
8920
        }
8921
        $sql .= ' ORDER BY display_order ASC';
8922
8923
        $result = Database::query($sql);
8924
        $arrLP = [];
8925
        while ($row = Database::fetch_array($result)) {
8926
            $arrLP[] = [
8927
                'iid' => $row['iid'],
8928
                'id' => $row['iid'],
8929
                'item_type' => $row['item_type'],
8930
                'title' => $this->cleanItemTitle($row['title']),
8931
                'title_raw' => $row['title'],
8932
                'path' => $row['path'],
8933
                'description' => Security::remove_XSS($row['description']),
8934
                'parent_item_id' => $row['parent_item_id'],
8935
                'previous_item_id' => $row['previous_item_id'],
8936
                'next_item_id' => $row['next_item_id'],
8937
                'display_order' => $row['display_order'],
8938
                'max_score' => $row['max_score'],
8939
                'min_score' => $row['min_score'],
8940
                'mastery_score' => $row['mastery_score'],
8941
                'prerequisite' => $row['prerequisite'],
8942
                'max_time_allowed' => $row['max_time_allowed'],
8943
                'prerequisite_min_score' => $row['prerequisite_min_score'],
8944
                'prerequisite_max_score' => $row['prerequisite_max_score'],
8945
            ];
8946
        }
8947
8948
        return $arrLP;
8949
    }
8950
8951
    /**
8952
     * Gets whether this SCORM learning path has been marked to use the score
8953
     * as progress. Takes into account whether the learnpath matches (SCORM
8954
     * content + less than 2 items).
8955
     *
8956
     * @return bool True if the score should be used as progress, false otherwise
8957
     */
8958
    public function getUseScoreAsProgress()
8959
    {
8960
        // If not a SCORM, we don't care about the setting
8961
        if (2 != $this->get_type()) {
8962
            return false;
8963
        }
8964
        // If more than one step in the SCORM, we don't care about the setting
8965
        if ($this->get_total_items_count() > 1) {
8966
            return false;
8967
        }
8968
        $extraFieldValue = new ExtraFieldValue('lp');
8969
        $doUseScore = false;
8970
        $useScore = $extraFieldValue->get_values_by_handler_and_field_variable(
8971
            $this->get_id(),
8972
            'use_score_as_progress'
8973
        );
8974
        if (!empty($useScore) && isset($useScore['value'])) {
8975
            $doUseScore = $useScore['value'];
8976
        }
8977
8978
        return $doUseScore;
8979
    }
8980
8981
    /**
8982
     * Get the user identifier (user_id or username
8983
     * Depends on scorm_api_username_as_student_id in app/config/configuration.php.
8984
     *
8985
     * @return string User ID or username, depending on configuration setting
8986
     */
8987
    public static function getUserIdentifierForExternalServices()
8988
    {
8989
        $scormApiExtraFieldUseStudentId = api_get_setting('lp.scorm_api_extrafield_to_use_as_student_id');
8990
        $extraFieldValue = new ExtraFieldValue('user');
8991
        $extrafield = $extraFieldValue->get_values_by_handler_and_field_variable(
8992
            api_get_user_id(),
8993
            $scormApiExtraFieldUseStudentId
8994
        );
8995
        if (is_array($extrafield) && isset($extrafield['value'])) {
8996
            return $extrafield['value'];
8997
        } else {
8998
            if ('true' === $scormApiExtraFieldUseStudentId) {
8999
                return api_get_user_info(api_get_user_id())['username'];
9000
            } else {
9001
                return api_get_user_id();
9002
            }
9003
        }
9004
    }
9005
9006
    /**
9007
     * Save the new order for learning path items.
9008
     *
9009
     * @param array $orderList A associative array with id and parent_id keys.
9010
     */
9011
    public static function sortItemByOrderList(CLpItem $rootItem, array $orderList = [], $flush = true, $lpItemRepo = null, $em = null)
9012
    {
9013
        if (empty($orderList)) {
9014
            return true;
9015
        }
9016
        if (!isset($lpItemRepo)) {
9017
            $lpItemRepo = Container::getLpItemRepository();
9018
        }
9019
        if (!isset($em)) {
9020
            $em = Database::getManager();
9021
        }
9022
        $counter = 2;
9023
        $rootItem->setDisplayOrder(1);
9024
        $rootItem->setPreviousItemId(null);
9025
        $em->persist($rootItem);
9026
        if ($flush) {
9027
            $em->flush();
9028
        }
9029
9030
        foreach ($orderList as $item) {
9031
            $itemId = $item->id ?? 0;
9032
            if (empty($itemId)) {
9033
                continue;
9034
            }
9035
            $parentId = $item->parent_id ?? 0;
9036
            $parent = $rootItem;
9037
            if (!empty($parentId)) {
9038
                $parentExists = $lpItemRepo->find($parentId);
9039
                if (null !== $parentExists) {
9040
                    $parent = $parentExists;
9041
                }
9042
            }
9043
9044
            /** @var CLpItem $itemEntity */
9045
            $itemEntity = $lpItemRepo->find($itemId);
9046
            $itemEntity->setParent($parent);
9047
            $itemEntity->setPreviousItemId(null);
9048
            $itemEntity->setNextItemId(null);
9049
            $itemEntity->setDisplayOrder($counter);
9050
9051
            $em->persist($itemEntity);
9052
            if ($flush) {
9053
                $em->flush();
9054
            }
9055
            $counter++;
9056
        }
9057
9058
        $lpItemRepo->recoverNode($rootItem, 'displayOrder');
9059
        $em->persist($rootItem);
9060
        if ($flush) {
9061
            $em->flush();
9062
        }
9063
9064
        return true;
9065
    }
9066
9067
    public static function move(int $lpId, string $direction)
9068
    {
9069
        $em = Database::getManager();
9070
        /** @var CLp $lp */
9071
        $lp = Container::getLpRepository()->find($lpId);
9072
        if ($lp) {
9073
            $course = api_get_course_entity();
9074
            $session = api_get_session_entity();
9075
            $group = api_get_group_entity();
9076
9077
            $link = $lp->getResourceNode()->getResourceLinkByContext($course, $session, $group);
9078
9079
            if ($link) {
9080
                if ('down' === $direction) {
9081
                    $link->moveDownPosition();
9082
                }
9083
                if ('up' === $direction) {
9084
                    $link->moveUpPosition();
9085
                }
9086
9087
                $em->flush();
9088
            }
9089
        }
9090
    }
9091
9092
    /**
9093
     * Get the depth level of LP item.
9094
     *
9095
     * @param array $items
9096
     * @param int   $currentItemId
9097
     *
9098
     * @return int
9099
     */
9100
    private static function get_level_for_item($items, $currentItemId)
9101
    {
9102
        $parentItemId = 0;
9103
        if (isset($items[$currentItemId])) {
9104
            $parentItemId = $items[$currentItemId]->parent;
9105
        }
9106
9107
        if (0 == $parentItemId) {
9108
            return 0;
9109
        }
9110
9111
        return self::get_level_for_item($items, $parentItemId) + 1;
9112
    }
9113
9114
    /**
9115
     * Generate the link for a learnpath category as course tool.
9116
     *
9117
     * @param int $categoryId
9118
     *
9119
     * @return string
9120
     */
9121
    private static function getCategoryLinkForTool($categoryId)
9122
    {
9123
        $categoryId = (int) $categoryId;
9124
        return 'lp/lp_controller.php?'.api_get_cidreq().'&'
9125
            .http_build_query(
9126
                [
9127
                    'action' => 'view_category',
9128
                    'id' => $categoryId,
9129
                ]
9130
            );
9131
    }
9132
9133
    /**
9134
     * Check and obtain the lp final item if exist.
9135
     *
9136
     * @return learnpathItem
9137
     */
9138
    private function getFinalItem()
9139
    {
9140
        if (empty($this->items)) {
9141
            return null;
9142
        }
9143
9144
        foreach ($this->items as $item) {
9145
            if ('final_item' !== $item->type) {
9146
                continue;
9147
            }
9148
9149
            return $item;
9150
        }
9151
    }
9152
9153
    /**
9154
     * Get the LP Final Item Template.
9155
     *
9156
     * @return string
9157
     */
9158
    private function getFinalItemTemplate()
9159
    {
9160
        return file_get_contents(api_get_path(SYS_CODE_PATH).'lp/final_item_template/template.html');
9161
    }
9162
9163
    /**
9164
     * Get the LP Final Item Url.
9165
     *
9166
     * @return string
9167
     */
9168
    private function getSavedFinalItem()
9169
    {
9170
        $finalItem = $this->getFinalItem();
9171
9172
        $repo = Container::getDocumentRepository();
9173
        /** @var CDocument $document */
9174
        $document = $repo->find($finalItem->path);
9175
9176
        return $document ? $repo->getResourceFileContent($document) : '';
9177
    }
9178
9179
    /**
9180
     * Recalculates the results for all exercises associated with the learning path (LP) for the given user.
9181
     */
9182
    public function recalculateResultsForLp(int $userId): void
9183
    {
9184
        $em = Database::getManager();
9185
        $lpItemRepo = $em->getRepository(CLpItem::class);
9186
        $lpItems = $lpItemRepo->findBy(['lp' => $this->lp_id]);
9187
9188
        if (empty($lpItems)) {
9189
            Display::addFlash(Display::return_message(get_lang('No item found'), 'error'));
9190
            return;
9191
        }
9192
9193
        $lpItemsById = [];
9194
        foreach ($lpItems as $item) {
9195
            $lpItemsById[$item->getIid()] = $item;
9196
        }
9197
9198
        $trackEExerciseRepo = $em->getRepository(TrackEExercise::class);
9199
        $trackExercises = $trackEExerciseRepo->createQueryBuilder('te')
9200
            ->where('te.origLpId = :lpId')
9201
            ->andWhere('te.user = :userId')
9202
            ->andWhere('te.origLpItemId IN (:lpItemIds)')
9203
            ->setParameter('lpId', $this->lp_id)
9204
            ->setParameter('userId', $userId)
9205
            ->setParameter('lpItemIds', array_keys($lpItemsById))
9206
            ->getQuery()
9207
            ->getResult();
9208
9209
        if (empty($trackExercises)) {
9210
            Display::addFlash(Display::return_message(get_lang('No test attempt found'), 'error'));
9211
            return;
9212
        }
9213
9214
        foreach ($trackExercises as $trackExercise) {
9215
            $exeId = $trackExercise->getExeId();
9216
            $lpItemId = $trackExercise->getOrigLpItemId();
9217
9218
            if (!isset($lpItemsById[$lpItemId])) {
9219
                continue;
9220
            }
9221
9222
            $lpItem = $lpItemsById[$lpItemId];
9223
            if ('quiz' !== $lpItem->getItemType()) {
9224
                continue;
9225
            }
9226
9227
            $quizId = (int) $lpItem->getPath();
9228
            $courseId = (int) $trackExercise->getCourse()->getId();
9229
            $updatedExercise = ExerciseLib::recalculateResult($exeId, $userId, $quizId, $courseId);
9230
            if ($updatedExercise instanceof TrackEExercise) {
9231
                Display::addFlash(Display::return_message(get_lang('Results recalculated'), 'success'));
9232
            } else {
9233
                Display::addFlash(Display::return_message(get_lang('Error recalculating results'), 'error'));
9234
            }
9235
        }
9236
    }
9237
9238
    /**
9239
     * Returns the video player HTML for a video-type document LP item.
9240
     *
9241
     * @param int $lpItemId
9242
     * @param string $autostart
9243
     *
9244
     * @return string
9245
     */
9246
    public function getVideoPlayer(CDocument $document, string $autostart = 'true'): string
9247
    {
9248
        $resourceNode = $document->getResourceNode();
9249
        $resourceFile = $resourceNode?->getFirstResourceFile();
9250
9251
        if (!$resourceNode || !$resourceFile) {
9252
            return '';
9253
        }
9254
9255
        $resourceNodeRepository = Container::getResourceNodeRepository();
9256
        $videoUrl = $resourceNodeRepository->getResourceFileUrl($resourceNode);
9257
9258
        if (empty($videoUrl)) {
9259
            return '';
9260
        }
9261
9262
        $fileName = $resourceFile->getTitle();
9263
        $ext = pathinfo($fileName, PATHINFO_EXTENSION);
9264
        $mimeType = $resourceFile->getMimeType() ?: 'video/mp4';
9265
        $autoplayAttr = ($autostart === 'true') ? 'autoplay muted playsinline' : '';
9266
9267
        $html = '';
9268
        $html .= '
9269
        <video id="lp-video" width="100%" height="auto" controls '.$autoplayAttr.'>
9270
            <source src="'.$videoUrl.'" type="$mimeType">
9271
        </video>';
9272
9273
        return $html;
9274
    }
9275
}
9276