Passed
Pull Request — master (#5803)
by
unknown
09:05
created

learnpath::restart()   B

Complexity

Conditions 6
Paths 14

Size

Total Lines 38
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

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