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