learnpath::createReadOutText()   F
last analyzed

Complexity

Conditions 27
Paths > 20000

Size

Total Lines 121
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 27
eloc 73
nc 54080
nop 4
dl 0
loc 121
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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