learnpath::__construct()   F
last analyzed

Complexity

Conditions 41
Paths > 20000

Size

Total Lines 252
Code Lines 170

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 41
eloc 170
c 0
b 0
f 0
nop 3
dl 0
loc 252
rs 0
nc 1354756

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