Passed
Pull Request — master (#6541)
by
unknown
08:03
created

learnpath::isUserSubscribedToLp()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 31
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
c 0
b 0
f 0
nc 4
nop 4
dl 0
loc 31
rs 9.8333
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
        // 1) Campos estándar de documento
5680
        if (null !== $lpItem) {
5681
            LearnPathItemForm::setForm($form, $action, $this, $lpItem);
5682
        }
5683
5684
        // 2) Sólo en edición, añade el checkbox y su valor por defecto
5685
        if ($action === 'edit' && $lpItem !== null) {
5686
            // Label con get_lang(), sin envoltorios extra
5687
            $form->addElement(
5688
                'checkbox',
5689
                'export_allowed',
5690
                get_lang('Allow PDF export for this item')
5691
            );
5692
            // Asigna sólo este valor, QuickForm lo fusiona con los demás defaults
5693
            $form->setDefaults([
5694
                'export_allowed' => $lpItem->isExportAllowed() ? 1 : 0
5695
            ]);
5696
        }
5697
5698
        // 3) Selector de carpetas en modo “add”
5699
        switch ($action) {
5700
            case 'add':
5701
                $folders = DocumentManager::get_all_document_folders(
5702
                    $courseInfo,
5703
                    0,
5704
                    true
5705
                );
5706
                DocumentManager::build_directory_selector(
5707
                    $folders,
5708
                    '',
5709
                    [],
5710
                    true,
5711
                    $form,
5712
                    'directory_parent_id'
5713
                );
5714
                if ($data) {
5715
                    $form->setDefaults([
5716
                        'directory_parent_id' => $data->getIid()
5717
                    ]);
5718
                }
5719
                break;
5720
        }
5721
5722
        // 4) Botón Guardar
5723
        $form->addButtonSave(get_lang('Save'), 'submit_button');
5724
5725
        return $form->returnForm();
5726
    }
5727
5728
5729
5730
    /**
5731
     * @param array  $courseInfo
5732
     * @param string $content
5733
     * @param string $title
5734
     * @param int    $parentId
5735
     *
5736
     * @return int
5737
     */
5738
    public function createReadOutText($courseInfo, $content = '', $title = '', $parentId = 0)
5739
    {
5740
        $creatorId = api_get_user_id();
5741
        $sessionId = api_get_session_id();
5742
5743
        // Generates folder
5744
        $result = $this->generate_lp_folder($courseInfo);
5745
        $dir = $result['dir'];
5746
5747
        if (empty($parentId) || '/' === $parentId) {
5748
            $postDir = isset($_POST['dir']) ? $_POST['dir'] : $dir;
5749
            $dir = isset($_GET['dir']) ? $_GET['dir'] : $postDir; // Please, do not modify this dirname formatting.
5750
5751
            if ('/' === $parentId) {
5752
                $dir = '/';
5753
            }
5754
5755
            // Please, do not modify this dirname formatting.
5756
            if (strstr($dir, '..')) {
5757
                $dir = '/';
5758
            }
5759
5760
            if (!empty($dir[0]) && '.' == $dir[0]) {
5761
                $dir = substr($dir, 1);
5762
            }
5763
            if (!empty($dir[0]) && '/' != $dir[0]) {
5764
                $dir = '/'.$dir;
5765
            }
5766
            if (isset($dir[strlen($dir) - 1]) && '/' != $dir[strlen($dir) - 1]) {
5767
                $dir .= '/';
5768
            }
5769
        } else {
5770
            $parentInfo = DocumentManager::get_document_data_by_id(
5771
                $parentId,
5772
                $courseInfo['code']
5773
            );
5774
            if (!empty($parentInfo)) {
5775
                $dir = $parentInfo['path'].'/';
5776
            }
5777
        }
5778
5779
        $filepath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/'.$dir;
5780
5781
        if (!is_dir($filepath)) {
5782
            $dir = '/';
5783
            $filepath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/'.$dir;
5784
        }
5785
5786
        $originalTitle = !empty($title) ? $title : $_POST['title'];
5787
5788
        if (!empty($title)) {
5789
            $title = api_replace_dangerous_char(stripslashes($title));
5790
        } else {
5791
            $title = api_replace_dangerous_char(stripslashes($_POST['title']));
5792
        }
5793
5794
        $title = disable_dangerous_file($title);
5795
        $filename = $title;
5796
        $content = !empty($content) ? $content : $_POST['content_lp'];
5797
        $tmpFileName = $filename;
5798
5799
        $i = 0;
5800
        while (file_exists($filepath.$tmpFileName.'.html')) {
5801
            $tmpFileName = $filename.'_'.++$i;
5802
        }
5803
5804
        $filename = $tmpFileName.'.html';
5805
        $content = stripslashes($content);
5806
5807
        if (file_exists($filepath.$filename)) {
5808
            return 0;
5809
        }
5810
5811
        $putContent = file_put_contents($filepath.$filename, $content);
5812
5813
        if (false === $putContent) {
5814
            return 0;
5815
        }
5816
5817
        $fileSize = filesize($filepath.$filename);
5818
        $saveFilePath = $dir.$filename;
5819
5820
        $document = DocumentManager::addDocument(
5821
            $courseInfo,
5822
            $saveFilePath,
5823
            'file',
5824
            $fileSize,
5825
            $tmpFileName,
5826
            '',
5827
            0, //readonly
5828
            true,
5829
            null,
5830
            $sessionId,
5831
            $creatorId
5832
        );
5833
5834
        $documentId = $document->getIid();
5835
5836
        if (!$document) {
5837
            return 0;
5838
        }
5839
5840
        $newComment = isset($_POST['comment']) ? trim($_POST['comment']) : '';
5841
        $newTitle = $originalTitle;
5842
5843
        if ($newComment || $newTitle) {
5844
            $em = Database::getManager();
5845
5846
            if ($newComment) {
5847
                $document->setComment($newComment);
5848
            }
5849
5850
            if ($newTitle) {
5851
                $document->setTitle($newTitle);
5852
            }
5853
5854
            $em->persist($document);
5855
            $em->flush();
5856
        }
5857
5858
        return $documentId;
5859
    }
5860
5861
    /**
5862
     * Displays the menu for manipulating a step.
5863
     *
5864
     * @return string
5865
     */
5866
    public function displayItemMenu(CLpItem $lpItem)
5867
    {
5868
        $item_id = $lpItem->getIid();
5869
        $audio = $lpItem->getAudio();
5870
        $itemType = $lpItem->getItemType();
5871
        $path = $lpItem->getPath();
5872
5873
        $return = '';
5874
        $audio_player = null;
5875
        // We display an audio player if needed.
5876
        if (!empty($audio)) {
5877
            /*$webAudioPath = '../..'.api_get_path(REL_COURSE_PATH).$_course['path'].'/document/audio/'.$row['audio'];
5878
            $audio_player .= '<div class="lp_mediaplayer" id="container">'
5879
                .'<audio src="'.$webAudioPath.'" controls>'
5880
                .'</div><br>';*/
5881
        }
5882
5883
        $url = api_get_self().'?'.api_get_cidreq().'&view=build&id='.$item_id.'&lp_id='.$this->lp_id;
5884
5885
        if (TOOL_LP_FINAL_ITEM !== $itemType) {
5886
            $return .= Display::url(
5887
                Display::getMdiIcon('pencil', 'ch-tool-icon', null, 22, get_lang('Edit')),
5888
                $url.'&action=edit_item&path_item='.$path
5889
            );
5890
5891
            /*$return .= Display::url(
5892
                Display::getMdiIcon('arrow-right-bold', 'ch-tool-icon', null, 22, get_lang('Move')),
5893
                $url.'&action=move_item'
5894
            );*/
5895
        }
5896
5897
        // Commented for now as prerequisites cannot be added to chapters.
5898
        if ('dir' !== $itemType) {
5899
            $return .= Display::url(
5900
                Display::getMdiIcon('graph', 'ch-tool-icon', null, 22, get_lang('Prerequisites')),
5901
                $url.'&action=edit_item_prereq'
5902
            );
5903
        }
5904
        $return .= Display::url(
5905
            Display::getMdiIcon('delete', 'ch-tool-icon', null, 22, get_lang('Delete')),
5906
            $url.'&action=delete_item'
5907
        );
5908
5909
        /*if (in_array($itemType, [TOOL_DOCUMENT, TOOL_LP_FINAL_ITEM, TOOL_HOTPOTATOES])) {
5910
            $documentData = DocumentManager::get_document_data_by_id($path, $course_code);
5911
            if (empty($documentData)) {
5912
                // Try with iid
5913
                $table = Database::get_course_table(TABLE_DOCUMENT);
5914
                $sql = "SELECT path FROM $table
5915
                        WHERE
5916
                              c_id = ".api_get_course_int_id()." AND
5917
                              iid = ".$path." AND
5918
                              path NOT LIKE '%_DELETED_%'";
5919
                $result = Database::query($sql);
5920
                $documentData = Database::fetch_array($result);
5921
                if ($documentData) {
5922
                    $documentData['absolute_path_from_document'] = '/document'.$documentData['path'];
5923
                }
5924
            }
5925
            if (isset($documentData['absolute_path_from_document'])) {
5926
                $return .= get_lang('File').': '.$documentData['absolute_path_from_document'];
5927
            }
5928
        }*/
5929
5930
        if (!empty($audio_player)) {
5931
            $return .= $audio_player;
5932
        }
5933
5934
        return Display::toolbarAction('lp_item', [$return]);
5935
    }
5936
5937
    /**
5938
     * Creates the javascript needed for filling up the checkboxes without page reload.
5939
     *
5940
     * @return string
5941
     */
5942
    public function get_js_dropdown_array()
5943
    {
5944
        $return = 'var child_name = new Array();'."\n";
5945
        $return .= 'var child_value = new Array();'."\n\n";
5946
        $return .= 'child_name[0] = new Array();'."\n";
5947
        $return .= 'child_value[0] = new Array();'."\n\n";
5948
5949
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
5950
        $sql = "SELECT * FROM ".$tbl_lp_item."
5951
                WHERE
5952
                    lp_id = ".$this->lp_id." AND
5953
                    parent_item_id = 0
5954
                ORDER BY display_order ASC";
5955
        Database::query($sql);
5956
        $i = 0;
5957
5958
        $list = $this->getItemsForForm(true);
5959
5960
        foreach ($list as $row_zero) {
5961
            if (TOOL_LP_FINAL_ITEM !== $row_zero['item_type']) {
5962
                if (TOOL_QUIZ == $row_zero['item_type']) {
5963
                    $row_zero['title'] = Exercise::get_formated_title_variable($row_zero['title']);
5964
                }
5965
                $js_var = json_encode(get_lang('After').' '.$row_zero['title']);
5966
                $return .= 'child_name[0]['.$i.'] = '.$js_var.' ;'."\n";
5967
                $return .= 'child_value[0]['.$i++.'] = "'.$row_zero['iid'].'";'."\n";
5968
            }
5969
        }
5970
5971
        $return .= "\n";
5972
        $sql = "SELECT * FROM $tbl_lp_item
5973
                WHERE lp_id = ".$this->lp_id;
5974
        $res = Database::query($sql);
5975
        while ($row = Database::fetch_array($res)) {
5976
            $sql_parent = "SELECT * FROM ".$tbl_lp_item."
5977
                           WHERE
5978
                                parent_item_id = ".$row['iid']."
5979
                           ORDER BY display_order ASC";
5980
            $res_parent = Database::query($sql_parent);
5981
            $i = 0;
5982
            $return .= 'child_name['.$row['iid'].'] = new Array();'."\n";
5983
            $return .= 'child_value['.$row['iid'].'] = new Array();'."\n\n";
5984
5985
            while ($row_parent = Database::fetch_array($res_parent)) {
5986
                $js_var = json_encode(get_lang('After').' '.$this->cleanItemTitle($row_parent['title']));
5987
                $return .= 'child_name['.$row['iid'].']['.$i.'] =   '.$js_var.' ;'."\n";
5988
                $return .= 'child_value['.$row['iid'].']['.$i++.'] = "'.$row_parent['iid'].'";'."\n";
5989
            }
5990
            $return .= "\n";
5991
        }
5992
5993
        $return .= "
5994
            function load_cbo(id) {
5995
                if (!id) {
5996
                    return false;
5997
                }
5998
5999
                var cbo = document.getElementById('previous');
6000
                if (cbo) {
6001
                    for(var i = cbo.length - 1; i > 0; i--) {
6002
                        cbo.options[i] = null;
6003
                    }
6004
                    var k=0;
6005
                    for (var i = 1; i <= child_name[id].length; i++){
6006
                        var option = new Option(child_name[id][i - 1], child_value[id][i - 1]);
6007
                        option.style.paddingLeft = '40px';
6008
                        cbo.options[i] = option;
6009
                        k = i;
6010
                    }
6011
                    cbo.options[k].selected = true;
6012
                }
6013
6014
                //$('#previous').selectpicker('refresh');
6015
            }";
6016
6017
        return $return;
6018
    }
6019
6020
    /**
6021
     * Display the form to allow moving an item.
6022
     *
6023
     * @param CLpItem $lpItem
6024
     *
6025
     * @throws Exception
6026
     *
6027
     *
6028
     * @return string HTML form
6029
     */
6030
    public function display_move_item($lpItem)
6031
    {
6032
        $return = '';
6033
        $path = $lpItem->getPath();
6034
6035
        if ($lpItem) {
6036
            $itemType = $lpItem->getItemType();
6037
            switch ($itemType) {
6038
                case 'dir':
6039
                case 'asset':
6040
                    $return .= $this->displayItemMenu($lpItem);
6041
                    $return .= $this->display_item_form(
6042
                        $lpItem,
6043
                        get_lang('Move the current section'),
6044
                        'move',
6045
                        $row
6046
                    );
6047
                    break;
6048
                case TOOL_DOCUMENT:
6049
                case 'video':
6050
                    $return .= $this->displayItemMenu($lpItem);
6051
                    $return .= $this->displayDocumentForm('move', $lpItem);
6052
                    break;
6053
                case TOOL_LINK:
6054
                    $link = null;
6055
                    if (!empty($path)) {
6056
                        $repo = Container::getLinkRepository();
6057
                        $link = $repo->find($path);
6058
                    }
6059
                    $return .= $this->displayItemMenu($lpItem);
6060
                    $return .= $this->display_link_form('move', $lpItem, $link);
6061
                    break;
6062
                case TOOL_HOTPOTATOES:
6063
                    $return .= $this->displayItemMenu($lpItem);
6064
                    $return .= $this->display_link_form('move', $lpItem, $row);
6065
                    break;
6066
                case TOOL_QUIZ:
6067
                    $return .= $this->displayItemMenu($lpItem);
6068
                    $return .= $this->display_quiz_form('move', $lpItem, $row);
6069
                    break;
6070
                case TOOL_STUDENTPUBLICATION:
6071
                    $return .= $this->displayItemMenu($lpItem);
6072
                    $return .= $this->display_student_publication_form('move', $lpItem, $row);
6073
                    break;
6074
                case TOOL_FORUM:
6075
                    $return .= $this->displayItemMenu($lpItem);
6076
                    $return .= $this->display_forum_form('move', $lpItem, $row);
6077
                    break;
6078
                case TOOL_THREAD:
6079
                    $return .= $this->displayItemMenu($lpItem);
6080
                    $return .= $this->display_forum_form('move', $lpItem, $row);
6081
                    break;
6082
            }
6083
        }
6084
6085
        return $return;
6086
    }
6087
6088
    /**
6089
     * Return HTML form to allow prerequisites selection.
6090
     *
6091
     * @todo use FormValidator
6092
     *
6093
     * @return string HTML form
6094
     */
6095
    public function displayItemPrerequisitesForm(CLpItem $lpItem)
6096
    {
6097
        $courseId = api_get_course_int_id();
6098
        $preRequisiteId = $lpItem->getPrerequisite();
6099
        $itemId = $lpItem->getIid();
6100
6101
        $return = Display::page_header(get_lang('Add/edit prerequisites').' '.$lpItem->getTitle());
6102
6103
        $return .= '<form method="POST">';
6104
        $return .= '<div class="table-responsive">';
6105
        $return .= '<table class="table table-hover">';
6106
        $return .= '<thead>';
6107
        $return .= '<tr>';
6108
        $return .= '<th>'.get_lang('Prerequisites').'</th>';
6109
        $return .= '<th width="140">'.get_lang('minimum').'</th>';
6110
        $return .= '<th width="140">'.get_lang('maximum').'</th>';
6111
        $return .= '</tr>';
6112
        $return .= '</thead>';
6113
6114
        // Adding the none option to the prerequisites see http://www.chamilo.org/es/node/146
6115
        $return .= '<tbody>';
6116
        $return .= '<tr>';
6117
        $return .= '<td colspan="3">';
6118
        $return .= '<div class="radio learnpath"><label for="idnone">';
6119
        $return .= '<input checked="checked" id="idnone" name="prerequisites" type="radio" />';
6120
        $return .= get_lang('none').'</label>';
6121
        $return .= '</div>';
6122
        $return .= '</tr>';
6123
6124
        // @todo use entitites
6125
        $tblLpItem = Database::get_course_table(TABLE_LP_ITEM);
6126
        $sql = "SELECT * FROM $tblLpItem
6127
                WHERE lp_id = ".$this->lp_id;
6128
        $result = Database::query($sql);
6129
6130
        $selectedMinScore = [];
6131
        $selectedMaxScore = [];
6132
        $masteryScore = [];
6133
        while ($row = Database::fetch_array($result)) {
6134
            if ($row['iid'] == $itemId) {
6135
                $selectedMinScore[$row['prerequisite']] = $row['prerequisite_min_score'];
6136
                $selectedMaxScore[$row['prerequisite']] = $row['prerequisite_max_score'];
6137
            }
6138
            $masteryScore[$row['iid']] = $row['mastery_score'];
6139
        }
6140
6141
        $displayOrder = $lpItem->getDisplayOrder();
6142
        $lpItemRepo = Container::getLpItemRepository();
6143
        $itemRoot = $lpItemRepo->getRootItem($this->get_id());
6144
        $em = Database::getManager();
6145
6146
        $currentItemId = $itemId;
6147
        $options = [
6148
            'decorate' => true,
6149
            'rootOpen' => function () {
6150
                return '';
6151
            },
6152
            'rootClose' => function () {
6153
                return '';
6154
            },
6155
            'childOpen' => function () {
6156
                return '';
6157
            },
6158
            'childClose' => '',
6159
            'nodeDecorator' => function ($item) use (
6160
                $currentItemId,
6161
                $preRequisiteId,
6162
                $courseId,
6163
                $selectedMaxScore,
6164
                $selectedMinScore,
6165
                $displayOrder,
6166
                $lpItemRepo,
6167
                $em
6168
            ) {
6169
                $itemId = $item['iid'];
6170
                $type = $item['itemType'];
6171
                $iconName = str_replace(' ', '', $type);
6172
                switch ($iconName) {
6173
                    case 'category':
6174
                    case 'chapter':
6175
                    case 'folder':
6176
                    case 'dir':
6177
                        $icon = Display::getMdiIcon(ObjectIcon::CHAPTER, 'ch-tool-icon', '', ICON_SIZE_TINY);
6178
                        break;
6179
                    default:
6180
                        $icon = Display::getMdiIcon(ObjectIcon::SINGLE_ELEMENT, 'ch-tool-icon', '', ICON_SIZE_TINY);
6181
                        break;
6182
                }
6183
6184
                if ($itemId == $currentItemId) {
6185
                    return '';
6186
                }
6187
6188
                if ($displayOrder < $item['displayOrder']) {
6189
                    return '';
6190
                }
6191
6192
                $selectedMaxScoreValue = isset($selectedMaxScore[$itemId]) ? $selectedMaxScore[$itemId] : $item['maxScore'];
6193
                $selectedMinScoreValue = $selectedMinScore[$itemId] ?? 0;
6194
                $masteryScoreAsMinValue = $masteryScore[$itemId] ?? 0;
6195
6196
                $return = '<tr>';
6197
                $return .= '<td '.((TOOL_QUIZ != $type && TOOL_HOTPOTATOES != $type) ? ' colspan="3"' : '').'>';
6198
                $return .= '<div style="margin-left:'.($item['lvl'] * 20).'px;" class="radio learnpath">';
6199
                $return .= '<label for="id'.$itemId.'">';
6200
6201
                $checked = '';
6202
                if (null !== $preRequisiteId) {
6203
                    $checked = in_array($preRequisiteId, [$itemId, $item['ref']]) ? ' checked="checked" ' : '';
6204
                }
6205
6206
                $disabled = 'dir' === $type ? ' disabled="disabled" ' : '';
6207
6208
                $return .= '<input
6209
                    '.$checked.' '.$disabled.'
6210
                    id="id'.$itemId.'"
6211
                    name="prerequisites"
6212
                    type="radio"
6213
                    value="'.$itemId.'" />';
6214
6215
                $return .= $icon.'&nbsp;&nbsp;'.$item['title'].'</label>';
6216
                $return .= '</div>';
6217
                $return .= '</td>';
6218
6219
                if (TOOL_QUIZ == $type) {
6220
                    // let's update max_score Tests information depending of the Tests Advanced properties
6221
                    $exercise = new Exercise($courseId);
6222
                    /** @var CLpItem $itemEntity */
6223
                    $itemEntity = $lpItemRepo->find($itemId);
6224
                    $exercise->read($item['path']);
6225
                    $itemEntity->setMaxScore($exercise->getMaxScore());
6226
                    $em->persist($itemEntity);
6227
                    $em->flush($itemEntity);
6228
6229
                    $item['maxScore'] = $exercise->getMaxScore();
6230
6231
                    if (empty($selectedMinScoreValue) && !empty($masteryScoreAsMinValue)) {
6232
                        // Backwards compatibility with 1.9.x use mastery_score as min value
6233
                        $selectedMinScoreValue = $masteryScoreAsMinValue;
6234
                    }
6235
                    $return .= '<td>';
6236
                    $return .= '<input
6237
                        class="form-control"
6238
                        size="4" maxlength="3"
6239
                        name="min_'.$itemId.'"
6240
                        type="number"
6241
                        min="0"
6242
                        step="any"
6243
                        max="'.$item['maxScore'].'"
6244
                        value="'.$selectedMinScoreValue.'"
6245
                    />';
6246
                    $return .= '</td>';
6247
                    $return .= '<td>';
6248
                    $return .= '<input
6249
                        class="form-control"
6250
                        size="4"
6251
                        maxlength="3"
6252
                        name="max_'.$itemId.'"
6253
                        type="number"
6254
                        min="0"
6255
                        step="any"
6256
                        max="'.$item['maxScore'].'"
6257
                        value="'.$selectedMaxScoreValue.'"
6258
                    />';
6259
                        $return .= '</td>';
6260
                    }
6261
6262
                if (TOOL_HOTPOTATOES == $type) {
6263
                    $return .= '<td>';
6264
                    $return .= '<input
6265
                        size="4"
6266
                        maxlength="3"
6267
                        name="min_'.$itemId.'"
6268
                        type="number"
6269
                        min="0"
6270
                        step="any"
6271
                        max="'.$item['maxScore'].'"
6272
                        value="'.$selectedMinScoreValue.'"
6273
                    />';
6274
                        $return .= '</td>';
6275
                        $return .= '<td>';
6276
                        $return .= '<input
6277
                        size="4"
6278
                        maxlength="3"
6279
                        name="max_'.$itemId.'"
6280
                        type="number"
6281
                        min="0"
6282
                        step="any"
6283
                        max="'.$item['maxScore'].'"
6284
                        value="'.$selectedMaxScoreValue.'"
6285
                    />';
6286
                    $return .= '</td>';
6287
                }
6288
                $return .= '</tr>';
6289
6290
                return $return;
6291
            },
6292
        ];
6293
6294
        $tree = $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
6295
        $return .= $tree;
6296
        $return .= '</tbody>';
6297
        $return .= '</table>';
6298
        $return .= '</div>';
6299
        $return .= '<div class="form-group">';
6300
        $return .= '<button class="btn btn--primary" name="submit_button" type="submit">'.
6301
            get_lang('Save prerequisites settings').'</button>';
6302
        $return .= '</form>';
6303
6304
        return $return;
6305
    }
6306
6307
    /**
6308
     * Return HTML list to allow prerequisites selection for lp.
6309
     */
6310
    public function display_lp_prerequisites_list(FormValidator $form)
6311
    {
6312
        $lp_id = $this->lp_id;
6313
        $lp = api_get_lp_entity($lp_id);
6314
        $prerequisiteId = $lp->getPrerequisite();
6315
6316
        $repo = Container::getLpRepository();
6317
        $qb = $repo->findAllByCourse(api_get_course_entity(), api_get_session_entity());
6318
        /** @var CLp[] $lps */
6319
        $lps = $qb->getQuery()->getResult();
6320
6321
        //$session_id = api_get_session_id();
6322
        /*$session_condition = api_get_session_condition($session_id, true, true);
6323
        $sql = "SELECT * FROM $tbl_lp
6324
                WHERE c_id = $course_id $session_condition
6325
                ORDER BY display_order ";
6326
        $rs = Database::query($sql);*/
6327
6328
        $items = [get_lang('none')];
6329
        foreach ($lps as $lp) {
6330
            $myLpId = $lp->getIid();
6331
            if ($myLpId == $lp_id) {
6332
                continue;
6333
            }
6334
            $items[$myLpId] = $lp->getTitle();
6335
            /*$return .= '<option
6336
                value="'.$myLpId.'" '.(($myLpId == $prerequisiteId) ? ' selected ' : '').'>'.
6337
                $lp->getName().
6338
                '</option>';*/
6339
        }
6340
6341
        $select = $form->addSelect('prerequisites', get_lang('Prerequisites'), $items);
6342
        $select->setSelected($prerequisiteId);
6343
    }
6344
6345
    /**
6346
     * Creates a list with all the documents in it.
6347
     *
6348
     * @param bool $showInvisibleFiles
6349
     *
6350
     * @throws Exception
6351
     *
6352
     *
6353
     * @return string
6354
     */
6355
    public function get_documents($showInvisibleFiles = false)
6356
    {
6357
        $sessionId = api_get_session_id();
6358
        $documentTree = DocumentManager::get_document_preview(
6359
            api_get_course_entity(),
6360
            $this->lp_id,
6361
            null,
6362
            $sessionId,
6363
            true,
6364
            null,
6365
            null,
6366
            $showInvisibleFiles,
6367
            true,
6368
            false,
6369
            true,
6370
            false,
6371
            [],
6372
            [],
6373
            ['file', 'folder']
6374
        );
6375
6376
        $form = new FormValidator(
6377
            'form_upload',
6378
            'POST',
6379
            $this->getCurrentBuildingModeURL(),
6380
            '',
6381
            ['enctype' => 'multipart/form-data']
6382
        );
6383
6384
        $folders = DocumentManager::get_all_document_folders(
6385
            api_get_course_info(),
6386
            0,
6387
            true
6388
        );
6389
6390
        $folder = $this->generate_lp_folder(api_get_course_info());
6391
6392
        DocumentManager::build_directory_selector(
6393
            $folders,
6394
            $folder->getIid(),
6395
            [],
6396
            true,
6397
            $form,
6398
            'directory_parent_id'
6399
        );
6400
6401
        $group = [
6402
            $form->createElement(
6403
                'radio',
6404
                'if_exists',
6405
                get_lang('If file exists:'),
6406
                get_lang('Do nothing'),
6407
                'nothing'
6408
            ),
6409
            $form->createElement(
6410
                'radio',
6411
                'if_exists',
6412
                null,
6413
                get_lang('Overwrite the existing file'),
6414
                'overwrite'
6415
            ),
6416
            $form->createElement(
6417
                'radio',
6418
                'if_exists',
6419
                null,
6420
                get_lang('Rename the uploaded file if it exists'),
6421
                'rename'
6422
            ),
6423
        ];
6424
        $form->addGroup($group, null, get_lang('If file exists:'));
6425
6426
        $fileExistsOption = api_get_setting('document.document_if_file_exists_option');
6427
        $defaultFileExistsOption = 'rename';
6428
        if (!empty($fileExistsOption)) {
6429
            $defaultFileExistsOption = $fileExistsOption;
6430
        }
6431
        $form->setDefaults(['if_exists' => $defaultFileExistsOption]);
6432
6433
        // Check box options
6434
        $form->addCheckBox(
6435
            'unzip',
6436
            get_lang('Options'),
6437
            get_lang('Uncompress zip')
6438
        );
6439
6440
        $url = api_get_path(WEB_AJAX_PATH).'document.ajax.php?'.api_get_cidreq().'&a=upload_file&curdirpath=';
6441
        $form->addMultipleUpload($url);
6442
6443
        $lpItem = (new CLpItem())
6444
            ->setTitle('')
6445
            ->setItemType(TOOL_DOCUMENT)
6446
        ;
6447
        $new = $this->displayDocumentForm('add', $lpItem);
6448
6449
        $videosTree = $this->get_videos();
6450
        $headers = [
6451
            get_lang('Files'),
6452
            get_lang('Videos'),
6453
            get_lang('Create a new document'),
6454
            get_lang('Upload'),
6455
        ];
6456
6457
        return Display::tabs(
6458
            $headers,
6459
            [$documentTree, $videosTree, $new, $form->returnForm()],
6460
            'subtab',
6461
            ['class' => 'mt-2']
6462
        );
6463
    }
6464
6465
    public function get_videos()
6466
    {
6467
        $sessionId = api_get_session_id();
6468
6469
        $documentTree = DocumentManager::get_document_preview(
6470
            api_get_course_entity(),
6471
            $this->lp_id,
6472
            null,
6473
            $sessionId,
6474
            true,
6475
            null,
6476
            null,
6477
            false,
6478
            false,
6479
            false,
6480
            true,
6481
            false,
6482
            [],
6483
            [],
6484
            'video'
6485
        );
6486
6487
        return $documentTree ?: get_lang('No videos found');
6488
    }
6489
6490
    /**
6491
     * Creates a list with all the exercises (quiz) in it.
6492
     *
6493
     * @return string
6494
     */
6495
    public function get_exercises()
6496
    {
6497
        $course_id = api_get_course_int_id();
6498
        $session_id = api_get_session_id();
6499
        $setting = 'true' === api_get_setting('lp.show_invisible_exercise_in_lp_toc');
6500
6501
        //$activeCondition = ' active <> -1 ';
6502
        $active = 2;
6503
        if ($setting) {
6504
            $active = 1;
6505
            //$activeCondition = ' active = 1 ';
6506
        }
6507
6508
        $categoryCondition = '';
6509
6510
        $keyword = $_REQUEST['keyword'] ?? null;
6511
        $categoryId = $_REQUEST['category_id'] ?? null;
6512
        /*if (api_get_configuration_value('allow_exercise_categories') && !empty($categoryId)) {
6513
            $categoryCondition = " AND exercise_category_id = $categoryId ";
6514
        }
6515
6516
        $keywordCondition = '';
6517
6518
        if (!empty($keyword)) {
6519
            $keyword = Database::escape_string($keyword);
6520
            $keywordCondition = " AND title LIKE '%$keyword%' ";
6521
        }
6522
        */
6523
        $course = api_get_course_entity($course_id);
6524
        $session = api_get_session_entity($session_id);
6525
6526
        $qb = Container::getQuizRepository()->findAllByCourse($course, $session, $keyword, $active, false, $categoryId);
6527
        /** @var CQuiz[] $exercises */
6528
        $exercises = $qb->getQuery()->getResult();
6529
6530
        /*$sql_quiz = "SELECT * FROM $tbl_quiz
6531
                     WHERE
6532
                            c_id = $course_id AND
6533
                            $activeCondition
6534
                            $condition_session
6535
                            $categoryCondition
6536
                            $keywordCondition
6537
                     ORDER BY title ASC";
6538
        $res_quiz = Database::query($sql_quiz);*/
6539
6540
        $currentUrl = api_get_self().'?'.api_get_cidreq().'&action=add_item&type=step&lp_id='.$this->lp_id.'#resource_tab-2';
6541
6542
        // Create a search-box
6543
        /*$form = new FormValidator('search_simple', 'get', $currentUrl);
6544
        $form->addHidden('action', 'add_item');
6545
        $form->addHidden('type', 'step');
6546
        $form->addHidden('lp_id', $this->lp_id);
6547
        $form->addHidden('lp_build_selected', '2');
6548
6549
        $form->addCourseHiddenParams();
6550
        $form->addText(
6551
            'keyword',
6552
            get_lang('Search'),
6553
            false,
6554
            [
6555
                'aria-label' => get_lang('Search'),
6556
            ]
6557
        );
6558
6559
        if (api_get_configuration_value('allow_exercise_categories')) {
6560
            $manager = new ExerciseCategoryManager();
6561
            $options = $manager->getCategoriesForSelect(api_get_course_int_id());
6562
            if (!empty($options)) {
6563
                $form->addSelect(
6564
                    'category_id',
6565
                    get_lang('Category'),
6566
                    $options,
6567
                    ['placeholder' => get_lang('Please select an option')]
6568
                );
6569
            }
6570
        }
6571
6572
        $form->addButtonSearch(get_lang('Search'));
6573
        $return = $form->returnForm();*/
6574
6575
        $return = '<ul class="mt-2 bg-white list-group lp_resource">';
6576
        $return .= '<li class="list-group-item lp_resource_element disable_drag">';
6577
        $return .= Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, 32, get_lang('New test'));
6578
        $return .= '<a
6579
            href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise_admin.php?'.api_get_cidreq().'&lp_id='.$this->lp_id.'">'.
6580
            get_lang('New test').'</a>';
6581
        $return .= '</li>';
6582
6583
        $previewIcon = Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview'));
6584
        $quizIcon = Display::getMdiIcon('order-bool-ascending-variant', 'ch-tool-icon', null, 16, get_lang('Exercise'));
6585
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6586
        $exerciseUrl = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq();
6587
        foreach ($exercises as $exercise) {
6588
            $exerciseId = $exercise->getIid();
6589
            $title = strip_tags(api_html_entity_decode($exercise->getTitle()));
6590
            $visibility = $exercise->isVisible($course, $session);
6591
6592
            $link = Display::url(
6593
                $previewIcon,
6594
                $exerciseUrl.'&exerciseId='.$exerciseId,
6595
                ['target' => '_blank']
6596
            );
6597
            $return .= '<li
6598
                class="list-group-item lp_resource_element"
6599
                id="'.$exerciseId.'"
6600
                data-id="'.$exerciseId.'"
6601
                title="'.$title.'">';
6602
            $return .= Display::url($moveIcon, '#', ['class' => 'moved']);
6603
            $return .= $quizIcon;
6604
            $sessionStar = '';
6605
            /*$sessionStar = api_get_session_image(
6606
                $row_quiz['session_id'],
6607
                $userInfo['status']
6608
            );*/
6609
            $return .= Display::url(
6610
                Security::remove_XSS(cut($title, 80)).$link.$sessionStar,
6611
                api_get_self().'?'.
6612
                    api_get_cidreq().'&action=add_item&type='.TOOL_QUIZ.'&file='.$exerciseId.'&lp_id='.$this->lp_id,
6613
                [
6614
                    'class' => false === $visibility ? 'moved text-muted ' : 'moved link_with_id',
6615
                    'data_type' => 'quiz',
6616
                    'data-id' => $exerciseId,
6617
                ]
6618
            );
6619
            $return .= '</li>';
6620
        }
6621
6622
        $return .= '</ul>';
6623
6624
        return $return;
6625
    }
6626
6627
    /**
6628
     * Creates a list with all the links in it.
6629
     *
6630
     * @return string
6631
     */
6632
    public function get_links()
6633
    {
6634
        $sessionId = api_get_session_id();
6635
        $repo = Container::getLinkRepository();
6636
6637
        $course = api_get_course_entity();
6638
        $session = api_get_session_entity($sessionId);
6639
        $qb = $repo->getResourcesByCourse($course, $session);
6640
        /** @var CLink[] $links */
6641
        $links = $qb->getQuery()->getResult();
6642
6643
        $selfUrl = api_get_self();
6644
        $courseIdReq = api_get_cidreq();
6645
        $userInfo = api_get_user_info();
6646
6647
        $moveEverywhereIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6648
6649
        $categorizedLinks = [];
6650
        $categories = [];
6651
6652
        foreach ($links as $link) {
6653
            $categoryId = null !== $link->getCategory() ? $link->getCategory()->getIid() : 0;
6654
            if (empty($categoryId)) {
6655
                $categories[0] = get_lang('Uncategorized');
6656
            } else {
6657
                $category = $link->getCategory();
6658
                $categories[$categoryId] = $category->getTitle();
6659
            }
6660
            $categorizedLinks[$categoryId][$link->getIid()] = $link;
6661
        }
6662
6663
        $linksHtmlCode =
6664
            '<script>
6665
            function toggle_tool(tool, id) {
6666
                if(document.getElementById(tool+"_"+id+"_content").style.display == "none"){
6667
                    document.getElementById(tool+"_"+id+"_content").style.display = "block";
6668
                    document.getElementById(tool+"_"+id+"_opener").src = "'.Display::returnIconPath('remove.gif').'";
6669
                } else {
6670
                    document.getElementById(tool+"_"+id+"_content").style.display = "none";
6671
                    document.getElementById(tool+"_"+id+"_opener").src = "'.Display::returnIconPath('add.png').'";
6672
                }
6673
            }
6674
        </script>
6675
6676
        <ul class="mt-2 bg-white list-group lp_resource">
6677
            <li class="list-group-item lp_resource_element disable_drag ">
6678
                '.Display::getMdiIcon(ObjectIcon::LINK, 'ch-tool-icon', null, ICON_SIZE_SMALL).'
6679
                <a
6680
                href="'.api_get_path(WEB_CODE_PATH).'link/link.php?'.$courseIdReq.'&action=addlink&lp_id='.$this->lp_id.'"
6681
                title="'.get_lang('Add a link').'">'.
6682
                get_lang('Add a link').'
6683
                </a>
6684
            </li>';
6685
        $linkIcon = Display::getMdiIcon('file-link', 'ch-tool-icon', null, 16, get_lang('Link'));
6686
        foreach ($categorizedLinks as $categoryId => $links) {
6687
            $linkNodes = null;
6688
            /** @var CLink $link */
6689
            foreach ($links as $key => $link) {
6690
                $title = $link->getTitle();
6691
                $id = $link->getIid();
6692
                $linkUrl = Display::url(
6693
                    Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6694
                    api_get_path(WEB_CODE_PATH).'link/link_goto.php?'.api_get_cidreq().'&link_id='.$key,
6695
                    ['target' => '_blank']
6696
                );
6697
6698
                if ($link->isVisible($course, $session)) {
6699
                    //$sessionStar = api_get_session_image($linkSessionId, $userInfo['status']);
6700
                    $sessionStar = '';
6701
                    $url = $selfUrl.'?'.$courseIdReq.'&action=add_item&type='.TOOL_LINK.'&file='.$key.'&lp_id='.$this->lp_id;
6702
                    $link = Display::url(
6703
                        Security::remove_XSS($title).$sessionStar.$linkUrl,
6704
                        $url,
6705
                        [
6706
                            'class' => 'moved link_with_id',
6707
                            'data-id' => $key,
6708
                            'data_type' => TOOL_LINK,
6709
                            'title' => $title,
6710
                        ]
6711
                    );
6712
                    $linkNodes .=
6713
                        "<li
6714
                            class='list-group-item lp_resource_element'
6715
                            id= $id
6716
                            data-id= $id
6717
                            >
6718
                         <a class='moved' href='#'>
6719
                            $moveEverywhereIcon
6720
                        </a>
6721
                        $linkIcon $link
6722
                        </li>";
6723
                }
6724
            }
6725
            $linksHtmlCode .=
6726
                '<li class="list-group-item disable_drag">
6727
                    <a style="cursor:hand" onclick="javascript: toggle_tool(\''.TOOL_LINK.'\','.$categoryId.')" >
6728
                        <img src="'.Display::returnIconPath('add.png').'" id="'.TOOL_LINK.'_'.$categoryId.'_opener"
6729
                        align="absbottom" />
6730
                    </a>
6731
                    <span style="vertical-align:middle">'.Security::remove_XSS($categories[$categoryId]).'</span>
6732
                </li>
6733
            '.
6734
                $linkNodes.
6735
            '';
6736
            //<div style="display:none" id="'.TOOL_LINK.'_'.$categoryId.'_content">'.
6737
        }
6738
        $linksHtmlCode .= '</ul>';
6739
6740
        return $linksHtmlCode;
6741
    }
6742
6743
    /**
6744
     * Creates a list with all the student publications in it.
6745
     *
6746
     * @return string
6747
     */
6748
    public function get_student_publications()
6749
    {
6750
        $return = '<ul class="mt-2 bg-white list-group lp_resource">';
6751
        $return .= '<li class="list-group-item lp_resource_element">';
6752
        $works = getWorkListTeacher(0, 100, null, null, null);
6753
        if (!empty($works)) {
6754
            $icon = Display::getMdiIcon('inbox-full', 'ch-tool-icon',null, 16, get_lang('Student publication'));
6755
            foreach ($works as $work) {
6756
                $workId = $work['iid'];
6757
                $link = Display::url(
6758
                    Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6759
                    api_get_path(WEB_CODE_PATH).'work/work_list_all.php?'.api_get_cidreq().'&id='.$workId,
6760
                    ['target' => '_blank']
6761
                );
6762
6763
                $return .= '<li
6764
                    class="list-group-item lp_resource_element"
6765
                    id="'.$workId.'"
6766
                    data-id="'.$workId.'"
6767
                    >';
6768
                $return .= '<a class="moved" href="#">';
6769
                $return .= Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6770
                $return .= '</a> ';
6771
6772
                $return .= $icon;
6773
                $return .= Display::url(
6774
                    Security::remove_XSS(cut(strip_tags($work['title']), 80)).' '.$link,
6775
                    api_get_self().'?'.
6776
                    api_get_cidreq().'&action=add_item&type='.TOOL_STUDENTPUBLICATION.'&file='.$work['iid'].'&lp_id='.$this->lp_id,
6777
                    [
6778
                        'class' => 'moved link_with_id',
6779
                        'data-id' => $work['iid'],
6780
                        'data_type' => TOOL_STUDENTPUBLICATION,
6781
                        'title' => Security::remove_XSS(cut(strip_tags($work['title']), 80)),
6782
                    ]
6783
                );
6784
                $return .= '</li>';
6785
            }
6786
        }
6787
6788
        $return .= '</ul>';
6789
6790
        return $return;
6791
    }
6792
6793
    /**
6794
     * Creates a list with all the forums in it.
6795
     *
6796
     * @return string
6797
     */
6798
    public function get_forums()
6799
    {
6800
        $forumCategories = get_forum_categories();
6801
        $forumsInNoCategory = get_forums_in_category(0);
6802
        if (!empty($forumsInNoCategory)) {
6803
            $forumCategories = array_merge(
6804
                $forumCategories,
6805
                [
6806
                    [
6807
                        'cat_id' => 0,
6808
                        'session_id' => 0,
6809
                        'visibility' => 1,
6810
                        'cat_comment' => null,
6811
                    ],
6812
                ]
6813
            );
6814
        }
6815
6816
        $a_forums = [];
6817
        $courseEntity = api_get_course_entity(api_get_course_int_id());
6818
        $sessionEntity = api_get_session_entity(api_get_session_id());
6819
6820
        foreach ($forumCategories as $forumCategory) {
6821
            // The forums in this category.
6822
            $forumsInCategory = get_forums_in_category($forumCategory->getIid());
6823
            if (!empty($forumsInCategory)) {
6824
                foreach ($forumsInCategory as $forum) {
6825
                    if ($forum->isVisible($courseEntity, $sessionEntity)) {
6826
                        $a_forums[] = $forum;
6827
                    }
6828
                }
6829
            }
6830
        }
6831
6832
        $return = '<ul class="mt-2 bg-white list-group lp_resource">';
6833
6834
        // First add link
6835
        $return .= '<li class="list-group-item lp_resource_element disable_drag">';
6836
        $return .= Display::getMdiIcon('comment-quote	', 'ch-tool-icon', null, 32, get_lang('Create a new forum'));
6837
        $return .= Display::url(
6838
            get_lang('Create a new forum'),
6839
            api_get_path(WEB_CODE_PATH).'forum/index.php?'.api_get_cidreq().'&'.http_build_query([
6840
                'action' => 'add',
6841
                'content' => 'forum',
6842
                'lp_id' => $this->lp_id,
6843
            ]),
6844
            ['title' => get_lang('Create a new forum')]
6845
        );
6846
        $return .= '</li>';
6847
6848
        $return .= '<script>
6849
            function toggle_forum(forum_id) {
6850
                if (document.getElementById("forum_"+forum_id+"_content").style.display == "none") {
6851
                    document.getElementById("forum_"+forum_id+"_content").style.display = "block";
6852
                    document.getElementById("forum_"+forum_id+"_opener").src = "'.Display::returnIconPath('remove.gif').'";
6853
                } else {
6854
                    document.getElementById("forum_"+forum_id+"_content").style.display = "none";
6855
                    document.getElementById("forum_"+forum_id+"_opener").src = "'.Display::returnIconPath('add.png').'";
6856
                }
6857
            }
6858
        </script>';
6859
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6860
        $userRights = api_is_allowed_to_edit(false, true);
6861
        foreach ($a_forums as $forum) {
6862
            $forumSession = $forum->getFirstResourceLink()->getSession();
6863
            $isForumSession = (null !== $forumSession);
6864
            $forumId = $forum->getIid();
6865
            $title = Security::remove_XSS($forum->getTitle());
6866
            $link = Display::url(
6867
                Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6868
                api_get_path(WEB_CODE_PATH).'forum/viewforum.php?'.api_get_cidreq().'&forum='.$forumId,
6869
                ['target' => '_blank']
6870
            );
6871
6872
            $return .= '<li
6873
                    class="list-group-item lp_resource_element"
6874
                    id="'.$forumId.'"
6875
                    data-id="'.$forumId.'"
6876
                    >';
6877
            $return .= '<a class="moved" href="#">';
6878
            $return .= $moveIcon;
6879
            $return .= ' </a>';
6880
            $return .= Display::getMdiIcon('comment-quote', 'ch-tool-icon', null, 16, get_lang('Forum'));
6881
6882
            $moveLink = Display::url(
6883
                $title,
6884
                api_get_self().'?'.
6885
                api_get_cidreq().'&action=add_item&type='.TOOL_FORUM.'&forum_id='.$forumId.'&lp_id='.$this->lp_id,
6886
                [
6887
                    'class' => 'moved link_with_id',
6888
                    'data-id' => $forumId,
6889
                    'data_type' => TOOL_FORUM,
6890
                    'title' => $title,
6891
                    'style' => 'vertical-align:middle',
6892
                ]
6893
            );
6894
            $return .= '<a onclick="javascript:toggle_forum('.$forumId.');" style="cursor:hand; vertical-align:middle">
6895
                    <img
6896
                        src="'.Display::returnIconPath('add.png').'"
6897
                        id="forum_'.$forumId.'_opener" align="absbottom"
6898
                     />
6899
                </a>
6900
                '.$moveLink;
6901
            $return .= '</li>';
6902
6903
            $return .= '<div style="display:none" id="forum_'.$forumId.'_content">';
6904
            $threads = get_threads($forumId);
6905
            if (is_array($threads)) {
6906
                foreach ($threads as $thread) {
6907
                    $threadId = $thread->getIid();
6908
                    $link = Display::url(
6909
                        Display::getMdiIcon('magnify-plus-outline', 'ch-tool-icon', null, 22, get_lang('Preview')),
6910
                        api_get_path(WEB_CODE_PATH).
6911
                        'forum/viewthread.php?'.api_get_cidreq().'&forum='.$forumId.'&thread='.$threadId,
6912
                        ['target' => '_blank']
6913
                    );
6914
6915
                    $return .= '<li
6916
                        class="list-group-item lp_resource_element"
6917
                      id="'.$threadId.'"
6918
                        data-id="'.$threadId.'"
6919
                    >';
6920
                    $return .= '&nbsp;<a class="moved" href="#">';
6921
                    $return .= $moveIcon;
6922
                    $return .= ' </a>';
6923
                    $return .= Display::getMdiIcon('format-quote-open', 'ch-tool-icon', null, 16, get_lang('Thread'));
6924
                    $return .= '<a
6925
                        class="moved link_with_id"
6926
                        data-id="'.$threadId.'"
6927
                        data_type="'.TOOL_THREAD.'"
6928
                        title="'.$thread->getTitle().'"
6929
                        href="'.api_get_self().'?'.api_get_cidreq().'&action=add_item&type='.TOOL_THREAD.'&thread_id='.$threadId.'&lp_id='.$this->lp_id.'"
6930
                        >'.
6931
                        Security::remove_XSS($thread->getTitle()).' '.$link.'</a>';
6932
                    $return .= '</li>';
6933
                }
6934
            }
6935
            $return .= '</div>';
6936
        }
6937
        $return .= '</ul>';
6938
6939
        return $return;
6940
    }
6941
6942
    /**
6943
     * Creates a list with all the surveys in it.
6944
     *
6945
     * @return string
6946
     */
6947
    public function getSurveys()
6948
    {
6949
        $return = '<ul class="mt-2 bg-white list-group lp_resource">';
6950
6951
        // First add link
6952
        $return .= '<li class="list-group-item lp_resource_element disable_drag">';
6953
        $return .= Display::getMdiIcon('clipboard-question-outline', 'ch-tool-icon', null, 32, get_lang('CreateNewSurvey'));
6954
        $return .= Display::url(
6955
            get_lang('Create a new survey'),
6956
            api_get_path(WEB_CODE_PATH).'survey/create_new_survey.php?'.api_get_cidreq().'&'.http_build_query([
6957
                'action' => 'add',
6958
                'lp_id' => $this->lp_id,
6959
            ]),
6960
            ['title' => get_lang('Create a new survey')]
6961
        );
6962
        $return .= '</li>';
6963
6964
        $surveys = SurveyManager::get_surveys(api_get_course_id(), api_get_session_id());
6965
        $moveIcon = Display::getMdiIcon('cursor-move', 'ch-tool-icon', '', 16, get_lang('Move'));
6966
6967
        foreach ($surveys as $survey) {
6968
            if (!empty($survey['iid'])) {
6969
                $surveyTitle = strip_tags($survey['title']);
6970
                $return .= '<li class="list-group-item lp_resource_element" id="'.$survey['iid'].'" data-id="'.$survey['iid'].'">';
6971
                $return .= '<a class="moved" href="#">';
6972
                $return .= $moveIcon;
6973
                $return .= ' </a>';
6974
                $return .= Display::getMdiIcon('poll', 'ch-tool-icon', null, 16, get_lang('Survey'));
6975
                $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>';
6976
                $return .= '</li>';
6977
            }
6978
        }
6979
6980
        $return .= '</ul>';
6981
6982
        return $return;
6983
    }
6984
6985
    /**
6986
     * Temp function to be moved in main_api or the best place around for this.
6987
     * Creates a file path if it doesn't exist.
6988
     *
6989
     * @param string $path
6990
     */
6991
    public function create_path($path)
6992
    {
6993
        $path_bits = explode('/', dirname($path));
6994
6995
        // IS_WINDOWS_OS has been defined in main_api.lib.php
6996
        $path_built = IS_WINDOWS_OS ? '' : '/';
6997
        foreach ($path_bits as $bit) {
6998
            if (!empty($bit)) {
6999
                $new_path = $path_built.$bit;
7000
                if (is_dir($new_path)) {
7001
                    $path_built = $new_path.'/';
7002
                } else {
7003
                    mkdir($new_path, api_get_permissions_for_new_directories());
7004
                    $path_built = $new_path.'/';
7005
                }
7006
            }
7007
        }
7008
    }
7009
7010
    /**
7011
     * @param int    $lp_id
7012
     * @param string $status
7013
     */
7014
    public function set_autolaunch($lp_id, $status)
7015
    {
7016
        $status = (int) $status;
7017
        $em = Database::getManager();
7018
        $repo = Container::getLpRepository();
7019
7020
        $session = api_get_session_entity();
7021
        $course = api_get_course_entity();
7022
7023
        $qb = $repo->getResourcesByCourse($course, $session);
7024
        $lps = $qb->getQuery()->getResult();
7025
7026
        foreach ($lps as $lp) {
7027
            $lp->setAutoLaunch(0);
7028
            $em->persist($lp);
7029
        }
7030
7031
        $em->flush();
7032
7033
        if ($status === 1) {
7034
            $lp = $repo->find($lp_id);
7035
            if ($lp) {
7036
                $lp->setAutolaunch(1);
7037
                $em->persist($lp);
7038
            }
7039
            $em->flush();
7040
        }
7041
    }
7042
7043
    /**
7044
     * Gets previous_item_id for the next element of the lp_item table.
7045
     *
7046
     * @author Isaac flores paz
7047
     *
7048
     * @return int Previous item ID
7049
     */
7050
    public function select_previous_item_id()
7051
    {
7052
        $course_id = api_get_course_int_id();
7053
        $table_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7054
7055
        // Get the max order of the items
7056
        $sql = "SELECT max(display_order) AS display_order FROM $table_lp_item
7057
                WHERE c_id = $course_id AND lp_id = ".$this->lp_id;
7058
        $rs_max_order = Database::query($sql);
7059
        $row_max_order = Database::fetch_object($rs_max_order);
7060
        $max_order = $row_max_order->display_order;
7061
        // Get the previous item ID
7062
        $sql = "SELECT iid as previous FROM $table_lp_item
7063
                WHERE
7064
                    c_id = $course_id AND
7065
                    lp_id = ".$this->lp_id." AND
7066
                    display_order = '$max_order' ";
7067
        $rs_max = Database::query($sql);
7068
        $row_max = Database::fetch_object($rs_max);
7069
7070
        // Return the previous item ID
7071
        return $row_max->previous;
7072
    }
7073
7074
    /**
7075
     * Copies an LP.
7076
     */
7077
    public function copy()
7078
    {
7079
        // Course builder
7080
        $cb = new CourseBuilder();
7081
7082
        //Setting tools that will be copied
7083
        $cb->set_tools_to_build(['learnpaths']);
7084
7085
        //Setting elements that will be copied
7086
        $cb->set_tools_specific_id_list(
7087
            ['learnpaths' => [$this->lp_id]]
7088
        );
7089
7090
        $course = $cb->build();
7091
7092
        //Course restorer
7093
        $course_restorer = new CourseRestorer($course);
7094
        $course_restorer->set_add_text_in_items(true);
7095
        $course_restorer->set_tool_copy_settings(
7096
            ['learnpaths' => ['reset_dates' => true]]
7097
        );
7098
        $course_restorer->restore(
7099
            api_get_course_id(),
7100
            api_get_session_id(),
7101
            false,
7102
            false
7103
        );
7104
    }
7105
7106
    /**
7107
     * Verify document size.
7108
     *
7109
     * @param string $s
7110
     *
7111
     * @return bool
7112
     */
7113
    public static function verify_document_size($s)
7114
    {
7115
        $post_max = ini_get('post_max_size');
7116
        if ('M' == substr($post_max, -1, 1)) {
7117
            $post_max = intval(substr($post_max, 0, -1)) * 1024 * 1024;
7118
        } elseif ('G' == substr($post_max, -1, 1)) {
7119
            $post_max = intval(substr($post_max, 0, -1)) * 1024 * 1024 * 1024;
7120
        }
7121
        $upl_max = ini_get('upload_max_filesize');
7122
        if ('M' == substr($upl_max, -1, 1)) {
7123
            $upl_max = intval(substr($upl_max, 0, -1)) * 1024 * 1024;
7124
        } elseif ('G' == substr($upl_max, -1, 1)) {
7125
            $upl_max = intval(substr($upl_max, 0, -1)) * 1024 * 1024 * 1024;
7126
        }
7127
7128
        $repo = Container::getDocumentRepository();
7129
        $documents_total_space = $repo->getTotalSpace(api_get_course_int_id());
7130
7131
        $course_max_space = DocumentManager::get_course_quota();
7132
        $total_size = filesize($s) + $documents_total_space;
7133
        if (filesize($s) > $post_max || filesize($s) > $upl_max || $total_size > $course_max_space) {
7134
            return true;
7135
        }
7136
7137
        return false;
7138
    }
7139
7140
    /**
7141
     * Clear LP prerequisites.
7142
     */
7143
    public function clearPrerequisites()
7144
    {
7145
        $course_id = $this->get_course_int_id();
7146
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7147
        $lp_id = $this->get_id();
7148
        // Cleaning prerequisites
7149
        $sql = "UPDATE $tbl_lp_item SET prerequisite = ''
7150
                WHERE lp_id = $lp_id";
7151
        Database::query($sql);
7152
7153
        // Cleaning mastery score for exercises
7154
        $sql = "UPDATE $tbl_lp_item SET mastery_score = ''
7155
                WHERE lp_id = $lp_id AND item_type = 'quiz'";
7156
        Database::query($sql);
7157
    }
7158
7159
    public function set_previous_step_as_prerequisite_for_all_items()
7160
    {
7161
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
7162
        $course_id = $this->get_course_int_id();
7163
        $lp_id = $this->get_id();
7164
7165
        if (!empty($this->items)) {
7166
            $previous_item_id = null;
7167
            $previous_item_max = 0;
7168
            $previous_item_type = null;
7169
            $last_item_not_dir = null;
7170
            $last_item_not_dir_type = null;
7171
            $last_item_not_dir_max = null;
7172
7173
            foreach ($this->ordered_items as $itemId) {
7174
                $item = $this->getItem($itemId);
7175
                // if there was a previous item... (otherwise jump to set it)
7176
                if (!empty($previous_item_id)) {
7177
                    $current_item_id = $item->get_id(); //save current id
7178
                    if ('dir' != $item->get_type()) {
7179
                        // Current item is not a folder, so it qualifies to get a prerequisites
7180
                        if ('quiz' == $last_item_not_dir_type) {
7181
                            // if previous is quiz, mark its max score as default score to be achieved
7182
                            $sql = "UPDATE $tbl_lp_item SET mastery_score = '$last_item_not_dir_max'
7183
                                    WHERE c_id = $course_id AND lp_id = $lp_id AND iid = $last_item_not_dir";
7184
                            Database::query($sql);
7185
                        }
7186
                        // now simply update the prerequisite to set it to the last non-chapter item
7187
                        $sql = "UPDATE $tbl_lp_item SET prerequisite = '$last_item_not_dir'
7188
                                WHERE lp_id = $lp_id AND iid = $current_item_id";
7189
                        Database::query($sql);
7190
                        // record item as 'non-chapter' reference
7191
                        $last_item_not_dir = $item->get_id();
7192
                        $last_item_not_dir_type = $item->get_type();
7193
                        $last_item_not_dir_max = $item->get_max();
7194
                    }
7195
                } else {
7196
                    if ('dir' != $item->get_type()) {
7197
                        // Current item is not a folder (but it is the first item) so record as last "non-chapter" item
7198
                        $last_item_not_dir = $item->get_id();
7199
                        $last_item_not_dir_type = $item->get_type();
7200
                        $last_item_not_dir_max = $item->get_max();
7201
                    }
7202
                }
7203
                // Saving the item as "previous item" for the next loop
7204
                $previous_item_id = $item->get_id();
7205
                $previous_item_max = $item->get_max();
7206
                $previous_item_type = $item->get_type();
7207
            }
7208
        }
7209
    }
7210
7211
    /**
7212
     * @param array $params
7213
     *
7214
     * @return int
7215
     */
7216
    public static function createCategory($params)
7217
    {
7218
        $courseEntity = api_get_course_entity(api_get_course_int_id());
7219
7220
        $item = new CLpCategory();
7221
        $item
7222
            ->setTitle($params['name'])
7223
            ->setParent($courseEntity)
7224
            ->addCourseLink($courseEntity, api_get_session_entity())
7225
        ;
7226
7227
        $repo = Container::getLpCategoryRepository();
7228
        $repo->create($item);
7229
7230
        return $item->getIid();
7231
    }
7232
7233
    /**
7234
     * @param array $params
7235
     */
7236
    public static function updateCategory($params)
7237
    {
7238
        $em = Database::getManager();
7239
        /** @var CLpCategory $item */
7240
        $item = $em->find(CLpCategory::class, $params['id']);
7241
        if ($item) {
7242
            $item->setTitle($params['name']);
7243
            $em->persist($item);
7244
            $em->flush();
7245
        }
7246
    }
7247
7248
    public static function moveUpCategory(int $id): void
7249
    {
7250
        $em = Database::getManager();
7251
        /** @var CLpCategory $item */
7252
        $item = $em->find(CLpCategory::class, $id);
7253
        if ($item) {
7254
            $course = api_get_course_entity();
7255
            $session = api_get_session_entity();
7256
7257
            $link = $item->resourceNode->getResourceLinkByContext($course, $session);
7258
7259
            if ($link) {
7260
                $link->moveUpPosition();
7261
7262
                $em->flush();
7263
            }
7264
        }
7265
    }
7266
7267
    public static function moveDownCategory(int $id): void
7268
    {
7269
        $em = Database::getManager();
7270
        /** @var CLpCategory $item */
7271
        $item = $em->find(CLpCategory::class, $id);
7272
        if ($item) {
7273
            $course = api_get_course_entity();
7274
            $session = api_get_session_entity();
7275
7276
            $link = $item->resourceNode->getResourceLinkByContext($course, $session);
7277
7278
            if ($link) {
7279
                $link->moveDownPosition();
7280
7281
                $em->flush();
7282
            }
7283
        }
7284
    }
7285
7286
    /**
7287
     * @param int $courseId
7288
     *
7289
     * @return int
7290
     */
7291
    public static function getCountCategories($courseId)
7292
    {
7293
        if (empty($courseId)) {
7294
            return 0;
7295
        }
7296
        $repo = Container::getLpCategoryRepository();
7297
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId));
7298
        $qb->addSelect('count(resource)');
7299
7300
        return (int) $qb->getQuery()->getSingleScalarResult();
7301
    }
7302
7303
    /**
7304
     * @param int $courseId
7305
     *
7306
     * @return CLpCategory[]
7307
     */
7308
    public static function getCategories($courseId)
7309
    {
7310
        // Using doctrine extensions
7311
        $repo = Container::getLpCategoryRepository();
7312
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId), api_get_session_entity(), null, null, true, true);
7313
7314
        return $qb->getQuery()->getResult();
7315
    }
7316
7317
    public static function getCategorySessionId($id)
7318
    {
7319
        if ('true' !== api_get_setting('lp.allow_session_lp_category')) {
7320
            return 0;
7321
        }
7322
7323
        $repo = Container::getLpCategoryRepository();
7324
        /** @var CLpCategory $category */
7325
        $category = $repo->find($id);
7326
7327
        $sessionId = 0;
7328
        $link = $category->getFirstResourceLink();
7329
        if ($link && $link->getSession()) {
7330
            $sessionId = (int) $link->getSession()->getId();
7331
        }
7332
7333
        return $sessionId;
7334
    }
7335
7336
    public static function deleteCategory(int $id): bool
7337
    {
7338
        $repo = Container::getLpCategoryRepository();
7339
        /** @var CLpCategory $category */
7340
        $category = $repo->find($id);
7341
        if ($category) {
7342
            $em = Database::getManager();
7343
            $lps = $category->getLps();
7344
7345
            foreach ($lps as $lp) {
7346
                $lp->setCategory(null);
7347
                $em->persist($lp);
7348
            }
7349
7350
            $course = api_get_course_entity();
7351
            $session = api_get_session_entity();
7352
7353
            $em->getRepository(ResourceLink::class)->removeByResourceInContext($category, $course, $session);
7354
7355
            return true;
7356
        }
7357
7358
        return false;
7359
    }
7360
7361
    /**
7362
     * @param int  $courseId
7363
     * @param bool $addSelectOption
7364
     *
7365
     * @return array
7366
     */
7367
    public static function getCategoryFromCourseIntoSelect($courseId, $addSelectOption = false)
7368
    {
7369
        $repo = Container::getLpCategoryRepository();
7370
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId), api_get_session_entity());
7371
        $items = $qb->getQuery()->getResult();
7372
7373
        $cats = [];
7374
        if ($addSelectOption) {
7375
            $cats = [get_lang('Select a category')];
7376
        }
7377
7378
        if (!empty($items)) {
7379
            foreach ($items as $cat) {
7380
                $cats[$cat->getIid()] = $cat->getTitle();
7381
            }
7382
        }
7383
7384
        return $cats;
7385
    }
7386
7387
    /**
7388
     * @param int   $courseId
7389
     * @param int   $lpId
7390
     * @param int   $user_id
7391
     *
7392
     * @return learnpath
7393
     */
7394
    public static function getLpFromSession(int $courseId, int $lpId, int $user_id)
7395
    {
7396
        $debug = 0;
7397
        $learnPath = null;
7398
        $lpObject = Session::read('lpobject');
7399
7400
        $repo = Container::getLpRepository();
7401
        $lp = $repo->find($lpId);
7402
        if (null !== $lpObject) {
7403
            /** @var learnpath $learnPath */
7404
            $learnPath = UnserializeApi::unserialize('lp', $lpObject);
7405
            $learnPath->entity = $lp;
7406
            if ($debug) {
7407
                error_log('getLpFromSession: unserialize');
7408
                error_log('------getLpFromSession------');
7409
                error_log('------unserialize------');
7410
                error_log("lp_view_session_id: ".$learnPath->lp_view_session_id);
7411
                error_log("api_get_sessionid: ".api_get_session_id());
7412
            }
7413
        }
7414
7415
        if (!is_object($learnPath)) {
7416
            $learnPath = new learnpath($lp, api_get_course_info_by_id($courseId), $user_id);
7417
            if ($debug) {
7418
                error_log('------getLpFromSession------');
7419
                error_log('getLpFromSession: create new learnpath');
7420
                error_log("create new LP with $courseId - $lpId - $user_id");
7421
                error_log("lp_view_session_id: ".$learnPath->lp_view_session_id);
7422
                error_log("api_get_sessionid: ".api_get_session_id());
7423
            }
7424
        }
7425
7426
        return $learnPath;
7427
    }
7428
7429
    /**
7430
     * @param int $itemId
7431
     *
7432
     * @return learnpathItem|false
7433
     */
7434
    public function getItem($itemId)
7435
    {
7436
        if (isset($this->items[$itemId]) && is_object($this->items[$itemId])) {
7437
            return $this->items[$itemId];
7438
        }
7439
7440
        return false;
7441
    }
7442
7443
    /**
7444
     * @return int
7445
     */
7446
    public function getCurrentAttempt()
7447
    {
7448
        $attempt = $this->getItem($this->get_current_item_id());
7449
        if ($attempt) {
7450
            return $attempt->get_attempt_id();
7451
        }
7452
7453
        return 0;
7454
    }
7455
7456
    /**
7457
     * @return int
7458
     */
7459
    public function getCategoryId()
7460
    {
7461
        return (int) $this->categoryId;
7462
    }
7463
7464
    /**
7465
     * Get whether this is a learning path with the possibility to subscribe
7466
     * users or not.
7467
     *
7468
     * @return int
7469
     */
7470
    public function getSubscribeUsers()
7471
    {
7472
        return $this->subscribeUsers;
7473
    }
7474
7475
    /**
7476
     * Calculate the count of stars for a user in this LP
7477
     * This calculation is based on the following rules:
7478
     * - the student gets one star when he gets to 50% of the learning path
7479
     * - the student gets a second star when the average score of all tests inside the learning path >= 50%
7480
     * - the student gets a third star when the average score of all tests inside the learning path >= 80%
7481
     * - the student gets the final star when the score for the *last* test is >= 80%.
7482
     *
7483
     * @param int $sessionId Optional. The session ID
7484
     *
7485
     * @return int The count of stars
7486
     */
7487
    public function getCalculateStars($sessionId = 0)
7488
    {
7489
        $stars = 0;
7490
        $progress = self::getProgress(
7491
            $this->lp_id,
7492
            $this->user_id,
7493
            $this->course_int_id,
7494
            $sessionId
7495
        );
7496
7497
        if ($progress >= 50) {
7498
            $stars++;
7499
        }
7500
7501
        // Calculate stars chapters evaluation
7502
        $exercisesItems = $this->getExercisesItems();
7503
7504
        if (!empty($exercisesItems)) {
7505
            $totalResult = 0;
7506
7507
            foreach ($exercisesItems as $exerciseItem) {
7508
                $exerciseResultInfo = Event::getExerciseResultsByUser(
7509
                    $this->user_id,
7510
                    $exerciseItem->path,
7511
                    $this->course_int_id,
7512
                    $sessionId,
7513
                    $this->lp_id,
7514
                    $exerciseItem->db_id
7515
                );
7516
7517
                $exerciseResultInfo = end($exerciseResultInfo);
7518
7519
                if (!$exerciseResultInfo) {
7520
                    continue;
7521
                }
7522
7523
                if (!empty($exerciseResultInfo['max_score'])) {
7524
                    $exerciseResult = $exerciseResultInfo['score'] * 100 / $exerciseResultInfo['max_score'];
7525
                } else {
7526
                    $exerciseResult = 0;
7527
                }
7528
                $totalResult += $exerciseResult;
7529
            }
7530
7531
            $totalExerciseAverage = $totalResult / (count($exercisesItems) > 0 ? count($exercisesItems) : 1);
7532
7533
            if ($totalExerciseAverage >= 50) {
7534
                $stars++;
7535
            }
7536
7537
            if ($totalExerciseAverage >= 80) {
7538
                $stars++;
7539
            }
7540
        }
7541
7542
        // Calculate star for final evaluation
7543
        $finalEvaluationItem = $this->getFinalEvaluationItem();
7544
7545
        if (!empty($finalEvaluationItem)) {
7546
            $evaluationResultInfo = Event::getExerciseResultsByUser(
7547
                $this->user_id,
7548
                $finalEvaluationItem->path,
7549
                $this->course_int_id,
7550
                $sessionId,
7551
                $this->lp_id,
7552
                $finalEvaluationItem->db_id
7553
            );
7554
7555
            $evaluationResultInfo = end($evaluationResultInfo);
7556
7557
            if ($evaluationResultInfo) {
7558
                $evaluationResult = $evaluationResultInfo['score'] * 100 / $evaluationResultInfo['max_score'];
7559
                if ($evaluationResult >= 80) {
7560
                    $stars++;
7561
                }
7562
            }
7563
        }
7564
7565
        return $stars;
7566
    }
7567
7568
    /**
7569
     * Get the items of exercise type.
7570
     *
7571
     * @return array The items. Otherwise return false
7572
     */
7573
    public function getExercisesItems()
7574
    {
7575
        $exercises = [];
7576
        foreach ($this->items as $item) {
7577
            if ('quiz' !== $item->type) {
7578
                continue;
7579
            }
7580
            $exercises[] = $item;
7581
        }
7582
7583
        array_pop($exercises);
7584
7585
        return $exercises;
7586
    }
7587
7588
    /**
7589
     * Get the item of exercise type (evaluation type).
7590
     *
7591
     * @return array The final evaluation. Otherwise return false
7592
     */
7593
    public function getFinalEvaluationItem()
7594
    {
7595
        $exercises = [];
7596
        foreach ($this->items as $item) {
7597
            if (TOOL_QUIZ !== $item->type) {
7598
                continue;
7599
            }
7600
7601
            $exercises[] = $item;
7602
        }
7603
7604
        return array_pop($exercises);
7605
    }
7606
7607
    /**
7608
     * Calculate the total points achieved for the current user in this learning path.
7609
     *
7610
     * @param int $sessionId Optional. The session Id
7611
     *
7612
     * @return int
7613
     */
7614
    public function getCalculateScore($sessionId = 0)
7615
    {
7616
        // Calculate stars chapters evaluation
7617
        $exercisesItems = $this->getExercisesItems();
7618
        $finalEvaluationItem = $this->getFinalEvaluationItem();
7619
        $totalExercisesResult = 0;
7620
        $totalEvaluationResult = 0;
7621
7622
        if (false !== $exercisesItems) {
7623
            foreach ($exercisesItems as $exerciseItem) {
7624
                $exerciseResultInfo = Event::getExerciseResultsByUser(
7625
                    $this->user_id,
7626
                    $exerciseItem->path,
7627
                    $this->course_int_id,
7628
                    $sessionId,
7629
                    $this->lp_id,
7630
                    $exerciseItem->db_id
7631
                );
7632
7633
                $exerciseResultInfo = end($exerciseResultInfo);
7634
7635
                if (!$exerciseResultInfo) {
7636
                    continue;
7637
                }
7638
7639
                $totalExercisesResult += $exerciseResultInfo['score'];
7640
            }
7641
        }
7642
7643
        if (!empty($finalEvaluationItem)) {
7644
            $evaluationResultInfo = Event::getExerciseResultsByUser(
7645
                $this->user_id,
7646
                $finalEvaluationItem->path,
7647
                $this->course_int_id,
7648
                $sessionId,
7649
                $this->lp_id,
7650
                $finalEvaluationItem->db_id
7651
            );
7652
7653
            $evaluationResultInfo = end($evaluationResultInfo);
7654
7655
            if ($evaluationResultInfo) {
7656
                $totalEvaluationResult += $evaluationResultInfo['score'];
7657
            }
7658
        }
7659
7660
        return $totalExercisesResult + $totalEvaluationResult;
7661
    }
7662
7663
    /**
7664
     * Check if URL is not allowed to be show in a iframe.
7665
     *
7666
     * @param string $src
7667
     *
7668
     * @return string
7669
     */
7670
    public function fixBlockedLinks($src)
7671
    {
7672
        $urlInfo = parse_url($src);
7673
7674
        $platformProtocol = 'https';
7675
        if (false === strpos(api_get_path(WEB_CODE_PATH), 'https')) {
7676
            $platformProtocol = 'http';
7677
        }
7678
7679
        $protocolFixApplied = false;
7680
        //Scheme validation to avoid "Notices" when the lesson doesn't contain a valid scheme
7681
        $scheme = isset($urlInfo['scheme']) ? $urlInfo['scheme'] : null;
7682
        $host = isset($urlInfo['host']) ? $urlInfo['host'] : null;
7683
7684
        if ($platformProtocol != $scheme) {
7685
            Session::write('x_frame_source', $src);
7686
            $src = 'blank.php?error=x_frames_options';
7687
            $protocolFixApplied = true;
7688
        }
7689
7690
        if (false == $protocolFixApplied) {
7691
            if (false === strpos(api_get_path(WEB_PATH), $host)) {
7692
                // Check X-Frame-Options
7693
                $ch = curl_init();
7694
                $options = [
7695
                    CURLOPT_URL => $src,
7696
                    CURLOPT_RETURNTRANSFER => true,
7697
                    CURLOPT_HEADER => true,
7698
                    CURLOPT_FOLLOWLOCATION => true,
7699
                    CURLOPT_ENCODING => "",
7700
                    CURLOPT_AUTOREFERER => true,
7701
                    CURLOPT_CONNECTTIMEOUT => 120,
7702
                    CURLOPT_TIMEOUT => 120,
7703
                    CURLOPT_MAXREDIRS => 10,
7704
                ];
7705
7706
                $proxySettings = api_get_setting('platform.proxy_settings', true);
7707
                if (!empty($proxySettings) &&
7708
                    isset($proxySettings['curl_setopt_array'])
7709
                ) {
7710
                    $options[CURLOPT_PROXY] = $proxySettings['curl_setopt_array']['CURLOPT_PROXY'];
7711
                    $options[CURLOPT_PROXYPORT] = $proxySettings['curl_setopt_array']['CURLOPT_PROXYPORT'];
7712
                }
7713
7714
                curl_setopt_array($ch, $options);
7715
                $response = curl_exec($ch);
7716
                $httpCode = curl_getinfo($ch);
7717
                $headers = substr($response, 0, $httpCode['header_size']);
7718
7719
                $error = false;
7720
                if (stripos($headers, 'X-Frame-Options: DENY') > -1
7721
                    //|| stripos($headers, 'X-Frame-Options: SAMEORIGIN') > -1
7722
                ) {
7723
                    $error = true;
7724
                }
7725
7726
                if ($error) {
7727
                    Session::write('x_frame_source', $src);
7728
                    $src = 'blank.php?error=x_frames_options';
7729
                }
7730
            }
7731
        }
7732
7733
        return $src;
7734
    }
7735
7736
    /**
7737
     * Check if this LP has a created forum in the basis course.
7738
     *
7739
     * @deprecated
7740
     *
7741
     * @return bool
7742
     */
7743
    public function lpHasForum()
7744
    {
7745
        $forumTable = Database::get_course_table(TABLE_FORUM);
7746
        $itemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
7747
7748
        $fakeFrom = "
7749
            $forumTable f
7750
            INNER JOIN $itemProperty ip
7751
            ON (f.forum_id = ip.ref AND f.c_id = ip.c_id)
7752
        ";
7753
7754
        $resultData = Database::select(
7755
            'COUNT(f.iid) AS qty',
7756
            $fakeFrom,
7757
            [
7758
                'where' => [
7759
                    'ip.visibility != ? AND ' => 2,
7760
                    'ip.tool = ? AND ' => TOOL_FORUM,
7761
                    'f.c_id = ? AND ' => intval($this->course_int_id),
7762
                    'f.lp_id = ?' => intval($this->lp_id),
7763
                ],
7764
            ],
7765
            'first'
7766
        );
7767
7768
        return $resultData['qty'] > 0;
7769
    }
7770
7771
    /**
7772
     * Get the forum for this learning path.
7773
     *
7774
     * @param int $sessionId
7775
     *
7776
     * @return array
7777
     */
7778
    public function getForum($sessionId = 0)
7779
    {
7780
        $repo = Container::getForumRepository();
7781
7782
        $course = api_get_course_entity();
7783
        $session = api_get_session_entity($sessionId);
7784
        $qb = $repo->getResourcesByCourse($course, $session);
7785
7786
        return $qb->getQuery()->getResult();
7787
    }
7788
7789
    /**
7790
     * Get the LP Final Item form.
7791
     *
7792
     * @throws Exception
7793
     *
7794
     *
7795
     * @return string
7796
     */
7797
    public function getFinalItemForm()
7798
    {
7799
        $finalItem = $this->getFinalItem();
7800
        $title = '';
7801
7802
        if ($finalItem) {
7803
            $title = $finalItem->get_title();
7804
            $buttonText = get_lang('Save');
7805
            $content = $this->getSavedFinalItem();
7806
        } else {
7807
            $buttonText = get_lang('Add this document to the course');
7808
            $content = $this->getFinalItemTemplate();
7809
        }
7810
7811
        $editorConfig = [
7812
            'ToolbarSet' => 'LearningPathDocuments',
7813
            'Width' => '100%',
7814
            'Height' => '500',
7815
            'FullPage' => true,
7816
        ];
7817
7818
        $url = api_get_self().'?'.api_get_cidreq().'&'.http_build_query([
7819
            'type' => 'document',
7820
            'lp_id' => $this->lp_id,
7821
        ]);
7822
7823
        $form = new FormValidator('final_item', 'POST', $url);
7824
        $form->addText('title', get_lang('Title'));
7825
        $form->addButtonSave($buttonText);
7826
        $form->addHtml(
7827
            Display::return_message(
7828
                'Variables :</br></br> <b>((certificate))</b> </br> <b>((skill))</b>',
7829
                'normal',
7830
                false
7831
            )
7832
        );
7833
7834
        $renderer = $form->defaultRenderer();
7835
        $renderer->setElementTemplate('&nbsp;{label}{element}', 'content_lp_certificate');
7836
7837
        $form->addHtmlEditor(
7838
            'content_lp_certificate',
7839
            null,
7840
            true,
7841
            false,
7842
            $editorConfig
7843
        );
7844
        $form->addHidden('action', 'add_final_item');
7845
        $form->addHidden('path', Session::read('pathItem'));
7846
        $form->addHidden('previous', $this->get_last());
7847
        $form->setDefaults(
7848
            ['title' => $title, 'content_lp_certificate' => $content]
7849
        );
7850
7851
        if ($form->validate()) {
7852
            $values = $form->exportValues();
7853
            $lastItemId = $this->getLastInFirstLevel();
7854
7855
            if (!$finalItem) {
7856
                $documentId = $this->create_document(
7857
                    $this->course_info,
7858
                    $values['content_lp_certificate'],
7859
                    $values['title']
7860
                );
7861
                $this->add_item(
7862
                    null,
7863
                    $lastItemId,
7864
                    'final_item',
7865
                    $documentId,
7866
                    $values['title'],
7867
                );
7868
7869
                Display::addFlash(
7870
                    Display::return_message(get_lang('Added'))
7871
                );
7872
            } else {
7873
                $this->edit_document();
7874
            }
7875
        }
7876
7877
        return $form->returnForm();
7878
    }
7879
7880
    /**
7881
     * Check if the current lp item is first, both, last or none from lp list.
7882
     *
7883
     * @param int $currentItemId
7884
     *
7885
     * @return string
7886
     */
7887
    public function isFirstOrLastItem($currentItemId)
7888
    {
7889
        $lpItemId = [];
7890
        $typeListNotToVerify = self::getChapterTypes();
7891
7892
        // Using get_toc() function instead $this->items because returns the correct order of the items
7893
        foreach ($this->get_toc() as $item) {
7894
            if (!in_array($item['type'], $typeListNotToVerify)) {
7895
                $lpItemId[] = $item['id'];
7896
            }
7897
        }
7898
7899
        $lastLpItemIndex = count($lpItemId) - 1;
7900
        $position = array_search($currentItemId, $lpItemId);
7901
7902
        switch ($position) {
7903
            case 0:
7904
                if (!$lastLpItemIndex) {
7905
                    $answer = 'both';
7906
                    break;
7907
                }
7908
7909
                $answer = 'first';
7910
                break;
7911
            case $lastLpItemIndex:
7912
                $answer = 'last';
7913
                break;
7914
            default:
7915
                $answer = 'none';
7916
        }
7917
7918
        return $answer;
7919
    }
7920
7921
    /**
7922
     * Get whether this is a learning path with the accumulated SCORM time or not.
7923
     *
7924
     * @return int
7925
     */
7926
    public function getAccumulateScormTime()
7927
    {
7928
        return $this->accumulateScormTime;
7929
    }
7930
7931
    /**
7932
     * Returns an HTML-formatted link to a resource, to incorporate directly into
7933
     * the new learning path tool.
7934
     *
7935
     * The function is a big switch on tool type.
7936
     * In each case, we query the corresponding table for information and build the link
7937
     * with that information.
7938
     *
7939
     * @author Yannick Warnier <[email protected]> - rebranding based on
7940
     * previous work (display_addedresource_link_in_learnpath())
7941
     *
7942
     * @param int $course_id      Course code
7943
     * @param int $learningPathId The learning path ID (in lp table)
7944
     * @param int $id_in_path     the unique index in the items table
7945
     * @param int $lpViewId
7946
     *
7947
     * @return string
7948
     */
7949
    public static function rl_get_resource_link_for_learnpath(
7950
        $course_id,
7951
        $learningPathId,
7952
        $id_in_path,
7953
        $lpViewId
7954
    ) {
7955
        $session_id = api_get_session_id();
7956
7957
        $learningPathId = (int) $learningPathId;
7958
        $id_in_path = (int) $id_in_path;
7959
        $lpViewId = (int) $lpViewId;
7960
7961
        $em = Database::getManager();
7962
        $lpItemRepo = $em->getRepository(CLpItem::class);
7963
7964
        /** @var CLpItem $rowItem */
7965
        $rowItem = $lpItemRepo->findOneBy([
7966
            'lp' => $learningPathId,
7967
            'iid' => $id_in_path,
7968
        ]);
7969
        $type = $rowItem->getItemType();
7970
        $id = empty($rowItem->getPath()) ? '0' : $rowItem->getPath();
7971
        $main_dir_path = api_get_path(WEB_CODE_PATH);
7972
        $link = '';
7973
        $extraParams = api_get_cidreq(true, true, 'learnpath').'&sid='.$session_id;
7974
7975
        switch ($type) {
7976
            case 'dir':
7977
                return $main_dir_path.'lp/blank.php';
7978
            case TOOL_CALENDAR_EVENT:
7979
                return $main_dir_path.'calendar/agenda.php?agenda_id='.$id.'&'.$extraParams;
7980
            case TOOL_ANNOUNCEMENT:
7981
                return $main_dir_path.'announcements/announcements.php?ann_id='.$id.'&'.$extraParams;
7982
            case TOOL_LINK:
7983
                $linkInfo = Link::getLinkInfo($id);
7984
                if (isset($linkInfo['url'])) {
7985
                    return $linkInfo['url'];
7986
                }
7987
7988
                return '';
7989
            case TOOL_QUIZ:
7990
                if (empty($id)) {
7991
                    return '';
7992
                }
7993
7994
                // Get the lp_item_view with the highest view_count.
7995
                $learnpathItemViewResult = $em
7996
                    ->getRepository(CLpItemView::class)
7997
                    ->findBy(
7998
                        ['item' => $rowItem->getIid(), 'view' => $lpViewId],
7999
                        ['viewCount' => 'DESC'],
8000
                        1
8001
                    );
8002
                /** @var CLpItemView $learnpathItemViewData */
8003
                $learnpathItemViewData = current($learnpathItemViewResult);
8004
                $learnpathItemViewId = $learnpathItemViewData ? $learnpathItemViewData->getIid() : 0;
8005
8006
                return $main_dir_path.'exercise/overview.php?'.$extraParams.'&'
8007
                    .http_build_query([
8008
                        'lp_init' => 1,
8009
                        'learnpath_item_view_id' => $learnpathItemViewId,
8010
                        'learnpath_id' => $learningPathId,
8011
                        'learnpath_item_id' => $id_in_path,
8012
                        'exerciseId' => $id,
8013
                    ]);
8014
            case TOOL_HOTPOTATOES:
8015
                return '';
8016
            case TOOL_FORUM:
8017
                return $main_dir_path.'forum/viewforum.php?forum='.$id.'&lp=true&'.$extraParams;
8018
            case TOOL_THREAD:
8019
                // forum post
8020
                $tbl_topics = Database::get_course_table(TABLE_FORUM_THREAD);
8021
                if (empty($id)) {
8022
                    return '';
8023
                }
8024
                $sql = "SELECT * FROM $tbl_topics WHERE iid=$id";
8025
                $result = Database::query($sql);
8026
                $row = Database::fetch_array($result);
8027
8028
                return $main_dir_path.'forum/viewthread.php?thread='.$id.'&forum='.$row['forum_id'].'&lp=true&'
8029
                    .$extraParams;
8030
            case TOOL_POST:
8031
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8032
                $result = Database::query("SELECT * FROM $tbl_post WHERE post_id=$id");
8033
                $row = Database::fetch_array($result);
8034
8035
                return $main_dir_path.'forum/viewthread.php?post='.$id.'&thread='.$row['thread_id'].'&forum='
8036
                    .$row['forum_id'].'&lp=true&'.$extraParams;
8037
            case TOOL_READOUT_TEXT:
8038
                return api_get_path(WEB_CODE_PATH).
8039
                    'lp/readout_text.php?&id='.$id.'&lp_id='.$learningPathId.'&'.$extraParams;
8040
            case TOOL_DOCUMENT:
8041
            case 'video':
8042
                $repo = Container::getDocumentRepository();
8043
                $document = $repo->find($rowItem->getPath());
8044
                if ($document) {
8045
                    $params = [
8046
                        'cid' => $course_id,
8047
                        'sid' => $session_id,
8048
                    ];
8049
8050
                    return $repo->getResourceFileUrl($document, $params, UrlGeneratorInterface::ABSOLUTE_URL);
8051
                }
8052
8053
                return null;
8054
            case TOOL_LP_FINAL_ITEM:
8055
                return api_get_path(WEB_CODE_PATH).'lp/lp_final_item.php?&id='.$id.'&lp_id='.$learningPathId.'&'
8056
                    .$extraParams;
8057
            case 'assignments':
8058
                return $main_dir_path.'work/work.php?'.$extraParams;
8059
            case TOOL_DROPBOX:
8060
                return $main_dir_path.'dropbox/index.php?'.$extraParams;
8061
            case 'introduction_text': //DEPRECATED
8062
                return '';
8063
            case TOOL_COURSE_DESCRIPTION:
8064
                return $main_dir_path.'course_description?'.$extraParams;
8065
            case TOOL_GROUP:
8066
                return $main_dir_path.'group/group.php?'.$extraParams;
8067
            case TOOL_USER:
8068
                return $main_dir_path.'user/user.php?'.$extraParams;
8069
            case TOOL_STUDENTPUBLICATION:
8070
                $repo = Container::getStudentPublicationRepository();
8071
                $publication = $repo->find($rowItem->getPath());
8072
                if ($publication && $publication->hasResourceNode()) {
8073
                    $nodeId = $publication->getResourceNode()->getId();
8074
                    $assignmentId = $publication->getIid();
8075
8076
                    return api_get_path(WEB_PATH) .
8077
                        "resources/assignment/$nodeId/submission/$assignmentId?" .
8078
                        http_build_query([
8079
                            'cid' => $course_id,
8080
                            'sid' => $session_id,
8081
                            'gid' => 0,
8082
                            'origin' => 'learnpath',
8083
                            'isStudentView' => 'true',
8084
                        ]);
8085
                }
8086
                return '';
8087
            case TOOL_SURVEY:
8088
8089
                $surveyId = (int) $id;
8090
                $repo = Container::getSurveyRepository();
8091
                if (!empty($surveyId)) {
8092
                    /** @var CSurvey $survey */
8093
                    $survey = $repo->find($surveyId);
8094
                    $autoSurveyLink = SurveyUtil::generateFillSurveyLink(
8095
                        $survey,
8096
                        'auto',
8097
                        api_get_course_entity($course_id),
8098
                        $session_id
8099
                    );
8100
                    $lpParams = [
8101
                        'lp_id' => $learningPathId,
8102
                        'lp_item_id' => $id_in_path,
8103
                        'origin' => 'learnpath',
8104
                    ];
8105
8106
                    return $autoSurveyLink.'&'.http_build_query($lpParams).'&'.$extraParams;
8107
                }
8108
        }
8109
8110
        return $link;
8111
    }
8112
8113
    /**
8114
     * Checks if any forum items in a given learning path are from the base course.
8115
     */
8116
    public static function isForumFromBaseCourse(int $learningPathId): bool
8117
    {
8118
        $itemRepository = Container::getLpItemRepository();
8119
        $forumRepository = Container::getForumRepository();
8120
        $forums = $itemRepository->findItemsByLearningPathAndType($learningPathId, 'forum');
8121
8122
        /* @var CLpItem $forumItem */
8123
        foreach ($forums as $forumItem) {
8124
            $forumId = (int) $forumItem->getPath();
8125
            $forum = $forumRepository->find($forumId);
8126
8127
            if ($forum !== null) {
8128
                $forumSession = $forum->getFirstResourceLink()->getSession();
8129
                if ($forumSession === null) {
8130
                    return true;
8131
                }
8132
            }
8133
        }
8134
8135
        return false;
8136
    }
8137
8138
    /**
8139
     * Gets the name of a resource (generally used in learnpath when no name is provided).
8140
     *
8141
     * @author Yannick Warnier <[email protected]>
8142
     *
8143
     * @param string $course_code    Course code
8144
     * @param int    $learningPathId
8145
     * @param int    $id_in_path     The resource ID
8146
     *
8147
     * @return string
8148
     */
8149
    public static function rl_get_resource_name($course_code, $learningPathId, $id_in_path)
8150
    {
8151
        $_course = api_get_course_info($course_code);
8152
        if (empty($_course)) {
8153
            return '';
8154
        }
8155
        $course_id = $_course['real_id'];
8156
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
8157
        $learningPathId = (int) $learningPathId;
8158
        $id_in_path = (int) $id_in_path;
8159
8160
        $sql = "SELECT item_type, title, ref
8161
                FROM $tbl_lp_item
8162
                WHERE c_id = $course_id AND lp_id = $learningPathId AND iid = $id_in_path";
8163
        $res_item = Database::query($sql);
8164
8165
        if (Database::num_rows($res_item) < 1) {
8166
            return '';
8167
        }
8168
        $row_item = Database::fetch_array($res_item);
8169
        $type = strtolower($row_item['item_type']);
8170
        $id = $row_item['ref'];
8171
        $output = '';
8172
8173
        switch ($type) {
8174
            case TOOL_CALENDAR_EVENT:
8175
                $TABLEAGENDA = Database::get_course_table(TABLE_AGENDA);
8176
                $result = Database::query("SELECT * FROM $TABLEAGENDA WHERE c_id = $course_id AND id=$id");
8177
                $myrow = Database::fetch_array($result);
8178
                $output = $myrow['title'];
8179
                break;
8180
            case TOOL_ANNOUNCEMENT:
8181
                $tbl_announcement = Database::get_course_table(TABLE_ANNOUNCEMENT);
8182
                $result = Database::query("SELECT * FROM $tbl_announcement WHERE c_id = $course_id AND id=$id");
8183
                $myrow = Database::fetch_array($result);
8184
                $output = $myrow['title'];
8185
                break;
8186
            case TOOL_LINK:
8187
                // Doesn't take $target into account.
8188
                $TABLETOOLLINK = Database::get_course_table(TABLE_LINK);
8189
                $result = Database::query("SELECT * FROM $TABLETOOLLINK WHERE c_id = $course_id AND id=$id");
8190
                $myrow = Database::fetch_array($result);
8191
                $output = $myrow['title'];
8192
                break;
8193
            case TOOL_QUIZ:
8194
                $TBL_EXERCICES = Database::get_course_table(TABLE_QUIZ_TEST);
8195
                $result = Database::query("SELECT * FROM $TBL_EXERCICES WHERE c_id = $course_id AND id = $id");
8196
                $myrow = Database::fetch_array($result);
8197
                $output = $myrow['title'];
8198
                break;
8199
            case TOOL_FORUM:
8200
                $TBL_FORUMS = Database::get_course_table(TABLE_FORUM);
8201
                $result = Database::query("SELECT * FROM $TBL_FORUMS WHERE c_id = $course_id AND forum_id = $id");
8202
                $myrow = Database::fetch_array($result);
8203
                $output = $myrow['title'];
8204
                break;
8205
            case TOOL_THREAD:
8206
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8207
                // Grabbing the title of the post.
8208
                $sql_title = "SELECT * FROM $tbl_post WHERE c_id = $course_id AND post_id=".$id;
8209
                $result_title = Database::query($sql_title);
8210
                $myrow_title = Database::fetch_array($result_title);
8211
                $output = $myrow_title['title'];
8212
                break;
8213
            case TOOL_POST:
8214
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
8215
                $sql = "SELECT * FROM $tbl_post p WHERE c_id = $course_id AND p.post_id = $id";
8216
                $result = Database::query($sql);
8217
                $post = Database::fetch_array($result);
8218
                $output = $post['title'];
8219
                break;
8220
            case 'dir':
8221
            case TOOL_DOCUMENT:
8222
            case 'video':
8223
                $title = $row_item['title'];
8224
                $output = '-';
8225
                if (!empty($title)) {
8226
                    $output = $title;
8227
                }
8228
                break;
8229
            case 'hotpotatoes':
8230
                $tbl_doc = Database::get_course_table(TABLE_DOCUMENT);
8231
                $result = Database::query("SELECT * FROM $tbl_doc WHERE c_id = $course_id AND iid = $id");
8232
                $myrow = Database::fetch_array($result);
8233
                $pathname = explode('/', $myrow['path']); // Making a correct name for the link.
8234
                $last = count($pathname) - 1; // Making a correct name for the link.
8235
                $filename = $pathname[$last]; // Making a correct name for the link.
8236
                $myrow['path'] = rawurlencode($myrow['path']);
8237
                $output = $filename;
8238
                break;
8239
        }
8240
8241
        return stripslashes($output);
8242
    }
8243
8244
    /**
8245
     * Get the parent names for the current item.
8246
     *
8247
     * @param int $newItemId Optional. The item ID
8248
     */
8249
    public function getCurrentItemParentNames($newItemId = 0): array
8250
    {
8251
        $newItemId = $newItemId ?: $this->get_current_item_id();
8252
        $return = [];
8253
        $item = $this->getItem($newItemId);
8254
8255
        $parent = null;
8256
        if ($item) {
8257
            $parent = $this->getItem($item->get_parent());
8258
        }
8259
8260
        while ($parent) {
8261
            $return[] = $parent->get_title();
8262
            $parent = $this->getItem($parent->get_parent());
8263
        }
8264
8265
        return array_reverse($return);
8266
    }
8267
8268
    /**
8269
     * Reads and process "lp_subscription_settings" setting.
8270
     *
8271
     * @return array
8272
     */
8273
    public static function getSubscriptionSettings()
8274
    {
8275
        $subscriptionSettings = api_get_setting('lp.lp_subscription_settings', true);
8276
        if (!is_array($subscriptionSettings)) {
8277
            // By default, allow both settings
8278
            $subscriptionSettings = [
8279
                'allow_add_users_to_lp' => true,
8280
                'allow_add_users_to_lp_category' => true,
8281
            ];
8282
        } else {
8283
            $subscriptionSettings = $subscriptionSettings['options'];
8284
        }
8285
8286
        return $subscriptionSettings;
8287
    }
8288
8289
    /**
8290
     * Exports a LP to a courseBuilder zip file. It adds the documents related to the LP.
8291
     */
8292
    public function exportToCourseBuildFormat()
8293
    {
8294
        if (!api_is_allowed_to_edit()) {
8295
            return false;
8296
        }
8297
8298
        $courseBuilder = new CourseBuilder();
8299
        $itemList = [];
8300
        /** @var learnpathItem $item */
8301
        foreach ($this->items as $item) {
8302
            $itemList[$item->get_type()][] = $item->get_path();
8303
        }
8304
8305
        if (empty($itemList)) {
8306
            return false;
8307
        }
8308
8309
        if (isset($itemList['document'])) {
8310
            // Get parents
8311
            foreach ($itemList['document'] as $documentId) {
8312
                $documentInfo = DocumentManager::get_document_data_by_id($documentId, api_get_course_id(), true);
8313
                if (!empty($documentInfo['parents'])) {
8314
                    foreach ($documentInfo['parents'] as $parentInfo) {
8315
                        if (in_array($parentInfo['iid'], $itemList['document'])) {
8316
                            continue;
8317
                        }
8318
                        $itemList['document'][] = $parentInfo['iid'];
8319
                    }
8320
                }
8321
            }
8322
8323
            $courseInfo = api_get_course_info();
8324
            foreach ($itemList['document'] as $documentId) {
8325
                $documentInfo = DocumentManager::get_document_data_by_id($documentId, api_get_course_id());
8326
                $items = DocumentManager::get_resources_from_source_html(
8327
                    $documentInfo['absolute_path'],
8328
                    true,
8329
                    TOOL_DOCUMENT
8330
                );
8331
8332
                if (!empty($items)) {
8333
                    foreach ($items as $item) {
8334
                        // Get information about source url
8335
                        $url = $item[0]; // url
8336
                        $scope = $item[1]; // scope (local, remote)
8337
                        $type = $item[2]; // type (rel, abs, url)
8338
8339
                        $origParseUrl = parse_url($url);
8340
                        $realOrigPath = isset($origParseUrl['path']) ? $origParseUrl['path'] : null;
8341
8342
                        if ('local' === $scope) {
8343
                            if ('abs' === $type || 'rel' === $type) {
8344
                                $documentFile = strstr($realOrigPath, 'document');
8345
                                if (false !== strpos($realOrigPath, $documentFile)) {
8346
                                    $documentFile = str_replace('document', '', $documentFile);
8347
                                    $itemDocumentId = DocumentManager::get_document_id($courseInfo, $documentFile);
8348
                                    // Document found! Add it to the list
8349
                                    if ($itemDocumentId) {
8350
                                        $itemList['document'][] = $itemDocumentId;
8351
                                    }
8352
                                }
8353
                            }
8354
                        }
8355
                    }
8356
                }
8357
            }
8358
8359
            $courseBuilder->build_documents(
8360
                api_get_session_id(),
8361
                $this->get_course_int_id(),
8362
                true,
8363
                $itemList['document']
8364
            );
8365
        }
8366
8367
        if (isset($itemList['quiz'])) {
8368
            $courseBuilder->build_quizzes(
8369
                api_get_session_id(),
8370
                $this->get_course_int_id(),
8371
                true,
8372
                $itemList['quiz']
8373
            );
8374
        }
8375
8376
        if (!empty($itemList['thread'])) {
8377
            $threadList = [];
8378
            $repo = Container::getForumThreadRepository();
8379
            foreach ($itemList['thread'] as $threadId) {
8380
                /** @var CForumThread $thread */
8381
                $thread = $repo->find($threadId);
8382
                if ($thread) {
8383
                    $itemList['forum'][] = $thread->getForum() ? $thread->getForum()->getIid() : 0;
8384
                    $threadList[] = $thread->getIid();
8385
                }
8386
            }
8387
8388
            if (!empty($threadList)) {
8389
                $courseBuilder->build_forum_topics(
8390
                    api_get_session_id(),
8391
                    $this->get_course_int_id(),
8392
                    null,
8393
                    $threadList
8394
                );
8395
            }
8396
        }
8397
8398
        $forumCategoryList = [];
8399
        if (isset($itemList['forum'])) {
8400
            foreach ($itemList['forum'] as $forumId) {
8401
                $forumInfo = get_forums($forumId);
8402
                $forumCategoryList[] = $forumInfo['forum_category'];
8403
            }
8404
        }
8405
8406
        if (!empty($forumCategoryList)) {
8407
            $courseBuilder->build_forum_category(
8408
                api_get_session_id(),
8409
                $this->get_course_int_id(),
8410
                true,
8411
                $forumCategoryList
8412
            );
8413
        }
8414
8415
        if (!empty($itemList['forum'])) {
8416
            $courseBuilder->build_forums(
8417
                api_get_session_id(),
8418
                $this->get_course_int_id(),
8419
                true,
8420
                $itemList['forum']
8421
            );
8422
        }
8423
8424
        if (isset($itemList['link'])) {
8425
            $courseBuilder->build_links(
8426
                api_get_session_id(),
8427
                $this->get_course_int_id(),
8428
                true,
8429
                $itemList['link']
8430
            );
8431
        }
8432
8433
        if (!empty($itemList['student_publication'])) {
8434
            $courseBuilder->build_works(
8435
                api_get_session_id(),
8436
                $this->get_course_int_id(),
8437
                true,
8438
                $itemList['student_publication']
8439
            );
8440
        }
8441
8442
        $courseBuilder->build_learnpaths(
8443
            api_get_session_id(),
8444
            $this->get_course_int_id(),
8445
            true,
8446
            [$this->get_id()],
8447
            false
8448
        );
8449
8450
        $courseBuilder->restoreDocumentsFromList();
8451
8452
        $zipFile = CourseArchiver::createBackup($courseBuilder->course);
8453
        $zipPath = CourseArchiver::getBackupDir().$zipFile;
8454
        $result = DocumentManager::file_send_for_download(
8455
            $zipPath,
8456
            true,
8457
            $this->get_name().'.zip'
8458
        );
8459
8460
        if ($result) {
8461
            api_not_allowed();
8462
        }
8463
8464
        return true;
8465
    }
8466
8467
    /**
8468
     * Get whether this is a learning path with the accumulated work time or not.
8469
     *
8470
     * @return int
8471
     */
8472
    public function getAccumulateWorkTime()
8473
    {
8474
        return (int) $this->accumulateWorkTime;
8475
    }
8476
8477
    /**
8478
     * Get whether this is a learning path with the accumulated work time or not.
8479
     *
8480
     * @return int
8481
     */
8482
    public function getAccumulateWorkTimeTotalCourse()
8483
    {
8484
        $table = Database::get_course_table(TABLE_LP_MAIN);
8485
        $sql = "SELECT SUM(accumulate_work_time) AS total
8486
                FROM $table
8487
                WHERE c_id = ".$this->course_int_id;
8488
        $result = Database::query($sql);
8489
        $row = Database::fetch_array($result);
8490
8491
        return (int) $row['total'];
8492
    }
8493
8494
    /**
8495
     * @param int $lpId
8496
     * @param int $courseId
8497
     *
8498
     * @return mixed
8499
     */
8500
    public static function getAccumulateWorkTimePrerequisite($lpId, $courseId)
8501
    {
8502
        $lpId = (int) $lpId;
8503
        $table = Database::get_course_table(TABLE_LP_MAIN);
8504
        $sql = "SELECT accumulate_work_time
8505
                FROM $table
8506
                WHERE iid = $lpId";
8507
        $result = Database::query($sql);
8508
        $row = Database::fetch_array($result);
8509
8510
        return $row['accumulate_work_time'];
8511
    }
8512
8513
    /**
8514
     * @param int $courseId
8515
     *
8516
     * @return int
8517
     */
8518
    public static function getAccumulateWorkTimeTotal($courseId)
8519
    {
8520
        $table = Database::get_course_table(TABLE_LP_MAIN);
8521
        $courseId = (int) $courseId;
8522
        $sql = "SELECT SUM(accumulate_work_time) AS total
8523
                FROM $table
8524
                WHERE c_id = $courseId";
8525
        $result = Database::query($sql);
8526
        $row = Database::fetch_array($result);
8527
8528
        return (int) $row['total'];
8529
    }
8530
8531
    /**
8532
     * In order to use the lp icon option you need to create the "lp_icon" LP extra field
8533
     * and put the images in.
8534
     */
8535
    public static function getIconSelect(): array
8536
    {
8537
        $theme = Container::$container->get(ThemeHelper::class)->getVisualTheme();
8538
        $filesystem = Container::$container->get('oneup_flysystem.themes_filesystem');
8539
8540
        if (!$filesystem->directoryExists("$theme/lp_icons")) {
8541
            return [];
8542
        }
8543
8544
        $icons = ['' => get_lang('Please select an option')];
8545
8546
        $iconFiles = $filesystem->listContents("$theme/lp_icons");
8547
        $allowedExtensions = ['image/jpeg', 'image/jpg', 'image/png'];
8548
8549
        foreach ($iconFiles as $iconFile) {
8550
            $mimeType = $filesystem->mimeType($iconFile->path());
8551
8552
            if (in_array($mimeType, $allowedExtensions)) {
8553
                $basename = basename($iconFile->path());
8554
                $icons[$basename] = $basename;
8555
            }
8556
        }
8557
8558
        return $icons;
8559
    }
8560
8561
    /**
8562
     * @param int $lpId
8563
     *
8564
     * @return string
8565
     */
8566
    public static function getSelectedIcon($lpId)
8567
    {
8568
        $extraFieldValue = new ExtraFieldValue('lp');
8569
        $lpIcon = $extraFieldValue->get_values_by_handler_and_field_variable($lpId, 'lp_icon');
8570
        $icon = '';
8571
        if (!empty($lpIcon) && isset($lpIcon['value'])) {
8572
            $icon = $lpIcon['value'];
8573
        }
8574
8575
        return $icon;
8576
    }
8577
8578
    public static function getSelectedIconHtml(int $lpId): string
8579
    {
8580
        $icon = self::getSelectedIcon($lpId);
8581
8582
        if (empty($icon)) {
8583
            return '';
8584
        }
8585
8586
        $path = Container::getThemeHelper()->getThemeAssetUrl("lp_icons/$icon");
8587
8588
        return Display::img($path);
8589
    }
8590
8591
    /**
8592
     * @param string $value
8593
     *
8594
     * @return string
8595
     */
8596
    public function cleanItemTitle($value)
8597
    {
8598
        $value = Security::remove_XSS(strip_tags($value));
8599
8600
        return $value;
8601
    }
8602
8603
    public function setItemTitle(FormValidator $form)
8604
    {
8605
        if ('true' === api_get_setting('editor.save_titles_as_html')) {
8606
            $form->addHtmlEditor(
8607
                'title',
8608
                get_lang('Title'),
8609
                true,
8610
                false,
8611
                ['ToolbarSet' => 'TitleAsHtml', 'id' => uniqid('editor')]
8612
            );
8613
        } else {
8614
            $form->addText('title', get_lang('Title'), true, ['id' => 'idTitle', 'class' => 'learnpath_item_form']);
8615
            $form->applyFilter('title', 'trim');
8616
            $form->applyFilter('title', 'html_filter');
8617
        }
8618
    }
8619
8620
    /**
8621
     * @return array
8622
     */
8623
    public function getItemsForForm($addParentCondition = false)
8624
    {
8625
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
8626
8627
        $sql = "SELECT * FROM $tbl_lp_item
8628
                WHERE path <> 'root' AND lp_id = ".$this->lp_id;
8629
8630
        if ($addParentCondition) {
8631
            $sql .= ' AND parent_item_id IS NULL ';
8632
        }
8633
        $sql .= ' ORDER BY display_order ASC';
8634
8635
        $result = Database::query($sql);
8636
        $arrLP = [];
8637
        while ($row = Database::fetch_array($result)) {
8638
            $arrLP[] = [
8639
                'iid' => $row['iid'],
8640
                'id' => $row['iid'],
8641
                'item_type' => $row['item_type'],
8642
                'title' => $this->cleanItemTitle($row['title']),
8643
                'title_raw' => $row['title'],
8644
                'path' => $row['path'],
8645
                'description' => Security::remove_XSS($row['description']),
8646
                'parent_item_id' => $row['parent_item_id'],
8647
                'previous_item_id' => $row['previous_item_id'],
8648
                'next_item_id' => $row['next_item_id'],
8649
                'display_order' => $row['display_order'],
8650
                'max_score' => $row['max_score'],
8651
                'min_score' => $row['min_score'],
8652
                'mastery_score' => $row['mastery_score'],
8653
                'prerequisite' => $row['prerequisite'],
8654
                'max_time_allowed' => $row['max_time_allowed'],
8655
                'prerequisite_min_score' => $row['prerequisite_min_score'],
8656
                'prerequisite_max_score' => $row['prerequisite_max_score'],
8657
            ];
8658
        }
8659
8660
        return $arrLP;
8661
    }
8662
8663
    /**
8664
     * Gets whether this SCORM learning path has been marked to use the score
8665
     * as progress. Takes into account whether the learnpath matches (SCORM
8666
     * content + less than 2 items).
8667
     *
8668
     * @return bool True if the score should be used as progress, false otherwise
8669
     */
8670
    public function getUseScoreAsProgress()
8671
    {
8672
        // If not a SCORM, we don't care about the setting
8673
        if (2 != $this->get_type()) {
8674
            return false;
8675
        }
8676
        // If more than one step in the SCORM, we don't care about the setting
8677
        if ($this->get_total_items_count() > 1) {
8678
            return false;
8679
        }
8680
        $extraFieldValue = new ExtraFieldValue('lp');
8681
        $doUseScore = false;
8682
        $useScore = $extraFieldValue->get_values_by_handler_and_field_variable(
8683
            $this->get_id(),
8684
            'use_score_as_progress'
8685
        );
8686
        if (!empty($useScore) && isset($useScore['value'])) {
8687
            $doUseScore = $useScore['value'];
8688
        }
8689
8690
        return $doUseScore;
8691
    }
8692
8693
    /**
8694
     * Get the user identifier (user_id or username
8695
     * Depends on scorm_api_username_as_student_id in app/config/configuration.php.
8696
     *
8697
     * @return string User ID or username, depending on configuration setting
8698
     */
8699
    public static function getUserIdentifierForExternalServices()
8700
    {
8701
        $scormApiExtraFieldUseStudentId = api_get_setting('lp.scorm_api_extrafield_to_use_as_student_id');
8702
        $extraFieldValue = new ExtraFieldValue('user');
8703
        $extrafield = $extraFieldValue->get_values_by_handler_and_field_variable(
8704
            api_get_user_id(),
8705
            $scormApiExtraFieldUseStudentId
8706
        );
8707
        if (is_array($extrafield) && isset($extrafield['value'])) {
8708
            return $extrafield['value'];
8709
        } else {
8710
            if ('true' === $scormApiExtraFieldUseStudentId) {
8711
                return api_get_user_info(api_get_user_id())['username'];
8712
            } else {
8713
                return api_get_user_id();
8714
            }
8715
        }
8716
    }
8717
8718
    /**
8719
     * Save the new order for learning path items.
8720
     *
8721
     * @param array $orderList A associative array with id and parent_id keys.
8722
     */
8723
    public static function sortItemByOrderList(CLpItem $rootItem, array $orderList = [], $flush = true, $lpItemRepo = null, $em = null)
8724
    {
8725
        if (empty($orderList)) {
8726
            return true;
8727
        }
8728
        if (!isset($lpItemRepo)) {
8729
            $lpItemRepo = Container::getLpItemRepository();
8730
        }
8731
        if (!isset($em)) {
8732
            $em = Database::getManager();
8733
        }
8734
        $counter = 2;
8735
        $rootItem->setDisplayOrder(1);
8736
        $rootItem->setPreviousItemId(null);
8737
        $em->persist($rootItem);
8738
        if ($flush) {
8739
            $em->flush();
8740
        }
8741
8742
        foreach ($orderList as $item) {
8743
            $itemId = $item->id ?? 0;
8744
            if (empty($itemId)) {
8745
                continue;
8746
            }
8747
            $parentId = $item->parent_id ?? 0;
8748
            $parent = $rootItem;
8749
            if (!empty($parentId)) {
8750
                $parentExists = $lpItemRepo->find($parentId);
8751
                if (null !== $parentExists) {
8752
                    $parent = $parentExists;
8753
                }
8754
            }
8755
8756
            /** @var CLpItem $itemEntity */
8757
            $itemEntity = $lpItemRepo->find($itemId);
8758
            $itemEntity->setParent($parent);
8759
            $itemEntity->setPreviousItemId(null);
8760
            $itemEntity->setNextItemId(null);
8761
            $itemEntity->setDisplayOrder($counter);
8762
8763
            $em->persist($itemEntity);
8764
            if ($flush) {
8765
                $em->flush();
8766
            }
8767
            $counter++;
8768
        }
8769
8770
        $lpItemRepo->recoverNode($rootItem, 'displayOrder');
8771
        $em->persist($rootItem);
8772
        if ($flush) {
8773
            $em->flush();
8774
        }
8775
8776
        return true;
8777
    }
8778
8779
    public static function move(int $lpId, string $direction)
8780
    {
8781
        $em = Database::getManager();
8782
        /** @var CLp $lp */
8783
        $lp = Container::getLpRepository()->find($lpId);
8784
        if ($lp) {
8785
            $course = api_get_course_entity();
8786
            $session = api_get_session_entity();
8787
            $group = api_get_group_entity();
8788
8789
            $link = $lp->getResourceNode()->getResourceLinkByContext($course, $session, $group);
8790
8791
            if ($link) {
8792
                if ('down' === $direction) {
8793
                    $link->moveDownPosition();
8794
                }
8795
                if ('up' === $direction) {
8796
                    $link->moveUpPosition();
8797
                }
8798
8799
                $em->flush();
8800
            }
8801
        }
8802
    }
8803
8804
    /**
8805
     * Get the depth level of LP item.
8806
     *
8807
     * @param array $items
8808
     * @param int   $currentItemId
8809
     *
8810
     * @return int
8811
     */
8812
    private static function get_level_for_item($items, $currentItemId)
8813
    {
8814
        $parentItemId = 0;
8815
        if (isset($items[$currentItemId])) {
8816
            $parentItemId = $items[$currentItemId]->parent;
8817
        }
8818
8819
        if (0 == $parentItemId) {
8820
            return 0;
8821
        }
8822
8823
        return self::get_level_for_item($items, $parentItemId) + 1;
8824
    }
8825
8826
    /**
8827
     * Generate the link for a learnpath category as course tool.
8828
     *
8829
     * @param int $categoryId
8830
     *
8831
     * @return string
8832
     */
8833
    private static function getCategoryLinkForTool($categoryId)
8834
    {
8835
        $categoryId = (int) $categoryId;
8836
        return 'lp/lp_controller.php?'.api_get_cidreq().'&'
8837
            .http_build_query(
8838
                [
8839
                    'action' => 'view_category',
8840
                    'id' => $categoryId,
8841
                ]
8842
            );
8843
    }
8844
8845
    /**
8846
     * Check and obtain the lp final item if exist.
8847
     *
8848
     * @return learnpathItem
8849
     */
8850
    private function getFinalItem()
8851
    {
8852
        if (empty($this->items)) {
8853
            return null;
8854
        }
8855
8856
        foreach ($this->items as $item) {
8857
            if ('final_item' !== $item->type) {
8858
                continue;
8859
            }
8860
8861
            return $item;
8862
        }
8863
    }
8864
8865
    /**
8866
     * Get the LP Final Item Template.
8867
     *
8868
     * @return string
8869
     */
8870
    private function getFinalItemTemplate()
8871
    {
8872
        return file_get_contents(api_get_path(SYS_CODE_PATH).'lp/final_item_template/template.html');
8873
    }
8874
8875
    /**
8876
     * Get the LP Final Item Url.
8877
     *
8878
     * @return string
8879
     */
8880
    private function getSavedFinalItem()
8881
    {
8882
        $finalItem = $this->getFinalItem();
8883
8884
        $repo = Container::getDocumentRepository();
8885
        /** @var CDocument $document */
8886
        $document = $repo->find($finalItem->path);
8887
8888
        return $document ? $repo->getResourceFileContent($document) : '';
8889
    }
8890
8891
    /**
8892
     * Recalculates the results for all exercises associated with the learning path (LP) for the given user.
8893
     */
8894
    public function recalculateResultsForLp(int $userId): void
8895
    {
8896
        $em = Database::getManager();
8897
        $lpItemRepo = $em->getRepository(CLpItem::class);
8898
        $lpItems = $lpItemRepo->findBy(['lp' => $this->lp_id]);
8899
8900
        if (empty($lpItems)) {
8901
            Display::addFlash(Display::return_message(get_lang('No items found'), 'error'));
8902
            return;
8903
        }
8904
8905
        $lpItemsById = [];
8906
        foreach ($lpItems as $item) {
8907
            $lpItemsById[$item->getIid()] = $item;
8908
        }
8909
8910
        $trackEExerciseRepo = $em->getRepository(TrackEExercise::class);
8911
        $trackExercises = $trackEExerciseRepo->createQueryBuilder('te')
8912
            ->where('te.origLpId = :lpId')
8913
            ->andWhere('te.user = :userId')
8914
            ->andWhere('te.origLpItemId IN (:lpItemIds)')
8915
            ->setParameter('lpId', $this->lp_id)
8916
            ->setParameter('userId', $userId)
8917
            ->setParameter('lpItemIds', array_keys($lpItemsById))
8918
            ->getQuery()
8919
            ->getResult();
8920
8921
        if (empty($trackExercises)) {
8922
            Display::addFlash(Display::return_message(get_lang('No exercise attempts found'), 'error'));
8923
            return;
8924
        }
8925
8926
        foreach ($trackExercises as $trackExercise) {
8927
            $exeId = $trackExercise->getExeId();
8928
            $lpItemId = $trackExercise->getOrigLpItemId();
8929
8930
            if (!isset($lpItemsById[$lpItemId])) {
8931
                continue;
8932
            }
8933
8934
            $lpItem = $lpItemsById[$lpItemId];
8935
            if ('quiz' !== $lpItem->getItemType()) {
8936
                continue;
8937
            }
8938
8939
            $quizId = (int) $lpItem->getPath();
8940
            $courseId = (int) $trackExercise->getCourse()->getId();
8941
            $updatedExercise = ExerciseLib::recalculateResult($exeId, $userId, $quizId, $courseId);
8942
            if ($updatedExercise instanceof TrackEExercise) {
8943
                Display::addFlash(Display::return_message(get_lang('Results recalculated'), 'success'));
8944
            } else {
8945
                Display::addFlash(Display::return_message(get_lang('Error recalculating results'), 'error'));
8946
            }
8947
        }
8948
    }
8949
8950
    /**
8951
     * Returns the video player HTML for a video-type document LP item.
8952
     *
8953
     * @param int $lpItemId
8954
     * @param string $autostart
8955
     *
8956
     * @return string
8957
     */
8958
    public function getVideoPlayer(CDocument $document, string $autostart = 'true'): string
8959
    {
8960
        $resourceNode = $document->getResourceNode();
8961
        $resourceFile = $resourceNode?->getFirstResourceFile();
8962
8963
        if (!$resourceNode || !$resourceFile) {
8964
            return '';
8965
        }
8966
8967
        $resourceNodeRepository = Container::getResourceNodeRepository();
8968
        $videoUrl = $resourceNodeRepository->getResourceFileUrl($resourceNode);
8969
8970
        if (empty($videoUrl)) {
8971
            return '';
8972
        }
8973
8974
        $fileName = $resourceFile->getTitle();
8975
        $ext = pathinfo($fileName, PATHINFO_EXTENSION);
8976
        $mimeType = $resourceFile->getMimeType() ?: 'video/mp4';
8977
        $autoplayAttr = ($autostart === 'true') ? 'autoplay muted playsinline' : '';
8978
8979
        $html = '';
8980
        $html .= '
8981
        <video id="lp-video" width="100%" height="auto" controls '.$autoplayAttr.'>
8982
            <source src="'.$videoUrl.'" type="$mimeType">
8983
        </video>';
8984
8985
        return $html;
8986
    }
8987
}
8988