Passed
Pull Request — master (#6750)
by
unknown
08:11
created

learnpath::verify_document_size()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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