Passed
Pull Request — master (#6536)
by
unknown
28:54 queued 19:00
created

learnpath::get_progress_bar()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 2
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\Course;
6
use Chamilo\CoreBundle\Entity\ResourceLink;
7
use Chamilo\CoreBundle\Entity\ResourceNode;
8
use Chamilo\CoreBundle\Entity\Session as SessionEntity;
9
use Chamilo\CoreBundle\Entity\TrackEExercise;
10
use Chamilo\CoreBundle\Entity\User;
11
use Chamilo\CoreBundle\Enums\ObjectIcon;
12
use Chamilo\CoreBundle\Event\Events;
13
use Chamilo\CoreBundle\Event\LearningPathEndedEvent;
14
use Chamilo\CoreBundle\Framework\Container;
15
use Chamilo\CoreBundle\Repository\TrackEDefaultRepository;
16
use Chamilo\CoreBundle\Helpers\ThemeHelper;
17
use Chamilo\CourseBundle\Component\CourseCopy\CourseArchiver;
18
use Chamilo\CourseBundle\Component\CourseCopy\CourseBuilder;
19
use Chamilo\CourseBundle\Component\CourseCopy\CourseRestorer;
20
use Chamilo\CourseBundle\Entity\CDocument;
21
use Chamilo\CourseBundle\Entity\CForumThread;
22
use Chamilo\CourseBundle\Entity\CLink;
23
use Chamilo\CourseBundle\Entity\CLp;
24
use Chamilo\CourseBundle\Entity\CLpCategory;
25
use Chamilo\CourseBundle\Entity\CLpItem;
26
use Chamilo\CourseBundle\Entity\CLpItemView;
27
use Chamilo\CourseBundle\Entity\CLpRelUser;
28
use Chamilo\CourseBundle\Entity\CQuiz;
29
use Chamilo\CourseBundle\Entity\CStudentPublication;
30
use Chamilo\CourseBundle\Entity\CSurvey;
31
use Chamilo\CourseBundle\Entity\CTool;
32
use Chamilo\CourseBundle\Repository\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)
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(
2596
                [
2597
                    'displayOrder' => Criteria::ASC,
2598
                ]
2599
            );
2600
        $items = $lp->getItems()->matching($criteria);
2601
        $items = $items->filter(
2602
            function (CLpItem $element) use ($parent) {
2603
                if ('root' === $element->getPath()) {
2604
                    return false;
2605
                }
2606
2607
                if (null !== $element->getParent()) {
2608
                    return $element->getParent()->getIid() === $parent;
2609
                }
2610
                return false;
2611
2612
            }
2613
        );
2614
        $list = [];
2615
        foreach ($items as $item) {
2616
            $itemId = $item->getIid();
2617
            $sublist = self::get_flat_ordered_items_list($lp, $itemId);
2618
            $list[] = $itemId;
2619
            foreach ($sublist as $subItem) {
2620
                $list[] = $subItem;
2621
            }
2622
        }
2623
2624
        return $list;
2625
    }
2626
2627
    public static function getChapterTypes(): array
2628
    {
2629
        return [
2630
            'dir',
2631
        ];
2632
    }
2633
2634
    /**
2635
     * Uses the table generated by get_toc() and returns an HTML-formatted string ready to display.
2636
     *
2637
     * @return array HTML TOC ready to display
2638
     */
2639
    public function getListArrayToc()
2640
    {
2641
        $lpItemRepo = Container::getLpItemRepository();
2642
        $itemRoot = $lpItemRepo->getRootItem($this->get_id());
2643
        $options = [
2644
            'decorate' => false,
2645
        ];
2646
2647
        return $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
2648
    }
2649
2650
    /**
2651
     * Returns an HTML-formatted string ready to display with teacher buttons
2652
     * in LP view menu.
2653
     *
2654
     * @return string HTML TOC ready to display
2655
     */
2656
    public function get_teacher_toc_buttons()
2657
    {
2658
        $isAllow = api_is_allowed_to_edit(null, true, false, false);
2659
        $hideIcons = api_get_configuration_value('hide_teacher_icons_lp');
2660
        $html = '';
2661
        if ($isAllow && false == $hideIcons) {
2662
            if ($this->get_lp_session_id() == api_get_session_id()) {
2663
                $html .= '<div id="actions_lp" class="actions_lp"><hr>';
2664
                $html .= '<div class="flex flex-wrap gap-1 justify-center">';
2665
                $html .= "<a
2666
                    class='btn btn-sm btn--plain'
2667
                    href='lp_controller.php?".api_get_cidreq()."&action=add_item&type=step&lp_id=".$this->lp_id."&isStudentView=false'
2668
                    target='_parent'>".
2669
                    Display::getMdiIcon('pencil').get_lang('Edit')."</a>";
2670
                $html .= '<a
2671
                    class="btn btn-sm btn--plain"
2672
                    href="lp_controller.php?'.api_get_cidreq()."&action=edit&lp_id=".$this->lp_id.'&isStudentView=false">'.
2673
                    Display::getMdiIcon('hammer-wrench').get_lang('Settings').'</a>';
2674
                $html .= '</div>';
2675
                $html .= '</div>';
2676
            }
2677
        }
2678
2679
        return $html;
2680
    }
2681
2682
    /**
2683
     * Gets the learnpath name/title.
2684
     *
2685
     * @return string Learnpath name/title
2686
     */
2687
    public function get_name()
2688
    {
2689
        if (!empty($this->name)) {
2690
            return $this->name;
2691
        }
2692
2693
        return 'N/A';
2694
    }
2695
2696
    /**
2697
     * @return string
2698
     */
2699
    public function getNameNoTags()
2700
    {
2701
        return strip_tags($this->get_name());
2702
    }
2703
2704
    /**
2705
     * Gets a link to the resource from the present location, depending on item ID.
2706
     *
2707
     * @param string $type         Type of link expected
2708
     * @param int    $item_id      Learnpath item ID
2709
     * @param bool   $provided_toc
2710
     *
2711
     * @return string $provided_toc Link to the lp_item resource
2712
     */
2713
    public function get_link($type = 'http', $item_id = 0, $provided_toc = false)
2714
    {
2715
        $course_id = $this->get_course_int_id();
2716
        $item_id = (int) $item_id;
2717
2718
        if (empty($item_id)) {
2719
            $item_id = $this->get_current_item_id();
2720
2721
            if (empty($item_id)) {
2722
                //still empty, this means there was no item_id given and we are not in an object context or
2723
                //the object property is empty, return empty link
2724
                $this->first();
2725
2726
                return '';
2727
            }
2728
        }
2729
2730
        $file = '';
2731
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
2732
        $lp_item_table = Database::get_course_table(TABLE_LP_ITEM);
2733
        $lp_item_view_table = Database::get_course_table(TABLE_LP_ITEM_VIEW);
2734
2735
        $sql = "SELECT
2736
                    l.lp_type as ltype,
2737
                    l.path as lpath,
2738
                    li.item_type as litype,
2739
                    li.path as lipath,
2740
                    li.parameters as liparams
2741
        		FROM $lp_table l
2742
                INNER JOIN $lp_item_table li
2743
                ON (li.lp_id = l.iid)
2744
        		WHERE
2745
        		    li.iid = $item_id
2746
        		";
2747
        $res = Database::query($sql);
2748
        if (Database::num_rows($res) > 0) {
2749
            $row = Database::fetch_array($res);
2750
            $lp_type = $row['ltype'];
2751
            $lp_path = $row['lpath'];
2752
            $lp_item_type = $row['litype'];
2753
            $lp_item_path = $row['lipath'];
2754
            $lp_item_params = $row['liparams'];
2755
            if (empty($lp_item_params) && false !== strpos($lp_item_path, '?')) {
2756
                [$lp_item_path, $lp_item_params] = explode('?', $lp_item_path);
2757
            }
2758
            //$sys_course_path = api_get_path(SYS_COURSE_PATH).api_get_course_path();
2759
            if ('http' === $type) {
2760
                //web path
2761
                //$course_path = api_get_path(WEB_COURSE_PATH).api_get_course_path();
2762
            } else {
2763
                //$course_path = $sys_course_path; //system path
2764
            }
2765
2766
            // Fixed issue BT#1272 - If the item type is a Chamilo Item (quiz, link, etc),
2767
            // then change the lp type to thread it as a normal Chamilo LP not a SCO.
2768
            if (in_array(
2769
                $lp_item_type,
2770
                ['quiz', 'document', 'final_item', 'link', 'forum', 'thread', 'student_publication', 'survey']
2771
            )
2772
            ) {
2773
                $lp_type = CLp::LP_TYPE;
2774
            }
2775
2776
            // Now go through the specific cases to get the end of the path
2777
            // @todo Use constants instead of int values.
2778
            switch ($lp_type) {
2779
                case CLp::LP_TYPE:
2780
                    $file = self::rl_get_resource_link_for_learnpath(
2781
                        $course_id,
2782
                        $this->get_id(),
2783
                        $item_id,
2784
                        $this->get_view_id()
2785
                    );
2786
                    switch ($lp_item_type) {
2787
                        case 'document':
2788
                            // Shows a button to download the file instead of just downloading the file directly.
2789
                            $documentPathInfo = pathinfo($file);
2790
                            if (isset($documentPathInfo['extension'])) {
2791
                                $parsed = parse_url($documentPathInfo['extension']);
2792
                                if (isset($parsed['path'])) {
2793
                                    $extension = $parsed['path'];
2794
                                    $extensionsToDownload = [
2795
                                        'zip',
2796
                                        'ppt',
2797
                                        'pptx',
2798
                                        'ods',
2799
                                        'xlsx',
2800
                                        'xls',
2801
                                        'csv',
2802
                                        'doc',
2803
                                        'docx',
2804
                                        'dot',
2805
                                    ];
2806
2807
                                    if (in_array($extension, $extensionsToDownload)) {
2808
                                        $file = api_get_path(WEB_CODE_PATH).
2809
                                            'lp/embed.php?type=download&source=file&lp_item_id='.$item_id.'&'.api_get_cidreq();
2810
                                    }
2811
                                }
2812
                            }
2813
                            break;
2814
                        case 'dir':
2815
                            $file = 'lp_content.php?type=dir';
2816
                            break;
2817
                        case 'link':
2818
                            if (Link::is_youtube_link($file)) {
2819
                                $src = Link::get_youtube_video_id($file);
2820
                                $file = api_get_path(WEB_CODE_PATH).'lp/embed.php?type=youtube&source='.$src;
2821
                            } elseif (Link::isVimeoLink($file)) {
2822
                                $src = Link::getVimeoLinkId($file);
2823
                                $file = api_get_path(WEB_CODE_PATH).'lp/embed.php?type=vimeo&source='.$src;
2824
                            } else {
2825
                                // If the current site is HTTPS and the link is
2826
                                // HTTP, browsers will refuse opening the link
2827
                                $urlId = api_get_current_access_url_id();
2828
                                $url = api_get_access_url($urlId, false);
2829
                                $protocol = substr($url['url'], 0, 5);
2830
                                if ('https' === $protocol) {
2831
                                    $linkProtocol = substr($file, 0, 5);
2832
                                    if ('http:' === $linkProtocol) {
2833
                                        //this is the special intervention case
2834
                                        $file = api_get_path(WEB_CODE_PATH).
2835
                                            'lp/embed.php?type=nonhttps&source='.urlencode($file);
2836
                                    }
2837
                                }
2838
                            }
2839
                            break;
2840
                        case 'quiz':
2841
                            // Check how much attempts of a exercise exits in lp
2842
                            $lp_item_id = $this->get_current_item_id();
2843
                            $lp_view_id = $this->get_view_id();
2844
2845
                            $prevent_reinit = null;
2846
                            if (isset($this->items[$this->current])) {
2847
                                $prevent_reinit = $this->items[$this->current]->get_prevent_reinit();
2848
                            }
2849
2850
                            if (empty($provided_toc)) {
2851
                                $list = $this->get_toc();
2852
                            } else {
2853
                                $list = $provided_toc;
2854
                            }
2855
2856
                            $type_quiz = false;
2857
                            foreach ($list as $toc) {
2858
                                if ($toc['id'] == $lp_item_id && 'quiz' === $toc['type']) {
2859
                                    $type_quiz = true;
2860
                                }
2861
                            }
2862
2863
                            if ($type_quiz) {
2864
                                $lp_item_id = (int) $lp_item_id;
2865
                                $lp_view_id = (int) $lp_view_id;
2866
                                $sql = "SELECT count(*) FROM $lp_item_view_table
2867
                                        WHERE
2868
                                            lp_item_id='".$lp_item_id."' AND
2869
                                            lp_view_id ='".$lp_view_id."' AND
2870
                                            status='completed'";
2871
                                $result = Database::query($sql);
2872
                                $row_count = Database:: fetch_row($result);
2873
                                $count_item_view = (int) $row_count[0];
2874
                                $not_multiple_attempt = 0;
2875
                                if (1 === $prevent_reinit && $count_item_view > 0) {
2876
                                    $not_multiple_attempt = 1;
2877
                                }
2878
                                $file .= '&not_multiple_attempt='.$not_multiple_attempt;
2879
                            }
2880
                            break;
2881
                    }
2882
2883
                    $tmp_array = explode('/', $file);
2884
                    $document_name = $tmp_array[count($tmp_array) - 1];
2885
                    if (strpos($document_name, '_DELETED_')) {
2886
                        $file = 'blank.php?error=document_deleted';
2887
                    }
2888
                    break;
2889
                case CLp::SCORM_TYPE:
2890
                    if ('dir' !== $lp_item_type) {
2891
                        // Quite complex here:
2892
                        // We want to make sure 'http://' (and similar) links can
2893
                        // be loaded as is (withouth the Chamilo path in front) but
2894
                        // some contents use this form: resource.htm?resource=http://blablabla
2895
                        // which means we have to find a protocol at the path's start, otherwise
2896
                        // it should not be considered as an external URL.
2897
                        // if ($this->prerequisites_match($item_id)) {
2898
                        if (0 != preg_match('#^[a-zA-Z]{2,5}://#', $lp_item_path)) {
2899
                            if ($this->debug > 2) {
2900
                                error_log('In learnpath::get_link() '.__LINE__.' - Found match for protocol in '.$lp_item_path, 0);
2901
                            }
2902
                            // Distant url, return as is.
2903
                            $file = $lp_item_path;
2904
                        } else {
2905
                            if ($this->debug > 2) {
2906
                                error_log('In learnpath::get_link() '.__LINE__.' - No starting protocol in '.$lp_item_path);
2907
                            }
2908
                            // Prevent getting untranslatable urls.
2909
                            $lp_item_path = preg_replace('/%2F/', '/', $lp_item_path);
2910
                            $lp_item_path = preg_replace('/%3A/', ':', $lp_item_path);
2911
2912
                            /*$asset = $this->getEntity()->getAsset();
2913
                            $folder = Container::getAssetRepository()->getFolder($asset);
2914
                            $hasFile = Container::getAssetRepository()->getFileSystem()->has($folder.$lp_item_path);
2915
                            $file = null;
2916
                            if ($hasFile) {
2917
                                $file = Container::getAssetRepository()->getAssetUrl($asset).'/'.$lp_item_path;
2918
                            }*/
2919
                            $file = $this->scormUrl.$lp_item_path;
2920
2921
                            // Prepare the path.
2922
                            /*$file = $course_path.'/scorm/'.$lp_path.'/'.$lp_item_path;
2923
                            // TODO: Fix this for urls with protocol header.
2924
                            $file = str_replace('//', '/', $file);
2925
                            $file = str_replace(':/', '://', $file);
2926
                            if ('/' === substr($lp_path, -1)) {
2927
                                $lp_path = substr($lp_path, 0, -1);
2928
                            }*/
2929
                            /*if (!$hasFile) {
2930
                                // if file not found.
2931
                                $decoded = html_entity_decode($lp_item_path);
2932
                                [$decoded] = explode('?', $decoded);
2933
                                if (!is_file(realpath($sys_course_path.'/scorm/'.$lp_path.'/'.$decoded))) {
2934
                                    $file = self::rl_get_resource_link_for_learnpath(
2935
                                        $course_id,
2936
                                        $this->get_id(),
2937
                                        $item_id,
2938
                                        $this->get_view_id()
2939
                                    );
2940
                                    if (empty($file)) {
2941
                                        $file = 'blank.php?error=document_not_found';
2942
                                    } else {
2943
                                        $tmp_array = explode('/', $file);
2944
                                        $document_name = $tmp_array[count($tmp_array) - 1];
2945
                                        if (strpos($document_name, '_DELETED_')) {
2946
                                            $file = 'blank.php?error=document_deleted';
2947
                                        } else {
2948
                                            $file = 'blank.php?error=document_not_found';
2949
                                        }
2950
                                    }
2951
                                } else {
2952
                                    $file = $course_path.'/scorm/'.$lp_path.'/'.$decoded;
2953
                                }
2954
                            }*/
2955
                        }
2956
2957
                        // We want to use parameters if they were defined in the imsmanifest
2958
                        if (false === strpos($file, 'blank.php')) {
2959
                            $lp_item_params = ltrim($lp_item_params, '?');
2960
                            $file .= (false === strstr($file, '?') ? '?' : '').$lp_item_params;
2961
                        }
2962
                    } else {
2963
                        $file = 'lp_content.php?type=dir';
2964
                    }
2965
                    break;
2966
                case CLp::AICC_TYPE:
2967
                    // Formatting AICC HACP append URL.
2968
                    $aicc_append = '?aicc_sid='.
2969
                        urlencode(session_id()).'&aicc_url='.urlencode(api_get_path(WEB_CODE_PATH).'lp/aicc_hacp.php').'&';
2970
                    if (!empty($lp_item_params)) {
2971
                        $aicc_append .= $lp_item_params.'&';
2972
                    }
2973
                    if ('dir' !== $lp_item_type) {
2974
                        // Quite complex here:
2975
                        // We want to make sure 'http://' (and similar) links can
2976
                        // be loaded as is (withouth the Chamilo path in front) but
2977
                        // some contents use this form: resource.htm?resource=http://blablabla
2978
                        // which means we have to find a protocol at the path's start, otherwise
2979
                        // it should not be considered as an external URL.
2980
                        if (0 != preg_match('#^[a-zA-Z]{2,5}://#', $lp_item_path)) {
2981
                            if ($this->debug > 2) {
2982
                                error_log('In learnpath::get_link() '.__LINE__.' - Found match for protocol in '.$lp_item_path, 0);
2983
                            }
2984
                            // Distant url, return as is.
2985
                            $file = $lp_item_path;
2986
                            // Enabled and modified by Ivan Tcholakov, 16-OCT-2008.
2987
                            /*
2988
                            if (stristr($file,'<servername>') !== false) {
2989
                                $file = str_replace('<servername>', $course_path.'/scorm/'.$lp_path.'/', $lp_item_path);
2990
                            }
2991
                            */
2992
                            if (false !== stripos($file, '<servername>')) {
2993
                                //$file = str_replace('<servername>',$course_path.'/scorm/'.$lp_path.'/',$lp_item_path);
2994
                                $web_course_path = str_replace('https://', '', str_replace('http://', '', $course_path));
2995
                                $file = str_replace('<servername>', $web_course_path.'/scorm/'.$lp_path, $lp_item_path);
2996
                            }
2997
2998
                            $file .= $aicc_append;
2999
                        } else {
3000
                            if ($this->debug > 2) {
3001
                                error_log('In learnpath::get_link() '.__LINE__.' - No starting protocol in '.$lp_item_path, 0);
3002
                            }
3003
                            // Prevent getting untranslatable urls.
3004
                            $lp_item_path = preg_replace('/%2F/', '/', $lp_item_path);
3005
                            $lp_item_path = preg_replace('/%3A/', ':', $lp_item_path);
3006
                            // Prepare the path - lp_path might be unusable because it includes the "aicc" subdir name.
3007
                            $file = $course_path.'/scorm/'.$lp_path.'/'.$lp_item_path;
3008
                            // TODO: Fix this for urls with protocol header.
3009
                            $file = str_replace('//', '/', $file);
3010
                            $file = str_replace(':/', '://', $file);
3011
                            $file .= $aicc_append;
3012
                        }
3013
                    } else {
3014
                        $file = 'lp_content.php?type=dir';
3015
                    }
3016
                    break;
3017
                case 4:
3018
                default:
3019
                    break;
3020
            }
3021
            // Replace &amp; by & because &amp; will break URL with params
3022
            $file = !empty($file) ? str_replace('&amp;', '&', $file) : '';
3023
        }
3024
        if ($this->debug > 2) {
3025
            error_log('In learnpath::get_link() - returning "'.$file.'" from get_link', 0);
3026
        }
3027
3028
        return $file;
3029
    }
3030
3031
    /**
3032
     * Gets the latest usable view or generate a new one.
3033
     *
3034
     * @param int $attempt_num Optional attempt number. If none given, takes the highest from the lp_view table
3035
     * @param int $userId      The user ID, as $this->get_user_id() is not always available
3036
     *
3037
     * @return int DB lp_view id
3038
     */
3039
    public function get_view($attempt_num = 0, $userId = null)
3040
    {
3041
        $search = '';
3042
        $attempt_num = (int) $attempt_num;
3043
        // Use $attempt_num to enable multi-views management (disabled so far).
3044
        if (!empty($attempt_num)) {
3045
            $search = 'AND view_count = '.$attempt_num;
3046
        }
3047
3048
        $course_id = api_get_course_int_id();
3049
        $sessionId = api_get_session_id();
3050
3051
        // Check user ID.
3052
        if (empty($userId)) {
3053
            if (empty($this->get_user_id())) {
3054
                $this->error = 'User ID is empty in learnpath::get_view()';
3055
3056
                return null;
3057
            } else {
3058
                $userId = $this->get_user_id();
3059
            }
3060
        }
3061
        $sessionCondition = api_get_session_condition($sessionId);
3062
3063
        // When missing $attempt_num, search for a unique lp_view record for this lp and user.
3064
        $table = Database::get_course_table(TABLE_LP_VIEW);
3065
        $sql = "SELECT iid FROM $table
3066
        		WHERE
3067
        		    c_id = $course_id AND
3068
        		    lp_id = ".$this->get_id()." AND
3069
        		    user_id = ".$userId."
3070
        		    $sessionCondition
3071
        		    $search
3072
                ORDER BY view_count DESC";
3073
        $res = Database::query($sql);
3074
        if (Database::num_rows($res) > 0) {
3075
            $row = Database::fetch_array($res);
3076
            $this->lp_view_id = $row['iid'];
3077
        } elseif (!api_is_invitee()) {
3078
            $params = [
3079
                'c_id' => $course_id,
3080
                'lp_id' => $this->get_id(),
3081
                'user_id' => $this->get_user_id(),
3082
                'view_count' => 1,
3083
                'last_item' => 0,
3084
            ];
3085
            if (!empty($sessionId)) {
3086
                $params['session_id']  = $sessionId;
3087
            }
3088
            $this->lp_view_id = Database::insert($table, $params);
3089
        }
3090
3091
        return $this->lp_view_id;
3092
    }
3093
3094
    /**
3095
     * Gets the current view id.
3096
     *
3097
     * @return int View ID (from lp_view)
3098
     */
3099
    public function get_view_id()
3100
    {
3101
        if (!empty($this->lp_view_id)) {
3102
            return (int) $this->lp_view_id;
3103
        }
3104
3105
        return 0;
3106
    }
3107
3108
    /**
3109
     * Gets the update queue.
3110
     *
3111
     * @return array Array containing IDs of items to be updated by JavaScript
3112
     */
3113
    public function get_update_queue()
3114
    {
3115
        return $this->update_queue;
3116
    }
3117
3118
    /**
3119
     * Gets the user ID.
3120
     *
3121
     * @return int User ID
3122
     */
3123
    public function get_user_id()
3124
    {
3125
        if (!empty($this->user_id)) {
3126
            return (int) $this->user_id;
3127
        }
3128
3129
        return false;
3130
    }
3131
3132
    /**
3133
     * Checks if any of the items has an audio element attached.
3134
     *
3135
     * @return bool True or false
3136
     */
3137
    public function has_audio()
3138
    {
3139
        $has = false;
3140
        foreach ($this->items as $i => $item) {
3141
            if (!empty($this->items[$i]->audio)) {
3142
                $has = true;
3143
                break;
3144
            }
3145
        }
3146
3147
        return $has;
3148
    }
3149
3150
    /**
3151
     * Updates learnpath attributes to point to the next element
3152
     * The last part is similar to set_current_item but processing the other way around.
3153
     */
3154
    public function next()
3155
    {
3156
        if ($this->debug > 0) {
3157
            error_log('In learnpath::next()', 0);
3158
        }
3159
        $this->last = $this->get_current_item_id();
3160
        $this->items[$this->last]->save(
3161
            false,
3162
            $this->prerequisites_match($this->last)
3163
        );
3164
        $this->autocomplete_parents($this->last);
3165
        $new_index = $this->get_next_index();
3166
        if ($this->debug > 2) {
3167
            error_log('New index: '.$new_index, 0);
3168
        }
3169
        $this->index = $new_index;
3170
        if ($this->debug > 2) {
3171
            error_log('Now having orderedlist['.$new_index.'] = '.$this->ordered_items[$new_index], 0);
3172
        }
3173
        $this->current = $this->ordered_items[$new_index];
3174
        if ($this->debug > 2) {
3175
            error_log('new item id is '.$this->current.'-'.$this->get_current_item_id(), 0);
3176
        }
3177
    }
3178
3179
    /**
3180
     * Open a resource = initialise all local variables relative to this resource. Depending on the child
3181
     * class, this might be redefined to allow several behaviours depending on the document type.
3182
     *
3183
     * @param int $id Resource ID
3184
     */
3185
    public function open($id)
3186
    {
3187
        // TODO:
3188
        // set the current resource attribute to this resource
3189
        // switch on element type (redefine in child class?)
3190
        // set status for this item to "opened"
3191
        // start timer
3192
        // initialise score
3193
        $this->index = 0; //or = the last item seen (see $this->last)
3194
    }
3195
3196
    /**
3197
     * Check that all prerequisites are fulfilled. Returns true and an
3198
     * empty string on success, returns false
3199
     * and the prerequisite string on error.
3200
     * This function is based on the rules for aicc_script language as
3201
     * described in the SCORM 1.2 CAM documentation page 108.
3202
     *
3203
     * @param int $itemId Optional item ID. If none given, uses the current open item.
3204
     *
3205
     * @return bool true if prerequisites are matched, false otherwise - Empty string if true returned, prerequisites
3206
     *              string otherwise
3207
     */
3208
    public function prerequisites_match($itemId = null)
3209
    {
3210
        $allow = ('true' === api_get_setting('lp.allow_teachers_to_access_blocked_lp_by_prerequisite'));
3211
        if ($allow) {
3212
            if (api_is_allowed_to_edit() ||
3213
                api_is_platform_admin(true) ||
3214
                api_is_drh() ||
3215
                api_is_coach(api_get_session_id(), api_get_course_int_id())
3216
            ) {
3217
                return true;
3218
            }
3219
        }
3220
3221
        $debug = $this->debug;
3222
        if ($debug > 0) {
3223
            error_log('In learnpath::prerequisites_match()');
3224
        }
3225
3226
        if (empty($itemId)) {
3227
            $itemId = $this->current;
3228
        }
3229
3230
        $currentItem = $this->getItem($itemId);
3231
3232
        if ($currentItem) {
3233
            if (2 == $this->type) {
3234
                // Getting prereq from scorm
3235
                $prereq_string = $this->get_scorm_prereq_string($itemId);
3236
            } else {
3237
                $prereq_string = $currentItem->get_prereq_string();
3238
            }
3239
3240
            if (empty($prereq_string)) {
3241
                if ($debug > 0) {
3242
                    error_log('Found prereq_string is empty return true');
3243
                }
3244
3245
                return true;
3246
            }
3247
3248
            // Clean spaces.
3249
            $prereq_string = str_replace(' ', '', $prereq_string);
3250
            if ($debug > 0) {
3251
                error_log('Found prereq_string: '.$prereq_string, 0);
3252
            }
3253
3254
            // Now send to the parse_prereq() function that will check this component's prerequisites.
3255
            $result = $currentItem->parse_prereq(
3256
                $prereq_string,
3257
                $this->items,
3258
                $this->refs_list,
3259
                $this->get_user_id()
3260
            );
3261
3262
            if (false === $result) {
3263
                $this->set_error_msg($currentItem->prereq_alert);
3264
            }
3265
        } else {
3266
            $result = true;
3267
            if ($debug > 1) {
3268
                error_log('$this->items['.$itemId.'] was not an object', 0);
3269
            }
3270
        }
3271
3272
        if ($debug > 1) {
3273
            error_log('End of prerequisites_match(). Error message is now '.$this->error, 0);
3274
        }
3275
3276
        return $result;
3277
    }
3278
3279
    /**
3280
     * Updates learnpath attributes to point to the previous element
3281
     * The last part is similar to set_current_item but processing the other way around.
3282
     */
3283
    public function previous()
3284
    {
3285
        $this->last = $this->get_current_item_id();
3286
        $this->items[$this->last]->save(
3287
            false,
3288
            $this->prerequisites_match($this->last)
3289
        );
3290
        $this->autocomplete_parents($this->last);
3291
        $new_index = $this->get_previous_index();
3292
        $this->index = $new_index;
3293
        $this->current = $this->ordered_items[$new_index];
3294
    }
3295
3296
    /**
3297
     * Publishes a learnpath. This basically means show or hide the learnpath
3298
     * to normal users.
3299
     * Can be used as abstract.
3300
     *
3301
     * @param int $id         Learnpath ID
3302
     * @param int $visibility New visibility (1 = visible/published, 0= invisible/draft)
3303
     *
3304
     * @return bool
3305
     */
3306
    public static function toggleVisibility($id, $visibility = 1)
3307
    {
3308
        $repo = Container::getLpRepository();
3309
        $lp = $repo->find($id);
3310
3311
        if (!$lp) {
3312
            return false;
3313
        }
3314
3315
        $visibility = (int) $visibility;
3316
3317
        $course = api_get_course_entity();
3318
        $session = api_get_session_entity();
3319
3320
        if (1 === $visibility) {
3321
            $repo->setVisibilityPublished($lp, $course, $session);
3322
        } else {
3323
            $repo->setVisibilityDraft($lp, $course, $session);
3324
        }
3325
3326
        return true;
3327
    }
3328
3329
    /**
3330
     * Publishes a learnpath category.
3331
     * This basically means show or hide the learnpath category to normal users.
3332
     *
3333
     * @param int $id
3334
     * @param int $visibility
3335
     *
3336
     * @return bool
3337
     */
3338
    public static function toggleCategoryVisibility($id, $visibility = 1)
3339
    {
3340
        $repo = Container::getLpCategoryRepository();
3341
        $resource = $repo->find($id);
3342
3343
        if (!$resource) {
3344
            return false;
3345
        }
3346
3347
        $visibility = (int) $visibility;
3348
3349
        $course = api_get_course_entity();
3350
        $session = api_get_session_entity();
3351
3352
        if (1 === $visibility) {
3353
            $repo->setVisibilityPublished($resource, $course, $session);
3354
        } else {
3355
            $repo->setVisibilityDraft($resource, $course, $session);
3356
            self::toggleCategoryPublish($id, 0);
3357
        }
3358
3359
        return false;
3360
    }
3361
3362
    /**
3363
     * Publishes a learnpath. This basically means show or hide the learnpath
3364
     * on the course homepage.
3365
     *
3366
     * @param int    $id            Learnpath id
3367
     * @param string $setVisibility New visibility (v/i - visible/invisible)
3368
     *
3369
     * @return bool
3370
     */
3371
    public static function togglePublish($id, $setVisibility = 'v')
3372
    {
3373
        $addShortcut = false;
3374
        if ('v' === $setVisibility) {
3375
            $addShortcut = true;
3376
        }
3377
        $repo = Container::getLpRepository();
3378
        /** @var CLp|null $lp */
3379
        $lp = $repo->find($id);
3380
        if (null === $lp) {
3381
            return false;
3382
        }
3383
        $repoShortcut = Container::getShortcutRepository();
3384
        if ($addShortcut) {
3385
            $repoShortcut->addShortCut($lp, api_get_user_entity(), api_get_course_entity(), api_get_session_entity());
3386
        } else {
3387
            $repoShortcut->removeShortCut($lp);
3388
        }
3389
3390
        return true;
3391
    }
3392
3393
    /**
3394
     * Show or hide the learnpath category on the course homepage.
3395
     *
3396
     * @param int $id
3397
     * @param int $setVisibility
3398
     *
3399
     * @return bool
3400
     */
3401
    public static function toggleCategoryPublish($id, $setVisibility = 1)
3402
    {
3403
        $setVisibility = (int) $setVisibility;
3404
        $addShortcut = false;
3405
        if (1 === $setVisibility) {
3406
            $addShortcut = true;
3407
        }
3408
3409
        $repo = Container::getLpCategoryRepository();
3410
        /** @var CLpCategory|null $lp */
3411
        $category = $repo->find($id);
3412
3413
        if (null === $category) {
3414
            return false;
3415
        }
3416
3417
        $repoShortcut = Container::getShortcutRepository();
3418
        if ($addShortcut) {
3419
            $courseEntity = api_get_course_entity(api_get_course_int_id());
3420
            $repoShortcut->addShortCut($category, api_get_user_entity(), $courseEntity, api_get_session_entity());
3421
        } else {
3422
            $repoShortcut->removeShortCut($category);
3423
        }
3424
3425
        return true;
3426
    }
3427
3428
    /**
3429
     * Check if the learnpath category is visible for a user.
3430
     *
3431
     * @return bool
3432
     */
3433
    public static function categoryIsVisibleForStudent(
3434
        CLpCategory $category,
3435
        User $user,
3436
        Course $course,
3437
        SessionEntity $session = null
3438
    ) {
3439
        $isAllowedToEdit = api_is_allowed_to_edit(null, true);
3440
3441
        if ($isAllowedToEdit) {
3442
            return true;
3443
        }
3444
3445
        $categoryVisibility = $category->isVisible($course, $session);
3446
3447
        if (!$categoryVisibility) {
3448
            return false;
3449
        }
3450
3451
        $subscriptionSettings = self::getSubscriptionSettings();
3452
3453
        if (false === $subscriptionSettings['allow_add_users_to_lp_category']) {
3454
            return true;
3455
        }
3456
3457
        $noUserSubscribed = false;
3458
        $noGroupSubscribed = true;
3459
        $users = $category->getUsers();
3460
        if (empty($users) || !$users->count()) {
3461
            $noUserSubscribed = true;
3462
        } elseif ($category->hasUserAdded($user)) {
3463
            return true;
3464
        }
3465
3466
        //$groups = GroupManager::getAllGroupPerUserSubscription($user->getId());
3467
3468
        return $noGroupSubscribed && $noUserSubscribed;
3469
    }
3470
3471
    /**
3472
     * Check if a learnpath category is published as course tool.
3473
     *
3474
     * @param int $courseId
3475
     *
3476
     * @return bool
3477
     */
3478
    public static function categoryIsPublished(CLpCategory $category, $courseId)
3479
    {
3480
        return false;
3481
        $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...
3482
        $em = Database::getManager();
3483
3484
        $tools = $em
3485
            ->createQuery("
3486
                SELECT t FROM ChamiloCourseBundle:CTool t
3487
                WHERE t.course = :course AND
3488
                    t.name = :name AND
3489
                    t.image LIKE 'lp_category.%' AND
3490
                    t.link LIKE :link
3491
            ")
3492
            ->setParameters([
3493
                'course' => $courseId,
3494
                'name' => strip_tags($category->getTitle()),
3495
                'link' => "$link%",
3496
            ])
3497
            ->getResult();
3498
3499
        /** @var CTool $tool */
3500
        $tool = current($tools);
3501
3502
        return $tool ? $tool->getVisibility() : false;
3503
    }
3504
3505
    /**
3506
     * Restart the whole learnpath. Return the URL of the first element.
3507
     * Make sure the results are saved with anoter method. This method should probably be redefined in children classes.
3508
     * To use a similar method  statically, use the create_new_attempt() method.
3509
     *
3510
     * @return bool
3511
     */
3512
    public function restart()
3513
    {
3514
        if ($this->debug > 0) {
3515
            error_log('In learnpath::restart()', 0);
3516
        }
3517
        // TODO
3518
        // Call autosave method to save the current progress.
3519
        //$this->index = 0;
3520
        if (api_is_invitee()) {
3521
            return false;
3522
        }
3523
        $session_id = api_get_session_id();
3524
        $course_id = api_get_course_int_id();
3525
        $lp_view_table = Database::get_course_table(TABLE_LP_VIEW);
3526
        $sql = "INSERT INTO $lp_view_table (c_id, lp_id, user_id, view_count, session_id)
3527
                VALUES ($course_id, ".$this->lp_id.",".$this->get_user_id().",".($this->attempt + 1).", $session_id)";
3528
        if ($this->debug > 2) {
3529
            error_log('Inserting new lp_view for restart: '.$sql, 0);
3530
        }
3531
        Database::query($sql);
3532
        $view_id = Database::insert_id();
3533
3534
        if ($view_id) {
3535
            $this->lp_view_id = $view_id;
3536
            $this->attempt = $this->attempt + 1;
3537
        } else {
3538
            $this->error = 'Could not insert into item_view table...';
3539
3540
            return false;
3541
        }
3542
        $this->autocomplete_parents($this->current);
3543
        foreach ($this->items as $index => $dummy) {
3544
            $this->items[$index]->restart();
3545
            $this->items[$index]->set_lp_view($this->lp_view_id);
3546
        }
3547
        $this->first();
3548
3549
        return true;
3550
    }
3551
3552
    /**
3553
     * Saves the current item.
3554
     *
3555
     * @return bool
3556
     */
3557
    public function save_current()
3558
    {
3559
        $debug = $this->debug;
3560
        // TODO: Do a better check on the index pointing to the right item (it is supposed to be working
3561
        // on $ordered_items[] but not sure it's always safe to use with $items[]).
3562
        if ($debug) {
3563
            error_log('save_current() saving item '.$this->current, 0);
3564
            error_log(''.print_r($this->items, true), 0);
3565
        }
3566
        if (isset($this->items[$this->current]) &&
3567
            is_object($this->items[$this->current])
3568
        ) {
3569
            if ($debug) {
3570
                error_log('Before save last_scorm_session_time: '.$this->items[$this->current]->getLastScormSessionTime());
3571
            }
3572
3573
            $res = $this->items[$this->current]->save(
3574
                false,
3575
                $this->prerequisites_match($this->current)
3576
            );
3577
            $this->autocomplete_parents($this->current);
3578
            $status = $this->items[$this->current]->get_status();
3579
            $this->update_queue[$this->current] = $status;
3580
3581
            if ($debug) {
3582
                error_log('After save last_scorm_session_time: '.$this->items[$this->current]->getLastScormSessionTime());
3583
            }
3584
3585
            return $res;
3586
        }
3587
3588
        return false;
3589
    }
3590
3591
    /**
3592
     * Saves the given item.
3593
     *
3594
     * @param int  $item_id      Optional (will take from $_REQUEST if null)
3595
     * @param bool $from_outside Save from url params (true) or from current attributes (false). Default true
3596
     *
3597
     * @return bool
3598
     */
3599
    public function save_item($item_id = null, $from_outside = true)
3600
    {
3601
        $debug = $this->debug;
3602
        if ($debug) {
3603
            error_log('In learnpath::save_item('.$item_id.','.intval($from_outside).')', 0);
3604
        }
3605
        // TODO: Do a better check on the index pointing to the right item (it is supposed to be working
3606
        // on $ordered_items[] but not sure it's always safe to use with $items[]).
3607
        if (empty($item_id)) {
3608
            $item_id = (int) $_REQUEST['id'];
3609
        }
3610
3611
        if (empty($item_id)) {
3612
            $item_id = $this->get_current_item_id();
3613
        }
3614
        if (isset($this->items[$item_id]) &&
3615
            is_object($this->items[$item_id])
3616
        ) {
3617
            if ($debug) {
3618
                error_log('Object exists');
3619
            }
3620
3621
            // Saving the item.
3622
            $res = $this->items[$item_id]->save(
3623
                $from_outside,
3624
                $this->prerequisites_match($item_id)
3625
            );
3626
3627
            if ($debug) {
3628
                error_log('update_queue before:');
3629
                error_log(print_r($this->update_queue, 1));
3630
            }
3631
            $this->autocomplete_parents($item_id);
3632
3633
            $status = $this->items[$item_id]->get_status();
3634
            $this->update_queue[$item_id] = $status;
3635
3636
            if ($debug) {
3637
                error_log('get_status(): '.$status);
3638
                error_log('update_queue after:');
3639
                error_log(print_r($this->update_queue, 1));
3640
            }
3641
3642
            return $res;
3643
        }
3644
3645
        return false;
3646
    }
3647
3648
    /**
3649
     * Saves the last item seen's ID only in case.
3650
     */
3651
    public function save_last()
3652
    {
3653
        $course_id = api_get_course_int_id();
3654
        $debug = $this->debug;
3655
        if ($debug) {
3656
            error_log('In learnpath::save_last()', 0);
3657
        }
3658
        $session_condition = api_get_session_condition(
3659
            api_get_session_id(),
3660
            true,
3661
            false
3662
        );
3663
        $table = Database::get_course_table(TABLE_LP_VIEW);
3664
3665
        $userId = $this->get_user_id();
3666
        if (empty($userId)) {
3667
            $userId = api_get_user_id();
3668
            if ($debug) {
3669
                error_log('$this->get_user_id() was empty, used api_get_user_id() instead in '.__FILE__.' line '.__LINE__);
3670
            }
3671
        }
3672
        if (isset($this->current) && !api_is_invitee()) {
3673
            if ($debug) {
3674
                error_log('Saving current item ('.$this->current.') for later review', 0);
3675
            }
3676
            $sql = "UPDATE $table SET
3677
                        last_item = ".$this->get_current_item_id()."
3678
                    WHERE
3679
                        c_id = $course_id AND
3680
                        lp_id = ".$this->get_id()." AND
3681
                        user_id = ".$userId." ".$session_condition;
3682
3683
            if ($debug) {
3684
                error_log('Saving last item seen : '.$sql, 0);
3685
            }
3686
            Database::query($sql);
3687
        }
3688
3689
        if (!api_is_invitee()) {
3690
            // Save progress.
3691
            [$progress] = $this->get_progress_bar_text('%');
3692
            $scoreAsProgressSetting = ('true' === api_get_setting('lp.lp_score_as_progress_enable'));
3693
            $scoreAsProgress = $this->getUseScoreAsProgress();
3694
            if ($scoreAsProgress && $scoreAsProgressSetting && (null === $score || empty($score) || -1 == $score)) {
3695
                if ($debug) {
3696
                    error_log("Return false: Dont save score: $score");
3697
                    error_log("progress: $progress");
3698
                }
3699
3700
                return false;
3701
            }
3702
3703
            if ($scoreAsProgress && $scoreAsProgressSetting) {
3704
                $storedProgress = self::getProgress(
3705
                    $this->get_id(),
3706
                    $userId,
3707
                    $course_id,
3708
                    $this->get_lp_session_id()
3709
                );
3710
3711
                // Check if the stored progress is higher than the new value
3712
                if ($storedProgress >= $progress) {
3713
                    if ($debug) {
3714
                        error_log("Return false: New progress value is lower than stored value - Current value: $storedProgress - New value: $progress [lp ".$this->get_id()." - user ".$userId."]");
3715
                    }
3716
3717
                    return false;
3718
                }
3719
            }
3720
            if ($progress >= 0 && $progress <= 100) {
3721
                $progress = (int) $progress;
3722
                $sql = "UPDATE $table SET
3723
                            progress = $progress
3724
                        WHERE
3725
                            c_id = $course_id AND
3726
                            lp_id = ".$this->get_id()." AND
3727
                            user_id = ".$userId." ".$session_condition;
3728
                // Ignore errors as some tables might not have the progress field just yet.
3729
                Database::query($sql);
3730
                $this->progress_db = $progress;
3731
3732
                if (100 == $progress) {
3733
                    Container::getEventDispatcher()->dispatch(
3734
                        new LearningPathEndedEvent(['lp_view_id' => $this->lp_view_id]),
3735
                        Events::LP_ENDED
3736
                    );
3737
                }
3738
            }
3739
        }
3740
    }
3741
3742
    /**
3743
     * Sets the current item ID (checks if valid and authorized first).
3744
     *
3745
     * @param int $item_id New item ID. If not given or not authorized, defaults to current
3746
     */
3747
    public function set_current_item($item_id = null)
3748
    {
3749
        $debug = $this->debug;
3750
        if ($debug) {
3751
            error_log('In learnpath::set_current_item('.$item_id.')', 0);
3752
        }
3753
        if (empty($item_id)) {
3754
            if ($debug) {
3755
                error_log('No new current item given, ignore...', 0);
3756
            }
3757
            // Do nothing.
3758
        } else {
3759
            if ($debug) {
3760
                error_log('New current item given is '.$item_id.'...', 0);
3761
            }
3762
            if (is_numeric($item_id)) {
3763
                $item_id = (int) $item_id;
3764
                // TODO: Check in database here.
3765
                $this->last = $this->current;
3766
                $this->current = $item_id;
3767
                // TODO: Update $this->index as well.
3768
                foreach ($this->ordered_items as $index => $item) {
3769
                    if ($item == $this->current) {
3770
                        $this->index = $index;
3771
                        break;
3772
                    }
3773
                }
3774
                if ($debug) {
3775
                    error_log('set_current_item('.$item_id.') done. Index is now : '.$this->index);
3776
                }
3777
            } else {
3778
                if ($debug) {
3779
                    error_log('set_current_item('.$item_id.') failed. Not a numeric value: ');
3780
                }
3781
            }
3782
        }
3783
    }
3784
3785
    /**
3786
     * Set index specified prefix terms for all items in this path.
3787
     *
3788
     * @param string $terms_string Comma-separated list of terms
3789
     * @param string $prefix       Xapian term prefix
3790
     *
3791
     * @return bool False on error, true otherwise
3792
     */
3793
    public function set_terms_by_prefix($terms_string, $prefix)
3794
    {
3795
        $course_id = api_get_course_int_id();
3796
        if ('true' !== api_get_setting('search_enabled')) {
3797
            return false;
3798
        }
3799
3800
        if (!extension_loaded('xapian')) {
3801
            return false;
3802
        }
3803
3804
        $terms_string = trim($terms_string);
3805
        $terms = explode(',', $terms_string);
3806
        array_walk($terms, 'trim_value');
3807
        $stored_terms = $this->get_common_index_terms_by_prefix($prefix);
3808
3809
        // Don't do anything if no change, verify only at DB, not the search engine.
3810
        if ((0 == count(array_diff($terms, $stored_terms))) && (0 == count(array_diff($stored_terms, $terms)))) {
3811
            return false;
3812
        }
3813
3814
        require_once 'xapian.php'; // TODO: Try catch every xapian use or make wrappers on API.
3815
        require_once api_get_path(LIBRARY_PATH).'search/xapian/XapianQuery.php';
3816
3817
        $items_table = Database::get_course_table(TABLE_LP_ITEM);
3818
        // TODO: Make query secure agains XSS : use member attr instead of post var.
3819
        $lp_id = (int) $_POST['lp_id'];
3820
        $sql = "SELECT * FROM $items_table WHERE c_id = $course_id AND lp_id = $lp_id";
3821
        $result = Database::query($sql);
3822
        $di = new ChamiloIndexer();
3823
3824
        while ($lp_item = Database::fetch_array($result)) {
3825
            // Get search_did.
3826
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
3827
            $sql = 'SELECT * FROM %s
3828
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
3829
                    LIMIT 1';
3830
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp_id, $lp_item['id']);
3831
3832
            //echo $sql; echo '<br>';
3833
            $res = Database::query($sql);
3834
            if (Database::num_rows($res) > 0) {
3835
                $se_ref = Database::fetch_array($res);
3836
                // Compare terms.
3837
                $doc = $di->get_document($se_ref['search_did']);
3838
                $xapian_terms = xapian_get_doc_terms($doc, $prefix);
3839
                $xterms = [];
3840
                foreach ($xapian_terms as $xapian_term) {
3841
                    $xterms[] = substr($xapian_term['name'], 1);
3842
                }
3843
3844
                $dterms = $terms;
3845
                $missing_terms = array_diff($dterms, $xterms);
3846
                $deprecated_terms = array_diff($xterms, $dterms);
3847
3848
                // Save it to search engine.
3849
                foreach ($missing_terms as $term) {
3850
                    $doc->add_term($prefix.$term, 1);
3851
                }
3852
                foreach ($deprecated_terms as $term) {
3853
                    $doc->remove_term($prefix.$term);
3854
                }
3855
                $di->getDb()->replace_document((int) $se_ref['search_did'], $doc);
3856
                $di->getDb()->flush();
3857
            }
3858
        }
3859
3860
        return true;
3861
    }
3862
3863
    /**
3864
     * Sets the previous item ID to a given ID. Generally, this should be set to the previous 'current' item.
3865
     *
3866
     * @param int $id DB ID of the item
3867
     */
3868
    public function set_previous_item($id)
3869
    {
3870
        if ($this->debug > 0) {
3871
            error_log('In learnpath::set_previous_item()', 0);
3872
        }
3873
        $this->last = $id;
3874
    }
3875
3876
    /**
3877
     * Sets and saves the expired_on date.
3878
     *
3879
     * @return bool Returns true if author's name is not empty
3880
     */
3881
    public function set_modified_on()
3882
    {
3883
        $this->modified_on = api_get_utc_datetime();
3884
        $table = Database::get_course_table(TABLE_LP_MAIN);
3885
        $lp_id = $this->get_id();
3886
        $sql = "UPDATE $table SET modified_on = '".$this->modified_on."'
3887
                WHERE iid = $lp_id";
3888
        Database::query($sql);
3889
3890
        return true;
3891
    }
3892
3893
    /**
3894
     * Sets the object's error message.
3895
     *
3896
     * @param string $error Error message. If empty, reinits the error string
3897
     */
3898
    public function set_error_msg($error = '')
3899
    {
3900
        if ($this->debug > 0) {
3901
            error_log('In learnpath::set_error_msg()', 0);
3902
        }
3903
        if (empty($error)) {
3904
            $this->error = '';
3905
        } else {
3906
            $this->error .= $error;
3907
        }
3908
    }
3909
3910
    /**
3911
     * Launches the current item if not 'sco'
3912
     * (starts timer and make sure there is a record ready in the DB).
3913
     *
3914
     * @param bool $allow_new_attempt Whether to allow a new attempt or not
3915
     *
3916
     * @return bool
3917
     */
3918
    public function start_current_item($allow_new_attempt = false)
3919
    {
3920
        $debug = $this->debug;
3921
        if ($debug) {
3922
            error_log('In learnpath::start_current_item()');
3923
            error_log('current: '.$this->current);
3924
        }
3925
        if (0 != $this->current && isset($this->items[$this->current]) &&
3926
            is_object($this->items[$this->current])
3927
        ) {
3928
            $type = $this->get_type();
3929
            $item_type = $this->items[$this->current]->get_type();
3930
            if ($debug) {
3931
                error_log('item type: '.$item_type);
3932
                error_log('lp type: '.$type);
3933
            }
3934
            if ((2 == $type && 'sco' !== $item_type) ||
3935
                (3 == $type && 'au' !== $item_type) ||
3936
                (1 == $type && TOOL_QUIZ != $item_type && TOOL_HOTPOTATOES != $item_type)
3937
            ) {
3938
                $this->items[$this->current]->open($allow_new_attempt);
3939
                $this->autocomplete_parents($this->current);
3940
                $prereq_check = $this->prerequisites_match($this->current);
3941
                if ($debug) {
3942
                    error_log('start_current_item will save item with prereq: '.$prereq_check);
3943
                }
3944
                $this->items[$this->current]->save(false, $prereq_check);
3945
            }
3946
            // If sco, then it is supposed to have been updated by some other call.
3947
            if ('sco' === $item_type) {
3948
                $this->items[$this->current]->restart();
3949
            }
3950
        }
3951
        if ($debug) {
3952
            error_log('lp_view_session_id');
3953
            error_log($this->lp_view_session_id);
3954
            error_log('api session id');
3955
            error_log(api_get_session_id());
3956
            error_log('End of learnpath::start_current_item()');
3957
        }
3958
3959
        return true;
3960
    }
3961
3962
    /**
3963
     * Stops the processing and counters for the old item (as held in $this->last).
3964
     *
3965
     * @return bool True/False
3966
     */
3967
    public function stop_previous_item()
3968
    {
3969
        $debug = $this->debug;
3970
        if ($debug) {
3971
            error_log('In learnpath::stop_previous_item()', 0);
3972
        }
3973
3974
        if (0 != $this->last && $this->last != $this->current &&
3975
            isset($this->items[$this->last]) && is_object($this->items[$this->last])
3976
        ) {
3977
            if ($debug) {
3978
                error_log('In learnpath::stop_previous_item() - '.$this->last.' is object');
3979
            }
3980
            switch ($this->get_type()) {
3981
                case '3':
3982
                    if ('au' != $this->items[$this->last]->get_type()) {
3983
                        if ($debug) {
3984
                            error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 3 is <> au');
3985
                        }
3986
                        $this->items[$this->last]->close();
3987
                    } else {
3988
                        if ($debug) {
3989
                            error_log('In learnpath::stop_previous_item() - Item is an AU, saving is managed by AICC signals');
3990
                        }
3991
                    }
3992
                    break;
3993
                case '2':
3994
                    if ('sco' != $this->items[$this->last]->get_type()) {
3995
                        if ($debug) {
3996
                            error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 2 is <> sco');
3997
                        }
3998
                        $this->items[$this->last]->close();
3999
                    } else {
4000
                        if ($debug) {
4001
                            error_log('In learnpath::stop_previous_item() - Item is a SCO, saving is managed by SCO signals');
4002
                        }
4003
                    }
4004
                    break;
4005
                case '1':
4006
                default:
4007
                    if ($debug) {
4008
                        error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 1 is asset');
4009
                    }
4010
                    $this->items[$this->last]->close();
4011
                    break;
4012
            }
4013
        } else {
4014
            if ($debug) {
4015
                error_log('In learnpath::stop_previous_item() - No previous element found, ignoring...');
4016
            }
4017
4018
            return false;
4019
        }
4020
4021
        return true;
4022
    }
4023
4024
    /**
4025
     * Updates the default view mode from fullscreen to embedded and inversely.
4026
     *
4027
     * @return string The current default view mode ('fullscreen' or 'embedded')
4028
     */
4029
    public function update_default_view_mode()
4030
    {
4031
        $table = Database::get_course_table(TABLE_LP_MAIN);
4032
        $sql = "SELECT * FROM $table
4033
                WHERE iid = ".$this->get_id();
4034
        $res = Database::query($sql);
4035
        if (Database::num_rows($res) > 0) {
4036
            $row = Database::fetch_array($res);
4037
            $default_view_mode = $row['default_view_mod'];
4038
            $view_mode = $default_view_mode;
4039
            switch ($default_view_mode) {
4040
                case 'fullscreen': // default with popup
4041
                    $view_mode = 'embedded';
4042
                    break;
4043
                case 'embedded': // default view with left menu
4044
                    $view_mode = 'embedframe';
4045
                    break;
4046
                case 'embedframe': //folded menu
4047
                    $view_mode = 'impress';
4048
                    break;
4049
                case 'impress':
4050
                    $view_mode = 'fullscreen';
4051
                    break;
4052
            }
4053
            $sql = "UPDATE $table SET default_view_mod = '$view_mode'
4054
                    WHERE iid = ".$this->get_id();
4055
            Database::query($sql);
4056
            $this->mode = $view_mode;
4057
4058
            return $view_mode;
4059
        }
4060
4061
        return -1;
4062
    }
4063
4064
    /**
4065
     * Updates the default behaviour about auto-commiting SCORM updates.
4066
     *
4067
     * @return bool True if auto-commit has been set to 'on', false otherwise
4068
     */
4069
    public function update_default_scorm_commit()
4070
    {
4071
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
4072
        $sql = "SELECT * FROM $lp_table
4073
                WHERE iid = ".$this->get_id();
4074
        $res = Database::query($sql);
4075
        if (Database::num_rows($res) > 0) {
4076
            $row = Database::fetch_array($res);
4077
            $force = $row['force_commit'];
4078
            if (1 == $force) {
4079
                $force = 0;
4080
                $force_return = false;
4081
            } elseif (0 == $force) {
4082
                $force = 1;
4083
                $force_return = true;
4084
            }
4085
            $sql = "UPDATE $lp_table SET force_commit = $force
4086
                    WHERE iid = ".$this->get_id();
4087
            Database::query($sql);
4088
            $this->force_commit = $force_return;
4089
4090
            return $force_return;
4091
        }
4092
4093
        return -1;
4094
    }
4095
4096
    /**
4097
     * Updates the order of learning paths (goes through all of them by order and fills the gaps).
4098
     *
4099
     * @return bool True on success, false on failure
4100
     */
4101
    public function update_display_order()
4102
    {
4103
        return;
4104
        $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...
4105
        $table = Database::get_course_table(TABLE_LP_MAIN);
4106
        $sql = "SELECT * FROM $table
4107
                WHERE c_id = $course_id
4108
                ORDER BY display_order";
4109
        $res = Database::query($sql);
4110
        if (false === $res) {
4111
            return false;
4112
        }
4113
4114
        $num = Database::num_rows($res);
4115
        // First check the order is correct, globally (might be wrong because
4116
        // of versions < 1.8.4).
4117
        if ($num > 0) {
4118
            $i = 1;
4119
            while ($row = Database::fetch_array($res)) {
4120
                if ($row['display_order'] != $i) {
4121
                    // If we find a gap in the order, we need to fix it.
4122
                    $sql = "UPDATE $table SET display_order = $i
4123
                            WHERE iid = ".$row['iid'];
4124
                    Database::query($sql);
4125
                }
4126
                $i++;
4127
            }
4128
        }
4129
4130
        return true;
4131
    }
4132
4133
    /**
4134
     * Updates the "prevent_reinit" value that enables control on reinitialising items on second view.
4135
     *
4136
     * @return bool True if prevent_reinit has been set to 'on', false otherwise (or 1 or 0 in this case)
4137
     */
4138
    public function update_reinit()
4139
    {
4140
        $force = $this->prevent_reinit;
4141
        if (1 == $force) {
4142
            $force = 0;
4143
        } elseif (0 == $force) {
4144
            $force = 1;
4145
        }
4146
4147
        $table = Database::get_course_table(TABLE_LP_MAIN);
4148
        $sql = "UPDATE $table SET prevent_reinit = $force
4149
                WHERE iid = ".$this->get_id();
4150
        Database::query($sql);
4151
        $this->prevent_reinit = $force;
4152
4153
        return $force;
4154
    }
4155
4156
    /**
4157
     * Determine the attempt_mode thanks to prevent_reinit and seriousgame_mode db flag.
4158
     *
4159
     * @return string 'single', 'multi' or 'seriousgame'
4160
     *
4161
     * @author ndiechburg <[email protected]>
4162
     */
4163
    public function get_attempt_mode()
4164
    {
4165
        //Set default value for seriousgame_mode
4166
        if (!isset($this->seriousgame_mode)) {
4167
            $this->seriousgame_mode = 0;
4168
        }
4169
        // Set default value for prevent_reinit
4170
        if (!isset($this->prevent_reinit)) {
4171
            $this->prevent_reinit = 1;
4172
        }
4173
        if (1 == $this->seriousgame_mode && 1 == $this->prevent_reinit) {
4174
            return 'seriousgame';
4175
        }
4176
        if (0 == $this->seriousgame_mode && 1 == $this->prevent_reinit) {
4177
            return 'single';
4178
        }
4179
        if (0 == $this->seriousgame_mode && 0 == $this->prevent_reinit) {
4180
            return 'multiple';
4181
        }
4182
4183
        return 'single';
4184
    }
4185
4186
    /**
4187
     * Register the attempt mode into db thanks to flags prevent_reinit and seriousgame_mode flags.
4188
     *
4189
     * @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...
4190
     *
4191
     * @return bool
4192
     *
4193
     * @author ndiechburg <[email protected]>
4194
     */
4195
    public function set_attempt_mode($mode)
4196
    {
4197
        switch ($mode) {
4198
            case 'seriousgame':
4199
                $sg_mode = 1;
4200
                $prevent_reinit = 1;
4201
                break;
4202
            case 'single':
4203
                $sg_mode = 0;
4204
                $prevent_reinit = 1;
4205
                break;
4206
            case 'multiple':
4207
                $sg_mode = 0;
4208
                $prevent_reinit = 0;
4209
                break;
4210
            default:
4211
                $sg_mode = 0;
4212
                $prevent_reinit = 0;
4213
                break;
4214
        }
4215
        $this->prevent_reinit = $prevent_reinit;
4216
        $this->seriousgame_mode = $sg_mode;
4217
        $table = Database::get_course_table(TABLE_LP_MAIN);
4218
        $sql = "UPDATE $table SET
4219
                prevent_reinit = $prevent_reinit ,
4220
                seriousgame_mode = $sg_mode
4221
                WHERE iid = ".$this->get_id();
4222
        $res = Database::query($sql);
4223
        if ($res) {
4224
            return true;
4225
        } else {
4226
            return false;
4227
        }
4228
    }
4229
4230
    /**
4231
     * Switch between multiple attempt, single attempt or serious_game mode (only for scorm).
4232
     *
4233
     * @author ndiechburg <[email protected]>
4234
     */
4235
    public function switch_attempt_mode()
4236
    {
4237
        $mode = $this->get_attempt_mode();
4238
        switch ($mode) {
4239
            case 'single':
4240
                $next_mode = 'multiple';
4241
                break;
4242
            case 'multiple':
4243
                $next_mode = 'seriousgame';
4244
                break;
4245
            case 'seriousgame':
4246
            default:
4247
                $next_mode = 'single';
4248
                break;
4249
        }
4250
        $this->set_attempt_mode($next_mode);
4251
    }
4252
4253
    /**
4254
     * Switch the lp in ktm mode. This is a special scorm mode with unique attempt
4255
     * but possibility to do again a completed item.
4256
     *
4257
     * @return bool true if seriousgame_mode has been set to 1, false otherwise
4258
     *
4259
     * @author ndiechburg <[email protected]>
4260
     */
4261
    public function set_seriousgame_mode()
4262
    {
4263
        $table = Database::get_course_table(TABLE_LP_MAIN);
4264
        $force = $this->seriousgame_mode;
4265
        if (1 == $force) {
4266
            $force = 0;
4267
        } elseif (0 == $force) {
4268
            $force = 1;
4269
        }
4270
        $sql = "UPDATE $table SET seriousgame_mode = $force
4271
                WHERE iid = ".$this->get_id();
4272
        Database::query($sql);
4273
        $this->seriousgame_mode = $force;
4274
4275
        return $force;
4276
    }
4277
4278
    /**
4279
     * Updates the "scorm_debug" value that shows or hide the debug window.
4280
     *
4281
     * @return bool True if scorm_debug has been set to 'on', false otherwise (or 1 or 0 in this case)
4282
     */
4283
    public function update_scorm_debug()
4284
    {
4285
        $table = Database::get_course_table(TABLE_LP_MAIN);
4286
        $force = $this->scorm_debug;
4287
        if (1 == $force) {
4288
            $force = 0;
4289
        } elseif (0 == $force) {
4290
            $force = 1;
4291
        }
4292
        $sql = "UPDATE $table SET debug = $force
4293
                WHERE iid = ".$this->get_id();
4294
        Database::query($sql);
4295
        $this->scorm_debug = $force;
4296
4297
        return $force;
4298
    }
4299
4300
    /**
4301
     * Function that creates a html list of learning path items so that we can add audio files to them.
4302
     *
4303
     * @author Kevin Van Den Haute
4304
     *
4305
     * @return string
4306
     */
4307
    public function overview()
4308
    {
4309
        $return = '';
4310
        $update_audio = $_GET['updateaudio'] ?? null;
4311
4312
        // we need to start a form when we want to update all the mp3 files
4313
        if ('true' == $update_audio) {
4314
            $return .= '<form action="'.api_get_self().'?'.api_get_cidreq().'&updateaudio='.Security::remove_XSS(
4315
                    $_GET['updateaudio']
4316
                ).'&action='.Security::remove_XSS(
4317
                    $_GET['action']
4318
                ).'&lp_id='.$_SESSION['oLP']->lp_id.'" method="post" enctype="multipart/form-data" name="updatemp3" id="updatemp3">';
4319
        }
4320
        $return .= '<div id="message"></div>';
4321
        if (0 == count($this->items)) {
4322
            $return .= Display::return_message(
4323
                get_lang(
4324
                    'You should add some items to your learning path, otherwise you won\'t be able to attach audio files to them'
4325
                ),
4326
                'normal'
4327
            );
4328
        } else {
4329
            $return_audio = '<table class="table table-hover table-striped data_table">';
4330
            $return_audio .= '<tr>';
4331
            $return_audio .= '<th width="40%">'.get_lang('Title').'</th>';
4332
            $return_audio .= '<th>'.get_lang('Audio').'</th>';
4333
            $return_audio .= '</tr>';
4334
4335
            if ('true' != $update_audio) {
4336
                /*$return .= '<div class="col-md-12">';
4337
                $return .= self::return_new_tree($update_audio);
4338
                $return .= '</div>';*/
4339
                $return .= Display::div(
4340
                    Display::url(get_lang('Save'), '#', ['id' => 'listSubmit', 'class' => 'btn btn--primary']),
4341
                    ['style' => 'float:left; margin-top:15px;width:100%']
4342
                );
4343
            } else {
4344
                //$return_audio .= self::return_new_tree($update_audio);
4345
                $return .= $return_audio.'</table>';
4346
            }
4347
4348
            // We need to close the form when we are updating the mp3 files.
4349
            if ('true' == $update_audio) {
4350
                $return .= '<div class="footer-audio">';
4351
                $return .= Display::button(
4352
                    'save_audio',
4353
                    '<em class="fa fa-file-audio-o"></em> '.get_lang('Save audio and organization'),
4354
                    ['class' => 'btn btn--primary', 'type' => 'submit']
4355
                );
4356
                $return .= '</div>';
4357
            }
4358
        }
4359
4360
        // We need to close the form when we are updating the mp3 files.
4361
        if ('true' === $update_audio && isset($this->arrMenu) && 0 != count($this->arrMenu)) {
4362
            $return .= '</form>';
4363
        }
4364
4365
        return $return;
4366
    }
4367
4368
    public function showBuildSideBar($updateAudio = false, $dropElementHere = false, $type = null)
4369
    {
4370
        $sureToDelete = trim(get_lang('Are you sure to delete?'));
4371
        $ajax_url = api_get_path(WEB_AJAX_PATH).'lp.ajax.php?lp_id='.$this->get_id().'&'.api_get_cidreq();
4372
4373
        $content = '
4374
        <script>
4375
            /*
4376
            Script to manipulate Learning Path items with Drag and drop
4377
             */
4378
            $(function() {
4379
                function refreshTree() {
4380
                    var params = "&a=get_lp_item_tree";
4381
                    $.get(
4382
                        "'.$ajax_url.'",
4383
                        params,
4384
                        function(result) {
4385
                            serialized = [];
4386
                            $("#lp_item_list").html(result);
4387
                            nestedSortable();
4388
                        }
4389
                    );
4390
                }
4391
4392
                const nestedQuery = ".nested-sortable";
4393
                const identifier = "id";
4394
                const root = document.getElementById("lp_item_list");
4395
4396
                var serialized = [];
4397
                function serialize(sortable) {
4398
                  var children = [].slice.call(sortable.children);
4399
                  for (var i in children) {
4400
                    var nested = children[i].querySelector(nestedQuery);
4401
                    var parentId = $(children[i]).parent().parent().attr("id");
4402
                    var id = children[i].dataset[identifier];
4403
                    if (typeof id === "undefined") {
4404
                        return;
4405
                    }
4406
                    serialized.push({
4407
                      id: children[i].dataset[identifier],
4408
                      parent_id: parentId
4409
                    });
4410
4411
                    if (nested) {
4412
                        serialize(nested);
4413
                    }
4414
                  }
4415
4416
                  return serialized;
4417
                }
4418
4419
                function nestedSortable() {
4420
                    let left = document.getElementsByClassName("nested-sortable");
4421
                    Array.prototype.forEach.call(left, function(resource) {
4422
                        Sortable.create(resource, {
4423
                            group: "nested",
4424
                            put: ["nested-sortable", ".lp_resource", ".nested-source"],
4425
                            animation: 150,
4426
                            //fallbackOnBody: true,
4427
                            swapThreshold: 0.65,
4428
                            dataIdAttr: "data-id",
4429
                            store: {
4430
                                set: function (sortable) {
4431
                                    var order = sortable.toArray();
4432
                                    console.log(order);
4433
                                }
4434
                            },
4435
                            onEnd: function(evt) {
4436
                                console.log("onEnd");
4437
                                let list = serialize(root);
4438
                                let order = "&a=update_lp_item_order&new_order=" + JSON.stringify(list);
4439
                                $.get(
4440
                                    "'.$ajax_url.'",
4441
                                    order,
4442
                                    function(reponse) {
4443
                                        $("#message").html(reponse);
4444
                                        refreshTree();
4445
                                    }
4446
                                );
4447
                            },
4448
                        });
4449
                    });
4450
                }
4451
4452
                nestedSortable();
4453
4454
                let resources = document.getElementsByClassName("lp_resource");
4455
                Array.prototype.forEach.call(resources, function(resource) {
4456
                    Sortable.create(resource, {
4457
                        group: "nested",
4458
                        put: ["nested-sortable"],
4459
                        filter: ".disable_drag",
4460
                        animation: 150,
4461
                        fallbackOnBody: true,
4462
                        swapThreshold: 0.65,
4463
                        dataIdAttr: "data-id",
4464
                        onRemove: function(evt) {
4465
                            console.log("onRemove");
4466
                            var itemEl = evt.item;
4467
                            var newIndex = evt.newIndex;
4468
                            var id = $(itemEl).attr("id");
4469
                            var parent_id = $(itemEl).parent().parent().attr("id");
4470
                            var type =  $(itemEl).find(".link_with_id").attr("data_type");
4471
                            var title = $(itemEl).find(".link_with_id").text();
4472
4473
                            let previousId = 0;
4474
                            if (0 !== newIndex) {
4475
                                previousId = $(itemEl).prev().attr("id");
4476
                            }
4477
                            var params = {
4478
                                "a": "add_lp_item",
4479
                                "id": id,
4480
                                "parent_id": parent_id,
4481
                                "previous_id": previousId,
4482
                                "type": type,
4483
                                "title" : title
4484
                            };
4485
                            console.log(params);
4486
                            $.ajax({
4487
                                type: "GET",
4488
                                url: "'.$ajax_url.'",
4489
                                data: params,
4490
                                success: function(itemId) {
4491
                                    $(itemEl).attr("id", itemId);
4492
                                    $(itemEl).attr("data-id", itemId);
4493
                                    let list = serialize(root);
4494
                                    let listInString = JSON.stringify(list);
4495
                                    if (typeof listInString === "undefined") {
4496
                                        listInString = "";
4497
                                    }
4498
                                    let order = "&a=update_lp_item_order&new_order=" + listInString;
4499
                                    $.get(
4500
                                        "'.$ajax_url.'",
4501
                                        order,
4502
                                        function(reponse) {
4503
                                            $("#message").html(reponse);
4504
                                            refreshTree();
4505
                                        }
4506
                                    );
4507
                                }
4508
                            });
4509
                        },
4510
                    });
4511
                });
4512
            });
4513
        </script>';
4514
4515
        $content .= "
4516
        <script>
4517
            function confirmation(name) {
4518
                if (confirm('$sureToDelete ' + name)) {
4519
                    return true;
4520
                } else {
4521
                    return false;
4522
                }
4523
            }
4524
            function refreshTree() {
4525
                var params = '&a=get_lp_item_tree';
4526
                $.get(
4527
                    '".$ajax_url."',
4528
                    params,
4529
                    function(result) {
4530
                        $('#lp_item_list').html(result);
4531
                    }
4532
                );
4533
            }
4534
4535
            $(function () {
4536
                //$('.scrollbar-inner').scrollbar();
4537
                /*$('#subtab').on('click', 'a:first', function() {
4538
                    window.location.reload();
4539
                });
4540
                $('#subtab ').on('click', 'a:first', function () {
4541
                    window.location.reload();
4542
                });*/
4543
4544
                expandColumnToggle('#hide_bar_template', {
4545
                    selector: '#lp_sidebar'
4546
                }, {
4547
                    selector: '#doc_form'
4548
                });
4549
4550
                $('.lp-btn-associate-forum').on('click', function (e) {
4551
                    var associate = confirm('".get_lang('ConfirmAssociateForumToLPItem')."');
4552
                    if (!associate) {
4553
                        e.preventDefault();
4554
                    }
4555
                });
4556
4557
                $('.lp-btn-dissociate-forum').on('click', function (e) {
4558
                    var dissociate = confirm('".get_lang('ConfirmDissociateForumToLPItem')."');
4559
                    if (!dissociate) {
4560
                        e.preventDefault();
4561
                    }
4562
                });
4563
4564
                // hide the current template list for new documment until it tab clicked
4565
                $('#frmModel').hide();
4566
            });
4567
4568
            // document template for new document tab handler
4569
            /*$(document).on('shown.bs.tab', 'a[data-toggle=\"tab\"]', function (e) {
4570
                var id = e.target.id;
4571
                if (id == 'subtab2') {
4572
                    $('#frmModel').show();
4573
                } else {
4574
                    $('#frmModel').hide();
4575
                }
4576
            });*/
4577
4578
          function deleteItem(event) {
4579
            var id = $(event).attr('data-id');
4580
            var title = $(event).attr('data-title');
4581
            var params = '&a=delete_item&id=' + id;
4582
            if (confirmation(title)) {
4583
                $.get(
4584
                    '".$ajax_url."',
4585
                    params,
4586
                    function(result) {
4587
                        refreshTree();
4588
                    }
4589
                );
4590
            }
4591
        }
4592
        </script>";
4593
4594
        $content .= $this->return_new_tree($updateAudio, $dropElementHere);
4595
        $documentId = isset($_GET['path_item']) ? (int) $_GET['path_item'] : 0;
4596
4597
        $repo = Container::getDocumentRepository();
4598
        $document = $repo->find($documentId);
4599
        if ($document) {
4600
            // Show the template list
4601
            $content .= '<div id="frmModel" class="scrollbar-inner lp-add-item"></div>';
4602
        }
4603
4604
        // Show the template list.
4605
        if (('document' === $type || 'step' === $type) && !isset($_GET['file'])) {
4606
            // Show the template list.
4607
            $content .= '<div id="frmModel" class="scrollbar-inner lp-add-item"></div>';
4608
        }
4609
4610
        return $content;
4611
    }
4612
4613
    /**
4614
     * @param bool  $updateAudio
4615
     * @param bool   $dropElement
4616
     *
4617
     * @return string
4618
     */
4619
    public function return_new_tree($updateAudio = false, $dropElement = false)
4620
    {
4621
        $list = $this->getBuildTree(false, $dropElement);
4622
        $return = Display::panelCollapse(
4623
            $this->name,
4624
            $list,
4625
            'scorm-list',
4626
            null,
4627
            'scorm-list-accordion',
4628
            'scorm-list-collapse'
4629
        );
4630
4631
        if ($updateAudio) {
4632
            //$return = $result['return_audio'];
4633
        }
4634
4635
        return $return;
4636
    }
4637
4638
    public function getBuildTree($noWrapper = false, $dropElement = false): string
4639
    {
4640
        $mainUrl = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?'.api_get_cidreq();
4641
        $upIcon = Display::getMdiIcon('arrow-up-bold', 'ch-tool-icon', '', 16, get_lang('Up'));
4642
        $disableUpIcon = Display::getMdiIcon('arrow-up-bold', 'ch-tool-icon-disabled', '', 16, get_lang('Up'));
4643
        $downIcon = Display::getMdiIcon('arrow-down-bold', 'ch-tool-icon', '', 16, get_lang('Down'));
4644
        $previewImage = Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', '', 16, get_lang('Preview'));
4645
4646
        $lpItemRepo = Container::getLpItemRepository();
4647
        $itemRoot = $lpItemRepo->getRootItem($this->get_id());
4648
4649
        $options = [
4650
            'decorate' => true,
4651
            'rootOpen' => function($tree) use ($noWrapper) {
4652
                if ($tree[0]['lvl'] === 1) {
4653
                    if ($noWrapper) {
4654
                        return '';
4655
                    }
4656
                    return '<ul id="lp_item_list" class="list-group nested-sortable">';
4657
                }
4658
4659
                return '<ul class="list-group nested-sortable">';
4660
            },
4661
            'rootClose' => function($tree) use ($noWrapper, $dropElement)  {
4662
                if ($tree[0]['lvl'] === 1) {
4663
                    if ($dropElement) {
4664
                        //return Display::return_message(get_lang('Drag and drop an element here'));
4665
                        //return $this->getDropElementHtml();
4666
                    }
4667
                    if ($noWrapper) {
4668
                        return '';
4669
                    }
4670
                }
4671
4672
                return '</ul>';
4673
            },
4674
            'childOpen' => function($child) {
4675
                $id = $child['iid'];
4676
                return '<li
4677
                    id="'.$id.'"
4678
                    data-id="'.$id.'"
4679
                    class=" flex flex-col list-group-item nested-'.$child['lvl'].'">';
4680
            },
4681
            'childClose' => '',
4682
            'nodeDecorator' => function ($node) use ($mainUrl, $previewImage, $upIcon, $downIcon) {
4683
                $fullTitle = $node['title'];
4684
                //$title = cut($fullTitle, self::MAX_LP_ITEM_TITLE_LENGTH);
4685
                $title = $fullTitle;
4686
                $itemId = $node['iid'];
4687
                $type = $node['itemType'];
4688
                $lpId = $this->get_id();
4689
4690
                $moveIcon = '';
4691
                if (TOOL_LP_FINAL_ITEM !== $type) {
4692
                    $moveIcon .= '<a class="moved" href="#">';
4693
                    $moveIcon .= Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
4694
                    $moveIcon .= '</a>';
4695
                }
4696
4697
                $iconName = str_replace(' ', '', $type);
4698
                $icon = '';
4699
                switch ($iconName) {
4700
                    case 'category':
4701
                    case 'chapter':
4702
                    case 'folder':
4703
                    case 'dir':
4704
                        $icon = Display::getMdiIcon(ObjectIcon::CHAPTER, 'ch-tool-icon', '', ICON_SIZE_TINY);
4705
                        break;
4706
                    default:
4707
                        $icon = Display::getMdiIcon(ObjectIcon::SINGLE_ELEMENT, 'ch-tool-icon', '', ICON_SIZE_TINY);
4708
                        break;
4709
                }
4710
4711
                $urlPreviewLink = $mainUrl.'&action=view_item&mode=preview_document&id='.$itemId.'&lp_id='.$lpId;
4712
                $previewIcon = Display::url(
4713
                    $previewImage,
4714
                    $urlPreviewLink,
4715
                    [
4716
                        'target' => '_blank',
4717
                        'class' => 'btn btn--plain',
4718
                        'data-title' => $title,
4719
                        'title' => $title,
4720
                    ]
4721
                );
4722
                $url = $mainUrl.'&view=build&id='.$itemId.'&lp_id='.$lpId;
4723
4724
                $preRequisitesIcon = Display::url(
4725
                    Display::getMdiIcon('graph', 'ch-tool-icon', '', 16, get_lang('Prerequisites')),
4726
                    $url.'&action=edit_item_prereq',
4727
                    ['class' => '']
4728
                );
4729
4730
                $editIcon = '<a
4731
                    href="'.$mainUrl.'&action=edit_item&view=build&id='.$itemId.'&lp_id='.$lpId.'&path_item='.$node['path'].'"
4732
                    class=""
4733
                    >';
4734
                $editIcon .= Display::getMdiIcon('pencil', 'ch-tool-icon', '', 16, get_lang('Edit section description/name'));
4735
                $editIcon .= '</a>';
4736
                $orderIcons = '';
4737
                /*if ('final_item' !== $type) {
4738
                    $orderIcons = Display::url(
4739
                        $upIcon,
4740
                        'javascript:void(0)',
4741
                        ['class' => 'btn btn--plain order_items', 'data-dir' => 'up', 'data-id' => $itemId]
4742
                    );
4743
                    $orderIcons .= Display::url(
4744
                        $downIcon,
4745
                        'javascript:void(0)',
4746
                        ['class' => 'btn btn--plain order_items', 'data-dir' => 'down', 'data-id' => $itemId]
4747
                    );
4748
                }*/
4749
4750
                $deleteIcon = ' <a
4751
                    data-id = '.$itemId.'
4752
                    data-title = \''.addslashes($title).'\'
4753
                    href="javascript:void(0);"
4754
                    onclick="return deleteItem(this);"
4755
                    class="">';
4756
                $deleteIcon .= Display::getMdiIcon('delete', 'ch-tool-icon', '', 16, get_lang('Delete section'));
4757
                $deleteIcon .= '</a>';
4758
                $extra = '';
4759
4760
                if ('dir' === $type && empty($node['__children'])) {
4761
                    $level = $node['lvl'] + 1;
4762
                    $extra = '<ul class="list-group nested-sortable">
4763
                                <li class="list-group-item list-group-item-empty nested-'.$level.'"></li>
4764
                              </ul>';
4765
                }
4766
4767
                $buttons = Display::tag(
4768
                    'div',
4769
                    "<div class=\"btn-group btn-group-sm\">
4770
                                $editIcon
4771
                                $preRequisitesIcon
4772
                                $orderIcons
4773
                                $deleteIcon
4774
                               </div>",
4775
                    ['class' => 'btn-toolbar button_actions']
4776
                );
4777
4778
                return
4779
                    "<div class='flex flex-row'> $moveIcon  $icon <span class='mx-1'>$title </span></div>
4780
                    $extra
4781
                    $buttons
4782
                    "
4783
                    ;
4784
            },
4785
        ];
4786
4787
        $tree = $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
4788
4789
        if (empty($tree) && $dropElement) {
4790
            return $this->getDropElementHtml($noWrapper);
4791
        }
4792
4793
        return $tree;
4794
    }
4795
4796
    public function getDropElementHtml($noWrapper = false)
4797
    {
4798
        $li = '<li class="list-group-item">'.
4799
            Display::return_message(get_lang('Drag and drop an element here')).
4800
            '</li>';
4801
        if ($noWrapper) {
4802
            return $li;
4803
        }
4804
4805
        return
4806
            '<ul id="lp_item_list" class="list-group nested-sortable">
4807
            '.$li.'
4808
            </ul>';
4809
    }
4810
4811
    /**
4812
     * This function builds the action menu.
4813
     *
4814
     * @param bool   $returnString           Optional
4815
     * @param bool   $showRequirementButtons Optional. Allow show the requirements button
4816
     * @param bool   $isConfigPage           Optional. If is the config page, show the edit button
4817
     * @param bool   $allowExpand            Optional. Allow show the expand/contract button
4818
     * @param string $action
4819
     * @param array  $extraField
4820
     *
4821
     * @return string
4822
     */
4823
    public function build_action_menu(
4824
        $returnString = false,
4825
        $showRequirementButtons = true,
4826
        $isConfigPage = false,
4827
        $allowExpand = true,
4828
        $action = '',
4829
        $extraField = []
4830
    ) {
4831
        $actionsRight = '';
4832
        $lpId = $this->lp_id;
4833
        if (!isset($extraField['backTo']) && empty($extraField['backTo'])) {
4834
            $back = Display::url(
4835
                Display::getMdiIcon('arrow-left-bold-box', 'ch-tool-icon', '', 32, get_lang('Back to learning paths')),
4836
                'lp_controller.php?'.api_get_cidreq()
4837
            );
4838
        } else {
4839
            $back = Display::url(
4840
                Display::getMdiIcon('arrow-left-bold-box', 'ch-tool-icon', '', 32, get_lang('Back')),
4841
                $extraField['backTo']
4842
            );
4843
        }
4844
4845
        /*if ($backToBuild) {
4846
            $back = Display::url(
4847
                Display::getMdiIcon('arrow-left-bold-box', 'ch-tool-icon', null, 32, get_lang('GoBack')),
4848
                "lp_controller.php?action=add_item&type=step&lp_id=$lpId&".api_get_cidreq()
4849
            );
4850
        }*/
4851
4852
        $actionsLeft = $back;
4853
4854
        $actionsLeft .= Display::url(
4855
            Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', '', 32, get_lang('Preview')),
4856
            'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4857
                'action' => 'view',
4858
                'lp_id' => $lpId,
4859
                'isStudentView' => 'true',
4860
            ])
4861
        );
4862
4863
        /*$actionsLeft .= Display::url(
4864
            Display::getMdiIcon('music-note-plus', 'ch-tool-icon', null, 32, get_lang('Add audio')),
4865
            'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4866
                'action' => 'admin_view',
4867
                'lp_id' => $lpId,
4868
                'updateaudio' => 'true',
4869
            ])
4870
        );*/
4871
4872
        $subscriptionSettings = self::getSubscriptionSettings();
4873
4874
        $request = api_request_uri();
4875
        if (false === strpos($request, 'edit')) {
4876
            $actionsLeft .= Display::url(
4877
                Display::getMdiIcon('hammer-wrench', 'ch-tool-icon', '', 32, get_lang('Course settings')),
4878
                'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4879
                    'action' => 'edit',
4880
                    'lp_id' => $lpId,
4881
                ])
4882
            );
4883
        }
4884
4885
        if ((false === strpos($request, 'build') &&
4886
            false === strpos($request, 'add_item')) ||
4887
            in_array($action, ['add_audio'], true)
4888
        ) {
4889
            $actionsLeft .= Display::url(
4890
                Display::getMdiIcon('pencil', 'ch-tool-icon', '', 32, get_lang('Edit')),
4891
                'lp_controller.php?'.http_build_query([
4892
                    'action' => 'build',
4893
                    'lp_id' => $lpId,
4894
                ]).'&'.api_get_cidreq()
4895
            );
4896
        }
4897
4898
        if (false === strpos(api_get_self(), 'lp_subscribe_users.php')) {
4899
            if (1 == $this->subscribeUsers &&
4900
                $subscriptionSettings['allow_add_users_to_lp']) {
4901
                $actionsLeft .= Display::url(
4902
                    Display::getMdiIcon('account-multiple-plus', 'ch-tool-icon', '', 32, get_lang('Subscribe users to learning path')),
4903
                    api_get_path(WEB_CODE_PATH)."lp/lp_subscribe_users.php?lp_id=$lpId&".api_get_cidreq()
4904
                );
4905
            }
4906
        }
4907
4908
        if ($allowExpand) {
4909
            /*$actionsLeft .= Display::url(
4910
                Display::getMdiIcon('arrow-expand-all', 'ch-tool-icon', null, 32, get_lang('Expand')).
4911
                Display::getMdiIcon('arrow-collapse-all', 'ch-tool-icon', null, 32, get_lang('Collapse')),
4912
                '#',
4913
                ['role' => 'button', 'id' => 'hide_bar_template']
4914
            );*/
4915
        }
4916
4917
        if ($showRequirementButtons) {
4918
            $buttons = [
4919
                [
4920
                    'title' => get_lang('Set previous step as prerequisite for each step'),
4921
                    'href' => 'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4922
                        'action' => 'set_previous_step_as_prerequisite',
4923
                        'lp_id' => $lpId,
4924
                    ]),
4925
                ],
4926
                [
4927
                    'title' => get_lang('Clear all prerequisites'),
4928
                    'href' => 'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4929
                        'action' => 'clear_prerequisites',
4930
                        'lp_id' => $lpId,
4931
                    ]),
4932
                ],
4933
            ];
4934
            $actionsRight = Display::groupButtonWithDropDown(
4935
                get_lang('Prerequisites options'),
4936
                $buttons,
4937
                true
4938
            );
4939
        }
4940
4941
        if (api_is_platform_admin() && isset($extraField['authorlp'])) {
4942
            $actionsLeft .= Display::url(
4943
                Display::getMdiIcon('account-multiple-plus', 'ch-tool-icon', '', 32, get_lang('Author')),
4944
                'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
4945
                    'action' => 'author_view',
4946
                    'lp_id' => $lpId,
4947
                ])
4948
            );
4949
        }
4950
4951
        $toolbar = Display::toolbarAction('actions-lp-controller', [$actionsLeft, $actionsRight]);
4952
4953
        if ($returnString) {
4954
            return $toolbar;
4955
        }
4956
4957
        echo $toolbar;
4958
    }
4959
4960
    /**
4961
     * Creates the default learning path folder.
4962
     *
4963
     * @param array $course
4964
     * @param int   $creatorId
4965
     *
4966
     * @return CDocument
4967
     */
4968
    public static function generate_learning_path_folder($course, $creatorId = 0)
4969
    {
4970
        // Creating learning_path folder
4971
        $dir = 'learning_path';
4972
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
4973
4974
        return create_unexisting_directory(
4975
            $course,
4976
            $creatorId,
4977
            0,
4978
            null,
4979
            0,
4980
            '',
4981
            $dir,
4982
            get_lang('Learning paths'),
4983
            0
4984
        );
4985
    }
4986
4987
    /**
4988
     * @param array  $course
4989
     * @param string $lp_name
4990
     * @param int    $creatorId
4991
     *
4992
     * @return CDocument
4993
     */
4994
    public function generate_lp_folder($course, $lp_name = '', $creatorId = 0)
4995
    {
4996
        $filepath = '';
4997
        $dir = '/learning_path/';
4998
4999
        if (empty($lp_name)) {
5000
            $lp_name = $this->name;
5001
        }
5002
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
5003
        $parent = self::generate_learning_path_folder($course, $creatorId);
5004
5005
        // Limits title size
5006
        $title = api_substr(api_replace_dangerous_char($lp_name), 0, 80);
5007
        $dir = $dir.$title;
5008
5009
        // Creating LP folder
5010
        $folder = null;
5011
        if ($parent) {
5012
            $folder = create_unexisting_directory(
5013
                $course,
5014
                $creatorId,
5015
                0,
5016
                0,
5017
                0,
5018
                $filepath,
5019
                $dir,
5020
                $lp_name,
5021
                '',
5022
                false,
5023
                false,
5024
                $parent
5025
            );
5026
        }
5027
5028
        return $folder;
5029
    }
5030
5031
    /**
5032
     * Create a new document //still needs some finetuning.
5033
     *
5034
     * @param array  $courseInfo
5035
     * @param string $content
5036
     * @param string $title
5037
     * @param string $extension
5038
     * @param int    $parentId
5039
     * @param int    $creatorId  creator id
5040
     *
5041
     * @return int
5042
     */
5043
    public function create_document(
5044
        $courseInfo,
5045
        $content = '',
5046
        $title = '',
5047
        $extension = 'html',
5048
        $parentId = 0,
5049
        $creatorId = 0
5050
    ) {
5051
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
5052
        $sessionId = api_get_session_id();
5053
5054
        // Generates folder
5055
        $this->generate_lp_folder($courseInfo);
5056
        // stripslashes() before calling api_replace_dangerous_char() because $_POST['title']
5057
        // is already escaped twice when it gets here.
5058
        $originalTitle = !empty($title) ? $title : $_POST['title'];
5059
        if (!empty($title)) {
5060
            $title = api_replace_dangerous_char(stripslashes($title));
5061
        } else {
5062
            $title = api_replace_dangerous_char(stripslashes($_POST['title']));
5063
        }
5064
5065
        $title = disable_dangerous_file($title);
5066
        $filename = $title;
5067
        $tmp_filename = "$filename.$extension";
5068
        /*$i = 0;
5069
        while (file_exists($filepath.$tmp_filename.'.'.$extension)) {
5070
            $tmp_filename = $filename.'_'.++$i;
5071
        }*/
5072
        $filename = $tmp_filename.'.'.$extension;
5073
5074
        if ('html' === $extension) {
5075
            $content = stripslashes($content);
5076
            $content = str_replace(
5077
                api_get_path(WEB_COURSE_PATH),
5078
                api_get_path(REL_PATH).'courses/',
5079
                $content
5080
            );
5081
5082
            // Change the path of mp3 to absolute.
5083
            // The first regexp deals with :// urls.
5084
            /*$content = preg_replace(
5085
                "|(flashvars=\"file=)([^:/]+)/|",
5086
                "$1".api_get_path(
5087
                    REL_COURSE_PATH
5088
                ).$courseInfo['path'].'/document/',
5089
                $content
5090
            );*/
5091
            // The second regexp deals with audio/ urls.
5092
            /*$content = preg_replace(
5093
                "|(flashvars=\"file=)([^/]+)/|",
5094
                "$1".api_get_path(
5095
                    REL_COURSE_PATH
5096
                ).$courseInfo['path'].'/document/$2/',
5097
                $content
5098
            );*/
5099
            // For flv player: To prevent edition problem with firefox,
5100
            // we have to use a strange tip (don't blame me please).
5101
            $content = str_replace(
5102
                '</body>',
5103
                '<style type="text/css">body{}</style></body>',
5104
                $content
5105
            );
5106
        }
5107
5108
        $document = DocumentManager::addDocument(
5109
            $courseInfo,
5110
            null,
5111
            'file',
5112
            '',
5113
            $tmp_filename,
5114
            '',
5115
            0, //readonly
5116
            true,
5117
            null,
5118
            $sessionId,
5119
            $creatorId,
5120
            false,
5121
            $content,
5122
            $parentId
5123
        );
5124
5125
        $document_id = $document->getIid();
5126
        if ($document_id) {
5127
            $new_comment = isset($_POST['comment']) ? trim($_POST['comment']) : '';
5128
            $new_title = $originalTitle;
5129
5130
            if ($new_comment || $new_title) {
5131
                $tbl_doc = Database::get_course_table(TABLE_DOCUMENT);
5132
                $ct = '';
5133
                if ($new_comment) {
5134
                    $ct .= ", comment='".Database::escape_string($new_comment)."'";
5135
                }
5136
                if ($new_title) {
5137
                    $ct .= ", title='".Database::escape_string($new_title)."' ";
5138
                }
5139
5140
                $sql = "UPDATE $tbl_doc SET ".substr($ct, 1)."
5141
                        WHERE iid = $document_id ";
5142
                Database::query($sql);
5143
            }
5144
        }
5145
5146
        return $document_id;
5147
    }
5148
5149
    /**
5150
     * Edit a document based on $_POST and $_GET parameters 'dir' and 'path'.
5151
     */
5152
    public function edit_document()
5153
    {
5154
        $repo = Container::getDocumentRepository();
5155
        if (isset($_REQUEST['document_id']) && !empty($_REQUEST['document_id'])) {
5156
            $id = (int) $_REQUEST['document_id'];
5157
            /** @var CDocument $document */
5158
            $document = $repo->find($id);
5159
            if ($document->getResourceNode()->hasEditableTextContent()) {
5160
                $repo->updateResourceFileContent($document, $_REQUEST['content_lp']);
5161
            }
5162
            $document->setTitle($_REQUEST['title']);
5163
            $repo->update($document);
5164
        }
5165
    }
5166
5167
    /**
5168
     * Displays the selected item, with a panel for manipulating the item.
5169
     *
5170
     * @param CLpItem $lpItem
5171
     * @param string  $msg
5172
     * @param bool    $show_actions
5173
     *
5174
     * @return string
5175
     */
5176
    public function display_item($lpItem, $msg = null, $show_actions = true)
5177
    {
5178
        $course_id = api_get_course_int_id();
5179
        $return = '';
5180
5181
        if (null === $lpItem) {
5182
            return '';
5183
        }
5184
        $item_id = $lpItem->getIid();
5185
        $itemType = $lpItem->getItemType();
5186
        $lpId = $lpItem->getLp()->getIid();
5187
        $path = $lpItem->getPath();
5188
5189
        Session::write('parent_item_id', 'dir' === $itemType ? $item_id : 0);
5190
5191
        // Prevents wrong parent selection for document, see Bug#1251.
5192
        if ('dir' !== $itemType) {
5193
            Session::write('parent_item_id', $lpItem->getParentItemId());
5194
        }
5195
5196
        if ($show_actions) {
5197
            $return .= $this->displayItemMenu($lpItem);
5198
        }
5199
        $return .= '<div style="padding:10px;">';
5200
5201
        if ('' != $msg) {
5202
            $return .= $msg;
5203
        }
5204
5205
        $return .= '<h3>'.$lpItem->getTitle().'</h3>';
5206
5207
        switch ($itemType) {
5208
            case TOOL_THREAD:
5209
                $link = $this->rl_get_resource_link_for_learnpath(
5210
                    $course_id,
5211
                    $lpId,
5212
                    $item_id,
5213
                    0
5214
                );
5215
                $return .= Display::url(
5216
                    get_lang('Go to thread'),
5217
                    $link,
5218
                    ['class' => 'btn btn--primary']
5219
                );
5220
                break;
5221
            case TOOL_FORUM:
5222
                $return .= Display::url(
5223
                    get_lang('Go to the forum'),
5224
                    api_get_path(WEB_CODE_PATH).'forum/viewforum.php?'.api_get_cidreq().'&forum='.$path,
5225
                    ['class' => 'btn btn--primary']
5226
                );
5227
                break;
5228
            case TOOL_QUIZ:
5229
                if (!empty($path)) {
5230
                    $exercise = new Exercise();
5231
                    $exercise->read($path);
5232
                    $return .= $exercise->description.'<br />';
5233
                    $return .= Display::url(
5234
                        get_lang('Go to exercise'),
5235
                        api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$exercise->id,
5236
                        ['class' => 'btn btn--primary']
5237
                    );
5238
                }
5239
                break;
5240
            case TOOL_LP_FINAL_ITEM:
5241
                $return .= $this->getSavedFinalItem();
5242
                break;
5243
            case TOOL_DOCUMENT:
5244
            case 'video':
5245
            case TOOL_READOUT_TEXT:
5246
                $repo = Container::getDocumentRepository();
5247
                /** @var CDocument $document */
5248
                $document = $repo->find($lpItem->getPath());
5249
                $return .= $this->display_document($document, true, true);
5250
                break;
5251
        }
5252
        $return .= '</div>';
5253
5254
        return $return;
5255
    }
5256
5257
    /**
5258
     * Shows the needed forms for editing a specific item.
5259
     *
5260
     * @param CLpItem $lpItem
5261
     *
5262
     * @throws Exception
5263
     *
5264
     *
5265
     * @return string
5266
     */
5267
    public function display_edit_item($lpItem, $excludeExtraFields = [])
5268
    {
5269
        $return = '';
5270
        if (empty($lpItem)) {
5271
            return '';
5272
        }
5273
        $itemType = $lpItem->getItemType();
5274
        $path = $lpItem->getPath();
5275
5276
        switch ($itemType) {
5277
            case 'dir':
5278
            case 'asset':
5279
            case 'sco':
5280
                if (isset($_GET['view']) && 'build' === $_GET['view']) {
5281
                    $return .= $this->displayItemMenu($lpItem);
5282
                    $return .= $this->display_item_form($lpItem, 'edit');
5283
                } else {
5284
                    $return .= $this->display_item_form($lpItem, 'edit_item');
5285
                }
5286
                break;
5287
            case TOOL_LP_FINAL_ITEM:
5288
            case TOOL_DOCUMENT:
5289
            case 'video':
5290
            case TOOL_READOUT_TEXT:
5291
                $return .= $this->displayItemMenu($lpItem);
5292
                $return .= $this->displayDocumentForm('edit', $lpItem);
5293
                break;
5294
            case TOOL_LINK:
5295
                $link = null;
5296
                if (!empty($path)) {
5297
                    $repo = Container::getLinkRepository();
5298
                    $link = $repo->find($path);
5299
                }
5300
                $return .= $this->displayItemMenu($lpItem);
5301
                $return .= $this->display_link_form('edit', $lpItem, $link);
5302
5303
                break;
5304
            case TOOL_QUIZ:
5305
                if (!empty($path)) {
5306
                    $repo = Container::getQuizRepository();
5307
                    $resource = $repo->find($path);
5308
                }
5309
                $return .= $this->displayItemMenu($lpItem);
5310
                $return .= $this->display_quiz_form('edit', $lpItem, $resource);
5311
                break;
5312
            case TOOL_STUDENTPUBLICATION:
5313
                if (!empty($path)) {
5314
                    $repo = Container::getStudentPublicationRepository();
5315
                    $resource = $repo->find($path);
5316
                }
5317
                $return .= $this->displayItemMenu($lpItem);
5318
                $return .= $this->display_student_publication_form('edit', $lpItem, $resource);
5319
                break;
5320
            case TOOL_FORUM:
5321
                if (!empty($path)) {
5322
                    $repo = Container::getForumRepository();
5323
                    $resource = $repo->find($path);
5324
                }
5325
                $return .= $this->displayItemMenu($lpItem);
5326
                $return .= $this->display_forum_form('edit', $lpItem, $resource);
5327
                break;
5328
            case TOOL_THREAD:
5329
                if (!empty($path)) {
5330
                    $repo = Container::getForumPostRepository();
5331
                    $resource = $repo->find($path);
5332
                }
5333
                $return .= $this->displayItemMenu($lpItem);
5334
                $return .= $this->display_thread_form('edit', $lpItem, $resource);
5335
                break;
5336
        }
5337
5338
        return $return;
5339
    }
5340
5341
    /**
5342
     * Function that displays a list with al the resources that
5343
     * could be added to the learning path.
5344
     *
5345
     * @throws Exception
5346
     */
5347
    public function displayResources(): string
5348
    {
5349
        // Get all the docs.
5350
        $documents = $this->get_documents(true);
5351
5352
        // Get all the exercises.
5353
        $exercises = $this->get_exercises();
5354
5355
        // Get all the links.
5356
        $links = $this->get_links();
5357
5358
        // Get all the student publications.
5359
        $works = $this->get_student_publications();
5360
5361
        // Get all the forums.
5362
        $forums = $this->get_forums();
5363
5364
        // Get all surveys
5365
        $surveys = $this->getSurveys();
5366
5367
        // Get the final item form (see BT#11048) .
5368
        $finish = $this->getFinalItemForm();
5369
        $size = ICON_SIZE_MEDIUM; //ICON_SIZE_BIG
5370
        $headers = [
5371
            Display::getMdiIcon('bookshelf', 'ch-tool-icon-gradient', '', 64, get_lang('Documents')),
5372
            Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon-gradient', '', 64, get_lang('Tests')),
5373
            Display::getMdiIcon('file-link', 'ch-tool-icon-gradient', '', 64, get_lang('Links')),
5374
            Display::getMdiIcon('inbox-full', 'ch-tool-icon-gradient', '', 64, get_lang('Assignments')),
5375
            Display::getMdiIcon('comment-quote', 'ch-tool-icon-gradient', '', 64, get_lang('Forums')),
5376
            Display::getMdiIcon('bookmark-multiple', 'ch-tool-icon-gradient', '', 64, get_lang('Add section')),
5377
            Display::getMdiIcon('form-dropdown', 'ch-tool-icon-gradient', '', 64, get_lang('Add survey')),
5378
            Display::getMdiIcon('certificate', 'ch-tool-icon-gradient', '', 64, get_lang('Certificate')),
5379
        ];
5380
        $content = '';
5381
        /*$content = Display::return_message(
5382
            get_lang('Click on the [Learner view] button to see your learning path'),
5383
            'normal'
5384
        );*/
5385
        $section = $this->displayNewSectionForm();
5386
        $selected = isset($_REQUEST['lp_build_selected']) ? (int) $_REQUEST['lp_build_selected'] : 0;
5387
5388
        return Display::tabs(
5389
            $headers,
5390
            [
5391
                $documents,
5392
                $exercises,
5393
                $links,
5394
                $works,
5395
                $forums,
5396
                $section,
5397
                $surveys,
5398
                $finish,
5399
            ],
5400
            'resource_tab',
5401
            [],
5402
            [],
5403
            $selected
5404
        );
5405
    }
5406
5407
    /**
5408
     * Returns the extension of a document.
5409
     *
5410
     * @param string $filename
5411
     *
5412
     * @return string Extension (part after the last dot)
5413
     */
5414
    public function get_extension($filename)
5415
    {
5416
        $explode = explode('.', $filename);
5417
5418
        return $explode[count($explode) - 1];
5419
    }
5420
5421
    /**
5422
     * @return string
5423
     */
5424
    public function getCurrentBuildingModeURL()
5425
    {
5426
        $pathItem = isset($_GET['path_item']) ? (int) $_GET['path_item'] : '';
5427
        $action = isset($_GET['action']) ? Security::remove_XSS($_GET['action']) : '';
5428
        $id = isset($_GET['id']) ? (int) $_GET['id'] : '';
5429
        $view = isset($_GET['view']) ? Security::remove_XSS($_GET['view']) : '';
5430
5431
        $currentUrl = api_get_self().'?'.api_get_cidreq().
5432
            '&action='.$action.'&lp_id='.$this->lp_id.'&path_item='.$pathItem.'&view='.$view.'&id='.$id;
5433
5434
        return $currentUrl;
5435
    }
5436
5437
    /**
5438
     * Displays a document by id.
5439
     *
5440
     * @param CDocument $document
5441
     * @param bool      $show_title
5442
     * @param bool      $iframe
5443
     * @param bool      $edit_link
5444
     *
5445
     * @return string
5446
     */
5447
    public function display_document($document, $show_title = false, $iframe = true, $edit_link = false)
5448
    {
5449
        $return = '';
5450
        if (!$document) {
5451
            return '';
5452
        }
5453
5454
        $repo = Container::getDocumentRepository();
5455
5456
        // TODO: Add a path filter.
5457
        if ($iframe) {
5458
            $url = $repo->getResourceFileUrl($document);
5459
5460
            $return .= '<iframe
5461
                id="learnpath_preview_frame"
5462
                frameborder="0"
5463
                height="400"
5464
                width="100%"
5465
                scrolling="auto"
5466
                src="'.$url.'"></iframe>';
5467
        } else {
5468
            $return = $repo->getResourceFileContent($document);
5469
        }
5470
5471
        return $return;
5472
    }
5473
5474
    /**
5475
     * Return HTML form to add/edit a link item.
5476
     *
5477
     * @param string  $action (add/edit)
5478
     * @param CLpItem $lpItem
5479
     * @param CLink   $link
5480
     *
5481
     * @throws Exception
5482
     *
5483
     *
5484
     * @return string HTML form
5485
     */
5486
    public function display_link_form($action, $lpItem, $link)
5487
    {
5488
        $item_url = '';
5489
        if ($link) {
5490
            $item_url = stripslashes($link->getUrl());
5491
        }
5492
        $form = new FormValidator(
5493
            'edit_link',
5494
            'POST',
5495
            $this->getCurrentBuildingModeURL()
5496
        );
5497
5498
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5499
5500
        $urlAttributes = ['class' => 'learnpath_item_form'];
5501
        $urlAttributes['disabled'] = 'disabled';
5502
        $form->addElement('url', 'url', get_lang('URL'), $urlAttributes);
5503
        $form->setDefault('url', $item_url);
5504
5505
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5506
5507
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
5508
    }
5509
5510
    /**
5511
     * Return HTML form to add/edit a quiz.
5512
     *
5513
     * @param string  $action   Action (add/edit)
5514
     * @param CLpItem $lpItem   Item ID if already exists
5515
     * @param CQuiz   $exercise Extra information (quiz ID if integer)
5516
     *
5517
     * @throws Exception
5518
     *
5519
     * @return string HTML form
5520
     */
5521
    public function display_quiz_form($action, $lpItem, $exercise)
5522
    {
5523
        $form = new FormValidator(
5524
            'quiz_form',
5525
            'POST',
5526
            $this->getCurrentBuildingModeURL()
5527
        );
5528
5529
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5530
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5531
5532
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
5533
    }
5534
5535
    /**
5536
     * Return the form to display the forum edit/add option.
5537
     *
5538
     * @param CLpItem $lpItem
5539
     *
5540
     * @throws Exception
5541
     *
5542
     * @return string HTML form
5543
     */
5544
    public function display_forum_form($action, $lpItem, $resource)
5545
    {
5546
        $form = new FormValidator(
5547
            'forum_form',
5548
            'POST',
5549
            $this->getCurrentBuildingModeURL()
5550
        );
5551
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5552
5553
        if ('add' === $action) {
5554
            $form->addButtonSave(get_lang('Add forum to course'), 'submit_button');
5555
        } else {
5556
            $form->addButtonSave(get_lang('Edit the current forum'), 'submit_button');
5557
        }
5558
5559
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
5560
    }
5561
5562
    /**
5563
     * Return HTML form to add/edit forum threads.
5564
     *
5565
     * @param string  $action
5566
     * @param CLpItem $lpItem
5567
     * @param string  $resource
5568
     *
5569
     * @throws Exception
5570
     *
5571
     * @return string HTML form
5572
     */
5573
    public function display_thread_form($action, $lpItem, $resource)
5574
    {
5575
        $form = new FormValidator(
5576
            'thread_form',
5577
            'POST',
5578
            $this->getCurrentBuildingModeURL()
5579
        );
5580
5581
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
5582
5583
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5584
5585
        return $form->returnForm();
5586
    }
5587
5588
    /**
5589
     * Return the HTML form to display an item (generally a dir item).
5590
     *
5591
     * @param CLpItem $lpItem
5592
     * @param string  $action
5593
     *
5594
     * @throws Exception
5595
     *
5596
     *
5597
     * @return string HTML form
5598
     */
5599
    public function display_item_form(
5600
        $lpItem,
5601
        $action = 'add_item'
5602
    ) {
5603
        $item_type = $lpItem->getItemType();
5604
5605
        $url = api_get_self().'?'.api_get_cidreq().'&action='.$action.'&type='.$item_type.'&lp_id='.$this->lp_id;
5606
5607
        $form = new FormValidator('form_'.$item_type, 'POST', $url);
5608
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
5609
5610
        $form->addButtonSave(get_lang('Save section'), 'submit_button');
5611
5612
        return $form->returnForm();
5613
    }
5614
5615
    /**
5616
     * Return HTML form to add/edit a student publication (work).
5617
     *
5618
     * @param string              $action
5619
     * @param CStudentPublication $resource
5620
     *
5621
     * @throws Exception
5622
     *
5623
     * @return string HTML form
5624
     */
5625
    public function display_student_publication_form($action, CLpItem $lpItem, $resource)
5626
    {
5627
        $form = new FormValidator('frm_student_publication', 'post', '#');
5628
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
5629
5630
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5631
5632
        $return = '<div class="sectioncomment">';
5633
        $return .= $form->returnForm();
5634
        $return .= '</div>';
5635
5636
        return $return;
5637
    }
5638
5639
    public function displayNewSectionForm()
5640
    {
5641
        $action = 'add_item';
5642
        $item_type = 'dir';
5643
5644
        $lpItem = (new CLpItem())
5645
            ->setTitle('')
5646
            ->setItemType('dir')
5647
        ;
5648
5649
        $url = api_get_self().'?'.api_get_cidreq().'&action='.$action.'&type='.$item_type.'&lp_id='.$this->lp_id;
5650
5651
        $form = new FormValidator('form_'.$item_type, 'POST', $url);
5652
        LearnPathItemForm::setForm($form, 'add', $this, $lpItem);
5653
5654
        $form->addButtonSave(get_lang('Save section'), 'submit_button');
5655
        $form->addElement('hidden', 'type', 'dir');
5656
5657
        return $form->returnForm();
5658
    }
5659
5660
    /**
5661
     * Returns the form to update or create a document.
5662
     *
5663
     * @param string  $action (add/edit)
5664
     * @param CLpItem $lpItem
5665
     *
5666
     *
5667
     * @throws Exception
5668
     *
5669
     * @return string HTML form
5670
     */
5671
    public function displayDocumentForm($action = 'add', $lpItem = null)
5672
    {
5673
        $courseInfo = api_get_course_info();
5674
5675
        $form = new FormValidator(
5676
            'form',
5677
            'POST',
5678
            $this->getCurrentBuildingModeURL(),
5679
            '',
5680
            ['enctype' => 'multipart/form-data']
5681
        );
5682
5683
        $data = $this->generate_lp_folder($courseInfo);
5684
5685
        if (null !== $lpItem) {
5686
            LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5687
        }
5688
5689
        switch ($action) {
5690
            case 'add':
5691
                $folders = DocumentManager::get_all_document_folders(
5692
                    $courseInfo,
5693
                    0,
5694
                    true
5695
                );
5696
                DocumentManager::build_directory_selector(
5697
                    $folders,
5698
                    '',
5699
                    [],
5700
                    true,
5701
                    $form,
5702
                    'directory_parent_id'
5703
                );
5704
5705
                if ($data) {
5706
                    $defaults['directory_parent_id'] = $data->getIid();
5707
                }
5708
5709
                break;
5710
        }
5711
5712
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5713
5714
        return $form->returnForm();
5715
    }
5716
5717
    /**
5718
     * @param array  $courseInfo
5719
     * @param string $content
5720
     * @param string $title
5721
     * @param int    $parentId
5722
     *
5723
     * @return int
5724
     */
5725
    public function createReadOutText($courseInfo, $content = '', $title = '', $parentId = 0)
5726
    {
5727
        $creatorId = api_get_user_id();
5728
        $sessionId = api_get_session_id();
5729
5730
        // Generates folder
5731
        $result = $this->generate_lp_folder($courseInfo);
5732
        $dir = $result['dir'];
5733
5734
        if (empty($parentId) || '/' === $parentId) {
5735
            $postDir = isset($_POST['dir']) ? $_POST['dir'] : $dir;
5736
            $dir = isset($_GET['dir']) ? $_GET['dir'] : $postDir; // Please, do not modify this dirname formatting.
5737
5738
            if ('/' === $parentId) {
5739
                $dir = '/';
5740
            }
5741
5742
            // Please, do not modify this dirname formatting.
5743
            if (strstr($dir, '..')) {
5744
                $dir = '/';
5745
            }
5746
5747
            if (!empty($dir[0]) && '.' == $dir[0]) {
5748
                $dir = substr($dir, 1);
5749
            }
5750
            if (!empty($dir[0]) && '/' != $dir[0]) {
5751
                $dir = '/'.$dir;
5752
            }
5753
            if (isset($dir[strlen($dir) - 1]) && '/' != $dir[strlen($dir) - 1]) {
5754
                $dir .= '/';
5755
            }
5756
        } else {
5757
            $parentInfo = DocumentManager::get_document_data_by_id(
5758
                $parentId,
5759
                $courseInfo['code']
5760
            );
5761
            if (!empty($parentInfo)) {
5762
                $dir = $parentInfo['path'].'/';
5763
            }
5764
        }
5765
5766
        $filepath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/'.$dir;
5767
5768
        if (!is_dir($filepath)) {
5769
            $dir = '/';
5770
            $filepath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/'.$dir;
5771
        }
5772
5773
        $originalTitle = !empty($title) ? $title : $_POST['title'];
5774
5775
        if (!empty($title)) {
5776
            $title = api_replace_dangerous_char(stripslashes($title));
5777
        } else {
5778
            $title = api_replace_dangerous_char(stripslashes($_POST['title']));
5779
        }
5780
5781
        $title = disable_dangerous_file($title);
5782
        $filename = $title;
5783
        $content = !empty($content) ? $content : $_POST['content_lp'];
5784
        $tmpFileName = $filename;
5785
5786
        $i = 0;
5787
        while (file_exists($filepath.$tmpFileName.'.html')) {
5788
            $tmpFileName = $filename.'_'.++$i;
5789
        }
5790
5791
        $filename = $tmpFileName.'.html';
5792
        $content = stripslashes($content);
5793
5794
        if (file_exists($filepath.$filename)) {
5795
            return 0;
5796
        }
5797
5798
        $putContent = file_put_contents($filepath.$filename, $content);
5799
5800
        if (false === $putContent) {
5801
            return 0;
5802
        }
5803
5804
        $fileSize = filesize($filepath.$filename);
5805
        $saveFilePath = $dir.$filename;
5806
5807
        $document = DocumentManager::addDocument(
5808
            $courseInfo,
5809
            $saveFilePath,
5810
            'file',
5811
            $fileSize,
5812
            $tmpFileName,
5813
            '',
5814
            0, //readonly
5815
            true,
5816
            null,
5817
            $sessionId,
5818
            $creatorId
5819
        );
5820
5821
        $documentId = $document->getIid();
5822
5823
        if (!$document) {
5824
            return 0;
5825
        }
5826
5827
        $newComment = isset($_POST['comment']) ? trim($_POST['comment']) : '';
5828
        $newTitle = $originalTitle;
5829
5830
        if ($newComment || $newTitle) {
5831
            $em = Database::getManager();
5832
5833
            if ($newComment) {
5834
                $document->setComment($newComment);
5835
            }
5836
5837
            if ($newTitle) {
5838
                $document->setTitle($newTitle);
5839
            }
5840
5841
            $em->persist($document);
5842
            $em->flush();
5843
        }
5844
5845
        return $documentId;
5846
    }
5847
5848
    /**
5849
     * Displays the menu for manipulating a step.
5850
     *
5851
     * @return string
5852
     */
5853
    public function displayItemMenu(CLpItem $lpItem)
5854
    {
5855
        $item_id = $lpItem->getIid();
5856
        $audio = $lpItem->getAudio();
5857
        $itemType = $lpItem->getItemType();
5858
        $path = $lpItem->getPath();
5859
5860
        $return = '';
5861
        $audio_player = null;
5862
        // We display an audio player if needed.
5863
        if (!empty($audio)) {
5864
            /*$webAudioPath = '../..'.api_get_path(REL_COURSE_PATH).$_course['path'].'/document/audio/'.$row['audio'];
5865
            $audio_player .= '<div class="lp_mediaplayer" id="container">'
5866
                .'<audio src="'.$webAudioPath.'" controls>'
5867
                .'</div><br>';*/
5868
        }
5869
5870
        $url = api_get_self().'?'.api_get_cidreq().'&view=build&id='.$item_id.'&lp_id='.$this->lp_id;
5871
5872
        if (TOOL_LP_FINAL_ITEM !== $itemType) {
5873
            $return .= Display::url(
5874
                Display::getMdiIcon('pencil', 'ch-tool-icon', null, 22, get_lang('Edit')),
5875
                $url.'&action=edit_item&path_item='.$path
5876
            );
5877
5878
            /*$return .= Display::url(
5879
                Display::getMdiIcon('arrow-right-bold', 'ch-tool-icon', null, 22, get_lang('Move')),
5880
                $url.'&action=move_item'
5881
            );*/
5882
        }
5883
5884
        // Commented for now as prerequisites cannot be added to chapters.
5885
        if ('dir' !== $itemType) {
5886
            $return .= Display::url(
5887
                Display::getMdiIcon('graph', 'ch-tool-icon', null, 22, get_lang('Prerequisites')),
5888
                $url.'&action=edit_item_prereq'
5889
            );
5890
        }
5891
        $return .= Display::url(
5892
            Display::getMdiIcon('delete', 'ch-tool-icon', null, 22, get_lang('Delete')),
5893
            $url.'&action=delete_item'
5894
        );
5895
5896
        /*if (in_array($itemType, [TOOL_DOCUMENT, TOOL_LP_FINAL_ITEM, TOOL_HOTPOTATOES])) {
5897
            $documentData = DocumentManager::get_document_data_by_id($path, $course_code);
5898
            if (empty($documentData)) {
5899
                // Try with iid
5900
                $table = Database::get_course_table(TABLE_DOCUMENT);
5901
                $sql = "SELECT path FROM $table
5902
                        WHERE
5903
                              c_id = ".api_get_course_int_id()." AND
5904
                              iid = ".$path." AND
5905
                              path NOT LIKE '%_DELETED_%'";
5906
                $result = Database::query($sql);
5907
                $documentData = Database::fetch_array($result);
5908
                if ($documentData) {
5909
                    $documentData['absolute_path_from_document'] = '/document'.$documentData['path'];
5910
                }
5911
            }
5912
            if (isset($documentData['absolute_path_from_document'])) {
5913
                $return .= get_lang('File').': '.$documentData['absolute_path_from_document'];
5914
            }
5915
        }*/
5916
5917
        if (!empty($audio_player)) {
5918
            $return .= $audio_player;
5919
        }
5920
5921
        return Display::toolbarAction('lp_item', [$return]);
5922
    }
5923
5924
    /**
5925
     * Creates the javascript needed for filling up the checkboxes without page reload.
5926
     *
5927
     * @return string
5928
     */
5929
    public function get_js_dropdown_array()
5930
    {
5931
        $return = 'var child_name = new Array();'."\n";
5932
        $return .= 'var child_value = new Array();'."\n\n";
5933
        $return .= 'child_name[0] = new Array();'."\n";
5934
        $return .= 'child_value[0] = new Array();'."\n\n";
5935
5936
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
5937
        $sql = "SELECT * FROM ".$tbl_lp_item."
5938
                WHERE
5939
                    lp_id = ".$this->lp_id." AND
5940
                    parent_item_id = 0
5941
                ORDER BY display_order ASC";
5942
        Database::query($sql);
5943
        $i = 0;
5944
5945
        $list = $this->getItemsForForm(true);
5946
5947
        foreach ($list as $row_zero) {
5948
            if (TOOL_LP_FINAL_ITEM !== $row_zero['item_type']) {
5949
                if (TOOL_QUIZ == $row_zero['item_type']) {
5950
                    $row_zero['title'] = Exercise::get_formated_title_variable($row_zero['title']);
5951
                }
5952
                $js_var = json_encode(get_lang('After').' '.$row_zero['title']);
5953
                $return .= 'child_name[0]['.$i.'] = '.$js_var.' ;'."\n";
5954
                $return .= 'child_value[0]['.$i++.'] = "'.$row_zero['iid'].'";'."\n";
5955
            }
5956
        }
5957
5958
        $return .= "\n";
5959
        $sql = "SELECT * FROM $tbl_lp_item
5960
                WHERE lp_id = ".$this->lp_id;
5961
        $res = Database::query($sql);
5962
        while ($row = Database::fetch_array($res)) {
5963
            $sql_parent = "SELECT * FROM ".$tbl_lp_item."
5964
                           WHERE
5965
                                parent_item_id = ".$row['iid']."
5966
                           ORDER BY display_order ASC";
5967
            $res_parent = Database::query($sql_parent);
5968
            $i = 0;
5969
            $return .= 'child_name['.$row['iid'].'] = new Array();'."\n";
5970
            $return .= 'child_value['.$row['iid'].'] = new Array();'."\n\n";
5971
5972
            while ($row_parent = Database::fetch_array($res_parent)) {
5973
                $js_var = json_encode(get_lang('After').' '.$this->cleanItemTitle($row_parent['title']));
5974
                $return .= 'child_name['.$row['iid'].']['.$i.'] =   '.$js_var.' ;'."\n";
5975
                $return .= 'child_value['.$row['iid'].']['.$i++.'] = "'.$row_parent['iid'].'";'."\n";
5976
            }
5977
            $return .= "\n";
5978
        }
5979
5980
        $return .= "
5981
            function load_cbo(id) {
5982
                if (!id) {
5983
                    return false;
5984
                }
5985
5986
                var cbo = document.getElementById('previous');
5987
                if (cbo) {
5988
                    for(var i = cbo.length - 1; i > 0; i--) {
5989
                        cbo.options[i] = null;
5990
                    }
5991
                    var k=0;
5992
                    for (var i = 1; i <= child_name[id].length; i++){
5993
                        var option = new Option(child_name[id][i - 1], child_value[id][i - 1]);
5994
                        option.style.paddingLeft = '40px';
5995
                        cbo.options[i] = option;
5996
                        k = i;
5997
                    }
5998
                    cbo.options[k].selected = true;
5999
                }
6000
6001
                //$('#previous').selectpicker('refresh');
6002
            }";
6003
6004
        return $return;
6005
    }
6006
6007
    /**
6008
     * Display the form to allow moving an item.
6009
     *
6010
     * @param CLpItem $lpItem
6011
     *
6012
     * @throws Exception
6013
     *
6014
     *
6015
     * @return string HTML form
6016
     */
6017
    public function display_move_item($lpItem)
6018
    {
6019
        $return = '';
6020
        $path = $lpItem->getPath();
6021
6022
        if ($lpItem) {
6023
            $itemType = $lpItem->getItemType();
6024
            switch ($itemType) {
6025
                case 'dir':
6026
                case 'asset':
6027
                    $return .= $this->displayItemMenu($lpItem);
6028
                    $return .= $this->display_item_form(
6029
                        $lpItem,
6030
                        get_lang('Move the current section'),
6031
                        'move',
6032
                        $row
6033
                    );
6034
                    break;
6035
                case TOOL_DOCUMENT:
6036
                case 'video':
6037
                    $return .= $this->displayItemMenu($lpItem);
6038
                    $return .= $this->displayDocumentForm('move', $lpItem);
6039
                    break;
6040
                case TOOL_LINK:
6041
                    $link = null;
6042
                    if (!empty($path)) {
6043
                        $repo = Container::getLinkRepository();
6044
                        $link = $repo->find($path);
6045
                    }
6046
                    $return .= $this->displayItemMenu($lpItem);
6047
                    $return .= $this->display_link_form('move', $lpItem, $link);
6048
                    break;
6049
                case TOOL_HOTPOTATOES:
6050
                    $return .= $this->displayItemMenu($lpItem);
6051
                    $return .= $this->display_link_form('move', $lpItem, $row);
6052
                    break;
6053
                case TOOL_QUIZ:
6054
                    $return .= $this->displayItemMenu($lpItem);
6055
                    $return .= $this->display_quiz_form('move', $lpItem, $row);
6056
                    break;
6057
                case TOOL_STUDENTPUBLICATION:
6058
                    $return .= $this->displayItemMenu($lpItem);
6059
                    $return .= $this->display_student_publication_form('move', $lpItem, $row);
6060
                    break;
6061
                case TOOL_FORUM:
6062
                    $return .= $this->displayItemMenu($lpItem);
6063
                    $return .= $this->display_forum_form('move', $lpItem, $row);
6064
                    break;
6065
                case TOOL_THREAD:
6066
                    $return .= $this->displayItemMenu($lpItem);
6067
                    $return .= $this->display_forum_form('move', $lpItem, $row);
6068
                    break;
6069
            }
6070
        }
6071
6072
        return $return;
6073
    }
6074
6075
    /**
6076
     * Return HTML form to allow prerequisites selection.
6077
     *
6078
     * @todo use FormValidator
6079
     *
6080
     * @return string HTML form
6081
     */
6082
    public function displayItemPrerequisitesForm(CLpItem $lpItem)
6083
    {
6084
        $courseId = api_get_course_int_id();
6085
        $preRequisiteId = $lpItem->getPrerequisite();
6086
        $itemId = $lpItem->getIid();
6087
6088
        $return = Display::page_header(get_lang('Add/edit prerequisites').' '.$lpItem->getTitle());
6089
6090
        $return .= '<form method="POST">';
6091
        $return .= '<div class="table-responsive">';
6092
        $return .= '<table class="table table-hover">';
6093
        $return .= '<thead>';
6094
        $return .= '<tr>';
6095
        $return .= '<th>'.get_lang('Prerequisites').'</th>';
6096
        $return .= '<th width="140">'.get_lang('minimum').'</th>';
6097
        $return .= '<th width="140">'.get_lang('maximum').'</th>';
6098
        $return .= '</tr>';
6099
        $return .= '</thead>';
6100
6101
        // Adding the none option to the prerequisites see http://www.chamilo.org/es/node/146
6102
        $return .= '<tbody>';
6103
        $return .= '<tr>';
6104
        $return .= '<td colspan="3">';
6105
        $return .= '<div class="radio learnpath"><label for="idnone">';
6106
        $return .= '<input checked="checked" id="idnone" name="prerequisites" type="radio" />';
6107
        $return .= get_lang('none').'</label>';
6108
        $return .= '</div>';
6109
        $return .= '</tr>';
6110
6111
        // @todo use entitites
6112
        $tblLpItem = Database::get_course_table(TABLE_LP_ITEM);
6113
        $sql = "SELECT * FROM $tblLpItem
6114
                WHERE lp_id = ".$this->lp_id;
6115
        $result = Database::query($sql);
6116
6117
        $selectedMinScore = [];
6118
        $selectedMaxScore = [];
6119
        $masteryScore = [];
6120
        while ($row = Database::fetch_array($result)) {
6121
            if ($row['iid'] == $itemId) {
6122
                $selectedMinScore[$row['prerequisite']] = $row['prerequisite_min_score'];
6123
                $selectedMaxScore[$row['prerequisite']] = $row['prerequisite_max_score'];
6124
            }
6125
            $masteryScore[$row['iid']] = $row['mastery_score'];
6126
        }
6127
6128
        $displayOrder = $lpItem->getDisplayOrder();
6129
        $lpItemRepo = Container::getLpItemRepository();
6130
        $itemRoot = $lpItemRepo->getRootItem($this->get_id());
6131
        $em = Database::getManager();
6132
6133
        $currentItemId = $itemId;
6134
        $options = [
6135
            'decorate' => true,
6136
            'rootOpen' => function () {
6137
                return '';
6138
            },
6139
            'rootClose' => function () {
6140
                return '';
6141
            },
6142
            'childOpen' => function () {
6143
                return '';
6144
            },
6145
            'childClose' => '',
6146
            'nodeDecorator' => function ($item) use (
6147
                $currentItemId,
6148
                $preRequisiteId,
6149
                $courseId,
6150
                $selectedMaxScore,
6151
                $selectedMinScore,
6152
                $displayOrder,
6153
                $lpItemRepo,
6154
                $em
6155
            ) {
6156
                $itemId = $item['iid'];
6157
                $type = $item['itemType'];
6158
                $iconName = str_replace(' ', '', $type);
6159
                switch ($iconName) {
6160
                    case 'category':
6161
                    case 'chapter':
6162
                    case 'folder':
6163
                    case 'dir':
6164
                        $icon = Display::getMdiIcon(ObjectIcon::CHAPTER, 'ch-tool-icon', '', ICON_SIZE_TINY);
6165
                        break;
6166
                    default:
6167
                        $icon = Display::getMdiIcon(ObjectIcon::SINGLE_ELEMENT, 'ch-tool-icon', '', ICON_SIZE_TINY);
6168
                        break;
6169
                }
6170
6171
                if ($itemId == $currentItemId) {
6172
                    return '';
6173
                }
6174
6175
                if ($displayOrder < $item['displayOrder']) {
6176
                    return '';
6177
                }
6178
6179
                $selectedMaxScoreValue = isset($selectedMaxScore[$itemId]) ? $selectedMaxScore[$itemId] : $item['maxScore'];
6180
                $selectedMinScoreValue = $selectedMinScore[$itemId] ?? 0;
6181
                $masteryScoreAsMinValue = $masteryScore[$itemId] ?? 0;
6182
6183
                $return = '<tr>';
6184
                $return .= '<td '.((TOOL_QUIZ != $type && TOOL_HOTPOTATOES != $type) ? ' colspan="3"' : '').'>';
6185
                $return .= '<div style="margin-left:'.($item['lvl'] * 20).'px;" class="radio learnpath">';
6186
                $return .= '<label for="id'.$itemId.'">';
6187
6188
                $checked = '';
6189
                if (null !== $preRequisiteId) {
6190
                    $checked = in_array($preRequisiteId, [$itemId, $item['ref']]) ? ' checked="checked" ' : '';
6191
                }
6192
6193
                $disabled = 'dir' === $type ? ' disabled="disabled" ' : '';
6194
6195
                $return .= '<input
6196
                    '.$checked.' '.$disabled.'
6197
                    id="id'.$itemId.'"
6198
                    name="prerequisites"
6199
                    type="radio"
6200
                    value="'.$itemId.'" />';
6201
6202
                $return .= $icon.'&nbsp;&nbsp;'.$item['title'].'</label>';
6203
                $return .= '</div>';
6204
                $return .= '</td>';
6205
6206
                if (TOOL_QUIZ == $type) {
6207
                    // let's update max_score Tests information depending of the Tests Advanced properties
6208
                    $exercise = new Exercise($courseId);
6209
                    /** @var CLpItem $itemEntity */
6210
                    $itemEntity = $lpItemRepo->find($itemId);
6211
                    $exercise->read($item['path']);
6212
                    $itemEntity->setMaxScore($exercise->getMaxScore());
6213
                    $em->persist($itemEntity);
6214
                    $em->flush($itemEntity);
6215
6216
                    $item['maxScore'] = $exercise->getMaxScore();
6217
6218
                    if (empty($selectedMinScoreValue) && !empty($masteryScoreAsMinValue)) {
6219
                        // Backwards compatibility with 1.9.x use mastery_score as min value
6220
                        $selectedMinScoreValue = $masteryScoreAsMinValue;
6221
                    }
6222
                    $return .= '<td>';
6223
                    $return .= '<input
6224
                        class="form-control"
6225
                        size="4" maxlength="3"
6226
                        name="min_'.$itemId.'"
6227
                        type="number"
6228
                        min="0"
6229
                        step="any"
6230
                        max="'.$item['maxScore'].'"
6231
                        value="'.$selectedMinScoreValue.'"
6232
                    />';
6233
                    $return .= '</td>';
6234
                    $return .= '<td>';
6235
                    $return .= '<input
6236
                        class="form-control"
6237
                        size="4"
6238
                        maxlength="3"
6239
                        name="max_'.$itemId.'"
6240
                        type="number"
6241
                        min="0"
6242
                        step="any"
6243
                        max="'.$item['maxScore'].'"
6244
                        value="'.$selectedMaxScoreValue.'"
6245
                    />';
6246
                        $return .= '</td>';
6247
                    }
6248
6249
                if (TOOL_HOTPOTATOES == $type) {
6250
                    $return .= '<td>';
6251
                    $return .= '<input
6252
                        size="4"
6253
                        maxlength="3"
6254
                        name="min_'.$itemId.'"
6255
                        type="number"
6256
                        min="0"
6257
                        step="any"
6258
                        max="'.$item['maxScore'].'"
6259
                        value="'.$selectedMinScoreValue.'"
6260
                    />';
6261
                        $return .= '</td>';
6262
                        $return .= '<td>';
6263
                        $return .= '<input
6264
                        size="4"
6265
                        maxlength="3"
6266
                        name="max_'.$itemId.'"
6267
                        type="number"
6268
                        min="0"
6269
                        step="any"
6270
                        max="'.$item['maxScore'].'"
6271
                        value="'.$selectedMaxScoreValue.'"
6272
                    />';
6273
                    $return .= '</td>';
6274
                }
6275
                $return .= '</tr>';
6276
6277
                return $return;
6278
            },
6279
        ];
6280
6281
        $tree = $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
6282
        $return .= $tree;
6283
        $return .= '</tbody>';
6284
        $return .= '</table>';
6285
        $return .= '</div>';
6286
        $return .= '<div class="form-group">';
6287
        $return .= '<button class="btn btn--primary" name="submit_button" type="submit">'.
6288
            get_lang('Save prerequisites settings').'</button>';
6289
        $return .= '</form>';
6290
6291
        return $return;
6292
    }
6293
6294
    /**
6295
     * Return HTML list to allow prerequisites selection for lp.
6296
     */
6297
    public function display_lp_prerequisites_list(FormValidator $form)
6298
    {
6299
        $lp_id = $this->lp_id;
6300
        $lp = api_get_lp_entity($lp_id);
6301
        $prerequisiteId = $lp->getPrerequisite();
6302
6303
        $repo = Container::getLpRepository();
6304
        $qb = $repo->findAllByCourse(api_get_course_entity(), api_get_session_entity());
6305
        /** @var CLp[] $lps */
6306
        $lps = $qb->getQuery()->getResult();
6307
6308
        //$session_id = api_get_session_id();
6309
        /*$session_condition = api_get_session_condition($session_id, true, true);
6310
        $sql = "SELECT * FROM $tbl_lp
6311
                WHERE c_id = $course_id $session_condition
6312
                ORDER BY display_order ";
6313
        $rs = Database::query($sql);*/
6314
6315
        $items = [get_lang('none')];
6316
        foreach ($lps as $lp) {
6317
            $myLpId = $lp->getIid();
6318
            if ($myLpId == $lp_id) {
6319
                continue;
6320
            }
6321
            $items[$myLpId] = $lp->getTitle();
6322
            /*$return .= '<option
6323
                value="'.$myLpId.'" '.(($myLpId == $prerequisiteId) ? ' selected ' : '').'>'.
6324
                $lp->getName().
6325
                '</option>';*/
6326
        }
6327
6328
        $select = $form->addSelect('prerequisites', get_lang('Prerequisites'), $items);
6329
        $select->setSelected($prerequisiteId);
6330
    }
6331
6332
    /**
6333
     * Creates a list with all the documents in it.
6334
     *
6335
     * @param bool $showInvisibleFiles
6336
     *
6337
     * @throws Exception
6338
     *
6339
     *
6340
     * @return string
6341
     */
6342
    public function get_documents($showInvisibleFiles = false)
6343
    {
6344
        $sessionId = api_get_session_id();
6345
        $documentTree = DocumentManager::get_document_preview(
6346
            api_get_course_entity(),
6347
            $this->lp_id,
6348
            null,
6349
            $sessionId,
6350
            true,
6351
            null,
6352
            null,
6353
            $showInvisibleFiles,
6354
            true,
6355
            false,
6356
            true,
6357
            false,
6358
            [],
6359
            [],
6360
            ['file', 'folder']
6361
        );
6362
6363
        $form = new FormValidator(
6364
            'form_upload',
6365
            'POST',
6366
            $this->getCurrentBuildingModeURL(),
6367
            '',
6368
            ['enctype' => 'multipart/form-data']
6369
        );
6370
6371
        $folders = DocumentManager::get_all_document_folders(
6372
            api_get_course_info(),
6373
            0,
6374
            true
6375
        );
6376
6377
        $folder = $this->generate_lp_folder(api_get_course_info());
6378
6379
        DocumentManager::build_directory_selector(
6380
            $folders,
6381
            $folder->getIid(),
6382
            [],
6383
            true,
6384
            $form,
6385
            'directory_parent_id'
6386
        );
6387
6388
        $group = [
6389
            $form->createElement(
6390
                'radio',
6391
                'if_exists',
6392
                get_lang('If file exists:'),
6393
                get_lang('Do nothing'),
6394
                'nothing'
6395
            ),
6396
            $form->createElement(
6397
                'radio',
6398
                'if_exists',
6399
                null,
6400
                get_lang('Overwrite the existing file'),
6401
                'overwrite'
6402
            ),
6403
            $form->createElement(
6404
                'radio',
6405
                'if_exists',
6406
                null,
6407
                get_lang('Rename the uploaded file if it exists'),
6408
                'rename'
6409
            ),
6410
        ];
6411
        $form->addGroup($group, null, get_lang('If file exists:'));
6412
6413
        $fileExistsOption = api_get_setting('document.document_if_file_exists_option');
6414
        $defaultFileExistsOption = 'rename';
6415
        if (!empty($fileExistsOption)) {
6416
            $defaultFileExistsOption = $fileExistsOption;
6417
        }
6418
        $form->setDefaults(['if_exists' => $defaultFileExistsOption]);
6419
6420
        // Check box options
6421
        $form->addCheckBox(
6422
            'unzip',
6423
            get_lang('Options'),
6424
            get_lang('Uncompress zip')
6425
        );
6426
6427
        $url = api_get_path(WEB_AJAX_PATH).'document.ajax.php?'.api_get_cidreq().'&a=upload_file&curdirpath=';
6428
        $form->addMultipleUpload($url);
6429
6430
        $lpItem = (new CLpItem())
6431
            ->setTitle('')
6432
            ->setItemType(TOOL_DOCUMENT)
6433
        ;
6434
        $new = $this->displayDocumentForm('add', $lpItem);
6435
6436
        $videosTree = $this->get_videos();
6437
        $headers = [
6438
            get_lang('Files'),
6439
            get_lang('Videos'),
6440
            get_lang('Create a new document'),
6441
            get_lang('Upload'),
6442
        ];
6443
6444
        return Display::tabs(
6445
            $headers,
6446
            [$documentTree, $videosTree, $new, $form->returnForm()],
6447
            'subtab',
6448
            ['class' => 'mt-2']
6449
        );
6450
    }
6451
6452
    public function get_videos()
6453
    {
6454
        $sessionId = api_get_session_id();
6455
6456
        $documentTree = DocumentManager::get_document_preview(
6457
            api_get_course_entity(),
6458
            $this->lp_id,
6459
            null,
6460
            $sessionId,
6461
            true,
6462
            null,
6463
            null,
6464
            false,
6465
            false,
6466
            false,
6467
            true,
6468
            false,
6469
            [],
6470
            [],
6471
            'video'
6472
        );
6473
6474
        return $documentTree ?: get_lang('No videos found');
6475
    }
6476
6477
    /**
6478
     * Creates a list with all the exercises (quiz) in it.
6479
     *
6480
     * @return string
6481
     */
6482
    public function get_exercises()
6483
    {
6484
        $course_id = api_get_course_int_id();
6485
        $session_id = api_get_session_id();
6486
        $setting = 'true' === api_get_setting('lp.show_invisible_exercise_in_lp_toc');
6487
6488
        //$activeCondition = ' active <> -1 ';
6489
        $active = 2;
6490
        if ($setting) {
6491
            $active = 1;
6492
            //$activeCondition = ' active = 1 ';
6493
        }
6494
6495
        $categoryCondition = '';
6496
6497
        $keyword = $_REQUEST['keyword'] ?? null;
6498
        $categoryId = $_REQUEST['category_id'] ?? null;
6499
        /*if (api_get_configuration_value('allow_exercise_categories') && !empty($categoryId)) {
6500
            $categoryCondition = " AND exercise_category_id = $categoryId ";
6501
        }
6502
6503
        $keywordCondition = '';
6504
6505
        if (!empty($keyword)) {
6506
            $keyword = Database::escape_string($keyword);
6507
            $keywordCondition = " AND title LIKE '%$keyword%' ";
6508
        }
6509
        */
6510
        $course = api_get_course_entity($course_id);
6511
        $session = api_get_session_entity($session_id);
6512
6513
        $qb = Container::getQuizRepository()->findAllByCourse($course, $session, $keyword, $active, false, $categoryId);
6514
        /** @var CQuiz[] $exercises */
6515
        $exercises = $qb->getQuery()->getResult();
6516
6517
        /*$sql_quiz = "SELECT * FROM $tbl_quiz
6518
                     WHERE
6519
                            c_id = $course_id AND
6520
                            $activeCondition
6521
                            $condition_session
6522
                            $categoryCondition
6523
                            $keywordCondition
6524
                     ORDER BY title ASC";
6525
        $res_quiz = Database::query($sql_quiz);*/
6526
6527
        $currentUrl = api_get_self().'?'.api_get_cidreq().'&action=add_item&type=step&lp_id='.$this->lp_id.'#resource_tab-2';
6528
6529
        // Create a search-box
6530
        /*$form = new FormValidator('search_simple', 'get', $currentUrl);
6531
        $form->addHidden('action', 'add_item');
6532
        $form->addHidden('type', 'step');
6533
        $form->addHidden('lp_id', $this->lp_id);
6534
        $form->addHidden('lp_build_selected', '2');
6535
6536
        $form->addCourseHiddenParams();
6537
        $form->addText(
6538
            'keyword',
6539
            get_lang('Search'),
6540
            false,
6541
            [
6542
                'aria-label' => get_lang('Search'),
6543
            ]
6544
        );
6545
6546
        if (api_get_configuration_value('allow_exercise_categories')) {
6547
            $manager = new ExerciseCategoryManager();
6548
            $options = $manager->getCategoriesForSelect(api_get_course_int_id());
6549
            if (!empty($options)) {
6550
                $form->addSelect(
6551
                    'category_id',
6552
                    get_lang('Category'),
6553
                    $options,
6554
                    ['placeholder' => get_lang('Please select an option')]
6555
                );
6556
            }
6557
        }
6558
6559
        $form->addButtonSearch(get_lang('Search'));
6560
        $return = $form->returnForm();*/
6561
6562
        $return = '<ul class="mt-2 bg-white list-group lp_resource">';
6563
        $return .= '<li class="list-group-item lp_resource_element disable_drag">';
6564
        $return .= Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, 32, get_lang('New test'));
6565
        $return .= '<a
6566
            href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise_admin.php?'.api_get_cidreq().'&lp_id='.$this->lp_id.'">'.
6567
            get_lang('New test').'</a>';
6568
        $return .= '</li>';
6569
6570
        $previewIcon = Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview'));
6571
        $quizIcon = Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, 16, get_lang('Exercise'));
6572
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6573
        $exerciseUrl = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq();
6574
        foreach ($exercises as $exercise) {
6575
            $exerciseId = $exercise->getIid();
6576
            $title = strip_tags(api_html_entity_decode($exercise->getTitle()));
6577
            $visibility = $exercise->isVisible($course, $session);
6578
6579
            $link = Display::url(
6580
                $previewIcon,
6581
                $exerciseUrl.'&exerciseId='.$exerciseId,
6582
                ['target' => '_blank']
6583
            );
6584
            $return .= '<li
6585
                class="list-group-item lp_resource_element"
6586
                id="'.$exerciseId.'"
6587
                data-id="'.$exerciseId.'"
6588
                title="'.$title.'">';
6589
            $return .= Display::url($moveIcon, '#', ['class' => 'moved']);
6590
            $return .= $quizIcon;
6591
            $sessionStar = '';
6592
            /*$sessionStar = api_get_session_image(
6593
                $row_quiz['session_id'],
6594
                $userInfo['status']
6595
            );*/
6596
            $return .= Display::url(
6597
                Security::remove_XSS(cut($title, 80)).$link.$sessionStar,
6598
                api_get_self().'?'.
6599
                    api_get_cidreq().'&action=add_item&type='.TOOL_QUIZ.'&file='.$exerciseId.'&lp_id='.$this->lp_id,
6600
                [
6601
                    'class' => false === $visibility ? 'moved text-muted ' : 'moved link_with_id',
6602
                    'data_type' => 'quiz',
6603
                    'data-id' => $exerciseId,
6604
                ]
6605
            );
6606
            $return .= '</li>';
6607
        }
6608
6609
        $return .= '</ul>';
6610
6611
        return $return;
6612
    }
6613
6614
    /**
6615
     * Creates a list with all the links in it.
6616
     *
6617
     * @return string
6618
     */
6619
    public function get_links()
6620
    {
6621
        $sessionId = api_get_session_id();
6622
        $repo = Container::getLinkRepository();
6623
6624
        $course = api_get_course_entity();
6625
        $session = api_get_session_entity($sessionId);
6626
        $qb = $repo->getResourcesByCourse($course, $session);
6627
        /** @var CLink[] $links */
6628
        $links = $qb->getQuery()->getResult();
6629
6630
        $selfUrl = api_get_self();
6631
        $courseIdReq = api_get_cidreq();
6632
        $userInfo = api_get_user_info();
6633
6634
        $moveEverywhereIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6635
6636
        $categorizedLinks = [];
6637
        $categories = [];
6638
6639
        foreach ($links as $link) {
6640
            $categoryId = null !== $link->getCategory() ? $link->getCategory()->getIid() : 0;
6641
            if (empty($categoryId)) {
6642
                $categories[0] = get_lang('Uncategorized');
6643
            } else {
6644
                $category = $link->getCategory();
6645
                $categories[$categoryId] = $category->getTitle();
6646
            }
6647
            $categorizedLinks[$categoryId][$link->getIid()] = $link;
6648
        }
6649
6650
        $linksHtmlCode =
6651
            '<script>
6652
            function toggle_tool(tool, id) {
6653
                if(document.getElementById(tool+"_"+id+"_content").style.display == "none"){
6654
                    document.getElementById(tool+"_"+id+"_content").style.display = "block";
6655
                    document.getElementById(tool+"_"+id+"_opener").src = "'.Display::returnIconPath('remove.gif').'";
6656
                } else {
6657
                    document.getElementById(tool+"_"+id+"_content").style.display = "none";
6658
                    document.getElementById(tool+"_"+id+"_opener").src = "'.Display::returnIconPath('add.png').'";
6659
                }
6660
            }
6661
        </script>
6662
6663
        <ul class="mt-2 bg-white list-group lp_resource">
6664
            <li class="list-group-item lp_resource_element disable_drag ">
6665
                '.Display::getMdiIcon(ObjectIcon::LINK, 'ch-tool-icon', null, ICON_SIZE_SMALL).'
6666
                <a
6667
                href="'.api_get_path(WEB_CODE_PATH).'link/link.php?'.$courseIdReq.'&action=addlink&lp_id='.$this->lp_id.'"
6668
                title="'.get_lang('Add a link').'">'.
6669
                get_lang('Add a link').'
6670
                </a>
6671
            </li>';
6672
        $linkIcon = Display::getMdiIcon('file-link', 'ch-tool-icon', null, 16, get_lang('Link'));
6673
        foreach ($categorizedLinks as $categoryId => $links) {
6674
            $linkNodes = null;
6675
            /** @var CLink $link */
6676
            foreach ($links as $key => $link) {
6677
                $title = $link->getTitle();
6678
                $id = $link->getIid();
6679
                $linkUrl = Display::url(
6680
                    Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6681
                    api_get_path(WEB_CODE_PATH).'link/link_goto.php?'.api_get_cidreq().'&link_id='.$key,
6682
                    ['target' => '_blank']
6683
                );
6684
6685
                if ($link->isVisible($course, $session)) {
6686
                    //$sessionStar = api_get_session_image($linkSessionId, $userInfo['status']);
6687
                    $sessionStar = '';
6688
                    $url = $selfUrl.'?'.$courseIdReq.'&action=add_item&type='.TOOL_LINK.'&file='.$key.'&lp_id='.$this->lp_id;
6689
                    $link = Display::url(
6690
                        Security::remove_XSS($title).$sessionStar.$linkUrl,
6691
                        $url,
6692
                        [
6693
                            'class' => 'moved link_with_id',
6694
                            'data-id' => $key,
6695
                            'data_type' => TOOL_LINK,
6696
                            'title' => $title,
6697
                        ]
6698
                    );
6699
                    $linkNodes .=
6700
                        "<li
6701
                            class='list-group-item lp_resource_element'
6702
                            id= $id
6703
                            data-id= $id
6704
                            >
6705
                         <a class='moved' href='#'>
6706
                            $moveEverywhereIcon
6707
                        </a>
6708
                        $linkIcon $link
6709
                        </li>";
6710
                }
6711
            }
6712
            $linksHtmlCode .=
6713
                '<li class="list-group-item disable_drag">
6714
                    <a style="cursor:hand" onclick="javascript: toggle_tool(\''.TOOL_LINK.'\','.$categoryId.')" >
6715
                        <img src="'.Display::returnIconPath('add.png').'" id="'.TOOL_LINK.'_'.$categoryId.'_opener"
6716
                        align="absbottom" />
6717
                    </a>
6718
                    <span style="vertical-align:middle">'.Security::remove_XSS($categories[$categoryId]).'</span>
6719
                </li>
6720
            '.
6721
                $linkNodes.
6722
            '';
6723
            //<div style="display:none" id="'.TOOL_LINK.'_'.$categoryId.'_content">'.
6724
        }
6725
        $linksHtmlCode .= '</ul>';
6726
6727
        return $linksHtmlCode;
6728
    }
6729
6730
    /**
6731
     * Creates a list with all the student publications in it.
6732
     *
6733
     * @return string
6734
     */
6735
    public function get_student_publications()
6736
    {
6737
        $return = '<ul class="mt-2 bg-white list-group lp_resource">';
6738
        $return .= '<li class="list-group-item lp_resource_element">';
6739
        $works = getWorkListTeacher(0, 100, null, null, null);
6740
        if (!empty($works)) {
6741
            $icon = Display::getMdiIcon('inbox-full', 'ch-tool-icon',null, 16, get_lang('Student publication'));
6742
            foreach ($works as $work) {
6743
                $workId = $work['iid'];
6744
                $link = Display::url(
6745
                    Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6746
                    api_get_path(WEB_CODE_PATH).'work/work_list_all.php?'.api_get_cidreq().'&id='.$workId,
6747
                    ['target' => '_blank']
6748
                );
6749
6750
                $return .= '<li
6751
                    class="list-group-item lp_resource_element"
6752
                    id="'.$workId.'"
6753
                    data-id="'.$workId.'"
6754
                    >';
6755
                $return .= '<a class="moved" href="#">';
6756
                $return .= Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6757
                $return .= '</a> ';
6758
6759
                $return .= $icon;
6760
                $return .= Display::url(
6761
                    Security::remove_XSS(cut(strip_tags($work['title']), 80)).' '.$link,
6762
                    api_get_self().'?'.
6763
                    api_get_cidreq().'&action=add_item&type='.TOOL_STUDENTPUBLICATION.'&file='.$work['iid'].'&lp_id='.$this->lp_id,
6764
                    [
6765
                        'class' => 'moved link_with_id',
6766
                        'data-id' => $work['iid'],
6767
                        'data_type' => TOOL_STUDENTPUBLICATION,
6768
                        'title' => Security::remove_XSS(cut(strip_tags($work['title']), 80)),
6769
                    ]
6770
                );
6771
                $return .= '</li>';
6772
            }
6773
        }
6774
6775
        $return .= '</ul>';
6776
6777
        return $return;
6778
    }
6779
6780
    /**
6781
     * Creates a list with all the forums in it.
6782
     *
6783
     * @return string
6784
     */
6785
    public function get_forums()
6786
    {
6787
        $forumCategories = get_forum_categories();
6788
        $forumsInNoCategory = get_forums_in_category(0);
6789
        if (!empty($forumsInNoCategory)) {
6790
            $forumCategories = array_merge(
6791
                $forumCategories,
6792
                [
6793
                    [
6794
                        'cat_id' => 0,
6795
                        'session_id' => 0,
6796
                        'visibility' => 1,
6797
                        'cat_comment' => null,
6798
                    ],
6799
                ]
6800
            );
6801
        }
6802
6803
        $a_forums = [];
6804
        $courseEntity = api_get_course_entity(api_get_course_int_id());
6805
        $sessionEntity = api_get_session_entity(api_get_session_id());
6806
6807
        foreach ($forumCategories as $forumCategory) {
6808
            // The forums in this category.
6809
            $forumsInCategory = get_forums_in_category($forumCategory->getIid());
6810
            if (!empty($forumsInCategory)) {
6811
                foreach ($forumsInCategory as $forum) {
6812
                    if ($forum->isVisible($courseEntity, $sessionEntity)) {
6813
                        $a_forums[] = $forum;
6814
                    }
6815
                }
6816
            }
6817
        }
6818
6819
        $return = '<ul class="mt-2 bg-white list-group lp_resource">';
6820
6821
        // First add link
6822
        $return .= '<li class="list-group-item lp_resource_element disable_drag">';
6823
        $return .= Display::getMdiIcon('comment-quote	', 'ch-tool-icon', null, 32, get_lang('Create a new forum'));
6824
        $return .= Display::url(
6825
            get_lang('Create a new forum'),
6826
            api_get_path(WEB_CODE_PATH).'forum/index.php?'.api_get_cidreq().'&'.http_build_query([
6827
                'action' => 'add',
6828
                'content' => 'forum',
6829
                'lp_id' => $this->lp_id,
6830
            ]),
6831
            ['title' => get_lang('Create a new forum')]
6832
        );
6833
        $return .= '</li>';
6834
6835
        $return .= '<script>
6836
            function toggle_forum(forum_id) {
6837
                if (document.getElementById("forum_"+forum_id+"_content").style.display == "none") {
6838
                    document.getElementById("forum_"+forum_id+"_content").style.display = "block";
6839
                    document.getElementById("forum_"+forum_id+"_opener").src = "'.Display::returnIconPath('remove.gif').'";
6840
                } else {
6841
                    document.getElementById("forum_"+forum_id+"_content").style.display = "none";
6842
                    document.getElementById("forum_"+forum_id+"_opener").src = "'.Display::returnIconPath('add.png').'";
6843
                }
6844
            }
6845
        </script>';
6846
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6847
        $userRights = api_is_allowed_to_edit(false, true);
6848
        foreach ($a_forums as $forum) {
6849
            $forumSession = $forum->getFirstResourceLink()->getSession();
6850
            $isForumSession = (null !== $forumSession);
6851
            $forumId = $forum->getIid();
6852
            $title = Security::remove_XSS($forum->getTitle());
6853
            $link = Display::url(
6854
                Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6855
                api_get_path(WEB_CODE_PATH).'forum/viewforum.php?'.api_get_cidreq().'&forum='.$forumId,
6856
                ['target' => '_blank']
6857
            );
6858
6859
            $return .= '<li
6860
                    class="list-group-item lp_resource_element"
6861
                    id="'.$forumId.'"
6862
                    data-id="'.$forumId.'"
6863
                    >';
6864
            $return .= '<a class="moved" href="#">';
6865
            $return .= $moveIcon;
6866
            $return .= ' </a>';
6867
            $return .= Display::getMdiIcon('comment-quote', 'ch-tool-icon', null, 16, get_lang('Forum'));
6868
6869
            $moveLink = Display::url(
6870
                $title,
6871
                api_get_self().'?'.
6872
                api_get_cidreq().'&action=add_item&type='.TOOL_FORUM.'&forum_id='.$forumId.'&lp_id='.$this->lp_id,
6873
                [
6874
                    'class' => 'moved link_with_id',
6875
                    'data-id' => $forumId,
6876
                    'data_type' => TOOL_FORUM,
6877
                    'title' => $title,
6878
                    'style' => 'vertical-align:middle',
6879
                ]
6880
            );
6881
            $return .= '<a onclick="javascript:toggle_forum('.$forumId.');" style="cursor:hand; vertical-align:middle">
6882
                    <img
6883
                        src="'.Display::returnIconPath('add.png').'"
6884
                        id="forum_'.$forumId.'_opener" align="absbottom"
6885
                     />
6886
                </a>
6887
                '.$moveLink;
6888
            $return .= '</li>';
6889
6890
            $return .= '<div style="display:none" id="forum_'.$forumId.'_content">';
6891
            $threads = get_threads($forumId);
6892
            if (is_array($threads)) {
6893
                foreach ($threads as $thread) {
6894
                    $threadId = $thread->getIid();
6895
                    $link = Display::url(
6896
                        Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6897
                        api_get_path(WEB_CODE_PATH).
6898
                        'forum/viewthread.php?'.api_get_cidreq().'&forum='.$forumId.'&thread='.$threadId,
6899
                        ['target' => '_blank']
6900
                    );
6901
6902
                    $return .= '<li
6903
                        class="list-group-item lp_resource_element"
6904
                      id="'.$threadId.'"
6905
                        data-id="'.$threadId.'"
6906
                    >';
6907
                    $return .= '&nbsp;<a class="moved" href="#">';
6908
                    $return .= $moveIcon;
6909
                    $return .= ' </a>';
6910
                    $return .= Display::getMdiIcon('format-quote-open', 'ch-tool-icon', null, 16, get_lang('Thread'));
6911
                    $return .= '<a
6912
                        class="moved link_with_id"
6913
                        data-id="'.$threadId.'"
6914
                        data_type="'.TOOL_THREAD.'"
6915
                        title="'.$thread->getTitle().'"
6916
                        href="'.api_get_self().'?'.api_get_cidreq().'&action=add_item&type='.TOOL_THREAD.'&thread_id='.$threadId.'&lp_id='.$this->lp_id.'"
6917
                        >'.
6918
                        Security::remove_XSS($thread->getTitle()).' '.$link.'</a>';
6919
                    $return .= '</li>';
6920
                }
6921
            }
6922
            $return .= '</div>';
6923
        }
6924
        $return .= '</ul>';
6925
6926
        return $return;
6927
    }
6928
6929
    /**
6930
     * Creates a list with all the surveys in it.
6931
     *
6932
     * @return string
6933
     */
6934
    public function getSurveys()
6935
    {
6936
        $return = '<ul class="mt-2 bg-white list-group lp_resource">';
6937
6938
        // First add link
6939
        $return .= '<li class="list-group-item lp_resource_element disable_drag">';
6940
        $return .= Display::getMdiIcon('clipboard-question-outline', 'ch-tool-icon', null, 32, get_lang('CreateNewSurvey'));
6941
        $return .= Display::url(
6942
            get_lang('Create a new survey'),
6943
            api_get_path(WEB_CODE_PATH).'survey/create_new_survey.php?'.api_get_cidreq().'&'.http_build_query([
6944
                'action' => 'add',
6945
                'lp_id' => $this->lp_id,
6946
            ]),
6947
            ['title' => get_lang('Create a new survey')]
6948
        );
6949
        $return .= '</li>';
6950
6951
        $surveys = SurveyManager::get_surveys(api_get_course_id(), api_get_session_id());
6952
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6953
6954
        foreach ($surveys as $survey) {
6955
            if (!empty($survey['iid'])) {
6956
                $surveyTitle = strip_tags($survey['title']);
6957
                $return .= '<li class="list-group-item lp_resource_element" id="'.$survey['iid'].'" data-id="'.$survey['iid'].'">';
6958
                $return .= '<a class="moved" href="#">';
6959
                $return .= $moveIcon;
6960
                $return .= ' </a>';
6961
                $return .= Display::getMdiIcon('poll', 'ch-tool-icon', null, 16, get_lang('Survey'));
6962
                $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>';
6963
                $return .= '</li>';
6964
            }
6965
        }
6966
6967
        $return .= '</ul>';
6968
6969
        return $return;
6970
    }
6971
6972
    /**
6973
     * Temp function to be moved in main_api or the best place around for this.
6974
     * Creates a file path if it doesn't exist.
6975
     *
6976
     * @param string $path
6977
     */
6978
    public function create_path($path)
6979
    {
6980
        $path_bits = explode('/', dirname($path));
6981
6982
        // IS_WINDOWS_OS has been defined in main_api.lib.php
6983
        $path_built = IS_WINDOWS_OS ? '' : '/';
6984
        foreach ($path_bits as $bit) {
6985
            if (!empty($bit)) {
6986
                $new_path = $path_built.$bit;
6987
                if (is_dir($new_path)) {
6988
                    $path_built = $new_path.'/';
6989
                } else {
6990
                    mkdir($new_path, api_get_permissions_for_new_directories());
6991
                    $path_built = $new_path.'/';
6992
                }
6993
            }
6994
        }
6995
    }
6996
6997
    /**
6998
     * @param int    $lp_id
6999
     * @param string $status
7000
     */
7001
    public function set_autolaunch($lp_id, $status)
7002
    {
7003
        $status = (int) $status;
7004
        $em = Database::getManager();
7005
        $repo = Container::getLpRepository();
7006
7007
        $session = api_get_session_entity();
7008
        $course = api_get_course_entity();
7009
7010
        $qb = $repo->getResourcesByCourse($course, $session);
7011
        $lps = $qb->getQuery()->getResult();
7012
7013
        foreach ($lps as $lp) {
7014
            $lp->setAutoLaunch(0);
7015
            $em->persist($lp);
7016
        }
7017
7018
        $em->flush();
7019
7020
        if ($status === 1) {
7021
            $lp = $repo->find($lp_id);
7022
            if ($lp) {
7023
                $lp->setAutolaunch(1);
7024
                $em->persist($lp);
7025
            }
7026
            $em->flush();
7027
        }
7028
    }
7029
7030
    /**
7031
     * Gets previous_item_id for the next element of the lp_item table.
7032
     *
7033
     * @author Isaac flores paz
7034
     *
7035
     * @return int Previous item ID
7036
     */
7037
    public function select_previous_item_id()
7038
    {
7039
        $course_id = api_get_course_int_id();
7040
        $table_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7041
7042
        // Get the max order of the items
7043
        $sql = "SELECT max(display_order) AS display_order FROM $table_lp_item
7044
                WHERE c_id = $course_id AND lp_id = ".$this->lp_id;
7045
        $rs_max_order = Database::query($sql);
7046
        $row_max_order = Database::fetch_object($rs_max_order);
7047
        $max_order = $row_max_order->display_order;
7048
        // Get the previous item ID
7049
        $sql = "SELECT iid as previous FROM $table_lp_item
7050
                WHERE
7051
                    c_id = $course_id AND
7052
                    lp_id = ".$this->lp_id." AND
7053
                    display_order = '$max_order' ";
7054
        $rs_max = Database::query($sql);
7055
        $row_max = Database::fetch_object($rs_max);
7056
7057
        // Return the previous item ID
7058
        return $row_max->previous;
7059
    }
7060
7061
    /**
7062
     * Copies an LP.
7063
     */
7064
    public function copy()
7065
    {
7066
        // Course builder
7067
        $cb = new CourseBuilder();
7068
7069
        //Setting tools that will be copied
7070
        $cb->set_tools_to_build(['learnpaths']);
7071
7072
        //Setting elements that will be copied
7073
        $cb->set_tools_specific_id_list(
7074
            ['learnpaths' => [$this->lp_id]]
7075
        );
7076
7077
        $course = $cb->build();
7078
7079
        //Course restorer
7080
        $course_restorer = new CourseRestorer($course);
7081
        $course_restorer->set_add_text_in_items(true);
7082
        $course_restorer->set_tool_copy_settings(
7083
            ['learnpaths' => ['reset_dates' => true]]
7084
        );
7085
        $course_restorer->restore(
7086
            api_get_course_id(),
7087
            api_get_session_id(),
7088
            false,
7089
            false
7090
        );
7091
    }
7092
7093
    public static function getQuotaInfo(string $localFilePath): array
7094
    {
7095
        $post_max_raw   = ini_get('post_max_size');
7096
        $post_max_bytes = (int) rtrim($post_max_raw, 'MG') * (str_ends_with($post_max_raw,'G') ? 1024**3 : 1024**2);
7097
        $upload_max_raw = ini_get('upload_max_filesize');
7098
        $upload_max_bytes = (int) rtrim($upload_max_raw, 'MG') * (str_ends_with($upload_max_raw,'G') ? 1024**3 : 1024**2);
7099
7100
        $em     = Database::getManager();
7101
        $course = api_get_course_entity(api_get_course_int_id());
7102
7103
        $nodes  = Container::getResourceNodeRepository()->findByResourceTypeAndCourse('file', $course);
7104
        $root   = null;
7105
        foreach ($nodes as $n) {
7106
            if ($n->getParent() === null) {
7107
                $root = $n; break;
7108
            }
7109
        }
7110
        $docsSize = $root
7111
            ? Container::getDocumentRepository()->getFolderSize($root, $course)
7112
            : 0;
7113
7114
        $assetRepo = Container::getAssetRepository();
7115
        $fs        = $assetRepo->getFileSystem();
7116
        $scormSize = 0;
7117
        foreach (Container::getLpRepository()->findScormByCourse($course) as $lp) {
7118
            if ($asset = $lp->getAsset()) {
7119
                $folder = $assetRepo->getFolder($asset);
7120
                if ($folder && $fs->directoryExists($folder)) {
7121
                    $scormSize += self::getFolderSize($folder);
7122
                }
7123
            }
7124
        }
7125
7126
        $uploadedSize = filesize($localFilePath);
7127
        $existingTotal = $docsSize + $scormSize;
7128
        $combined = $existingTotal + $uploadedSize;
7129
7130
        $quotaMb = DocumentManager::get_course_quota();
7131
        $quotaBytes = $quotaMb * 1024 * 1024;
7132
7133
        return [
7134
            'post_max'      => $post_max_bytes,
7135
            'upload_max'    => $upload_max_bytes,
7136
            'docs_size'     => $docsSize,
7137
            'scorm_size'    => $scormSize,
7138
            'existing_total'=> $existingTotal,
7139
            'uploaded_size' => $uploadedSize,
7140
            'combined'      => $combined,
7141
            'quota_bytes'   => $quotaBytes,
7142
        ];
7143
    }
7144
7145
    /**
7146
     * Verify document size.
7147
     */
7148
    public static function verify_document_size(string $localFilePath): bool
7149
    {
7150
        $info = self::getQuotaInfo($localFilePath);
7151
        if ($info['uploaded_size'] > $info['post_max']
7152
            || $info['uploaded_size'] > $info['upload_max']
7153
            || $info['combined']    > $info['quota_bytes']
7154
        ) {
7155
            Container::getSession()->set('quota_info', $info);
7156
            return true;
7157
        }
7158
7159
        return false;
7160
    }
7161
7162
    private static function getFolderSize(string $path): int
7163
    {
7164
        $size     = 0;
7165
        $iterator = new \RecursiveIteratorIterator(
7166
            new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
7167
        );
7168
        foreach ($iterator as $file) {
7169
            if ($file->isFile()) {
7170
                $size += $file->getSize();
7171
            }
7172
        }
7173
        return $size;
7174
    }
7175
7176
    /**
7177
     * Clear LP prerequisites.
7178
     */
7179
    public function clearPrerequisites()
7180
    {
7181
        $course_id = $this->get_course_int_id();
7182
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7183
        $lp_id = $this->get_id();
7184
        // Cleaning prerequisites
7185
        $sql = "UPDATE $tbl_lp_item SET prerequisite = ''
7186
                WHERE lp_id = $lp_id";
7187
        Database::query($sql);
7188
7189
        // Cleaning mastery score for exercises
7190
        $sql = "UPDATE $tbl_lp_item SET mastery_score = ''
7191
                WHERE lp_id = $lp_id AND item_type = 'quiz'";
7192
        Database::query($sql);
7193
    }
7194
7195
    public function set_previous_step_as_prerequisite_for_all_items()
7196
    {
7197
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7198
        $course_id = $this->get_course_int_id();
7199
        $lp_id = $this->get_id();
7200
7201
        if (!empty($this->items)) {
7202
            $previous_item_id = null;
7203
            $previous_item_max = 0;
7204
            $previous_item_type = null;
7205
            $last_item_not_dir = null;
7206
            $last_item_not_dir_type = null;
7207
            $last_item_not_dir_max = null;
7208
7209
            foreach ($this->ordered_items as $itemId) {
7210
                $item = $this->getItem($itemId);
7211
                // if there was a previous item... (otherwise jump to set it)
7212
                if (!empty($previous_item_id)) {
7213
                    $current_item_id = $item->get_id(); //save current id
7214
                    if ('dir' != $item->get_type()) {
7215
                        // Current item is not a folder, so it qualifies to get a prerequisites
7216
                        if ('quiz' == $last_item_not_dir_type) {
7217
                            // if previous is quiz, mark its max score as default score to be achieved
7218
                            $sql = "UPDATE $tbl_lp_item SET mastery_score = '$last_item_not_dir_max'
7219
                                    WHERE c_id = $course_id AND lp_id = $lp_id AND iid = $last_item_not_dir";
7220
                            Database::query($sql);
7221
                        }
7222
                        // now simply update the prerequisite to set it to the last non-chapter item
7223
                        $sql = "UPDATE $tbl_lp_item SET prerequisite = '$last_item_not_dir'
7224
                                WHERE lp_id = $lp_id AND iid = $current_item_id";
7225
                        Database::query($sql);
7226
                        // record item as 'non-chapter' reference
7227
                        $last_item_not_dir = $item->get_id();
7228
                        $last_item_not_dir_type = $item->get_type();
7229
                        $last_item_not_dir_max = $item->get_max();
7230
                    }
7231
                } else {
7232
                    if ('dir' != $item->get_type()) {
7233
                        // Current item is not a folder (but it is the first item) so record as last "non-chapter" item
7234
                        $last_item_not_dir = $item->get_id();
7235
                        $last_item_not_dir_type = $item->get_type();
7236
                        $last_item_not_dir_max = $item->get_max();
7237
                    }
7238
                }
7239
                // Saving the item as "previous item" for the next loop
7240
                $previous_item_id = $item->get_id();
7241
                $previous_item_max = $item->get_max();
7242
                $previous_item_type = $item->get_type();
7243
            }
7244
        }
7245
    }
7246
7247
    /**
7248
     * @param array $params
7249
     *
7250
     * @return int
7251
     */
7252
    public static function createCategory($params)
7253
    {
7254
        $courseEntity = api_get_course_entity(api_get_course_int_id());
7255
7256
        $item = new CLpCategory();
7257
        $item
7258
            ->setTitle($params['name'])
7259
            ->setParent($courseEntity)
7260
            ->addCourseLink($courseEntity, api_get_session_entity())
7261
        ;
7262
7263
        $repo = Container::getLpCategoryRepository();
7264
        $repo->create($item);
7265
7266
        return $item->getIid();
7267
    }
7268
7269
    /**
7270
     * @param array $params
7271
     */
7272
    public static function updateCategory($params)
7273
    {
7274
        $em = Database::getManager();
7275
        /** @var CLpCategory $item */
7276
        $item = $em->find(CLpCategory::class, $params['id']);
7277
        if ($item) {
7278
            $item->setTitle($params['name']);
7279
            $em->persist($item);
7280
            $em->flush();
7281
        }
7282
    }
7283
7284
    public static function moveUpCategory(int $id): void
7285
    {
7286
        $em = Database::getManager();
7287
        /** @var CLpCategory $item */
7288
        $item = $em->find(CLpCategory::class, $id);
7289
        if ($item) {
7290
            $course = api_get_course_entity();
7291
            $session = api_get_session_entity();
7292
7293
            $link = $item->resourceNode->getResourceLinkByContext($course, $session);
7294
7295
            if ($link) {
7296
                $link->moveUpPosition();
7297
7298
                $em->flush();
7299
            }
7300
        }
7301
    }
7302
7303
    public static function moveDownCategory(int $id): void
7304
    {
7305
        $em = Database::getManager();
7306
        /** @var CLpCategory $item */
7307
        $item = $em->find(CLpCategory::class, $id);
7308
        if ($item) {
7309
            $course = api_get_course_entity();
7310
            $session = api_get_session_entity();
7311
7312
            $link = $item->resourceNode->getResourceLinkByContext($course, $session);
7313
7314
            if ($link) {
7315
                $link->moveDownPosition();
7316
7317
                $em->flush();
7318
            }
7319
        }
7320
    }
7321
7322
    /**
7323
     * @param int $courseId
7324
     *
7325
     * @return int
7326
     */
7327
    public static function getCountCategories($courseId)
7328
    {
7329
        if (empty($courseId)) {
7330
            return 0;
7331
        }
7332
        $repo = Container::getLpCategoryRepository();
7333
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId));
7334
        $qb->addSelect('count(resource)');
7335
7336
        return (int) $qb->getQuery()->getSingleScalarResult();
7337
    }
7338
7339
    /**
7340
     * @param int $courseId
7341
     *
7342
     * @return CLpCategory[]
7343
     */
7344
    public static function getCategories($courseId)
7345
    {
7346
        // Using doctrine extensions
7347
        $repo = Container::getLpCategoryRepository();
7348
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId), api_get_session_entity(), null, null, true, true);
7349
7350
        return $qb->getQuery()->getResult();
7351
    }
7352
7353
    public static function getCategorySessionId($id)
7354
    {
7355
        if ('true' !== api_get_setting('lp.allow_session_lp_category')) {
7356
            return 0;
7357
        }
7358
7359
        $repo = Container::getLpCategoryRepository();
7360
        /** @var CLpCategory $category */
7361
        $category = $repo->find($id);
7362
7363
        $sessionId = 0;
7364
        $link = $category->getFirstResourceLink();
7365
        if ($link && $link->getSession()) {
7366
            $sessionId = (int) $link->getSession()->getId();
7367
        }
7368
7369
        return $sessionId;
7370
    }
7371
7372
    public static function deleteCategory(int $id): bool
7373
    {
7374
        $repo = Container::getLpCategoryRepository();
7375
        /** @var CLpCategory $category */
7376
        $category = $repo->find($id);
7377
        if ($category) {
7378
            $em = Database::getManager();
7379
            $lps = $category->getLps();
7380
7381
            foreach ($lps as $lp) {
7382
                $lp->setCategory(null);
7383
                $em->persist($lp);
7384
            }
7385
7386
            $course = api_get_course_entity();
7387
            $session = api_get_session_entity();
7388
7389
            $em->getRepository(ResourceLink::class)->removeByResourceInContext($category, $course, $session);
7390
7391
            return true;
7392
        }
7393
7394
        return false;
7395
    }
7396
7397
    /**
7398
     * @param int  $courseId
7399
     * @param bool $addSelectOption
7400
     *
7401
     * @return array
7402
     */
7403
    public static function getCategoryFromCourseIntoSelect($courseId, $addSelectOption = false)
7404
    {
7405
        $repo = Container::getLpCategoryRepository();
7406
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId), api_get_session_entity());
7407
        $items = $qb->getQuery()->getResult();
7408
7409
        $cats = [];
7410
        if ($addSelectOption) {
7411
            $cats = [get_lang('Select a category')];
7412
        }
7413
7414
        if (!empty($items)) {
7415
            foreach ($items as $cat) {
7416
                $cats[$cat->getIid()] = $cat->getTitle();
7417
            }
7418
        }
7419
7420
        return $cats;
7421
    }
7422
7423
    /**
7424
     * @param int   $courseId
7425
     * @param int   $lpId
7426
     * @param int   $user_id
7427
     *
7428
     * @return learnpath
7429
     */
7430
    public static function getLpFromSession(int $courseId, int $lpId, int $user_id)
7431
    {
7432
        $debug = 0;
7433
        $learnPath = null;
7434
        $lpObject = Session::read('lpobject');
7435
7436
        $repo = Container::getLpRepository();
7437
        $lp = $repo->find($lpId);
7438
        if (null !== $lpObject) {
7439
            /** @var learnpath $learnPath */
7440
            $learnPath = UnserializeApi::unserialize('lp', $lpObject);
7441
            $learnPath->entity = $lp;
7442
            if ($debug) {
7443
                error_log('getLpFromSession: unserialize');
7444
                error_log('------getLpFromSession------');
7445
                error_log('------unserialize------');
7446
                error_log("lp_view_session_id: ".$learnPath->lp_view_session_id);
7447
                error_log("api_get_sessionid: ".api_get_session_id());
7448
            }
7449
        }
7450
7451
        if (!is_object($learnPath)) {
7452
            $learnPath = new learnpath($lp, api_get_course_info_by_id($courseId), $user_id);
7453
            if ($debug) {
7454
                error_log('------getLpFromSession------');
7455
                error_log('getLpFromSession: create new learnpath');
7456
                error_log("create new LP with $courseId - $lpId - $user_id");
7457
                error_log("lp_view_session_id: ".$learnPath->lp_view_session_id);
7458
                error_log("api_get_sessionid: ".api_get_session_id());
7459
            }
7460
        }
7461
7462
        return $learnPath;
7463
    }
7464
7465
    /**
7466
     * @param int $itemId
7467
     *
7468
     * @return learnpathItem|false
7469
     */
7470
    public function getItem($itemId)
7471
    {
7472
        if (isset($this->items[$itemId]) && is_object($this->items[$itemId])) {
7473
            return $this->items[$itemId];
7474
        }
7475
7476
        return false;
7477
    }
7478
7479
    /**
7480
     * @return int
7481
     */
7482
    public function getCurrentAttempt()
7483
    {
7484
        $attempt = $this->getItem($this->get_current_item_id());
7485
        if ($attempt) {
7486
            return $attempt->get_attempt_id();
7487
        }
7488
7489
        return 0;
7490
    }
7491
7492
    /**
7493
     * @return int
7494
     */
7495
    public function getCategoryId()
7496
    {
7497
        return (int) $this->categoryId;
7498
    }
7499
7500
    /**
7501
     * Get whether this is a learning path with the possibility to subscribe
7502
     * users or not.
7503
     *
7504
     * @return int
7505
     */
7506
    public function getSubscribeUsers()
7507
    {
7508
        return $this->subscribeUsers;
7509
    }
7510
7511
    /**
7512
     * Calculate the count of stars for a user in this LP
7513
     * This calculation is based on the following rules:
7514
     * - the student gets one star when he gets to 50% of the learning path
7515
     * - the student gets a second star when the average score of all tests inside the learning path >= 50%
7516
     * - the student gets a third star when the average score of all tests inside the learning path >= 80%
7517
     * - the student gets the final star when the score for the *last* test is >= 80%.
7518
     *
7519
     * @param int $sessionId Optional. The session ID
7520
     *
7521
     * @return int The count of stars
7522
     */
7523
    public function getCalculateStars($sessionId = 0)
7524
    {
7525
        $stars = 0;
7526
        $progress = self::getProgress(
7527
            $this->lp_id,
7528
            $this->user_id,
7529
            $this->course_int_id,
7530
            $sessionId
7531
        );
7532
7533
        if ($progress >= 50) {
7534
            $stars++;
7535
        }
7536
7537
        // Calculate stars chapters evaluation
7538
        $exercisesItems = $this->getExercisesItems();
7539
7540
        if (!empty($exercisesItems)) {
7541
            $totalResult = 0;
7542
7543
            foreach ($exercisesItems as $exerciseItem) {
7544
                $exerciseResultInfo = Event::getExerciseResultsByUser(
7545
                    $this->user_id,
7546
                    $exerciseItem->path,
7547
                    $this->course_int_id,
7548
                    $sessionId,
7549
                    $this->lp_id,
7550
                    $exerciseItem->db_id
7551
                );
7552
7553
                $exerciseResultInfo = end($exerciseResultInfo);
7554
7555
                if (!$exerciseResultInfo) {
7556
                    continue;
7557
                }
7558
7559
                if (!empty($exerciseResultInfo['max_score'])) {
7560
                    $exerciseResult = $exerciseResultInfo['score'] * 100 / $exerciseResultInfo['max_score'];
7561
                } else {
7562
                    $exerciseResult = 0;
7563
                }
7564
                $totalResult += $exerciseResult;
7565
            }
7566
7567
            $totalExerciseAverage = $totalResult / (count($exercisesItems) > 0 ? count($exercisesItems) : 1);
7568
7569
            if ($totalExerciseAverage >= 50) {
7570
                $stars++;
7571
            }
7572
7573
            if ($totalExerciseAverage >= 80) {
7574
                $stars++;
7575
            }
7576
        }
7577
7578
        // Calculate star for final evaluation
7579
        $finalEvaluationItem = $this->getFinalEvaluationItem();
7580
7581
        if (!empty($finalEvaluationItem)) {
7582
            $evaluationResultInfo = Event::getExerciseResultsByUser(
7583
                $this->user_id,
7584
                $finalEvaluationItem->path,
7585
                $this->course_int_id,
7586
                $sessionId,
7587
                $this->lp_id,
7588
                $finalEvaluationItem->db_id
7589
            );
7590
7591
            $evaluationResultInfo = end($evaluationResultInfo);
7592
7593
            if ($evaluationResultInfo) {
7594
                $evaluationResult = $evaluationResultInfo['score'] * 100 / $evaluationResultInfo['max_score'];
7595
                if ($evaluationResult >= 80) {
7596
                    $stars++;
7597
                }
7598
            }
7599
        }
7600
7601
        return $stars;
7602
    }
7603
7604
    /**
7605
     * Get the items of exercise type.
7606
     *
7607
     * @return array The items. Otherwise return false
7608
     */
7609
    public function getExercisesItems()
7610
    {
7611
        $exercises = [];
7612
        foreach ($this->items as $item) {
7613
            if ('quiz' !== $item->type) {
7614
                continue;
7615
            }
7616
            $exercises[] = $item;
7617
        }
7618
7619
        array_pop($exercises);
7620
7621
        return $exercises;
7622
    }
7623
7624
    /**
7625
     * Get the item of exercise type (evaluation type).
7626
     *
7627
     * @return array The final evaluation. Otherwise return false
7628
     */
7629
    public function getFinalEvaluationItem()
7630
    {
7631
        $exercises = [];
7632
        foreach ($this->items as $item) {
7633
            if (TOOL_QUIZ !== $item->type) {
7634
                continue;
7635
            }
7636
7637
            $exercises[] = $item;
7638
        }
7639
7640
        return array_pop($exercises);
7641
    }
7642
7643
    /**
7644
     * Calculate the total points achieved for the current user in this learning path.
7645
     *
7646
     * @param int $sessionId Optional. The session Id
7647
     *
7648
     * @return int
7649
     */
7650
    public function getCalculateScore($sessionId = 0)
7651
    {
7652
        // Calculate stars chapters evaluation
7653
        $exercisesItems = $this->getExercisesItems();
7654
        $finalEvaluationItem = $this->getFinalEvaluationItem();
7655
        $totalExercisesResult = 0;
7656
        $totalEvaluationResult = 0;
7657
7658
        if (false !== $exercisesItems) {
7659
            foreach ($exercisesItems as $exerciseItem) {
7660
                $exerciseResultInfo = Event::getExerciseResultsByUser(
7661
                    $this->user_id,
7662
                    $exerciseItem->path,
7663
                    $this->course_int_id,
7664
                    $sessionId,
7665
                    $this->lp_id,
7666
                    $exerciseItem->db_id
7667
                );
7668
7669
                $exerciseResultInfo = end($exerciseResultInfo);
7670
7671
                if (!$exerciseResultInfo) {
7672
                    continue;
7673
                }
7674
7675
                $totalExercisesResult += $exerciseResultInfo['score'];
7676
            }
7677
        }
7678
7679
        if (!empty($finalEvaluationItem)) {
7680
            $evaluationResultInfo = Event::getExerciseResultsByUser(
7681
                $this->user_id,
7682
                $finalEvaluationItem->path,
7683
                $this->course_int_id,
7684
                $sessionId,
7685
                $this->lp_id,
7686
                $finalEvaluationItem->db_id
7687
            );
7688
7689
            $evaluationResultInfo = end($evaluationResultInfo);
7690
7691
            if ($evaluationResultInfo) {
7692
                $totalEvaluationResult += $evaluationResultInfo['score'];
7693
            }
7694
        }
7695
7696
        return $totalExercisesResult + $totalEvaluationResult;
7697
    }
7698
7699
    /**
7700
     * Check if URL is not allowed to be show in a iframe.
7701
     *
7702
     * @param string $src
7703
     *
7704
     * @return string
7705
     */
7706
    public function fixBlockedLinks($src)
7707
    {
7708
        $urlInfo = parse_url($src);
7709
7710
        $platformProtocol = 'https';
7711
        if (false === strpos(api_get_path(WEB_CODE_PATH), 'https')) {
7712
            $platformProtocol = 'http';
7713
        }
7714
7715
        $protocolFixApplied = false;
7716
        //Scheme validation to avoid "Notices" when the lesson doesn't contain a valid scheme
7717
        $scheme = isset($urlInfo['scheme']) ? $urlInfo['scheme'] : null;
7718
        $host = isset($urlInfo['host']) ? $urlInfo['host'] : null;
7719
7720
        if ($platformProtocol != $scheme) {
7721
            Session::write('x_frame_source', $src);
7722
            $src = 'blank.php?error=x_frames_options';
7723
            $protocolFixApplied = true;
7724
        }
7725
7726
        if (false == $protocolFixApplied) {
7727
            if (false === strpos(api_get_path(WEB_PATH), $host)) {
7728
                // Check X-Frame-Options
7729
                $ch = curl_init();
7730
                $options = [
7731
                    CURLOPT_URL => $src,
7732
                    CURLOPT_RETURNTRANSFER => true,
7733
                    CURLOPT_HEADER => true,
7734
                    CURLOPT_FOLLOWLOCATION => true,
7735
                    CURLOPT_ENCODING => "",
7736
                    CURLOPT_AUTOREFERER => true,
7737
                    CURLOPT_CONNECTTIMEOUT => 120,
7738
                    CURLOPT_TIMEOUT => 120,
7739
                    CURLOPT_MAXREDIRS => 10,
7740
                ];
7741
7742
                $proxySettings = api_get_setting('platform.proxy_settings', true);
7743
                if (!empty($proxySettings) &&
7744
                    isset($proxySettings['curl_setopt_array'])
7745
                ) {
7746
                    $options[CURLOPT_PROXY] = $proxySettings['curl_setopt_array']['CURLOPT_PROXY'];
7747
                    $options[CURLOPT_PROXYPORT] = $proxySettings['curl_setopt_array']['CURLOPT_PROXYPORT'];
7748
                }
7749
7750
                curl_setopt_array($ch, $options);
7751
                $response = curl_exec($ch);
7752
                $httpCode = curl_getinfo($ch);
7753
                $headers = substr($response, 0, $httpCode['header_size']);
7754
7755
                $error = false;
7756
                if (stripos($headers, 'X-Frame-Options: DENY') > -1
7757
                    //|| stripos($headers, 'X-Frame-Options: SAMEORIGIN') > -1
7758
                ) {
7759
                    $error = true;
7760
                }
7761
7762
                if ($error) {
7763
                    Session::write('x_frame_source', $src);
7764
                    $src = 'blank.php?error=x_frames_options';
7765
                }
7766
            }
7767
        }
7768
7769
        return $src;
7770
    }
7771
7772
    /**
7773
     * Check if this LP has a created forum in the basis course.
7774
     *
7775
     * @deprecated
7776
     *
7777
     * @return bool
7778
     */
7779
    public function lpHasForum()
7780
    {
7781
        $forumTable = Database::get_course_table(TABLE_FORUM);
7782
        $itemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
7783
7784
        $fakeFrom = "
7785
            $forumTable f
7786
            INNER JOIN $itemProperty ip
7787
            ON (f.forum_id = ip.ref AND f.c_id = ip.c_id)
7788
        ";
7789
7790
        $resultData = Database::select(
7791
            'COUNT(f.iid) AS qty',
7792
            $fakeFrom,
7793
            [
7794
                'where' => [
7795
                    'ip.visibility != ? AND ' => 2,
7796
                    'ip.tool = ? AND ' => TOOL_FORUM,
7797
                    'f.c_id = ? AND ' => intval($this->course_int_id),
7798
                    'f.lp_id = ?' => intval($this->lp_id),
7799
                ],
7800
            ],
7801
            'first'
7802
        );
7803
7804
        return $resultData['qty'] > 0;
7805
    }
7806
7807
    /**
7808
     * Get the forum for this learning path.
7809
     *
7810
     * @param int $sessionId
7811
     *
7812
     * @return array
7813
     */
7814
    public function getForum($sessionId = 0)
7815
    {
7816
        $repo = Container::getForumRepository();
7817
7818
        $course = api_get_course_entity();
7819
        $session = api_get_session_entity($sessionId);
7820
        $qb = $repo->getResourcesByCourse($course, $session);
7821
7822
        return $qb->getQuery()->getResult();
7823
    }
7824
7825
    /**
7826
     * Get the LP Final Item form.
7827
     *
7828
     * @throws Exception
7829
     *
7830
     *
7831
     * @return string
7832
     */
7833
    public function getFinalItemForm()
7834
    {
7835
        $finalItem = $this->getFinalItem();
7836
        $title = '';
7837
7838
        if ($finalItem) {
7839
            $title = $finalItem->get_title();
7840
            $buttonText = get_lang('Save');
7841
            $content = $this->getSavedFinalItem();
7842
        } else {
7843
            $buttonText = get_lang('Add this document to the course');
7844
            $content = $this->getFinalItemTemplate();
7845
        }
7846
7847
        $editorConfig = [
7848
            'ToolbarSet' => 'LearningPathDocuments',
7849
            'Width' => '100%',
7850
            'Height' => '500',
7851
            'FullPage' => true,
7852
        ];
7853
7854
        $url = api_get_self().'?'.api_get_cidreq().'&'.http_build_query([
7855
            'type' => 'document',
7856
            'lp_id' => $this->lp_id,
7857
        ]);
7858
7859
        $form = new FormValidator('final_item', 'POST', $url);
7860
        $form->addText('title', get_lang('Title'));
7861
        $form->addButtonSave($buttonText);
7862
        $form->addHtml(
7863
            Display::return_message(
7864
                'Variables :</br></br> <b>((certificate))</b> </br> <b>((skill))</b>',
7865
                'normal',
7866
                false
7867
            )
7868
        );
7869
7870
        $renderer = $form->defaultRenderer();
7871
        $renderer->setElementTemplate('&nbsp;{label}{element}', 'content_lp_certificate');
7872
7873
        $form->addHtmlEditor(
7874
            'content_lp_certificate',
7875
            null,
7876
            true,
7877
            false,
7878
            $editorConfig
7879
        );
7880
        $form->addHidden('action', 'add_final_item');
7881
        $form->addHidden('path', Session::read('pathItem'));
7882
        $form->addHidden('previous', $this->get_last());
7883
        $form->setDefaults(
7884
            ['title' => $title, 'content_lp_certificate' => $content]
7885
        );
7886
7887
        if ($form->validate()) {
7888
            $values = $form->exportValues();
7889
            $lastItemId = $this->getLastInFirstLevel();
7890
7891
            if (!$finalItem) {
7892
                $documentId = $this->create_document(
7893
                    $this->course_info,
7894
                    $values['content_lp_certificate'],
7895
                    $values['title']
7896
                );
7897
                $this->add_item(
7898
                    null,
7899
                    $lastItemId,
7900
                    'final_item',
7901
                    $documentId,
7902
                    $values['title'],
7903
                );
7904
7905
                Display::addFlash(
7906
                    Display::return_message(get_lang('Added'))
7907
                );
7908
            } else {
7909
                $this->edit_document();
7910
            }
7911
        }
7912
7913
        return $form->returnForm();
7914
    }
7915
7916
    /**
7917
     * Check if the current lp item is first, both, last or none from lp list.
7918
     *
7919
     * @param int $currentItemId
7920
     *
7921
     * @return string
7922
     */
7923
    public function isFirstOrLastItem($currentItemId)
7924
    {
7925
        $lpItemId = [];
7926
        $typeListNotToVerify = self::getChapterTypes();
7927
7928
        // Using get_toc() function instead $this->items because returns the correct order of the items
7929
        foreach ($this->get_toc() as $item) {
7930
            if (!in_array($item['type'], $typeListNotToVerify)) {
7931
                $lpItemId[] = $item['id'];
7932
            }
7933
        }
7934
7935
        $lastLpItemIndex = count($lpItemId) - 1;
7936
        $position = array_search($currentItemId, $lpItemId);
7937
7938
        switch ($position) {
7939
            case 0:
7940
                if (!$lastLpItemIndex) {
7941
                    $answer = 'both';
7942
                    break;
7943
                }
7944
7945
                $answer = 'first';
7946
                break;
7947
            case $lastLpItemIndex:
7948
                $answer = 'last';
7949
                break;
7950
            default:
7951
                $answer = 'none';
7952
        }
7953
7954
        return $answer;
7955
    }
7956
7957
    /**
7958
     * Get whether this is a learning path with the accumulated SCORM time or not.
7959
     *
7960
     * @return int
7961
     */
7962
    public function getAccumulateScormTime()
7963
    {
7964
        return $this->accumulateScormTime;
7965
    }
7966
7967
    /**
7968
     * Returns an HTML-formatted link to a resource, to incorporate directly into
7969
     * the new learning path tool.
7970
     *
7971
     * The function is a big switch on tool type.
7972
     * In each case, we query the corresponding table for information and build the link
7973
     * with that information.
7974
     *
7975
     * @author Yannick Warnier <[email protected]> - rebranding based on
7976
     * previous work (display_addedresource_link_in_learnpath())
7977
     *
7978
     * @param int $course_id      Course code
7979
     * @param int $learningPathId The learning path ID (in lp table)
7980
     * @param int $id_in_path     the unique index in the items table
7981
     * @param int $lpViewId
7982
     *
7983
     * @return string
7984
     */
7985
    public static function rl_get_resource_link_for_learnpath(
7986
        $course_id,
7987
        $learningPathId,
7988
        $id_in_path,
7989
        $lpViewId
7990
    ) {
7991
        $session_id = api_get_session_id();
7992
7993
        $learningPathId = (int) $learningPathId;
7994
        $id_in_path = (int) $id_in_path;
7995
        $lpViewId = (int) $lpViewId;
7996
7997
        $em = Database::getManager();
7998
        $lpItemRepo = $em->getRepository(CLpItem::class);
7999
8000
        /** @var CLpItem $rowItem */
8001
        $rowItem = $lpItemRepo->findOneBy([
8002
            'lp' => $learningPathId,
8003
            'iid' => $id_in_path,
8004
        ]);
8005
        $type = $rowItem->getItemType();
8006
        $id = empty($rowItem->getPath()) ? '0' : $rowItem->getPath();
8007
        $main_dir_path = api_get_path(WEB_CODE_PATH);
8008
        $link = '';
8009
        $extraParams = api_get_cidreq(true, true, 'learnpath').'&sid='.$session_id;
8010
8011
        switch ($type) {
8012
            case 'dir':
8013
                return $main_dir_path.'lp/blank.php';
8014
            case TOOL_CALENDAR_EVENT:
8015
                return $main_dir_path.'calendar/agenda.php?agenda_id='.$id.'&'.$extraParams;
8016
            case TOOL_ANNOUNCEMENT:
8017
                return $main_dir_path.'announcements/announcements.php?ann_id='.$id.'&'.$extraParams;
8018
            case TOOL_LINK:
8019
                $linkInfo = Link::getLinkInfo($id);
8020
                if (isset($linkInfo['url'])) {
8021
                    return $linkInfo['url'];
8022
                }
8023
8024
                return '';
8025
            case TOOL_QUIZ:
8026
                if (empty($id)) {
8027
                    return '';
8028
                }
8029
8030
                // Get the lp_item_view with the highest view_count.
8031
                $learnpathItemViewResult = $em
8032
                    ->getRepository(CLpItemView::class)
8033
                    ->findBy(
8034
                        ['item' => $rowItem->getIid(), 'view' => $lpViewId],
8035
                        ['viewCount' => 'DESC'],
8036
                        1
8037
                    );
8038
                /** @var CLpItemView $learnpathItemViewData */
8039
                $learnpathItemViewData = current($learnpathItemViewResult);
8040
                $learnpathItemViewId = $learnpathItemViewData ? $learnpathItemViewData->getIid() : 0;
8041
8042
                return $main_dir_path.'exercise/overview.php?'.$extraParams.'&'
8043
                    .http_build_query([
8044
                        'lp_init' => 1,
8045
                        'learnpath_item_view_id' => $learnpathItemViewId,
8046
                        'learnpath_id' => $learningPathId,
8047
                        'learnpath_item_id' => $id_in_path,
8048
                        'exerciseId' => $id,
8049
                    ]);
8050
            case TOOL_HOTPOTATOES:
8051
                return '';
8052
            case TOOL_FORUM:
8053
                return $main_dir_path.'forum/viewforum.php?forum='.$id.'&lp=true&'.$extraParams;
8054
            case TOOL_THREAD:
8055
                // forum post
8056
                $tbl_topics = Database::get_course_table(TABLE_FORUM_THREAD);
8057
                if (empty($id)) {
8058
                    return '';
8059
                }
8060
                $sql = "SELECT * FROM $tbl_topics WHERE iid=$id";
8061
                $result = Database::query($sql);
8062
                $row = Database::fetch_array($result);
8063
8064
                return $main_dir_path.'forum/viewthread.php?thread='.$id.'&forum='.$row['forum_id'].'&lp=true&'
8065
                    .$extraParams;
8066
            case TOOL_POST:
8067
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8068
                $result = Database::query("SELECT * FROM $tbl_post WHERE post_id=$id");
8069
                $row = Database::fetch_array($result);
8070
8071
                return $main_dir_path.'forum/viewthread.php?post='.$id.'&thread='.$row['thread_id'].'&forum='
8072
                    .$row['forum_id'].'&lp=true&'.$extraParams;
8073
            case TOOL_READOUT_TEXT:
8074
                return api_get_path(WEB_CODE_PATH).
8075
                    'lp/readout_text.php?&id='.$id.'&lp_id='.$learningPathId.'&'.$extraParams;
8076
            case TOOL_DOCUMENT:
8077
            case 'video':
8078
                $repo = Container::getDocumentRepository();
8079
                $document = $repo->find($rowItem->getPath());
8080
                if ($document) {
8081
                    $params = [
8082
                        'cid' => $course_id,
8083
                        'sid' => $session_id,
8084
                    ];
8085
8086
                    return $repo->getResourceFileUrl($document, $params, UrlGeneratorInterface::ABSOLUTE_URL);
8087
                }
8088
8089
                return null;
8090
            case TOOL_LP_FINAL_ITEM:
8091
                return api_get_path(WEB_CODE_PATH).'lp/lp_final_item.php?&id='.$id.'&lp_id='.$learningPathId.'&'
8092
                    .$extraParams;
8093
            case 'assignments':
8094
                return $main_dir_path.'work/work.php?'.$extraParams;
8095
            case TOOL_DROPBOX:
8096
                return $main_dir_path.'dropbox/index.php?'.$extraParams;
8097
            case 'introduction_text': //DEPRECATED
8098
                return '';
8099
            case TOOL_COURSE_DESCRIPTION:
8100
                return $main_dir_path.'course_description?'.$extraParams;
8101
            case TOOL_GROUP:
8102
                return $main_dir_path.'group/group.php?'.$extraParams;
8103
            case TOOL_USER:
8104
                return $main_dir_path.'user/user.php?'.$extraParams;
8105
            case TOOL_STUDENTPUBLICATION:
8106
                $repo = Container::getStudentPublicationRepository();
8107
                $publication = $repo->find($rowItem->getPath());
8108
                if ($publication && $publication->hasResourceNode()) {
8109
                    $nodeId = $publication->getResourceNode()->getId();
8110
                    $assignmentId = $publication->getIid();
8111
8112
                    return api_get_path(WEB_PATH) .
8113
                        "resources/assignment/$nodeId/submission/$assignmentId?" .
8114
                        http_build_query([
8115
                            'cid' => $course_id,
8116
                            'sid' => $session_id,
8117
                            'gid' => 0,
8118
                            'origin' => 'learnpath',
8119
                            'isStudentView' => 'true',
8120
                        ]);
8121
                }
8122
                return '';
8123
            case TOOL_SURVEY:
8124
8125
                $surveyId = (int) $id;
8126
                $repo = Container::getSurveyRepository();
8127
                if (!empty($surveyId)) {
8128
                    /** @var CSurvey $survey */
8129
                    $survey = $repo->find($surveyId);
8130
                    $autoSurveyLink = SurveyUtil::generateFillSurveyLink(
8131
                        $survey,
8132
                        'auto',
8133
                        api_get_course_entity($course_id),
8134
                        $session_id
8135
                    );
8136
                    $lpParams = [
8137
                        'lp_id' => $learningPathId,
8138
                        'lp_item_id' => $id_in_path,
8139
                        'origin' => 'learnpath',
8140
                    ];
8141
8142
                    return $autoSurveyLink.'&'.http_build_query($lpParams).'&'.$extraParams;
8143
                }
8144
        }
8145
8146
        return $link;
8147
    }
8148
8149
    /**
8150
     * Checks if any forum items in a given learning path are from the base course.
8151
     */
8152
    public static function isForumFromBaseCourse(int $learningPathId): bool
8153
    {
8154
        $itemRepository = Container::getLpItemRepository();
8155
        $forumRepository = Container::getForumRepository();
8156
        $forums = $itemRepository->findItemsByLearningPathAndType($learningPathId, 'forum');
8157
8158
        /* @var CLpItem $forumItem */
8159
        foreach ($forums as $forumItem) {
8160
            $forumId = (int) $forumItem->getPath();
8161
            $forum = $forumRepository->find($forumId);
8162
8163
            if ($forum !== null) {
8164
                $forumSession = $forum->getFirstResourceLink()->getSession();
8165
                if ($forumSession === null) {
8166
                    return true;
8167
                }
8168
            }
8169
        }
8170
8171
        return false;
8172
    }
8173
8174
    /**
8175
     * Gets the name of a resource (generally used in learnpath when no name is provided).
8176
     *
8177
     * @author Yannick Warnier <[email protected]>
8178
     *
8179
     * @param string $course_code    Course code
8180
     * @param int    $learningPathId
8181
     * @param int    $id_in_path     The resource ID
8182
     *
8183
     * @return string
8184
     */
8185
    public static function rl_get_resource_name($course_code, $learningPathId, $id_in_path)
8186
    {
8187
        $_course = api_get_course_info($course_code);
8188
        if (empty($_course)) {
8189
            return '';
8190
        }
8191
        $course_id = $_course['real_id'];
8192
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
8193
        $learningPathId = (int) $learningPathId;
8194
        $id_in_path = (int) $id_in_path;
8195
8196
        $sql = "SELECT item_type, title, ref
8197
                FROM $tbl_lp_item
8198
                WHERE c_id = $course_id AND lp_id = $learningPathId AND iid = $id_in_path";
8199
        $res_item = Database::query($sql);
8200
8201
        if (Database::num_rows($res_item) < 1) {
8202
            return '';
8203
        }
8204
        $row_item = Database::fetch_array($res_item);
8205
        $type = strtolower($row_item['item_type']);
8206
        $id = $row_item['ref'];
8207
        $output = '';
8208
8209
        switch ($type) {
8210
            case TOOL_CALENDAR_EVENT:
8211
                $TABLEAGENDA = Database::get_course_table(TABLE_AGENDA);
8212
                $result = Database::query("SELECT * FROM $TABLEAGENDA WHERE c_id = $course_id AND id=$id");
8213
                $myrow = Database::fetch_array($result);
8214
                $output = $myrow['title'];
8215
                break;
8216
            case TOOL_ANNOUNCEMENT:
8217
                $tbl_announcement = Database::get_course_table(TABLE_ANNOUNCEMENT);
8218
                $result = Database::query("SELECT * FROM $tbl_announcement WHERE c_id = $course_id AND id=$id");
8219
                $myrow = Database::fetch_array($result);
8220
                $output = $myrow['title'];
8221
                break;
8222
            case TOOL_LINK:
8223
                // Doesn't take $target into account.
8224
                $TABLETOOLLINK = Database::get_course_table(TABLE_LINK);
8225
                $result = Database::query("SELECT * FROM $TABLETOOLLINK WHERE c_id = $course_id AND id=$id");
8226
                $myrow = Database::fetch_array($result);
8227
                $output = $myrow['title'];
8228
                break;
8229
            case TOOL_QUIZ:
8230
                $TBL_EXERCICES = Database::get_course_table(TABLE_QUIZ_TEST);
8231
                $result = Database::query("SELECT * FROM $TBL_EXERCICES WHERE c_id = $course_id AND id = $id");
8232
                $myrow = Database::fetch_array($result);
8233
                $output = $myrow['title'];
8234
                break;
8235
            case TOOL_FORUM:
8236
                $TBL_FORUMS = Database::get_course_table(TABLE_FORUM);
8237
                $result = Database::query("SELECT * FROM $TBL_FORUMS WHERE c_id = $course_id AND forum_id = $id");
8238
                $myrow = Database::fetch_array($result);
8239
                $output = $myrow['title'];
8240
                break;
8241
            case TOOL_THREAD:
8242
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8243
                // Grabbing the title of the post.
8244
                $sql_title = "SELECT * FROM $tbl_post WHERE c_id = $course_id AND post_id=".$id;
8245
                $result_title = Database::query($sql_title);
8246
                $myrow_title = Database::fetch_array($result_title);
8247
                $output = $myrow_title['title'];
8248
                break;
8249
            case TOOL_POST:
8250
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8251
                $sql = "SELECT * FROM $tbl_post p WHERE c_id = $course_id AND p.post_id = $id";
8252
                $result = Database::query($sql);
8253
                $post = Database::fetch_array($result);
8254
                $output = $post['title'];
8255
                break;
8256
            case 'dir':
8257
            case TOOL_DOCUMENT:
8258
            case 'video':
8259
                $title = $row_item['title'];
8260
                $output = '-';
8261
                if (!empty($title)) {
8262
                    $output = $title;
8263
                }
8264
                break;
8265
            case 'hotpotatoes':
8266
                $tbl_doc = Database::get_course_table(TABLE_DOCUMENT);
8267
                $result = Database::query("SELECT * FROM $tbl_doc WHERE c_id = $course_id AND iid = $id");
8268
                $myrow = Database::fetch_array($result);
8269
                $pathname = explode('/', $myrow['path']); // Making a correct name for the link.
8270
                $last = count($pathname) - 1; // Making a correct name for the link.
8271
                $filename = $pathname[$last]; // Making a correct name for the link.
8272
                $myrow['path'] = rawurlencode($myrow['path']);
8273
                $output = $filename;
8274
                break;
8275
        }
8276
8277
        return stripslashes($output);
8278
    }
8279
8280
    /**
8281
     * Get the parent names for the current item.
8282
     *
8283
     * @param int $newItemId Optional. The item ID
8284
     */
8285
    public function getCurrentItemParentNames($newItemId = 0): array
8286
    {
8287
        $newItemId = $newItemId ?: $this->get_current_item_id();
8288
        $return = [];
8289
        $item = $this->getItem($newItemId);
8290
8291
        $parent = null;
8292
        if ($item) {
8293
            $parent = $this->getItem($item->get_parent());
8294
        }
8295
8296
        while ($parent) {
8297
            $return[] = $parent->get_title();
8298
            $parent = $this->getItem($parent->get_parent());
8299
        }
8300
8301
        return array_reverse($return);
8302
    }
8303
8304
    /**
8305
     * Reads and process "lp_subscription_settings" setting.
8306
     *
8307
     * @return array
8308
     */
8309
    public static function getSubscriptionSettings()
8310
    {
8311
        $subscriptionSettings = api_get_setting('lp.lp_subscription_settings', true);
8312
        if (!is_array($subscriptionSettings)) {
8313
            // By default, allow both settings
8314
            $subscriptionSettings = [
8315
                'allow_add_users_to_lp' => true,
8316
                'allow_add_users_to_lp_category' => true,
8317
            ];
8318
        } else {
8319
            $subscriptionSettings = $subscriptionSettings['options'];
8320
        }
8321
8322
        return $subscriptionSettings;
8323
    }
8324
8325
    /**
8326
     * Exports a LP to a courseBuilder zip file. It adds the documents related to the LP.
8327
     */
8328
    public function exportToCourseBuildFormat()
8329
    {
8330
        if (!api_is_allowed_to_edit()) {
8331
            return false;
8332
        }
8333
8334
        $courseBuilder = new CourseBuilder();
8335
        $itemList = [];
8336
        /** @var learnpathItem $item */
8337
        foreach ($this->items as $item) {
8338
            $itemList[$item->get_type()][] = $item->get_path();
8339
        }
8340
8341
        if (empty($itemList)) {
8342
            return false;
8343
        }
8344
8345
        if (isset($itemList['document'])) {
8346
            // Get parents
8347
            foreach ($itemList['document'] as $documentId) {
8348
                $documentInfo = DocumentManager::get_document_data_by_id($documentId, api_get_course_id(), true);
8349
                if (!empty($documentInfo['parents'])) {
8350
                    foreach ($documentInfo['parents'] as $parentInfo) {
8351
                        if (in_array($parentInfo['iid'], $itemList['document'])) {
8352
                            continue;
8353
                        }
8354
                        $itemList['document'][] = $parentInfo['iid'];
8355
                    }
8356
                }
8357
            }
8358
8359
            $courseInfo = api_get_course_info();
8360
            foreach ($itemList['document'] as $documentId) {
8361
                $documentInfo = DocumentManager::get_document_data_by_id($documentId, api_get_course_id());
8362
                $items = DocumentManager::get_resources_from_source_html(
8363
                    $documentInfo['absolute_path'],
8364
                    true,
8365
                    TOOL_DOCUMENT
8366
                );
8367
8368
                if (!empty($items)) {
8369
                    foreach ($items as $item) {
8370
                        // Get information about source url
8371
                        $url = $item[0]; // url
8372
                        $scope = $item[1]; // scope (local, remote)
8373
                        $type = $item[2]; // type (rel, abs, url)
8374
8375
                        $origParseUrl = parse_url($url);
8376
                        $realOrigPath = isset($origParseUrl['path']) ? $origParseUrl['path'] : null;
8377
8378
                        if ('local' === $scope) {
8379
                            if ('abs' === $type || 'rel' === $type) {
8380
                                $documentFile = strstr($realOrigPath, 'document');
8381
                                if (false !== strpos($realOrigPath, $documentFile)) {
8382
                                    $documentFile = str_replace('document', '', $documentFile);
8383
                                    $itemDocumentId = DocumentManager::get_document_id($courseInfo, $documentFile);
8384
                                    // Document found! Add it to the list
8385
                                    if ($itemDocumentId) {
8386
                                        $itemList['document'][] = $itemDocumentId;
8387
                                    }
8388
                                }
8389
                            }
8390
                        }
8391
                    }
8392
                }
8393
            }
8394
8395
            $courseBuilder->build_documents(
8396
                api_get_session_id(),
8397
                $this->get_course_int_id(),
8398
                true,
8399
                $itemList['document']
8400
            );
8401
        }
8402
8403
        if (isset($itemList['quiz'])) {
8404
            $courseBuilder->build_quizzes(
8405
                api_get_session_id(),
8406
                $this->get_course_int_id(),
8407
                true,
8408
                $itemList['quiz']
8409
            );
8410
        }
8411
8412
        if (!empty($itemList['thread'])) {
8413
            $threadList = [];
8414
            $repo = Container::getForumThreadRepository();
8415
            foreach ($itemList['thread'] as $threadId) {
8416
                /** @var CForumThread $thread */
8417
                $thread = $repo->find($threadId);
8418
                if ($thread) {
8419
                    $itemList['forum'][] = $thread->getForum() ? $thread->getForum()->getIid() : 0;
8420
                    $threadList[] = $thread->getIid();
8421
                }
8422
            }
8423
8424
            if (!empty($threadList)) {
8425
                $courseBuilder->build_forum_topics(
8426
                    api_get_session_id(),
8427
                    $this->get_course_int_id(),
8428
                    null,
8429
                    $threadList
8430
                );
8431
            }
8432
        }
8433
8434
        $forumCategoryList = [];
8435
        if (isset($itemList['forum'])) {
8436
            foreach ($itemList['forum'] as $forumId) {
8437
                $forumInfo = get_forums($forumId);
8438
                $forumCategoryList[] = $forumInfo['forum_category'];
8439
            }
8440
        }
8441
8442
        if (!empty($forumCategoryList)) {
8443
            $courseBuilder->build_forum_category(
8444
                api_get_session_id(),
8445
                $this->get_course_int_id(),
8446
                true,
8447
                $forumCategoryList
8448
            );
8449
        }
8450
8451
        if (!empty($itemList['forum'])) {
8452
            $courseBuilder->build_forums(
8453
                api_get_session_id(),
8454
                $this->get_course_int_id(),
8455
                true,
8456
                $itemList['forum']
8457
            );
8458
        }
8459
8460
        if (isset($itemList['link'])) {
8461
            $courseBuilder->build_links(
8462
                api_get_session_id(),
8463
                $this->get_course_int_id(),
8464
                true,
8465
                $itemList['link']
8466
            );
8467
        }
8468
8469
        if (!empty($itemList['student_publication'])) {
8470
            $courseBuilder->build_works(
8471
                api_get_session_id(),
8472
                $this->get_course_int_id(),
8473
                true,
8474
                $itemList['student_publication']
8475
            );
8476
        }
8477
8478
        $courseBuilder->build_learnpaths(
8479
            api_get_session_id(),
8480
            $this->get_course_int_id(),
8481
            true,
8482
            [$this->get_id()],
8483
            false
8484
        );
8485
8486
        $courseBuilder->restoreDocumentsFromList();
8487
8488
        $zipFile = CourseArchiver::createBackup($courseBuilder->course);
8489
        $zipPath = CourseArchiver::getBackupDir().$zipFile;
8490
        $result = DocumentManager::file_send_for_download(
8491
            $zipPath,
8492
            true,
8493
            $this->get_name().'.zip'
8494
        );
8495
8496
        if ($result) {
8497
            api_not_allowed();
8498
        }
8499
8500
        return true;
8501
    }
8502
8503
    /**
8504
     * Get whether this is a learning path with the accumulated work time or not.
8505
     *
8506
     * @return int
8507
     */
8508
    public function getAccumulateWorkTime()
8509
    {
8510
        return (int) $this->accumulateWorkTime;
8511
    }
8512
8513
    /**
8514
     * Get whether this is a learning path with the accumulated work time or not.
8515
     *
8516
     * @return int
8517
     */
8518
    public function getAccumulateWorkTimeTotalCourse()
8519
    {
8520
        $table = Database::get_course_table(TABLE_LP_MAIN);
8521
        $sql = "SELECT SUM(accumulate_work_time) AS total
8522
                FROM $table
8523
                WHERE c_id = ".$this->course_int_id;
8524
        $result = Database::query($sql);
8525
        $row = Database::fetch_array($result);
8526
8527
        return (int) $row['total'];
8528
    }
8529
8530
    /**
8531
     * @param int $lpId
8532
     * @param int $courseId
8533
     *
8534
     * @return mixed
8535
     */
8536
    public static function getAccumulateWorkTimePrerequisite($lpId, $courseId)
8537
    {
8538
        $lpId = (int) $lpId;
8539
        $table = Database::get_course_table(TABLE_LP_MAIN);
8540
        $sql = "SELECT accumulate_work_time
8541
                FROM $table
8542
                WHERE iid = $lpId";
8543
        $result = Database::query($sql);
8544
        $row = Database::fetch_array($result);
8545
8546
        return $row['accumulate_work_time'];
8547
    }
8548
8549
    /**
8550
     * @param int $courseId
8551
     *
8552
     * @return int
8553
     */
8554
    public static function getAccumulateWorkTimeTotal($courseId)
8555
    {
8556
        $table = Database::get_course_table(TABLE_LP_MAIN);
8557
        $courseId = (int) $courseId;
8558
        $sql = "SELECT SUM(accumulate_work_time) AS total
8559
                FROM $table
8560
                WHERE c_id = $courseId";
8561
        $result = Database::query($sql);
8562
        $row = Database::fetch_array($result);
8563
8564
        return (int) $row['total'];
8565
    }
8566
8567
    /**
8568
     * In order to use the lp icon option you need to create the "lp_icon" LP extra field
8569
     * and put the images in.
8570
     */
8571
    public static function getIconSelect(): array
8572
    {
8573
        $theme = Container::$container->get(ThemeHelper::class)->getVisualTheme();
8574
        $filesystem = Container::$container->get('oneup_flysystem.themes_filesystem');
8575
8576
        if (!$filesystem->directoryExists("$theme/lp_icons")) {
8577
            return [];
8578
        }
8579
8580
        $icons = ['' => get_lang('Please select an option')];
8581
8582
        $iconFiles = $filesystem->listContents("$theme/lp_icons");
8583
        $allowedExtensions = ['image/jpeg', 'image/jpg', 'image/png'];
8584
8585
        foreach ($iconFiles as $iconFile) {
8586
            $mimeType = $filesystem->mimeType($iconFile->path());
8587
8588
            if (in_array($mimeType, $allowedExtensions)) {
8589
                $basename = basename($iconFile->path());
8590
                $icons[$basename] = $basename;
8591
            }
8592
        }
8593
8594
        return $icons;
8595
    }
8596
8597
    /**
8598
     * @param int $lpId
8599
     *
8600
     * @return string
8601
     */
8602
    public static function getSelectedIcon($lpId)
8603
    {
8604
        $extraFieldValue = new ExtraFieldValue('lp');
8605
        $lpIcon = $extraFieldValue->get_values_by_handler_and_field_variable($lpId, 'lp_icon');
8606
        $icon = '';
8607
        if (!empty($lpIcon) && isset($lpIcon['value'])) {
8608
            $icon = $lpIcon['value'];
8609
        }
8610
8611
        return $icon;
8612
    }
8613
8614
    public static function getSelectedIconHtml(int $lpId): string
8615
    {
8616
        $icon = self::getSelectedIcon($lpId);
8617
8618
        if (empty($icon)) {
8619
            return '';
8620
        }
8621
8622
        $path = Container::getThemeHelper()->getThemeAssetUrl("lp_icons/$icon");
8623
8624
        return Display::img($path);
8625
    }
8626
8627
    /**
8628
     * @param string $value
8629
     *
8630
     * @return string
8631
     */
8632
    public function cleanItemTitle($value)
8633
    {
8634
        $value = Security::remove_XSS(strip_tags($value));
8635
8636
        return $value;
8637
    }
8638
8639
    public function setItemTitle(FormValidator $form)
8640
    {
8641
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
8642
            $form->addHtmlEditor(
8643
                'title',
8644
                get_lang('Title'),
8645
                true,
8646
                false,
8647
                ['ToolbarSet' => 'TitleAsHtml', 'id' => uniqid('editor')]
8648
            );
8649
        } else {
8650
            $form->addText('title', get_lang('Title'), true, ['id' => 'idTitle', 'class' => 'learnpath_item_form']);
8651
            $form->applyFilter('title', 'trim');
8652
            $form->applyFilter('title', 'html_filter');
8653
        }
8654
    }
8655
8656
    /**
8657
     * @return array
8658
     */
8659
    public function getItemsForForm($addParentCondition = false)
8660
    {
8661
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
8662
8663
        $sql = "SELECT * FROM $tbl_lp_item
8664
                WHERE path <> 'root' AND lp_id = ".$this->lp_id;
8665
8666
        if ($addParentCondition) {
8667
            $sql .= ' AND parent_item_id IS NULL ';
8668
        }
8669
        $sql .= ' ORDER BY display_order ASC';
8670
8671
        $result = Database::query($sql);
8672
        $arrLP = [];
8673
        while ($row = Database::fetch_array($result)) {
8674
            $arrLP[] = [
8675
                'iid' => $row['iid'],
8676
                'id' => $row['iid'],
8677
                'item_type' => $row['item_type'],
8678
                'title' => $this->cleanItemTitle($row['title']),
8679
                'title_raw' => $row['title'],
8680
                'path' => $row['path'],
8681
                'description' => Security::remove_XSS($row['description']),
8682
                'parent_item_id' => $row['parent_item_id'],
8683
                'previous_item_id' => $row['previous_item_id'],
8684
                'next_item_id' => $row['next_item_id'],
8685
                'display_order' => $row['display_order'],
8686
                'max_score' => $row['max_score'],
8687
                'min_score' => $row['min_score'],
8688
                'mastery_score' => $row['mastery_score'],
8689
                'prerequisite' => $row['prerequisite'],
8690
                'max_time_allowed' => $row['max_time_allowed'],
8691
                'prerequisite_min_score' => $row['prerequisite_min_score'],
8692
                'prerequisite_max_score' => $row['prerequisite_max_score'],
8693
            ];
8694
        }
8695
8696
        return $arrLP;
8697
    }
8698
8699
    /**
8700
     * Gets whether this SCORM learning path has been marked to use the score
8701
     * as progress. Takes into account whether the learnpath matches (SCORM
8702
     * content + less than 2 items).
8703
     *
8704
     * @return bool True if the score should be used as progress, false otherwise
8705
     */
8706
    public function getUseScoreAsProgress()
8707
    {
8708
        // If not a SCORM, we don't care about the setting
8709
        if (2 != $this->get_type()) {
8710
            return false;
8711
        }
8712
        // If more than one step in the SCORM, we don't care about the setting
8713
        if ($this->get_total_items_count() > 1) {
8714
            return false;
8715
        }
8716
        $extraFieldValue = new ExtraFieldValue('lp');
8717
        $doUseScore = false;
8718
        $useScore = $extraFieldValue->get_values_by_handler_and_field_variable(
8719
            $this->get_id(),
8720
            'use_score_as_progress'
8721
        );
8722
        if (!empty($useScore) && isset($useScore['value'])) {
8723
            $doUseScore = $useScore['value'];
8724
        }
8725
8726
        return $doUseScore;
8727
    }
8728
8729
    /**
8730
     * Get the user identifier (user_id or username
8731
     * Depends on scorm_api_username_as_student_id in app/config/configuration.php.
8732
     *
8733
     * @return string User ID or username, depending on configuration setting
8734
     */
8735
    public static function getUserIdentifierForExternalServices()
8736
    {
8737
        $scormApiExtraFieldUseStudentId = api_get_setting('lp.scorm_api_extrafield_to_use_as_student_id');
8738
        $extraFieldValue = new ExtraFieldValue('user');
8739
        $extrafield = $extraFieldValue->get_values_by_handler_and_field_variable(
8740
            api_get_user_id(),
8741
            $scormApiExtraFieldUseStudentId
8742
        );
8743
        if (is_array($extrafield) && isset($extrafield['value'])) {
8744
            return $extrafield['value'];
8745
        } else {
8746
            if ('true' === $scormApiExtraFieldUseStudentId) {
8747
                return api_get_user_info(api_get_user_id())['username'];
8748
            } else {
8749
                return api_get_user_id();
8750
            }
8751
        }
8752
    }
8753
8754
    /**
8755
     * Save the new order for learning path items.
8756
     *
8757
     * @param array $orderList A associative array with id and parent_id keys.
8758
     */
8759
    public static function sortItemByOrderList(CLpItem $rootItem, array $orderList = [], $flush = true, $lpItemRepo = null, $em = null)
8760
    {
8761
        if (empty($orderList)) {
8762
            return true;
8763
        }
8764
        if (!isset($lpItemRepo)) {
8765
            $lpItemRepo = Container::getLpItemRepository();
8766
        }
8767
        if (!isset($em)) {
8768
            $em = Database::getManager();
8769
        }
8770
        $counter = 2;
8771
        $rootItem->setDisplayOrder(1);
8772
        $rootItem->setPreviousItemId(null);
8773
        $em->persist($rootItem);
8774
        if ($flush) {
8775
            $em->flush();
8776
        }
8777
8778
        foreach ($orderList as $item) {
8779
            $itemId = $item->id ?? 0;
8780
            if (empty($itemId)) {
8781
                continue;
8782
            }
8783
            $parentId = $item->parent_id ?? 0;
8784
            $parent = $rootItem;
8785
            if (!empty($parentId)) {
8786
                $parentExists = $lpItemRepo->find($parentId);
8787
                if (null !== $parentExists) {
8788
                    $parent = $parentExists;
8789
                }
8790
            }
8791
8792
            /** @var CLpItem $itemEntity */
8793
            $itemEntity = $lpItemRepo->find($itemId);
8794
            $itemEntity->setParent($parent);
8795
            $itemEntity->setPreviousItemId(null);
8796
            $itemEntity->setNextItemId(null);
8797
            $itemEntity->setDisplayOrder($counter);
8798
8799
            $em->persist($itemEntity);
8800
            if ($flush) {
8801
                $em->flush();
8802
            }
8803
            $counter++;
8804
        }
8805
8806
        $lpItemRepo->recoverNode($rootItem, 'displayOrder');
8807
        $em->persist($rootItem);
8808
        if ($flush) {
8809
            $em->flush();
8810
        }
8811
8812
        return true;
8813
    }
8814
8815
    public static function move(int $lpId, string $direction)
8816
    {
8817
        $em = Database::getManager();
8818
        /** @var CLp $lp */
8819
        $lp = Container::getLpRepository()->find($lpId);
8820
        if ($lp) {
8821
            $course = api_get_course_entity();
8822
            $session = api_get_session_entity();
8823
            $group = api_get_group_entity();
8824
8825
            $link = $lp->getResourceNode()->getResourceLinkByContext($course, $session, $group);
8826
8827
            if ($link) {
8828
                if ('down' === $direction) {
8829
                    $link->moveDownPosition();
8830
                }
8831
                if ('up' === $direction) {
8832
                    $link->moveUpPosition();
8833
                }
8834
8835
                $em->flush();
8836
            }
8837
        }
8838
    }
8839
8840
    /**
8841
     * Get the depth level of LP item.
8842
     *
8843
     * @param array $items
8844
     * @param int   $currentItemId
8845
     *
8846
     * @return int
8847
     */
8848
    private static function get_level_for_item($items, $currentItemId)
8849
    {
8850
        $parentItemId = 0;
8851
        if (isset($items[$currentItemId])) {
8852
            $parentItemId = $items[$currentItemId]->parent;
8853
        }
8854
8855
        if (0 == $parentItemId) {
8856
            return 0;
8857
        }
8858
8859
        return self::get_level_for_item($items, $parentItemId) + 1;
8860
    }
8861
8862
    /**
8863
     * Generate the link for a learnpath category as course tool.
8864
     *
8865
     * @param int $categoryId
8866
     *
8867
     * @return string
8868
     */
8869
    private static function getCategoryLinkForTool($categoryId)
8870
    {
8871
        $categoryId = (int) $categoryId;
8872
        return 'lp/lp_controller.php?'.api_get_cidreq().'&'
8873
            .http_build_query(
8874
                [
8875
                    'action' => 'view_category',
8876
                    'id' => $categoryId,
8877
                ]
8878
            );
8879
    }
8880
8881
    /**
8882
     * Check and obtain the lp final item if exist.
8883
     *
8884
     * @return learnpathItem
8885
     */
8886
    private function getFinalItem()
8887
    {
8888
        if (empty($this->items)) {
8889
            return null;
8890
        }
8891
8892
        foreach ($this->items as $item) {
8893
            if ('final_item' !== $item->type) {
8894
                continue;
8895
            }
8896
8897
            return $item;
8898
        }
8899
    }
8900
8901
    /**
8902
     * Get the LP Final Item Template.
8903
     *
8904
     * @return string
8905
     */
8906
    private function getFinalItemTemplate()
8907
    {
8908
        return file_get_contents(api_get_path(SYS_CODE_PATH).'lp/final_item_template/template.html');
8909
    }
8910
8911
    /**
8912
     * Get the LP Final Item Url.
8913
     *
8914
     * @return string
8915
     */
8916
    private function getSavedFinalItem()
8917
    {
8918
        $finalItem = $this->getFinalItem();
8919
8920
        $repo = Container::getDocumentRepository();
8921
        /** @var CDocument $document */
8922
        $document = $repo->find($finalItem->path);
8923
8924
        return $document ? $repo->getResourceFileContent($document) : '';
8925
    }
8926
8927
    /**
8928
     * Recalculates the results for all exercises associated with the learning path (LP) for the given user.
8929
     */
8930
    public function recalculateResultsForLp(int $userId): void
8931
    {
8932
        $em = Database::getManager();
8933
        $lpItemRepo = $em->getRepository(CLpItem::class);
8934
        $lpItems = $lpItemRepo->findBy(['lp' => $this->lp_id]);
8935
8936
        if (empty($lpItems)) {
8937
            Display::addFlash(Display::return_message(get_lang('No items found'), 'error'));
8938
            return;
8939
        }
8940
8941
        $lpItemsById = [];
8942
        foreach ($lpItems as $item) {
8943
            $lpItemsById[$item->getIid()] = $item;
8944
        }
8945
8946
        $trackEExerciseRepo = $em->getRepository(TrackEExercise::class);
8947
        $trackExercises = $trackEExerciseRepo->createQueryBuilder('te')
8948
            ->where('te.origLpId = :lpId')
8949
            ->andWhere('te.user = :userId')
8950
            ->andWhere('te.origLpItemId IN (:lpItemIds)')
8951
            ->setParameter('lpId', $this->lp_id)
8952
            ->setParameter('userId', $userId)
8953
            ->setParameter('lpItemIds', array_keys($lpItemsById))
8954
            ->getQuery()
8955
            ->getResult();
8956
8957
        if (empty($trackExercises)) {
8958
            Display::addFlash(Display::return_message(get_lang('No exercise attempts found'), 'error'));
8959
            return;
8960
        }
8961
8962
        foreach ($trackExercises as $trackExercise) {
8963
            $exeId = $trackExercise->getExeId();
8964
            $lpItemId = $trackExercise->getOrigLpItemId();
8965
8966
            if (!isset($lpItemsById[$lpItemId])) {
8967
                continue;
8968
            }
8969
8970
            $lpItem = $lpItemsById[$lpItemId];
8971
            if ('quiz' !== $lpItem->getItemType()) {
8972
                continue;
8973
            }
8974
8975
            $quizId = (int) $lpItem->getPath();
8976
            $courseId = (int) $trackExercise->getCourse()->getId();
8977
            $updatedExercise = ExerciseLib::recalculateResult($exeId, $userId, $quizId, $courseId);
8978
            if ($updatedExercise instanceof TrackEExercise) {
8979
                Display::addFlash(Display::return_message(get_lang('Results recalculated'), 'success'));
8980
            } else {
8981
                Display::addFlash(Display::return_message(get_lang('Error recalculating results'), 'error'));
8982
            }
8983
        }
8984
    }
8985
8986
    /**
8987
     * Returns the video player HTML for a video-type document LP item.
8988
     *
8989
     * @param int $lpItemId
8990
     * @param string $autostart
8991
     *
8992
     * @return string
8993
     */
8994
    public function getVideoPlayer(CDocument $document, string $autostart = 'true'): string
8995
    {
8996
        $resourceNode = $document->getResourceNode();
8997
        $resourceFile = $resourceNode?->getFirstResourceFile();
8998
8999
        if (!$resourceNode || !$resourceFile) {
9000
            return '';
9001
        }
9002
9003
        $resourceNodeRepository = Container::getResourceNodeRepository();
9004
        $videoUrl = $resourceNodeRepository->getResourceFileUrl($resourceNode);
9005
9006
        if (empty($videoUrl)) {
9007
            return '';
9008
        }
9009
9010
        $fileName = $resourceFile->getTitle();
9011
        $ext = pathinfo($fileName, PATHINFO_EXTENSION);
9012
        $mimeType = $resourceFile->getMimeType() ?: 'video/mp4';
9013
        $autoplayAttr = ($autostart === 'true') ? 'autoplay muted playsinline' : '';
9014
9015
        $html = '';
9016
        $html .= '
9017
        <video id="lp-video" width="100%" height="auto" controls '.$autoplayAttr.'>
9018
            <source src="'.$videoUrl.'" type="$mimeType">
9019
        </video>';
9020
9021
        return $html;
9022
    }
9023
}
9024