Passed
Pull Request — master (#6541)
by
unknown
12:15 queued 03:49
created

learnpath::get_view_id()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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