learnpath::set_autolaunch()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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