learnpath::displayItemPrerequisitesForm()   F
last analyzed

Complexity

Conditions 19
Paths 3

Size

Total Lines 210
Code Lines 139

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 19
eloc 139
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 210
rs 3.6133

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

    return false;
}

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

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

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

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

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

    return false;
}

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

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

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

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

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

    return false;
}

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

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