Passed
Push — master ( 82afe7...83c1fc )
by Yannick
13:03 queued 04:26
created

learnpath::get_videos()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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