Passed
Push — master ( 8311f5...c33ddb )
by Yannick
08:30
created

learnpath::getSelectedIcon()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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