Passed
Push — master ( f23aa1...456a08 )
by Julito
11:24 queued 01:42
created

learnpath::get_progress_bar_mode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 7
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\Filesystem\Filesystem;
28
use Symfony\Component\Finder\Finder;
29
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
30
31
/**
32
 * Class learnpath
33
 * This class defines the parent attributes and methods for Chamilo learnpaths
34
 * and SCORM learnpaths. It is used by the scorm class.
35
 *
36
 * @todo decouple class
37
 *
38
 * @author  Yannick Warnier <[email protected]>
39
 * @author  Julio Montoya   <[email protected]> Several improvements and fixes
40
 */
41
class learnpath
42
{
43
    public const MAX_LP_ITEM_TITLE_LENGTH = 36;
44
    public const STATUS_CSS_CLASS_NAME = [
45
        'not attempted' => 'scorm_not_attempted',
46
        'incomplete' => 'scorm_not_attempted',
47
        'failed' => 'scorm_failed',
48
        'completed' => 'scorm_completed',
49
        'passed' => 'scorm_completed',
50
        'succeeded' => 'scorm_completed',
51
        'browsed' => 'scorm_completed',
52
    ];
53
54
    public $attempt = 0; // The number for the current ID view.
55
    public $cc; // Course (code) this learnpath is located in. @todo change name for something more comprensible ...
56
    public $current; // Id of the current item the user is viewing.
57
    public $current_score; // The score of the current item.
58
    public $current_time_start; // The time the user loaded this resource (this does not mean he can see it yet).
59
    public $current_time_stop; // The time the user closed this resource.
60
    public $default_status = 'not attempted';
61
    public $encoding = 'UTF-8';
62
    public $error = '';
63
    public $force_commit = false; // For SCORM only- if true will send a scorm LMSCommit() request on each LMSSetValue()
64
    public $index; // The index of the active learnpath_item in $ordered_items array.
65
    /** @var learnpathItem[] */
66
    public $items = [];
67
    public $last; // item_id of last item viewed in the learning path.
68
    public $last_item_seen = 0; // In case we have already come in this lp, reuse the last item seen if authorized.
69
    public $license; // Which license this course has been given - not used yet on 20060522.
70
    public $lp_id; // DB iid for this learnpath.
71
    public $lp_view_id; // DB ID for lp_view
72
    public $maker; // Which maker has conceived the content (ENI, Articulate, ...).
73
    public $message = '';
74
    public $mode = 'embedded'; // Holds the video display mode (fullscreen or embedded).
75
    public $name; // Learnpath name (they generally have one).
76
    public $ordered_items = []; // List of the learnpath items in the order they are to be read.
77
    public $path = ''; // Path inside the scorm directory (if scorm).
78
    public $theme; // The current theme of the learning path.
79
    public $accumulateScormTime; // Flag to decide whether to accumulate SCORM time or not
80
    public $accumulateWorkTime; // The min time of learnpath
81
82
    // Tells if all the items of the learnpath can be tried again. Defaults to "no" (=1).
83
    public $prevent_reinit = 1;
84
85
    // Describes the mode of progress bar display.
86
    public $seriousgame_mode = 0;
87
    public $progress_bar_mode = '%';
88
89
    // Percentage progress as saved in the db.
90
    public $progress_db = 0;
91
    public $proximity; // Wether the content is distant or local or unknown.
92
    public $refs_list = []; //list of items by ref => db_id. Used only for prerequisites match.
93
    // !!!This array (refs_list) is built differently depending on the nature of the LP.
94
    // If SCORM, uses ref, if Chamilo, uses id to keep a unique value.
95
    public $type; //type of learnpath. Could be 'chamilo', 'scorm', 'scorm2004', 'aicc', ...
96
    // TODO: Check if this type variable is useful here (instead of just in the controller script).
97
    public $user_id; //ID of the user that is viewing/using the course
98
    public $update_queue = [];
99
    public $scorm_debug = 0;
100
    public $arrMenu = []; // Array for the menu items.
101
    public $debug = 0; // Logging level.
102
    public $lp_session_id = 0;
103
    public $lp_view_session_id = 0; // The specific view might be bound to a session.
104
    public $prerequisite = 0;
105
    public $use_max_score = 1; // 1 or 0
106
    public $subscribeUsers = 0; // Subscribe users or not
107
    public $created_on = '';
108
    public $modified_on = '';
109
    public $publicated_on = '';
110
    public $expired_on = '';
111
    public $ref;
112
    public $course_int_id;
113
    public $course_info;
114
    public $categoryId;
115
    public $scormUrl;
116
    public $entity;
117
118
    public function __construct(CLp $entity = null, $course_info, $user_id)
119
    {
120
        $debug = $this->debug;
121
        $user_id = (int) $user_id;
122
        $this->encoding = api_get_system_encoding();
123
        $lp_id = 0;
124
        if (null !== $entity) {
125
            $lp_id = (int) $entity->getIid();
126
        }
127
        $course_info = empty($course_info) ? api_get_course_info() : $course_info;
128
        $course_id = (int) $course_info['real_id'];
129
        $this->course_info = $course_info;
130
        $this->set_course_int_id($course_id);
131
        if (empty($lp_id) || empty($course_id)) {
132
            $this->error = "Parameter is empty: LpId:'$lp_id', courseId: '$lp_id'";
133
        } else {
134
            $this->entity = $entity;
135
            $this->lp_id = $lp_id;
136
            $this->type = $entity->getLpType();
137
            $this->name = stripslashes($entity->getName());
138
            $this->proximity = $entity->getContentLocal();
139
            $this->theme = $entity->getTheme();
140
            $this->maker = $entity->getContentLocal();
141
            $this->prevent_reinit = $entity->getPreventReinit();
142
            $this->seriousgame_mode = $entity->getSeriousgameMode();
143
            $this->license = $entity->getContentLicense();
144
            $this->scorm_debug = $entity->getDebug();
145
            $this->js_lib = $entity->getJsLib();
146
            $this->path = $entity->getPath();
147
            $this->author = $entity->getAuthor();
148
            $this->hide_toc_frame = $entity->getHideTocFrame();
149
            //$this->lp_session_id = $entity->getSessionId();
150
            $this->use_max_score = $entity->getUseMaxScore();
151
            $this->subscribeUsers = $entity->getSubscribeUsers();
152
            $this->created_on = $entity->getCreatedOn()->format('Y-m-d H:i:s');
153
            $this->modified_on = $entity->getModifiedOn()->format('Y-m-d H:i:s');
154
            $this->ref = $entity->getRef();
155
            $this->categoryId = 0;
156
            if ($entity->getCategory()) {
157
                $this->categoryId = $entity->getCategory()->getIid();
158
            }
159
160
            if ($entity->hasAsset()) {
161
                $asset = $entity->getAsset();
162
                $this->scormUrl = Container::getAssetRepository()->getAssetUrl($asset).'/';
163
            }
164
165
            $this->accumulateScormTime = $entity->getAccumulateWorkTime();
166
167
            if (!empty($entity->getPublicatedOn())) {
168
                $this->publicated_on = $entity->getPublicatedOn()->format('Y-m-d H:i:s');
169
            }
170
171
            if (!empty($entity->getExpiredOn())) {
172
                $this->expired_on = $entity->getExpiredOn()->format('Y-m-d H:i:s');
173
            }
174
            if (2 == $this->type) {
175
                if (1 == $entity->getForceCommit()) {
176
                    $this->force_commit = true;
177
                }
178
            }
179
            $this->mode = $entity->getDefaultViewMod();
180
181
            // Check user ID.
182
            if (empty($user_id)) {
183
                $this->error = 'User ID is empty';
184
            } else {
185
                $this->user_id = $user_id;
186
            }
187
188
            // End of variables checking.
189
            $session_id = api_get_session_id();
190
            //  Get the session condition for learning paths of the base + session.
191
            $session = api_get_session_condition($session_id);
192
            // Now get the latest attempt from this user on this LP, if available, otherwise create a new one.
193
            $lp_table = Database::get_course_table(TABLE_LP_VIEW);
194
195
            // Selecting by view_count descending allows to get the highest view_count first.
196
            $sql = "SELECT * FROM $lp_table
197
                    WHERE
198
                        c_id = $course_id AND
199
                        lp_id = $lp_id AND
200
                        user_id = $user_id
201
                        $session
202
                    ORDER BY view_count DESC";
203
            $res = Database::query($sql);
204
205
            if (Database::num_rows($res) > 0) {
206
                $row = Database::fetch_array($res);
207
                $this->attempt = $row['view_count'];
208
                $this->lp_view_id = $row['iid'];
209
                $this->last_item_seen = $row['last_item'];
210
                $this->progress_db = $row['progress'];
211
                $this->lp_view_session_id = $row['session_id'];
212
            } elseif (!api_is_invitee()) {
213
                $this->attempt = 1;
214
                $params = [
215
                    'c_id' => $course_id,
216
                    'lp_id' => $lp_id,
217
                    'user_id' => $user_id,
218
                    'view_count' => 1,
219
                    //'session_id' => $session_id,
220
                    'last_item' => 0,
221
                ];
222
                if (!empty($session_id)) {
223
                    $params['session_id'] = $session_id;
224
                }
225
                $this->last_item_seen = 0;
226
                $this->lp_view_session_id = $session_id;
227
                $this->lp_view_id = Database::insert($lp_table, $params);
228
            }
229
230
            $criteria = Criteria::create()
231
                ->orderBy(
232
                    [
233
                        'parent' => Criteria::ASC,
234
                        'displayOrder' => Criteria::ASC,
235
                    ]
236
                );
237
            $items = $this->entity->getItems()->matching($criteria);
238
            $lp_item_id_list = [];
239
            foreach ($items as $item) {
240
                $itemId = $item->getIid();
241
                $lp_item_id_list[] = $itemId;
242
                switch ($this->type) {
243
                    case 3: //aicc
244
                        $oItem = new aiccItem('db', $itemId, $course_id);
245
                        if (is_object($oItem)) {
246
                            $my_item_id = $oItem->get_id();
247
                            $oItem->set_lp_view($this->lp_view_id, $course_id);
248
                            $oItem->set_prevent_reinit($this->prevent_reinit);
249
                            // Don't use reference here as the next loop will make the pointed object change.
250
                            $this->items[$my_item_id] = $oItem;
251
                            $this->refs_list[$oItem->ref] = $my_item_id;
252
                        }
253
                        break;
254
                    case 2:
255
                        $oItem = new scormItem('db', $itemId, $course_id);
256
                        if (is_object($oItem)) {
257
                            $my_item_id = $oItem->get_id();
258
                            $oItem->set_lp_view($this->lp_view_id, $course_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[$my_item_id] = $oItem;
262
                            $this->refs_list[$oItem->ref] = $my_item_id;
263
                        }
264
                        break;
265
                    case 1:
266
                    default:
267
                        $oItem = new learnpathItem($itemId, $course_id, $item);
268
                        if (is_object($oItem)) {
269
                            $my_item_id = $oItem->get_id();
270
                            // Moved down to when we are sure the item_view exists.
271
                            //$oItem->set_lp_view($this->lp_view_id);
272
                            $oItem->set_prevent_reinit($this->prevent_reinit);
273
                            // Don't use reference here as the next loop will make the pointed object change.
274
                            $this->items[$my_item_id] = $oItem;
275
                            $this->refs_list[$my_item_id] = $my_item_id;
276
                        }
277
                        break;
278
                }
279
280
                // Setting the object level with variable $this->items[$i][parent]
281
                foreach ($this->items as $itemLPObject) {
282
                    $level = self::get_level_for_item($this->items, $itemLPObject->db_id);
283
                    $itemLPObject->level = $level;
284
                }
285
286
                // Setting the view in the item object.
287
                if (is_object($this->items[$itemId])) {
288
                    $this->items[$itemId]->set_lp_view($this->lp_view_id, $course_id);
289
                    if (TOOL_HOTPOTATOES == $this->items[$itemId]->get_type()) {
290
                        $this->items[$itemId]->current_start_time = 0;
291
                        $this->items[$itemId]->current_stop_time = 0;
292
                    }
293
                }
294
            }
295
296
            if (!empty($lp_item_id_list)) {
297
                $lp_item_id_list_to_string = implode("','", $lp_item_id_list);
298
                if (!empty($lp_item_id_list_to_string)) {
299
                    // Get last viewing vars.
300
                    $itemViewTable = Database::get_course_table(TABLE_LP_ITEM_VIEW);
301
                    // This query should only return one or zero result.
302
                    $sql = "SELECT lp_item_id, status
303
                            FROM $itemViewTable
304
                            WHERE
305
                                lp_view_id = ".$this->get_view_id()." AND
306
                                lp_item_id IN ('".$lp_item_id_list_to_string."')
307
                            ORDER BY view_count DESC ";
308
                    $status_list = [];
309
                    $res = Database::query($sql);
310
                    while ($row = Database:: fetch_array($res)) {
311
                        $status_list[$row['lp_item_id']] = $row['status'];
312
                    }
313
314
                    foreach ($lp_item_id_list as $item_id) {
315
                        if (isset($status_list[$item_id])) {
316
                            $status = $status_list[$item_id];
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 (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
                                        $course_id
350
                                    );
351
                                }
352
                            }
353
                        }
354
                    }
355
                }
356
            }
357
358
            $this->ordered_items = self::get_flat_ordered_items_list($this->entity, null);
359
            $this->max_ordered_items = 0;
360
            foreach ($this->ordered_items as $index => $dummy) {
361
                if ($index > $this->max_ordered_items && !empty($dummy)) {
362
                    $this->max_ordered_items = $index;
363
                }
364
            }
365
            // TODO: Define the current item better.
366
            $this->first();
367
            if ($debug) {
368
                error_log('lp_view_session_id '.$this->lp_view_session_id);
369
                error_log('End of learnpath constructor for learnpath '.$this->get_id());
370
            }
371
        }
372
    }
373
374
    /**
375
     * @return int
376
     */
377
    public function get_course_int_id()
378
    {
379
        return $this->course_int_id ?? api_get_course_int_id();
380
    }
381
382
    /**
383
     * @param $course_id
384
     *
385
     * @return int
386
     */
387
    public function set_course_int_id($course_id)
388
    {
389
        return $this->course_int_id = (int) $course_id;
390
    }
391
392
    /**
393
     * Function rewritten based on old_add_item() from Yannick Warnier.
394
     * Due the fact that users can decide where the item should come, I had to overlook this function and
395
     * I found it better to rewrite it. Old function is still available.
396
     * Added also the possibility to add a description.
397
     *
398
     * @param CLpItem $parent
399
     * @param int     $previousId
400
     * @param string  $type
401
     * @param int     $id resource ID (ref)
402
     * @param string  $title
403
     * @param string  $description
404
     * @param int     $prerequisites
405
     * @param int     $max_time_allowed
406
     * @param int     $userId
407
     *
408
     * @return int
409
     */
410
    public function add_item(
411
        ?CLpItem $parent,
412
        $previousId,
413
        $type,
414
        $id,
415
        $title,
416
        $description = '',
417
        $prerequisites = 0,
418
        $max_time_allowed = 0
419
    ) {
420
        $type = empty($type) ? 'dir' : $type;
421
        $course_id = $this->course_info['real_id'];
422
        if (empty($course_id)) {
423
            // Sometimes Oogie doesn't catch the course info but sets $this->cc
424
            $this->course_info = api_get_course_info($this->cc);
425
            $course_id = $this->course_info['real_id'];
426
        }
427
        $id = (int) $id;
428
        $max_time_allowed = (int) $max_time_allowed;
429
        if (empty($max_time_allowed)) {
430
            $max_time_allowed = 0;
431
        }
432
        $max_score = 100;
433
        if ('quiz' === $type && $id) {
434
            /*$sql = 'SELECT SUM(ponderation)
435
                    FROM '.Database::get_course_table(TABLE_QUIZ_QUESTION).' as q
436
                    INNER JOIN '.Database::get_course_table(TABLE_QUIZ_TEST_QUESTION).' as quiz_rel_question
437
                    ON q.iid = quiz_rel_question.question_id
438
                    WHERE
439
                        quiz_rel_question.quiz_id = '.$id;
440
            $rsQuiz = Database::query($sql);
441
            $max_score = Database::result($rsQuiz, 0, 0);*/
442
443
            // Disabling the exercise if we add it inside a LP
444
            $exercise = new Exercise($course_id);
445
            $exercise->read($id);
446
            $max_score = $exercise->get_max_score();
447
448
            $exercise->disable();
449
            $exercise->save();
450
            $title = $exercise->get_formated_title();
451
        }
452
453
        $lpItem = new CLpItem();
454
        $lpItem
455
            ->setTitle($title)
456
            ->setDescription($description)
457
            ->setPath($id)
458
            ->setLp($this->entity)
459
            ->setItemType($type)
460
            ->setMaxScore($max_score)
461
            ->setMaxTimeAllowed($max_time_allowed)
462
            ->setPrerequisite($prerequisites)
463
            //->setDisplayOrder($display_order + 1)
464
            //->setNextItemId((int) $next)
465
            //->setPreviousItemId($previous)
466
        ;
467
468
        if (!empty($parent))  {
469
            $lpItem->setParent($parent);
470
        }
471
        $em = Database::getManager();
472
        $em->persist($lpItem);
473
        $em->flush();
474
475
        $new_item_id = $lpItem->getIid();
476
        if ($new_item_id) {
477
            // @todo fix upload audio.
478
            // Upload audio.
479
            /*if (!empty($_FILES['mp3']['name'])) {
480
                // Create the audio folder if it does not exist yet.
481
                $filepath = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document/';
482
                if (!is_dir($filepath.'audio')) {
483
                    mkdir(
484
                        $filepath.'audio',
485
                        api_get_permissions_for_new_directories()
486
                    );
487
                    DocumentManager::addDocument(
488
                        $_course,
489
                        '/audio',
490
                        'folder',
491
                        0,
492
                        'audio',
493
                        '',
494
                        0,
495
                        true,
496
                        null,
497
                        $sessionId,
498
                        $userId
499
                    );
500
                }
501
502
                $file_path = handle_uploaded_document(
503
                    $_course,
504
                    $_FILES['mp3'],
505
                    api_get_path(SYS_COURSE_PATH).$_course['path'].'/document',
506
                    '/audio',
507
                    $userId,
508
                    '',
509
                    '',
510
                    '',
511
                    '',
512
                    false
513
                );
514
515
                // Getting the filename only.
516
                $file_components = explode('/', $file_path);
517
                $file = $file_components[count($file_components) - 1];
518
519
                // Store the mp3 file in the lp_item table.
520
                $sql = "UPDATE $tbl_lp_item SET
521
                          audio = '".Database::escape_string($file)."'
522
                        WHERE iid = '".intval($new_item_id)."'";
523
                Database::query($sql);
524
            }*/
525
        }
526
527
        return $new_item_id;
528
    }
529
530
    /**
531
     * Static admin function allowing addition of a learnpath to a course.
532
     *
533
     * @param string $courseCode
534
     * @param string $name
535
     * @param string $description
536
     * @param string $learnpath
537
     * @param string $origin
538
     * @param string $zipname       Zip file containing the learnpath or directory containing the learnpath
539
     * @param string $publicated_on
540
     * @param string $expired_on
541
     * @param int    $categoryId
542
     * @param int    $userId
543
     *
544
     * @return CLp
545
     */
546
    public static function add_lp(
547
        $courseCode,
548
        $name,
549
        $description = '',
550
        $learnpath = 'guess',
551
        $origin = 'zip',
552
        $zipname = '',
553
        $publicated_on = '',
554
        $expired_on = '',
555
        $categoryId = 0,
556
        $userId = 0
557
    ) {
558
        global $charset;
559
560
        if (!empty($courseCode)) {
561
            $courseInfo = api_get_course_info($courseCode);
562
            $course_id = $courseInfo['real_id'];
563
        } else {
564
            $course_id = api_get_course_int_id();
565
            $courseInfo = api_get_course_info();
566
        }
567
568
        $categoryId = (int) $categoryId;
569
570
        if (empty($publicated_on)) {
571
            $publicated_on = null;
572
        } else {
573
            $publicated_on = api_get_utc_datetime($publicated_on, true, true);
574
        }
575
576
        if (empty($expired_on)) {
577
            $expired_on = null;
578
        } else {
579
            $expired_on = api_get_utc_datetime($expired_on, true, true);
580
        }
581
582
        /*$check_name = "SELECT * FROM $tbl_lp
583
                       WHERE c_id = $course_id AND name = '".Database::escape_string($name)."'";
584
        $res_name = Database::query($check_name);
585
586
        while (Database::num_rows($res_name)) {
587
            // There is already one such name, update the current one a bit.
588
            $i++;
589
            $name = $name.' - '.$i;
590
            $check_name = "SELECT * FROM $tbl_lp
591
                           WHERE c_id = $course_id AND name = '".Database::escape_string($name)."' ";
592
            $res_name = Database::query($check_name);
593
        }*/
594
        // New name does not exist yet; keep it.
595
        // Escape description.
596
        // Kevin: added htmlentities().
597
        $description = Database::escape_string(api_htmlentities($description, ENT_QUOTES, $charset));
598
        $type = 1;
599
        switch ($learnpath) {
600
            case 'guess':
601
            case 'aicc':
602
                break;
603
            case 'dokeos':
604
            case 'chamilo':
605
                $type = 1;
606
                break;
607
        }
608
609
        $id = null;
610
        $sessionEntity = api_get_session_entity();
611
        $courseEntity = api_get_course_entity($courseInfo['real_id']);
612
        $lp = null;
613
        switch ($origin) {
614
            case 'zip':
615
                // Check zip name string. If empty, we are currently creating a new Chamilo learnpath.
616
                break;
617
            case 'manual':
618
            default:
619
                /*$get_max = "SELECT MAX(display_order)
620
                            FROM $tbl_lp WHERE c_id = $course_id";
621
                $res_max = Database::query($get_max);
622
                if (Database::num_rows($res_max) < 1) {
623
                    $dsp = 1;
624
                } else {
625
                    $row = Database::fetch_array($res_max);
626
                    $dsp = $row[0] + 1;
627
                }*/
628
629
                $dsp = 1;
630
                $category = null;
631
                if (!empty($categoryId)) {
632
                    $category = Container::getLpCategoryRepository()->find($categoryId);
633
                }
634
635
                $lp = new CLp();
636
                $lp
637
                    ->setLpType($type)
638
                    ->setName($name)
639
                    ->setDescription($description)
640
                    ->setDisplayOrder($dsp)
641
                    ->setCategory($category)
642
                    ->setPublicatedOn($publicated_on)
643
                    ->setExpiredOn($expired_on)
644
                    ->setParent($courseEntity)
645
                    ->addCourseLink($courseEntity, $sessionEntity)
646
                ;
647
648
                $em = Database::getManager();
649
                $em->persist($lp);
650
                $em->flush();
651
                break;
652
        }
653
654
        return $lp;
655
    }
656
657
    /**
658
     * Auto completes the parents of an item in case it's been completed or passed.
659
     *
660
     * @param int $item Optional ID of the item from which to look for parents
661
     */
662
    public function autocomplete_parents($item)
663
    {
664
        $debug = $this->debug;
665
666
        if (empty($item)) {
667
            $item = $this->current;
668
        }
669
670
        $currentItem = $this->getItem($item);
671
        if ($currentItem) {
672
            $parent_id = $currentItem->get_parent();
673
            $parent = $this->getItem($parent_id);
674
            if ($parent) {
675
                // if $item points to an object and there is a parent.
676
                if ($debug) {
677
                    error_log(
678
                        'Autocompleting parent of item '.$item.' '.
679
                        $currentItem->get_title().'" (item '.$parent_id.' "'.$parent->get_title().'") ',
680
                        0
681
                    );
682
                }
683
684
                // New experiment including failed and browsed in completed status.
685
                //$current_status = $currentItem->get_status();
686
                //if ($currentItem->is_done() || $current_status == 'browsed' || $current_status == 'failed') {
687
                // Fixes chapter auto complete
688
                if (true) {
689
                    // If the current item is completed or passes or succeeded.
690
                    $updateParentStatus = true;
691
                    if ($debug) {
692
                        error_log('Status of current item is alright');
693
                    }
694
695
                    foreach ($parent->get_children() as $childItemId) {
696
                        $childItem = $this->getItem($childItemId);
697
698
                        // If children was not set try to get the info
699
                        if (empty($childItem->db_item_view_id)) {
700
                            $childItem->set_lp_view($this->lp_view_id, $this->course_int_id);
701
                        }
702
703
                        // Check all his brothers (parent's children) for completion status.
704
                        if ($childItemId != $item) {
705
                            if ($debug) {
706
                                error_log(
707
                                    'Looking at brother #'.$childItemId.' "'.$childItem->get_title().'", status is '.$childItem->get_status(),
708
                                    0
709
                                );
710
                            }
711
                            // Trying completing parents of failed and browsed items as well.
712
                            if ($childItem->status_is(
713
                                [
714
                                    'completed',
715
                                    'passed',
716
                                    'succeeded',
717
                                    'browsed',
718
                                    'failed',
719
                                ]
720
                            )
721
                            ) {
722
                                // Keep completion status to true.
723
                                continue;
724
                            } else {
725
                                if ($debug > 2) {
726
                                    error_log(
727
                                        '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,
728
                                        0
729
                                    );
730
                                }
731
                                $updateParentStatus = false;
732
                                break;
733
                            }
734
                        }
735
                    }
736
737
                    if ($updateParentStatus) {
738
                        // If all the children were completed:
739
                        $parent->set_status('completed');
740
                        $parent->save(false, $this->prerequisites_match($parent->get_id()));
741
                        // Force the status to "completed"
742
                        //$this->update_queue[$parent->get_id()] = $parent->get_status();
743
                        $this->update_queue[$parent->get_id()] = 'completed';
744
                        if ($debug) {
745
                            error_log(
746
                                'Added parent #'.$parent->get_id().' "'.$parent->get_title().'" to update queue status: completed '.
747
                                print_r($this->update_queue, 1),
748
                                0
749
                            );
750
                        }
751
                        // Recursive call.
752
                        $this->autocomplete_parents($parent->get_id());
753
                    }
754
                }
755
            } else {
756
                if ($debug) {
757
                    error_log("Parent #$parent_id does not exists");
758
                }
759
            }
760
        } else {
761
            if ($debug) {
762
                error_log("#$item is an item that doesn't have parents");
763
            }
764
        }
765
    }
766
767
    /**
768
     * Closes the current resource.
769
     *
770
     * Stops the timer
771
     * Saves into the database if required
772
     * Clears the current resource data from this object
773
     *
774
     * @return bool True on success, false on failure
775
     */
776
    public function close()
777
    {
778
        if (empty($this->lp_id)) {
779
            $this->error = 'Trying to close this learnpath but no ID is set';
780
781
            return false;
782
        }
783
        $this->current_time_stop = time();
784
        $this->ordered_items = [];
785
        $this->index = 0;
786
        unset($this->lp_id);
787
        //unset other stuff
788
        return true;
789
    }
790
791
    /**
792
     * Static admin function allowing removal of a learnpath.
793
     *
794
     * @param array  $courseInfo
795
     * @param int    $id         Learnpath ID
796
     * @param string $delete     Whether to delete data or keep it (default: 'keep', others: 'remove')
797
     *
798
     * @return bool True on success, false on failure (might change that to return number of elements deleted)
799
     */
800
    public function delete($courseInfo = null, $id = null, $delete = 'keep')
801
    {
802
        $course_id = api_get_course_int_id();
803
        if (!empty($courseInfo)) {
804
            $course_id = isset($courseInfo['real_id']) ? $courseInfo['real_id'] : $course_id;
805
        }
806
807
        // TODO: Implement a way of getting this to work when the current object is not set.
808
        // In clear: implement this in the item class as well (abstract class) and use the given ID in queries.
809
        // If an ID is specifically given and the current LP is not the same, prevent delete.
810
        if (!empty($id) && ($id != $this->lp_id)) {
811
            return false;
812
        }
813
814
        $lp_item = Database::get_course_table(TABLE_LP_ITEM);
815
        $lp_view = Database::get_course_table(TABLE_LP_VIEW);
816
        $lp_item_view = Database::get_course_table(TABLE_LP_ITEM_VIEW);
817
818
        // Delete lp item id.
819
        foreach ($this->items as $lpItemId => $dummy) {
820
            $sql = "DELETE FROM $lp_item_view
821
                    WHERE lp_item_id = '".$lpItemId."'";
822
            Database::query($sql);
823
        }
824
825
        // Proposed by Christophe (nickname: clefevre)
826
        $sql = "DELETE FROM $lp_item
827
                WHERE lp_id = ".$this->lp_id;
828
        Database::query($sql);
829
830
        $sql = "DELETE FROM $lp_view
831
                WHERE lp_id = ".$this->lp_id;
832
        Database::query($sql);
833
834
        //self::toggleVisibility($this->lp_id, 0);
835
836
        /*if (2 == $this->type || 3 == $this->type) {
837
            // This is a scorm learning path, delete the files as well.
838
            $sql = "SELECT path FROM $lp
839
                    WHERE iid = ".$this->lp_id;
840
            $res = Database::query($sql);
841
            if (Database::num_rows($res) > 0) {
842
                $row = Database::fetch_array($res);
843
                $path = $row['path'];
844
                $sql = "SELECT iid FROM $lp
845
                        WHERE
846
                            c_id = $course_id AND
847
                            path = '$path' AND
848
                            iid != ".$this->lp_id;
849
                $res = Database::query($sql);
850
                if (Database::num_rows($res) > 0) {
851
                    // Another learning path uses this directory, so don't delete it.
852
                    if ($this->debug > 2) {
853
                        error_log('In learnpath::delete(), found other LP using path '.$path.', keeping directory', 0);
854
                    }
855
                } else {
856
                    // No other LP uses that directory, delete it.
857
                    $course_rel_dir = api_get_course_path().'/scorm/'; // scorm dir web path starting from /courses
858
                    // The absolute system path for this course.
859
                    $course_scorm_dir = api_get_path(SYS_COURSE_PATH).$course_rel_dir;
860
                    if ('remove' == $delete && is_dir($course_scorm_dir.$path) && !empty($course_scorm_dir)) {
861
                        if ($this->debug > 2) {
862
                            error_log('In learnpath::delete(), found SCORM, deleting directory: '.$course_scorm_dir.$path, 0);
863
                        }
864
                        // Proposed by Christophe (clefevre).
865
                        if (0 == strcmp(substr($path, -2), "/.")) {
866
                            $path = substr($path, 0, -1); // Remove "." at the end.
867
                        }
868
                        //exec('rm -rf ' . $course_scorm_dir . $path); // See Bug #5208, this is not OS-portable way.
869
                        rmdirr($course_scorm_dir.$path);
870
                    }
871
                }
872
            }
873
        }*/
874
875
        $table = Database::get_course_table(TABLE_LP_REL_USERGROUP);
876
        $sql = "DELETE FROM $table
877
                WHERE
878
                    lp_id = {$this->lp_id}";
879
        Database::query($sql);
880
881
        $repo = Container::getLpRepository();
882
        $lp = $repo->find($this->lp_id);
883
        Database::getManager()->remove($lp);
884
        Database::getManager()->flush();
885
886
        // Updates the display order of all lps.
887
        $this->update_display_order();
888
889
        $link_info = GradebookUtils::isResourceInCourseGradebook(
890
            api_get_course_id(),
891
            4,
892
            $id,
893
            api_get_session_id()
894
        );
895
896
        if (false !== $link_info) {
897
            GradebookUtils::remove_resource_from_course_gradebook($link_info['id']);
898
        }
899
900
        if ('true' === api_get_setting('search_enabled')) {
901
            delete_all_values_for_item($this->cc, TOOL_LEARNPATH, $this->lp_id);
902
        }
903
    }
904
905
    /**
906
     * Removes all the children of one item - dangerous!
907
     *
908
     * @param int $id Element ID of which children have to be removed
909
     *
910
     * @return int Total number of children removed
911
     */
912
    public function delete_children_items($id)
913
    {
914
        $course_id = $this->course_info['real_id'];
915
916
        $num = 0;
917
        $id = (int) $id;
918
        if (empty($id) || empty($course_id)) {
919
            return false;
920
        }
921
        $lp_item = Database::get_course_table(TABLE_LP_ITEM);
922
        $sql = "SELECT * FROM $lp_item
923
                WHERE parent_item_id = $id";
924
        $res = Database::query($sql);
925
        while ($row = Database::fetch_array($res)) {
926
            $num += $this->delete_children_items($row['iid']);
927
            $sql = "DELETE FROM $lp_item
928
                    WHERE iid = ".$row['iid'];
929
            Database::query($sql);
930
            $num++;
931
        }
932
933
        return $num;
934
    }
935
936
    /**
937
     * Removes an item from the current learnpath.
938
     *
939
     * @param int $id Elem ID (0 if first)
940
     *
941
     * @return int Number of elements moved
942
     *
943
     * @todo implement resource removal
944
     */
945
    public function delete_item($id)
946
    {
947
        $course_id = api_get_course_int_id();
948
        $id = (int) $id;
949
        // TODO: Implement the resource removal.
950
        if (empty($id) || empty($course_id)) {
951
            return false;
952
        }
953
954
        $repo = Container::getLpItemRepository();
955
        $item = $repo->find($id);
956
        if (null === $item) {
957
            return false;
958
        }
959
960
        $em = Database::getManager();
961
        $repo->removeFromTree($item);
962
        $em->flush();
963
        $lp_item = Database::get_course_table(TABLE_LP_ITEM);
964
965
        //Removing prerequisites since the item will not longer exist
966
        $sql_all = "UPDATE $lp_item SET prerequisite = ''
967
                    WHERE prerequisite = '$id'";
968
        Database::query($sql_all);
969
970
        $sql = "UPDATE $lp_item
971
                SET previous_item_id = ".$this->getLastInFirstLevel()."
972
                WHERE lp_id = {$this->lp_id} AND item_type = '".TOOL_LP_FINAL_ITEM."'";
973
        Database::query($sql);
974
975
        // Remove from search engine if enabled.
976
        if ('true' === api_get_setting('search_enabled')) {
977
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
978
            $sql = 'SELECT * FROM %s
979
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
980
                    LIMIT 1';
981
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp, $id);
982
            $res = Database::query($sql);
983
            if (Database::num_rows($res) > 0) {
984
                $row2 = Database::fetch_array($res);
985
                $di = new ChamiloIndexer();
986
                $di->remove_document($row2['search_did']);
987
            }
988
            $sql = 'DELETE FROM %s
989
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
990
                    LIMIT 1';
991
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp, $id);
992
            Database::query($sql);
993
        }
994
    }
995
996
    /**
997
     * Updates an item's content in place.
998
     *
999
     * @param int    $id               Element ID
1000
     * @param int    $parent           Parent item ID
1001
     * @param int    $previous         Previous item ID
1002
     * @param string $title            Item title
1003
     * @param string $description      Item description
1004
     * @param string $prerequisites    Prerequisites (optional)
1005
     * @param array  $audio            The array resulting of the $_FILES[mp3] element
1006
     * @param int    $max_time_allowed
1007
     * @param string $url
1008
     *
1009
     * @return bool True on success, false on error
1010
     */
1011
    public function edit_item(
1012
        $id,
1013
        $parent,
1014
        $previous,
1015
        $title,
1016
        $description,
1017
        $prerequisites = '0',
1018
        $audio = [],
1019
        $max_time_allowed = 0,
1020
        $url = ''
1021
    ) {
1022
        $_course = api_get_course_info();
1023
        $id = (int) $id;
1024
1025
        if (empty($id) || empty($_course)) {
1026
            return false;
1027
        }
1028
        $repo = Container::getLpItemRepository();
1029
        /** @var CLpItem $item */
1030
        $item = $repo->find($id);
1031
        if (null === $item) {
1032
            return false;
1033
        }
1034
1035
        $item
1036
            ->setTitle($title)
1037
            ->setDescription($description)
1038
            ->setPrerequisite($prerequisites)
1039
            ->setMaxTimeAllowed((int) $max_time_allowed)
1040
        ;
1041
1042
        $em = Database::getManager();
1043
        if (!empty($parent)) {
1044
            $parent = $repo->find($parent);
1045
            $item->setParent($parent);
1046
        } else {
1047
            $item->setParent(null);
1048
        }
1049
1050
        if (!empty($previous)) {
1051
            $previous = $repo->find($previous);
1052
            $repo->persistAsNextSiblingOf( $item, $previous);
1053
            //$repo->reorder($item, 'parent');
1054
            //$repo->persistAsNextSiblingOf()
1055
        } else {
1056
            //$previous = $repo->find($this->get_first_item_id());
1057
            //$repo->persistAsPrevSibling($previous);
1058
            //$repo->persistAsPrevSiblingOf()
1059
            $em->persist($item);
1060
        }
1061
1062
        $em->flush();
1063
        /*$repo->verify();
1064
        $repo->recover();*/
1065
        $audio_update_sql = '';
1066
        // @todo implement audio upload.
1067
        /*if (is_array($audio) && !empty($audio['tmp_name']) && 0 === $audio['error']) {
1068
            // Create the audio folder if it does not exist yet.
1069
            //$filepath = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document/';
1070
            if (!is_dir($filepath.'audio')) {
1071
                //mkdir($filepath.'audio', api_get_permissions_for_new_directories());
1072
                $audio_id = DocumentManager::addDocument(
1073
                    $_course,
1074
                    '/audio',
1075
                    'folder',
1076
                    0,
1077
                    'audio'
1078
                );
1079
            }
1080
1081
            // Upload file in documents.
1082
            $pi = pathinfo($audio['name']);
1083
            if ('mp3' === $pi['extension']) {
1084
                $c_det = api_get_course_info($this->cc);
1085
                //$bp = api_get_path(SYS_COURSE_PATH).$c_det['path'].'/document';
1086
                $path = handle_uploaded_document(
1087
                    $c_det,
1088
                    $audio,
1089
                    $bp,
1090
                    '/audio',
1091
                    api_get_user_id(),
1092
                    0,
1093
                    null,
1094
                    0,
1095
                    'rename',
1096
                    false,
1097
                    0
1098
                );
1099
                $path = substr($path, 7);
1100
                // Update reference in lp_item - audio path is the path from inside de document/audio/ dir.
1101
                $audio_update_sql = ", audio = '".Database::escape_string($path)."' ";
1102
            }
1103
        }*/
1104
1105
        if ('link' === $item->getItemType()) {
1106
            $link = new Link();
1107
            $linkId = $item->getPath();
1108
            $link->updateLink($linkId, $url);
1109
        }
1110
    }
1111
1112
    /**
1113
     * Updates an item's prereq in place.
1114
     *
1115
     * @param int    $id              Element ID
1116
     * @param string $prerequisite_id Prerequisite Element ID
1117
     * @param int    $minScore        Prerequisite min score
1118
     * @param int    $maxScore        Prerequisite max score
1119
     *
1120
     * @return bool True on success, false on error
1121
     */
1122
    public function edit_item_prereq($id, $prerequisite_id, $minScore = 0, $maxScore = 100)
1123
    {
1124
        $id = (int) $id;
1125
1126
        if (empty($id)) {
1127
            return false;
1128
        }
1129
        $prerequisite_id = (int) $prerequisite_id;
1130
1131
        if (empty($minScore) || $minScore < 0) {
1132
            $minScore = 0;
1133
        }
1134
1135
        if (empty($maxScore) || $maxScore < 0) {
1136
            $maxScore = 100;
1137
        }
1138
1139
        $minScore = (float) $minScore;
1140
        $maxScore = (float) $maxScore;
1141
1142
        if (empty($prerequisite_id)) {
1143
            $prerequisite_id = 'NULL';
1144
            $minScore = 0;
1145
            $maxScore = 100;
1146
        }
1147
1148
        $table = Database::get_course_table(TABLE_LP_ITEM);
1149
        $sql = " UPDATE $table
1150
                 SET
1151
                    prerequisite = $prerequisite_id ,
1152
                    prerequisite_min_score = $minScore ,
1153
                    prerequisite_max_score = $maxScore
1154
                 WHERE iid = $id";
1155
        Database::query($sql);
1156
1157
        return true;
1158
    }
1159
1160
    /**
1161
     * Get the specific prefix index terms of this learning path.
1162
     *
1163
     * @param string $prefix
1164
     *
1165
     * @return array Array of terms
1166
     */
1167
    public function get_common_index_terms_by_prefix($prefix)
1168
    {
1169
        $terms = get_specific_field_values_list_by_prefix(
1170
            $prefix,
1171
            $this->cc,
1172
            TOOL_LEARNPATH,
1173
            $this->lp_id
1174
        );
1175
        $prefix_terms = [];
1176
        if (!empty($terms)) {
1177
            foreach ($terms as $term) {
1178
                $prefix_terms[] = $term['value'];
1179
            }
1180
        }
1181
1182
        return $prefix_terms;
1183
    }
1184
1185
    /**
1186
     * Gets the number of items currently completed.
1187
     *
1188
     * @param bool $failedStatusException flag to determine the failed status is not considered progressed
1189
     *
1190
     * @return int The number of items currently completed
1191
     */
1192
    public function get_complete_items_count($failedStatusException = false)
1193
    {
1194
        $i = 0;
1195
        $completedStatusList = [
1196
            'completed',
1197
            'passed',
1198
            'succeeded',
1199
            'browsed',
1200
        ];
1201
1202
        if (!$failedStatusException) {
1203
            $completedStatusList[] = 'failed';
1204
        }
1205
1206
        foreach ($this->items as $id => $dummy) {
1207
            // Trying failed and browsed considered "progressed" as well.
1208
            if ($this->items[$id]->status_is($completedStatusList) &&
1209
                'dir' !== $this->items[$id]->get_type()
1210
            ) {
1211
                $i++;
1212
            }
1213
        }
1214
1215
        return $i;
1216
    }
1217
1218
    /**
1219
     * Gets the current item ID.
1220
     *
1221
     * @return int The current learnpath item id
1222
     */
1223
    public function get_current_item_id()
1224
    {
1225
        $current = 0;
1226
        if (!empty($this->current)) {
1227
            $current = (int) $this->current;
1228
        }
1229
1230
        return $current;
1231
    }
1232
1233
    /**
1234
     * Force to get the first learnpath item id.
1235
     *
1236
     * @return int The current learnpath item id
1237
     */
1238
    public function get_first_item_id()
1239
    {
1240
        $current = 0;
1241
        if (is_array($this->ordered_items)) {
1242
            $current = $this->ordered_items[0];
1243
        }
1244
1245
        return $current;
1246
    }
1247
1248
    /**
1249
     * Gets the total number of items available for viewing in this SCORM.
1250
     *
1251
     * @return int The total number of items
1252
     */
1253
    public function get_total_items_count()
1254
    {
1255
        return count($this->items);
1256
    }
1257
1258
    /**
1259
     * Gets the total number of items available for viewing in this SCORM but without chapters.
1260
     *
1261
     * @return int The total no-chapters number of items
1262
     */
1263
    public function getTotalItemsCountWithoutDirs()
1264
    {
1265
        $total = 0;
1266
        $typeListNotToCount = self::getChapterTypes();
1267
        foreach ($this->items as $temp2) {
1268
            if (!in_array($temp2->get_type(), $typeListNotToCount)) {
1269
                $total++;
1270
            }
1271
        }
1272
1273
        return $total;
1274
    }
1275
1276
    /**
1277
     *  Sets the first element URL.
1278
     */
1279
    public function first()
1280
    {
1281
        if ($this->debug > 0) {
1282
            error_log('In learnpath::first()', 0);
1283
            error_log('$this->last_item_seen '.$this->last_item_seen);
1284
        }
1285
1286
        // Test if the last_item_seen exists and is not a dir.
1287
        if (0 == count($this->ordered_items)) {
1288
            $this->index = 0;
1289
        }
1290
1291
        if (!empty($this->last_item_seen) &&
1292
            !empty($this->items[$this->last_item_seen]) &&
1293
            'dir' != $this->items[$this->last_item_seen]->get_type()
1294
            //with this change (below) the LP will NOT go to the next item, it will take lp item we left
1295
            //&& !$this->items[$this->last_item_seen]->is_done()
1296
        ) {
1297
            if ($this->debug > 2) {
1298
                error_log(
1299
                    'In learnpath::first() - Last item seen is '.$this->last_item_seen.' of type '.
1300
                    $this->items[$this->last_item_seen]->get_type()
1301
                );
1302
            }
1303
            $index = -1;
1304
            foreach ($this->ordered_items as $myindex => $item_id) {
1305
                if ($item_id == $this->last_item_seen) {
1306
                    $index = $myindex;
1307
                    break;
1308
                }
1309
            }
1310
            if (-1 == $index) {
1311
                // Index hasn't changed, so item not found - panic (this shouldn't happen).
1312
                if ($this->debug > 2) {
1313
                    error_log('Last item ('.$this->last_item_seen.') was found in items but not in ordered_items, panic!', 0);
1314
                }
1315
1316
                return false;
1317
            } else {
1318
                $this->last = $this->last_item_seen;
1319
                $this->current = $this->last_item_seen;
1320
                $this->index = $index;
1321
            }
1322
        } else {
1323
            if ($this->debug > 2) {
1324
                error_log('In learnpath::first() - No last item seen', 0);
1325
            }
1326
            $index = 0;
1327
            // Loop through all ordered items and stop at the first item that is
1328
            // not a directory *and* that has not been completed yet.
1329
            while (!empty($this->ordered_items[$index]) &&
1330
                is_a($this->items[$this->ordered_items[$index]], 'learnpathItem') &&
1331
                (
1332
                    'dir' === $this->items[$this->ordered_items[$index]]->get_type() ||
1333
                    true === $this->items[$this->ordered_items[$index]]->is_done()
1334
                ) && $index < $this->max_ordered_items) {
1335
                $index++;
1336
            }
1337
1338
            $this->last = $this->current;
1339
            // current is
1340
            $this->current = isset($this->ordered_items[$index]) ? $this->ordered_items[$index] : null;
1341
            $this->index = $index;
1342
            if ($this->debug > 2) {
1343
                error_log('$index '.$index);
1344
                error_log('In learnpath::first() - No last item seen');
1345
                error_log('New last = '.$this->last.'('.$this->ordered_items[$index].')');
1346
            }
1347
        }
1348
        if ($this->debug > 2) {
1349
            error_log('In learnpath::first() - First item is '.$this->get_current_item_id());
1350
        }
1351
    }
1352
1353
    /**
1354
     * Gets the js library from the database.
1355
     *
1356
     * @return string The name of the javascript library to be used
1357
     */
1358
    public function get_js_lib()
1359
    {
1360
        $lib = '';
1361
        if (!empty($this->js_lib)) {
1362
            $lib = $this->js_lib;
1363
        }
1364
1365
        return $lib;
1366
    }
1367
1368
    /**
1369
     * Gets the learnpath database ID.
1370
     *
1371
     * @return int Learnpath ID in the lp table
1372
     */
1373
    public function get_id()
1374
    {
1375
        if (!empty($this->lp_id)) {
1376
            return (int) $this->lp_id;
1377
        }
1378
1379
        return 0;
1380
    }
1381
1382
    /**
1383
     * Gets the last element URL.
1384
     *
1385
     * @return string URL to load into the viewer
1386
     */
1387
    public function get_last()
1388
    {
1389
        // This is just in case the lesson doesn't cointain a valid scheme, just to avoid "Notices"
1390
        if (count($this->ordered_items) > 0) {
1391
            $this->index = count($this->ordered_items) - 1;
1392
1393
            return $this->ordered_items[$this->index];
1394
        }
1395
1396
        return false;
1397
    }
1398
1399
    /**
1400
     * Get the last element in the first level.
1401
     * Unlike learnpath::get_last this function doesn't consider the subsection' elements.
1402
     *
1403
     * @return mixed
1404
     */
1405
    public function getLastInFirstLevel()
1406
    {
1407
        try {
1408
            $lastId = Database::getManager()
1409
                ->createQuery('SELECT i.iid FROM ChamiloCourseBundle:CLpItem i
1410
                WHERE i.lp = :lp AND i.parent IS NULL AND i.itemType != :type ORDER BY i.displayOrder DESC')
1411
                ->setMaxResults(1)
1412
                ->setParameters(['lp' => $this->lp_id, 'type' => TOOL_LP_FINAL_ITEM])
1413
                ->getSingleScalarResult();
1414
1415
            return $lastId;
1416
        } catch (Exception $exception) {
1417
            return 0;
1418
        }
1419
    }
1420
1421
    /**
1422
     * Gets the navigation bar for the learnpath display screen.
1423
     *
1424
     * @param string $barId
1425
     *
1426
     * @return string The HTML string to use as a navigation bar
1427
     */
1428
    public function get_navigation_bar($barId = '')
1429
    {
1430
        if (empty($barId)) {
1431
            $barId = 'control-top';
1432
        }
1433
        $lpId = $this->lp_id;
1434
        $mycurrentitemid = $this->get_current_item_id();
1435
        $reportingText = get_lang('Reporting');
1436
        $previousText = get_lang('Previous');
1437
        $nextText = get_lang('Next');
1438
        $fullScreenText = get_lang('Back to normal screen');
1439
1440
        $settings = api_get_configuration_value('lp_view_settings');
1441
        $display = $settings['display'] ?? false;
1442
        $reportingIcon = '
1443
            <a class="icon-toolbar"
1444
                id="stats_link"
1445
                href="lp_controller.php?action=stats&'.api_get_cidreq(true).'&lp_id='.$lpId.'"
1446
                onclick="window.parent.API.save_asset(); return true;"
1447
                target="content_name" title="'.$reportingText.'">
1448
                <span class="fa fa-info"></span><span class="sr-only">'.$reportingText.'</span>
1449
            </a>';
1450
1451
        if (!empty($display)) {
1452
            $showReporting = isset($display['show_reporting_icon']) ? $display['show_reporting_icon'] : true;
1453
            if (false === $showReporting) {
1454
                $reportingIcon = '';
1455
            }
1456
        }
1457
1458
        $hideArrows = false;
1459
        if (isset($settings['display']) && isset($settings['display']['hide_lp_arrow_navigation'])) {
1460
            $hideArrows = $settings['display']['hide_lp_arrow_navigation'];
1461
        }
1462
1463
        $previousIcon = '';
1464
        $nextIcon = '';
1465
        if (false === $hideArrows) {
1466
            $previousIcon = '
1467
                <a class="icon-toolbar" id="scorm-previous" href="#"
1468
                    onclick="switch_item('.$mycurrentitemid.',\'previous\');return false;" title="'.$previousText.'">
1469
                    <span class="fa fa-chevron-left"></span><span class="sr-only">'.$previousText.'</span>
1470
                </a>';
1471
1472
            $nextIcon = '
1473
                <a class="icon-toolbar" id="scorm-next" href="#"
1474
                    onclick="switch_item('.$mycurrentitemid.',\'next\');return false;" title="'.$nextText.'">
1475
                    <span class="fa fa-chevron-right"></span><span class="sr-only">'.$nextText.'</span>
1476
                </a>';
1477
        }
1478
1479
        if ('fullscreen' === $this->mode) {
1480
            $navbar = '
1481
                  <span id="'.$barId.'" class="buttons">
1482
                    '.$reportingIcon.'
1483
                    '.$previousIcon.'
1484
                    '.$nextIcon.'
1485
                    <a class="icon-toolbar" id="view-embedded"
1486
                        href="lp_controller.php?action=mode&mode=embedded" target="_top" title="'.$fullScreenText.'">
1487
                        <span class="fa fa-columns"></span><span class="sr-only">'.$fullScreenText.'</span>
1488
                    </a>
1489
                  </span>';
1490
        } else {
1491
            $navbar = '
1492
                 <span id="'.$barId.'" class="buttons text-right">
1493
                    '.$reportingIcon.'
1494
                    '.$previousIcon.'
1495
                    '.$nextIcon.'
1496
                </span>';
1497
        }
1498
1499
        return $navbar;
1500
    }
1501
1502
    /**
1503
     * Gets the next resource in queue (url).
1504
     *
1505
     * @return string URL to load into the viewer
1506
     */
1507
    public function get_next_index()
1508
    {
1509
        // TODO
1510
        $index = $this->index;
1511
        $index++;
1512
        while (
1513
            !empty($this->ordered_items[$index]) && ('dir' == $this->items[$this->ordered_items[$index]]->get_type()) &&
1514
            $index < $this->max_ordered_items
1515
        ) {
1516
            $index++;
1517
            if ($index == $this->max_ordered_items) {
1518
                if ('dir' === $this->items[$this->ordered_items[$index]]->get_type()) {
1519
                    return $this->index;
1520
                }
1521
1522
                return $index;
1523
            }
1524
        }
1525
        if (empty($this->ordered_items[$index])) {
1526
            return $this->index;
1527
        }
1528
1529
        return $index;
1530
    }
1531
1532
    /**
1533
     * Gets item_id for the next element.
1534
     *
1535
     * @return int Next item (DB) ID
1536
     */
1537
    public function get_next_item_id()
1538
    {
1539
        $new_index = $this->get_next_index();
1540
        if (!empty($new_index)) {
1541
            if (isset($this->ordered_items[$new_index])) {
1542
                return $this->ordered_items[$new_index];
1543
            }
1544
        }
1545
1546
        return 0;
1547
    }
1548
1549
    /**
1550
     * Returns the package type ('scorm','aicc','scorm2004','ppt'...).
1551
     *
1552
     * Generally, the package provided is in the form of a zip file, so the function
1553
     * has been written to test a zip file. If not a zip, the function will return the
1554
     * default return value: ''
1555
     *
1556
     * @param string $filePath the path to the file
1557
     * @param string $file_name the original name of the file
1558
     *
1559
     * @return string 'scorm','aicc','scorm2004','error-empty-package'
1560
     *                if the package is empty, or '' if the package cannot be recognized
1561
     */
1562
    public static function getPackageType($filePath, $file_name)
1563
    {
1564
        // Get name of the zip file without the extension.
1565
        $file_info = pathinfo($file_name);
1566
        $extension = $file_info['extension']; // Extension only.
1567
        if (!empty($_POST['ppt2lp']) && !in_array(strtolower($extension), [
1568
                'dll',
1569
                'exe',
1570
            ])) {
1571
            return 'oogie';
1572
        }
1573
        if (!empty($_POST['woogie']) && !in_array(strtolower($extension), [
1574
                'dll',
1575
                'exe',
1576
            ])) {
1577
            return 'woogie';
1578
        }
1579
1580
        $zipFile = new ZipFile();
1581
        $zipFile->openFile($filePath);
1582
        $zipContentArray = $zipFile->getEntries();
1583
        $package_type = '';
1584
        $manifest = '';
1585
        $aicc_match_crs = 0;
1586
        $aicc_match_au = 0;
1587
        $aicc_match_des = 0;
1588
        $aicc_match_cst = 0;
1589
        $countItems = 0;
1590
        // The following loop should be stopped as soon as we found the right imsmanifest.xml (how to recognize it?).
1591
        if ($zipContentArray) {
1592
            $countItems = count($zipContentArray);
1593
            if ($countItems > 0) {
1594
                foreach ($zipContentArray as $thisContent) {
1595
                    $fileName = basename($thisContent->getName());
1596
                    if (preg_match('~.(php.*|phtml)$~i', $fileName)) {
1597
                        // New behaviour: Don't do anything. These files will be removed in scorm::import_package.
1598
                    } elseif (false !== stristr($fileName, 'imsmanifest.xml')) {
1599
                        $manifest = $fileName; // Just the relative directory inside scorm/
1600
                        $package_type = 'scorm';
1601
                        break; // Exit the foreach loop.
1602
                    } elseif (
1603
                        preg_match('/aicc\//i', $fileName) ||
1604
                        in_array(
1605
                            strtolower(pathinfo($fileName, PATHINFO_EXTENSION)),
1606
                            ['crs', 'au', 'des', 'cst']
1607
                        )
1608
                    ) {
1609
                        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
1610
                        switch ($ext) {
1611
                            case 'crs':
1612
                                $aicc_match_crs = 1;
1613
                                break;
1614
                            case 'au':
1615
                                $aicc_match_au = 1;
1616
                                break;
1617
                            case 'des':
1618
                                $aicc_match_des = 1;
1619
                                break;
1620
                            case 'cst':
1621
                                $aicc_match_cst = 1;
1622
                                break;
1623
                            default:
1624
                                break;
1625
                        }
1626
                        //break; // Don't exit the loop, because if we find an imsmanifest afterwards, we want it, not the AICC.
1627
                    } else {
1628
                        $package_type = '';
1629
                    }
1630
                }
1631
            }
1632
        }
1633
1634
        if (empty($package_type) && 4 == ($aicc_match_crs + $aicc_match_au + $aicc_match_des + $aicc_match_cst)) {
1635
            // If found an aicc directory... (!= false means it cannot be false (error) or 0 (no match)).
1636
            $package_type = 'aicc';
1637
        }
1638
1639
        // Try with chamilo course builder
1640
        if (empty($package_type)) {
1641
            // Sometimes users will try to upload an empty zip, or a zip with
1642
            // only a folder. Catch that and make the calling function aware.
1643
            // If the single file was the imsmanifest.xml, then $package_type
1644
            // would be 'scorm' and we wouldn't be here.
1645
            if ($countItems < 2) {
1646
                return 'error-empty-package';
1647
            }
1648
            $package_type = 'chamilo';
1649
        }
1650
1651
        return $package_type;
1652
    }
1653
1654
    /**
1655
     * Gets the previous resource in queue (url). Also initialises time values for this viewing.
1656
     *
1657
     * @return string URL to load into the viewer
1658
     */
1659
    public function get_previous_index()
1660
    {
1661
        $index = $this->index;
1662
        if (isset($this->ordered_items[$index - 1])) {
1663
            $index--;
1664
            while (isset($this->ordered_items[$index]) &&
1665
                ('dir' === $this->items[$this->ordered_items[$index]]->get_type())
1666
            ) {
1667
                $index--;
1668
                if ($index < 0) {
1669
                    return $this->index;
1670
                }
1671
            }
1672
        }
1673
1674
        return $index;
1675
    }
1676
1677
    /**
1678
     * Gets item_id for the next element.
1679
     *
1680
     * @return int Previous item (DB) ID
1681
     */
1682
    public function get_previous_item_id()
1683
    {
1684
        $index = $this->get_previous_index();
1685
1686
        return $this->ordered_items[$index];
1687
    }
1688
1689
    /**
1690
     * Returns the HTML necessary to print a mediaplayer block inside a page.
1691
     *
1692
     * @param int    $lpItemId
1693
     * @param string $autostart
1694
     *
1695
     * @return string The mediaplayer HTML
1696
     */
1697
    public function get_mediaplayer($lpItemId, $autostart = 'true')
1698
    {
1699
        $courseInfo = api_get_course_info();
1700
        $lpItemId = (int) $lpItemId;
1701
1702
        if (empty($courseInfo) || empty($lpItemId)) {
1703
            return '';
1704
        }
1705
        $item = isset($this->items[$lpItemId]) ? $this->items[$lpItemId] : null;
1706
1707
        if (empty($item)) {
1708
            return '';
1709
        }
1710
1711
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
1712
        $tbl_lp_item_view = Database::get_course_table(TABLE_LP_ITEM_VIEW);
1713
        $itemViewId = (int) $item->db_item_view_id;
1714
1715
        // Getting all the information about the item.
1716
        $sql = "SELECT lp_view.status
1717
                FROM $tbl_lp_item as lpi
1718
                INNER JOIN $tbl_lp_item_view as lp_view
1719
                ON (lpi.iid = lp_view.lp_item_id)
1720
                WHERE
1721
                    lp_view.iid = $itemViewId AND
1722
                    lpi.iid = $lpItemId
1723
                ";
1724
        $result = Database::query($sql);
1725
        $row = Database::fetch_assoc($result);
1726
        $output = '';
1727
        $audio = $item->audio;
1728
1729
        if (!empty($audio)) {
1730
            $list = $_SESSION['oLP']->get_toc();
1731
1732
            switch ($item->get_type()) {
1733
                case 'quiz':
1734
                    $type_quiz = false;
1735
                    foreach ($list as $toc) {
1736
                        if ($toc['id'] == $_SESSION['oLP']->current) {
1737
                            $type_quiz = true;
1738
                        }
1739
                    }
1740
1741
                    if ($type_quiz) {
1742
                        if (1 == $_SESSION['oLP']->prevent_reinit) {
1743
                            $autostart_audio = 'completed' === $row['status'] ? 'false' : 'true';
1744
                        } else {
1745
                            $autostart_audio = $autostart;
1746
                        }
1747
                    }
1748
                    break;
1749
                case TOOL_READOUT_TEXT:
1750
                    $autostart_audio = 'false';
1751
                    break;
1752
                default:
1753
                    $autostart_audio = 'true';
1754
            }
1755
1756
            $file = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document'.$audio;
1757
            $url = api_get_path(WEB_COURSE_PATH).$courseInfo['path'].'/document'.$audio.'?'.api_get_cidreq();
1758
1759
            $player = Display::getMediaPlayer(
1760
                $file,
1761
                [
1762
                    'id' => 'lp_audio_media_player',
1763
                    'url' => $url,
1764
                    'autoplay' => $autostart_audio,
1765
                    'width' => '100%',
1766
                ]
1767
            );
1768
1769
            // The mp3 player.
1770
            $output = '<div id="container">';
1771
            $output .= $player;
1772
            $output .= '</div>';
1773
        }
1774
1775
        return $output;
1776
    }
1777
1778
    /**
1779
     * @param int    $studentId
1780
     * @param int    $prerequisite
1781
     * @param Course $course
1782
     * @param int    $sessionId
1783
     *
1784
     * @return bool
1785
     */
1786
    public static function isBlockedByPrerequisite(
1787
        $studentId,
1788
        $prerequisite,
1789
        Course $course,
1790
        $sessionId
1791
    ) {
1792
        if (null === $course) {
1793
            return false;
1794
        }
1795
1796
        $courseId = $course->getId();
1797
1798
        $allow = api_get_configuration_value('allow_teachers_to_access_blocked_lp_by_prerequisite');
1799
        if ($allow) {
1800
            if (api_is_allowed_to_edit() ||
1801
                api_is_platform_admin(true) ||
1802
                api_is_drh() ||
1803
                api_is_coach($sessionId, $courseId, false)
1804
            ) {
1805
                return false;
1806
            }
1807
        }
1808
1809
        $isBlocked = false;
1810
        if (!empty($prerequisite)) {
1811
            $progress = self::getProgress(
1812
                $prerequisite,
1813
                $studentId,
1814
                $courseId,
1815
                $sessionId
1816
            );
1817
            if ($progress < 100) {
1818
                $isBlocked = true;
1819
            }
1820
1821
            if (Tracking::minimumTimeAvailable($sessionId, $courseId)) {
1822
                // Block if it does not exceed minimum time
1823
                // Minimum time (in minutes) to pass the learning path
1824
                $accumulateWorkTime = self::getAccumulateWorkTimePrerequisite($prerequisite, $courseId);
1825
1826
                if ($accumulateWorkTime > 0) {
1827
                    // Total time in course (sum of times in learning paths from course)
1828
                    $accumulateWorkTimeTotal = self::getAccumulateWorkTimeTotal($courseId);
1829
1830
                    // Connect with the plugin_licences_course_session table
1831
                    // which indicates what percentage of the time applies
1832
                    // Minimum connection percentage
1833
                    $perc = 100;
1834
                    // Time from the course
1835
                    $tc = $accumulateWorkTimeTotal;
1836
1837
                    // Percentage of the learning paths
1838
                    $pl = $accumulateWorkTime / $accumulateWorkTimeTotal;
1839
                    // Minimum time for each learning path
1840
                    $accumulateWorkTime = ($pl * $tc * $perc / 100);
1841
1842
                    // Spent time (in seconds) so far in the learning path
1843
                    $lpTimeList = Tracking::getCalculateTime($studentId, $courseId, $sessionId);
1844
                    $lpTime = isset($lpTimeList[TOOL_LEARNPATH][$prerequisite]) ? $lpTimeList[TOOL_LEARNPATH][$prerequisite] : 0;
1845
1846
                    if ($lpTime < ($accumulateWorkTime * 60)) {
1847
                        $isBlocked = true;
1848
                    }
1849
                }
1850
            }
1851
        }
1852
1853
        return $isBlocked;
1854
    }
1855
1856
    /**
1857
     * Checks if the learning path is visible for student after the progress
1858
     * of its prerequisite is completed, considering the time availability and
1859
     * the LP visibility.
1860
     */
1861
    public static function is_lp_visible_for_student(CLp $lp, $student_id, Course $course, SessionEntity $session = null): bool
1862
    {
1863
        if (null === $course) {
1864
            return false;
1865
        }
1866
1867
        $sessionId = $session ? $session->getId() : 0;
1868
        $courseId = $course->getId();
1869
        $visibility = $lp->isVisible($course, $session);
1870
1871
        // If the item was deleted.
1872
        if (false === $visibility) {
1873
            return false;
1874
        }
1875
1876
        $now = time();
1877
        if ($lp->hasCategory()) {
1878
            $category = $lp->getCategory();
1879
1880
            if (false === self::categoryIsVisibleForStudent(
1881
                    $category,
1882
                    api_get_user_entity($student_id),
1883
                    $course,
1884
                    $session
1885
                )) {
1886
                return false;
1887
            }
1888
1889
            $prerequisite = $lp->getPrerequisite();
1890
            $is_visible = true;
1891
1892
            $isBlocked = self::isBlockedByPrerequisite(
1893
                $student_id,
1894
                $prerequisite,
1895
                $course,
1896
                $sessionId
1897
            );
1898
1899
            if ($isBlocked) {
1900
                $is_visible = false;
1901
            }
1902
1903
            // Also check the time availability of the LP
1904
            if ($is_visible) {
1905
                // Adding visibility restrictions
1906
                if (null !== $lp->getPublicatedOn()) {
1907
                    if ($now < $lp->getPublicatedOn()->getTimestamp()) {
1908
                        $is_visible = false;
1909
                    }
1910
                }
1911
                // Blocking empty start times see BT#2800
1912
                global $_custom;
1913
                if (isset($_custom['lps_hidden_when_no_start_date']) &&
1914
                    $_custom['lps_hidden_when_no_start_date']
1915
                ) {
1916
                    if (null !== $lp->getPublicatedOn()) {
1917
                        $is_visible = false;
1918
                    }
1919
                }
1920
1921
                if (null !== $lp->getExpiredOn()) {
1922
                    if ($now > $lp->getExpiredOn()->getTimestamp()) {
1923
                        $is_visible = false;
1924
                    }
1925
                }
1926
            }
1927
1928
            if ($is_visible) {
1929
                $subscriptionSettings = self::getSubscriptionSettings();
1930
1931
                // Check if the subscription users/group to a LP is ON
1932
                if (1 == $lp->getSubscribeUsers() &&
1933
                    true === $subscriptionSettings['allow_add_users_to_lp']
1934
                ) {
1935
                    // Try group
1936
                    $is_visible = false;
1937
                    // Checking only the user visibility
1938
                    // @todo fix visibility
1939
                    $userVisibility = 1;
1940
                    if (1 == $userVisibility) {
1941
                        $is_visible = true;
1942
                    } else {
1943
                        $userGroups = GroupManager::getAllGroupPerUserSubscription($student_id, $courseId);
1944
                        if (!empty($userGroups)) {
1945
                            foreach ($userGroups as $groupInfo) {
1946
                                $groupId = $groupInfo['iid'];
1947
                                // @todo fix visibility.
1948
                                $userVisibility = 1;
1949
                                if (1 == $userVisibility) {
1950
                                    $is_visible = true;
1951
                                    break;
1952
                                }
1953
                            }
1954
                        }
1955
                    }
1956
                }
1957
            }
1958
1959
            return $is_visible;
1960
        }
1961
1962
        return true;
1963
    }
1964
1965
    /**
1966
     * @param int $lpId
1967
     * @param int $userId
1968
     * @param int $courseId
1969
     * @param int $sessionId
1970
     *
1971
     * @return int
1972
     */
1973
    public static function getProgress($lpId, $userId, $courseId, $sessionId = 0)
1974
    {
1975
        $lpId = (int) $lpId;
1976
        $userId = (int) $userId;
1977
        $courseId = (int) $courseId;
1978
        $sessionId = (int) $sessionId;
1979
1980
        $sessionCondition = api_get_session_condition($sessionId);
1981
        $table = Database::get_course_table(TABLE_LP_VIEW);
1982
        $sql = "SELECT progress FROM $table
1983
                WHERE
1984
                    c_id = $courseId AND
1985
                    lp_id = $lpId AND
1986
                    user_id = $userId $sessionCondition ";
1987
        $res = Database::query($sql);
1988
1989
        $progress = 0;
1990
        if (Database::num_rows($res) > 0) {
1991
            $row = Database::fetch_array($res);
1992
            $progress = (int) $row['progress'];
1993
        }
1994
1995
        return $progress;
1996
    }
1997
1998
    /**
1999
     * @param array $lpList
2000
     * @param int   $userId
2001
     * @param int   $courseId
2002
     * @param int   $sessionId
2003
     *
2004
     * @return array
2005
     */
2006
    public static function getProgressFromLpList($lpList, $userId, $courseId, $sessionId = 0)
2007
    {
2008
        $lpList = array_map('intval', $lpList);
2009
        if (empty($lpList)) {
2010
            return [];
2011
        }
2012
2013
        $lpList = implode("','", $lpList);
2014
2015
        $userId = (int) $userId;
2016
        $courseId = (int) $courseId;
2017
        $sessionId = (int) $sessionId;
2018
2019
        $sessionCondition = api_get_session_condition($sessionId);
2020
        $table = Database::get_course_table(TABLE_LP_VIEW);
2021
        $sql = "SELECT lp_id, progress FROM $table
2022
                WHERE
2023
                    c_id = $courseId AND
2024
                    lp_id IN ('".$lpList."') AND
2025
                    user_id = $userId $sessionCondition ";
2026
        $res = Database::query($sql);
2027
2028
        if (Database::num_rows($res) > 0) {
2029
            $list = [];
2030
            while ($row = Database::fetch_array($res)) {
2031
                $list[$row['lp_id']] = $row['progress'];
2032
            }
2033
2034
            return $list;
2035
        }
2036
2037
        return [];
2038
    }
2039
2040
    /**
2041
     * Displays a progress bar
2042
     * completed so far.
2043
     *
2044
     * @param int    $percentage Progress value to display
2045
     * @param string $text_add   Text to display near the progress value
2046
     *
2047
     * @return string HTML string containing the progress bar
2048
     */
2049
    public static function get_progress_bar($percentage = -1, $text_add = '')
2050
    {
2051
        $text = $percentage.$text_add;
2052
2053
        return '<div class="progress">
2054
            <div id="progress_bar_value"
2055
                class="progress-bar progress-bar-success" role="progressbar"
2056
                aria-valuenow="'.$percentage.'" aria-valuemin="0" aria-valuemax="100" style="width: '.$text.';">
2057
            '.$text.'
2058
            </div>
2059
        </div>';
2060
    }
2061
2062
    /**
2063
     * @param string $mode can be '%' or 'abs'
2064
     *                     otherwise this value will be used $this->progress_bar_mode
2065
     *
2066
     * @return string
2067
     */
2068
    public function getProgressBar($mode = null)
2069
    {
2070
        [$percentage, $text_add] = $this->get_progress_bar_text($mode);
2071
2072
        return self::get_progress_bar($percentage, $text_add);
2073
    }
2074
2075
    /**
2076
     * Gets the progress bar info to display inside the progress bar.
2077
     * Also used by scorm_api.php.
2078
     *
2079
     * @param string $mode Mode of display (can be '%' or 'abs').abs means
2080
     *                     we display a number of completed elements per total elements
2081
     * @param int    $add  Additional steps to fake as completed
2082
     *
2083
     * @return array Percentage or number and symbol (% or /xx)
2084
     */
2085
    public function get_progress_bar_text($mode = '', $add = 0)
2086
    {
2087
        if (empty($mode)) {
2088
            $mode = $this->progress_bar_mode;
2089
        }
2090
        $text = '';
2091
        $percentage = 0;
2092
        // If the option to use the score as progress is set for this learning
2093
        // path, then the rules are completely different: we assume only one
2094
        // item exists and the progress of the LP depends on the score
2095
        $scoreAsProgressSetting = api_get_configuration_value('lp_score_as_progress_enable');
2096
        if (true === $scoreAsProgressSetting) {
2097
            $scoreAsProgress = $this->getUseScoreAsProgress();
2098
            if ($scoreAsProgress) {
2099
                // Get single item's score
2100
                $itemId = $this->get_current_item_id();
2101
                $item = $this->getItem($itemId);
2102
                $score = $item->get_score();
2103
                $maxScore = $item->get_max();
2104
                if ($mode = '%') {
2105
                    if (!empty($maxScore)) {
2106
                        $percentage = ((float) $score / (float) $maxScore) * 100;
2107
                    }
2108
                    $percentage = number_format($percentage, 0);
2109
                    $text = '%';
2110
                } else {
2111
                    $percentage = $score;
2112
                    $text = '/'.$maxScore;
2113
                }
2114
2115
                return [$percentage, $text];
2116
            }
2117
        }
2118
        // otherwise just continue the normal processing of progress
2119
        $total_items = $this->getTotalItemsCountWithoutDirs();
2120
        $completeItems = $this->get_complete_items_count();
2121
        if (0 != $add) {
2122
            $completeItems += $add;
2123
        }
2124
        if ($completeItems > $total_items) {
2125
            $completeItems = $total_items;
2126
        }
2127
        if ('%' === $mode) {
2128
            if ($total_items > 0) {
2129
                $percentage = ((float) $completeItems / (float) $total_items) * 100;
2130
            }
2131
            $percentage = number_format($percentage, 0);
2132
            $text = '%';
2133
        } elseif ('abs' === $mode) {
2134
            $percentage = $completeItems;
2135
            $text = '/'.$total_items;
2136
        }
2137
2138
        return [
2139
            $percentage,
2140
            $text,
2141
        ];
2142
    }
2143
2144
    /**
2145
     * Gets the progress bar mode.
2146
     *
2147
     * @return string The progress bar mode attribute
2148
     */
2149
    public function get_progress_bar_mode()
2150
    {
2151
        if (!empty($this->progress_bar_mode)) {
2152
            return $this->progress_bar_mode;
2153
        }
2154
2155
        return '%';
2156
    }
2157
2158
    /**
2159
     * Gets the learnpath theme (remote or local).
2160
     *
2161
     * @return string Learnpath theme
2162
     */
2163
    public function get_theme()
2164
    {
2165
        if (!empty($this->theme)) {
2166
            return $this->theme;
2167
        }
2168
2169
        return '';
2170
    }
2171
2172
    /**
2173
     * Gets the learnpath session id.
2174
     *
2175
     * @return int
2176
     */
2177
    public function get_lp_session_id()
2178
    {
2179
        if (!empty($this->lp_session_id)) {
2180
            return (int) $this->lp_session_id;
2181
        }
2182
2183
        return 0;
2184
    }
2185
2186
    /**
2187
     * Generate a new prerequisites string for a given item. If this item was a sco and
2188
     * its prerequisites were strings (instead of IDs), then transform those strings into
2189
     * IDs, knowing that SCORM IDs are kept in the "ref" field of the lp_item table.
2190
     * Prefix all item IDs that end-up in the prerequisites string by "ITEM_" to use the
2191
     * same rule as the scormExport() method.
2192
     *
2193
     * @param int $item_id Item ID
2194
     *
2195
     * @return string Prerequisites string ready for the export as SCORM
2196
     */
2197
    public function get_scorm_prereq_string($item_id)
2198
    {
2199
        if ($this->debug > 0) {
2200
            error_log('In learnpath::get_scorm_prereq_string()');
2201
        }
2202
        if (!is_object($this->items[$item_id])) {
2203
            return false;
2204
        }
2205
        /** @var learnpathItem $oItem */
2206
        $oItem = $this->items[$item_id];
2207
        $prereq = $oItem->get_prereq_string();
2208
2209
        if (empty($prereq)) {
2210
            return '';
2211
        }
2212
        if (preg_match('/^\d+$/', $prereq) &&
2213
            isset($this->items[$prereq]) &&
2214
            is_object($this->items[$prereq])
2215
        ) {
2216
            // If the prerequisite is a simple integer ID and this ID exists as an item ID,
2217
            // then simply return it (with the ITEM_ prefix).
2218
            //return 'ITEM_' . $prereq;
2219
            return $this->items[$prereq]->ref;
2220
        } else {
2221
            if (isset($this->refs_list[$prereq])) {
2222
                // It's a simple string item from which the ID can be found in the refs list,
2223
                // so we can transform it directly to an ID for export.
2224
                return $this->items[$this->refs_list[$prereq]]->ref;
2225
            } elseif (isset($this->refs_list['ITEM_'.$prereq])) {
2226
                return $this->items[$this->refs_list['ITEM_'.$prereq]]->ref;
2227
            } else {
2228
                // The last case, if it's a complex form, then find all the IDs (SCORM strings)
2229
                // and replace them, one by one, by the internal IDs (chamilo db)
2230
                // TODO: Modify the '*' replacement to replace the multiplier in front of it
2231
                // by a space as well.
2232
                $find = [
2233
                    '&',
2234
                    '|',
2235
                    '~',
2236
                    '=',
2237
                    '<>',
2238
                    '{',
2239
                    '}',
2240
                    '*',
2241
                    '(',
2242
                    ')',
2243
                ];
2244
                $replace = [
2245
                    ' ',
2246
                    ' ',
2247
                    ' ',
2248
                    ' ',
2249
                    ' ',
2250
                    ' ',
2251
                    ' ',
2252
                    ' ',
2253
                    ' ',
2254
                    ' ',
2255
                ];
2256
                $prereq_mod = str_replace($find, $replace, $prereq);
2257
                $ids = explode(' ', $prereq_mod);
2258
                foreach ($ids as $id) {
2259
                    $id = trim($id);
2260
                    if (isset($this->refs_list[$id])) {
2261
                        $prereq = preg_replace(
2262
                            '/[^a-zA-Z_0-9]('.$id.')[^a-zA-Z_0-9]/',
2263
                            'ITEM_'.$this->refs_list[$id],
2264
                            $prereq
2265
                        );
2266
                    }
2267
                }
2268
2269
                return $prereq;
2270
            }
2271
        }
2272
    }
2273
2274
    /**
2275
     * Returns the XML DOM document's node.
2276
     *
2277
     * @param resource $children Reference to a list of objects to search for the given ITEM_*
2278
     * @param string   $id       The identifier to look for
2279
     *
2280
     * @return mixed The reference to the element found with that identifier. False if not found
2281
     */
2282
    public function get_scorm_xml_node(&$children, $id)
2283
    {
2284
        for ($i = 0; $i < $children->length; $i++) {
2285
            $item_temp = $children->item($i);
2286
            if ('item' === $item_temp->nodeName) {
2287
                if ($item_temp->getAttribute('identifier') == $id) {
2288
                    return $item_temp;
2289
                }
2290
            }
2291
            $subchildren = $item_temp->childNodes;
2292
            if ($subchildren && $subchildren->length > 0) {
2293
                $val = $this->get_scorm_xml_node($subchildren, $id);
2294
                if (is_object($val)) {
2295
                    return $val;
2296
                }
2297
            }
2298
        }
2299
2300
        return false;
2301
    }
2302
2303
    /**
2304
     * Gets the status list for all LP's items.
2305
     *
2306
     * @return array Array of [index] => [item ID => current status]
2307
     */
2308
    public function get_items_status_list()
2309
    {
2310
        $list = [];
2311
        foreach ($this->ordered_items as $item_id) {
2312
            $list[] = [
2313
                $item_id => $this->items[$item_id]->get_status(),
2314
            ];
2315
        }
2316
2317
        return $list;
2318
    }
2319
2320
    /**
2321
     * Return the number of interactions for the given learnpath Item View ID.
2322
     * This method can be used as static.
2323
     *
2324
     * @param int $lp_iv_id  Item View ID
2325
     * @param int $course_id course id
2326
     *
2327
     * @return int
2328
     */
2329
    public static function get_interactions_count_from_db($lp_iv_id, $course_id)
2330
    {
2331
        $table = Database::get_course_table(TABLE_LP_IV_INTERACTION);
2332
        $lp_iv_id = (int) $lp_iv_id;
2333
        $course_id = (int) $course_id;
2334
2335
        $sql = "SELECT count(*) FROM $table
2336
                WHERE c_id = $course_id AND lp_iv_id = $lp_iv_id";
2337
        $res = Database::query($sql);
2338
        $num = 0;
2339
        if (Database::num_rows($res)) {
2340
            $row = Database::fetch_array($res);
2341
            $num = $row[0];
2342
        }
2343
2344
        return $num;
2345
    }
2346
2347
    /**
2348
     * Return the interactions as an array for the given lp_iv_id.
2349
     * This method can be used as static.
2350
     *
2351
     * @param int $lp_iv_id Learnpath Item View ID
2352
     *
2353
     * @return array
2354
     *
2355
     * @todo    Transcode labels instead of switching to HTML (which requires to know the encoding of the LP)
2356
     */
2357
    public static function get_iv_interactions_array($lp_iv_id, $course_id = 0)
2358
    {
2359
        $course_id = empty($course_id) ? api_get_course_int_id() : (int) $course_id;
2360
        $list = [];
2361
        $table = Database::get_course_table(TABLE_LP_IV_INTERACTION);
2362
        $lp_iv_id = (int) $lp_iv_id;
2363
2364
        if (empty($lp_iv_id) || empty($course_id)) {
2365
            return [];
2366
        }
2367
2368
        $sql = "SELECT * FROM $table
2369
                WHERE c_id = ".$course_id." AND lp_iv_id = $lp_iv_id
2370
                ORDER BY order_id ASC";
2371
        $res = Database::query($sql);
2372
        $num = Database::num_rows($res);
2373
        if ($num > 0) {
2374
            $list[] = [
2375
                'order_id' => api_htmlentities(get_lang('Order'), ENT_QUOTES),
2376
                'id' => api_htmlentities(get_lang('Interaction ID'), ENT_QUOTES),
2377
                'type' => api_htmlentities(get_lang('Type'), ENT_QUOTES),
2378
                'time' => api_htmlentities(get_lang('Time (finished at...)'), ENT_QUOTES),
2379
                'correct_responses' => api_htmlentities(get_lang('Correct answers'), ENT_QUOTES),
2380
                'student_response' => api_htmlentities(get_lang('Learner answers'), ENT_QUOTES),
2381
                'result' => api_htmlentities(get_lang('Result'), ENT_QUOTES),
2382
                'latency' => api_htmlentities(get_lang('Time spent'), ENT_QUOTES),
2383
                'student_response_formatted' => '',
2384
            ];
2385
            while ($row = Database::fetch_array($res)) {
2386
                $studentResponseFormatted = urldecode($row['student_response']);
2387
                $content_student_response = explode('__|', $studentResponseFormatted);
2388
                if (count($content_student_response) > 0) {
2389
                    if (count($content_student_response) >= 3) {
2390
                        // Pop the element off the end of array.
2391
                        array_pop($content_student_response);
2392
                    }
2393
                    $studentResponseFormatted = implode(',', $content_student_response);
2394
                }
2395
2396
                $list[] = [
2397
                    'order_id' => $row['order_id'] + 1,
2398
                    'id' => urldecode($row['interaction_id']), //urldecode because they often have %2F or stuff like that
2399
                    'type' => $row['interaction_type'],
2400
                    'time' => $row['completion_time'],
2401
                    'correct_responses' => '', // Hide correct responses from students.
2402
                    'student_response' => $row['student_response'],
2403
                    'result' => $row['result'],
2404
                    'latency' => $row['latency'],
2405
                    'student_response_formatted' => $studentResponseFormatted,
2406
                ];
2407
            }
2408
        }
2409
2410
        return $list;
2411
    }
2412
2413
    /**
2414
     * Return the number of objectives for the given learnpath Item View ID.
2415
     * This method can be used as static.
2416
     *
2417
     * @param int $lp_iv_id  Item View ID
2418
     * @param int $course_id Course ID
2419
     *
2420
     * @return int Number of objectives
2421
     */
2422
    public static function get_objectives_count_from_db($lp_iv_id, $course_id)
2423
    {
2424
        $table = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
2425
        $course_id = (int) $course_id;
2426
        $lp_iv_id = (int) $lp_iv_id;
2427
        $sql = "SELECT count(*) FROM $table
2428
                WHERE c_id = $course_id AND lp_iv_id = $lp_iv_id";
2429
        //@todo seems that this always returns 0
2430
        $res = Database::query($sql);
2431
        $num = 0;
2432
        if (Database::num_rows($res)) {
2433
            $row = Database::fetch_array($res);
2434
            $num = $row[0];
2435
        }
2436
2437
        return $num;
2438
    }
2439
2440
    /**
2441
     * Return the objectives as an array for the given lp_iv_id.
2442
     * This method can be used as static.
2443
     *
2444
     * @param int $lpItemViewId Learnpath Item View ID
2445
     * @param int $course_id
2446
     *
2447
     * @return array
2448
     *
2449
     * @todo    Translate labels
2450
     */
2451
    public static function get_iv_objectives_array($lpItemViewId = 0, $course_id = 0)
2452
    {
2453
        $course_id = empty($course_id) ? api_get_course_int_id() : (int) $course_id;
2454
        $lpItemViewId = (int) $lpItemViewId;
2455
2456
        if (empty($course_id) || empty($lpItemViewId)) {
2457
            return [];
2458
        }
2459
2460
        $table = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
2461
        $sql = "SELECT * FROM $table
2462
                WHERE c_id = $course_id AND lp_iv_id = $lpItemViewId
2463
                ORDER BY order_id ASC";
2464
        $res = Database::query($sql);
2465
        $num = Database::num_rows($res);
2466
        $list = [];
2467
        if ($num > 0) {
2468
            $list[] = [
2469
                'order_id' => api_htmlentities(get_lang('Order'), ENT_QUOTES),
2470
                'objective_id' => api_htmlentities(get_lang('Objective ID'), ENT_QUOTES),
2471
                'score_raw' => api_htmlentities(get_lang('Objective raw score'), ENT_QUOTES),
2472
                'score_max' => api_htmlentities(get_lang('Objective max score'), ENT_QUOTES),
2473
                'score_min' => api_htmlentities(get_lang('Objective min score'), ENT_QUOTES),
2474
                'status' => api_htmlentities(get_lang('Objective status'), ENT_QUOTES),
2475
            ];
2476
            while ($row = Database::fetch_array($res)) {
2477
                $list[] = [
2478
                    'order_id' => $row['order_id'] + 1,
2479
                    'objective_id' => urldecode($row['objective_id']), // urldecode() because they often have %2F
2480
                    'score_raw' => $row['score_raw'],
2481
                    'score_max' => $row['score_max'],
2482
                    'score_min' => $row['score_min'],
2483
                    'status' => $row['status'],
2484
                ];
2485
            }
2486
        }
2487
2488
        return $list;
2489
    }
2490
2491
    /**
2492
     * Generate and return the table of contents for this learnpath. The (flat) table returned can be
2493
     * used by get_html_toc() to be ready to display.
2494
     *
2495
     * @return array TOC as a table with 4 elements per row: title, link, status and level
2496
     */
2497
    public function get_toc()
2498
    {
2499
        $toc = [];
2500
        foreach ($this->ordered_items as $item_id) {
2501
            // TODO: Change this link generation and use new function instead.
2502
            $toc[] = [
2503
                'id' => $item_id,
2504
                'title' => $this->items[$item_id]->get_title(),
2505
                'status' => $this->items[$item_id]->get_status(),
2506
                'status_class' => self::getStatusCSSClassName($this->items[$item_id]->get_status()),
2507
                'level' => $this->items[$item_id]->get_level(),
2508
                'type' => $this->items[$item_id]->get_type(),
2509
                'description' => $this->items[$item_id]->get_description(),
2510
                'path' => $this->items[$item_id]->get_path(),
2511
                'parent' => $this->items[$item_id]->get_parent(),
2512
            ];
2513
        }
2514
2515
        return $toc;
2516
    }
2517
2518
    /**
2519
     * Returns the CSS class name associated with a given item status.
2520
     *
2521
     * @param $status string an item status
2522
     *
2523
     * @return string CSS class name
2524
     */
2525
    public static function getStatusCSSClassName($status)
2526
    {
2527
        if (array_key_exists($status, self::STATUS_CSS_CLASS_NAME)) {
2528
            return self::STATUS_CSS_CLASS_NAME[$status];
2529
        }
2530
2531
        return '';
2532
    }
2533
2534
    /**
2535
     * Generate and return the table of contents for this learnpath. The JS
2536
     * table returned is used inside of scorm_api.php.
2537
     *
2538
     * @param string $varname
2539
     *
2540
     * @return string A JS array variable construction
2541
     */
2542
    public function get_items_details_as_js($varname = 'olms.lms_item_types')
2543
    {
2544
        $toc = $varname.' = new Array();';
2545
        foreach ($this->ordered_items as $item_id) {
2546
            $toc .= $varname."['i$item_id'] = '".$this->items[$item_id]->get_type()."';";
2547
        }
2548
2549
        return $toc;
2550
    }
2551
2552
    /**
2553
     * Gets the learning path type.
2554
     *
2555
     * @param bool $get_name Return the name? If false, return the ID. Default is false.
2556
     *
2557
     * @return mixed Type ID or name, depending on the parameter
2558
     */
2559
    public function get_type($get_name = false)
2560
    {
2561
        $res = false;
2562
        if (!empty($this->type) && (!$get_name)) {
2563
            $res = $this->type;
2564
        }
2565
2566
        return $res;
2567
    }
2568
2569
    /**
2570
     * Gets the learning path type as static method.
2571
     *
2572
     * @param int $lp_id
2573
     *
2574
     * @return mixed Type ID or name, depending on the parameter
2575
     */
2576
    public static function get_type_static($lp_id = 0)
2577
    {
2578
        $tbl_lp = Database::get_course_table(TABLE_LP_MAIN);
2579
        $lp_id = (int) $lp_id;
2580
        $sql = "SELECT lp_type FROM $tbl_lp
2581
                WHERE iid = $lp_id";
2582
        $res = Database::query($sql);
2583
        if (false === $res) {
2584
            return null;
2585
        }
2586
        if (Database::num_rows($res) <= 0) {
2587
            return null;
2588
        }
2589
        $row = Database::fetch_array($res);
2590
2591
        return $row['lp_type'];
2592
    }
2593
2594
    /**
2595
     * Gets a flat list of item IDs ordered for display (level by level ordered by order_display)
2596
     * This method can be used as abstract and is recursive.
2597
     *
2598
     * @param CLp $lp
2599
     * @param int $parent    Parent ID of the items to look for
2600
     *
2601
     * @return array Ordered list of item IDs (empty array on error)
2602
     */
2603
    public static function get_flat_ordered_items_list(CLp $lp, $parent = 0)
2604
    {
2605
        $parent = (int) $parent;
2606
2607
        $criteria = Criteria::create()
2608
            ->orderBy(
2609
                [
2610
                    'displayOrder' => Criteria::ASC,
2611
                ]
2612
            );
2613
        $items = $lp->getItems()->matching($criteria);
2614
        $items = $items->filter(
2615
            function (CLpItem $element) use ($parent) {
2616
                if (empty($parent)) {
2617
                    $parent = null;
2618
                    return $element->getParent() === $parent;
2619
                } else {
2620
                    if (null !== $element->getParent()) {
2621
                        return $element->getParent()->getIid() === $parent;
2622
                    }
2623
                    return false;
2624
                }
2625
            }
2626
        );
2627
2628
        /*
2629
         $lp = (int) $lp;
2630
        $parent = (int) $parent;
2631
2632
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
2633
        $sql = "SELECT iid FROM $tbl_lp_item
2634
                WHERE lp_id = $lp AND parent_item_id = $parent
2635
                ORDER BY display_order";
2636
2637
        $res = Database::query($sql);
2638
        while ($row = Database::fetch_array($res)) {
2639
            $sublist = self::get_flat_ordered_items_list(
2640
                $lp,
2641
                $row['iid'],
2642
                $course_id
2643
            );
2644
            $list[] = $row['iid'];
2645
            foreach ($sublist as $item) {
2646
                $list[] = $item;
2647
            }
2648
        }
2649
        */
2650
2651
        $list = [];
2652
        foreach ($items as $item) {
2653
            $itemId = $item->getIid();
2654
            $sublist = self::get_flat_ordered_items_list($lp, $itemId);
2655
            $list[] = $itemId;
2656
            foreach ($sublist as $subItem) {
2657
                $list[] = $subItem;
2658
            }
2659
        }
2660
2661
        return $list;
2662
    }
2663
2664
    public static function getChapterTypes(): array
2665
    {
2666
        return [
2667
            'dir',
2668
        ];
2669
    }
2670
2671
    /**
2672
     * Uses the table generated by get_toc() and returns an HTML-formatted string ready to display.
2673
     *
2674
     * @param array $toc_list
2675
     *
2676
     * @return array HTML TOC ready to display
2677
     */
2678
    public function getListArrayToc($toc_list = [])
2679
    {
2680
        $lpItemRepo = Container::getLpItemRepository();
2681
        $itemRoot = $lpItemRepo->getItemRoot($this->get_id());
2682
        $options = [
2683
            'decorate' => false,
2684
        ];
2685
        $list = $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
2686
2687
        return $list;
2688
2689
2690
        if (empty($toc_list)) {
0 ignored issues
show
Unused Code introduced by
IfNode 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...
2691
            $toc_list = $this->get_toc();
2692
        }
2693
        // Temporary variables.
2694
        $currentItemId = $this->get_current_item_id();
2695
        $list = [];
2696
        $arrayList = [];
2697
2698
        foreach ($toc_list as $item) {
2699
            $list['id'] = $item['id'];
2700
            $list['status'] = $item['status'];
2701
            $cssStatus = null;
2702
2703
            if (array_key_exists($item['status'], self::STATUS_CSS_CLASS_NAME)) {
2704
                $cssStatus = self::STATUS_CSS_CLASS_NAME[$item['status']];
2705
            }
2706
2707
            $classStyle = ' ';
2708
            $dirTypes = self::getChapterTypes();
2709
2710
            if (in_array($item['type'], $dirTypes)) {
2711
                $classStyle = 'scorm_item_section ';
2712
            }
2713
            if ($item['id'] == $this->current) {
2714
                $classStyle = 'scorm_item_normal '.$classStyle.'scorm_highlight';
2715
            } elseif (!in_array($item['type'], $dirTypes)) {
2716
                $classStyle = 'scorm_item_normal '.$classStyle.' ';
2717
            }
2718
            $title = $item['title'];
2719
            if (empty($title)) {
2720
                $title = self::rl_get_resource_name(
2721
                    api_get_course_id(),
2722
                    $this->get_id(),
2723
                    $item['id']
2724
                );
2725
            }
2726
            $title = Security::remove_XSS($item['title']);
2727
2728
            if (empty($item['description'])) {
2729
                $list['description'] = $title;
2730
            } else {
2731
                $list['description'] = $item['description'];
2732
            }
2733
2734
            $list['class'] = $classStyle.' '.$cssStatus;
2735
            $list['level'] = $item['level'];
2736
            $list['type'] = $item['type'];
2737
2738
            if (in_array($item['type'], $dirTypes)) {
2739
                $list['css_level'] = 'level_'.$item['level'];
2740
            } else {
2741
                $list['css_level'] = 'level_'.$item['level'].' scorm_type_'.self::format_scorm_type_item($item['type']);
2742
            }
2743
2744
            if (in_array($item['type'], $dirTypes)) {
2745
                $list['title'] = stripslashes($title);
2746
            } else {
2747
                $list['title'] = stripslashes($title);
2748
                $list['url'] = $this->get_link('http', $item['id'], $toc_list);
2749
                $list['current_id'] = $currentItemId;
2750
            }
2751
            $arrayList[] = $list;
2752
        }
2753
2754
        return $arrayList;
2755
    }
2756
2757
    /**
2758
     * Returns an HTML-formatted string ready to display with teacher buttons
2759
     * in LP view menu.
2760
     *
2761
     * @return string HTML TOC ready to display
2762
     */
2763
    public function get_teacher_toc_buttons()
2764
    {
2765
        $isAllow = api_is_allowed_to_edit(null, true, false, false);
2766
        $hideIcons = api_get_configuration_value('hide_teacher_icons_lp');
2767
        $html = '';
2768
        if ($isAllow && false == $hideIcons) {
2769
            if ($this->get_lp_session_id() == api_get_session_id()) {
2770
                $html .= '<div id="actions_lp" class="actions_lp"><hr>';
2771
                $html .= '<div class="flex flex-row justify-center">';
2772
                $html .= "<a
2773
                    class='btn btn-sm btn-default'
2774
                    href='lp_controller.php?".api_get_cidreq()."&action=build&lp_id=".$this->lp_id."&isStudentView=false'
2775
                    target='_parent'>".
2776
                    Display::returnFontAwesomeIcon('street-view').get_lang('Overview')."</a>";
2777
                $html .= "<a
2778
                    class='btn btn-sm btn-default'
2779
                    href='lp_controller.php?".api_get_cidreq()."&action=add_item&type=step&lp_id=".$this->lp_id."&isStudentView=false'
2780
                    target='_parent'>".
2781
                    Display::returnFontAwesomeIcon('pencil-alt').get_lang('Edit')."</a>";
2782
                $html .= '<a
2783
                    class="btn btn-sm btn-default"
2784
                    href="lp_controller.php?'.api_get_cidreq()."&action=edit&lp_id=".$this->lp_id.'&isStudentView=false">'.
2785
                    Display::returnFontAwesomeIcon('cog').get_lang('Settings').'</a>';
2786
                $html .= '</div>';
2787
                $html .= '</div>';
2788
            }
2789
        }
2790
2791
        return $html;
2792
    }
2793
2794
    /**
2795
     * Gets the learnpath name/title.
2796
     *
2797
     * @return string Learnpath name/title
2798
     */
2799
    public function get_name()
2800
    {
2801
        if (!empty($this->name)) {
2802
            return $this->name;
2803
        }
2804
2805
        return 'N/A';
2806
    }
2807
2808
    /**
2809
     * @return string
2810
     */
2811
    public function getNameNoTags()
2812
    {
2813
        return strip_tags($this->get_name());
2814
    }
2815
2816
    /**
2817
     * Gets a link to the resource from the present location, depending on item ID.
2818
     *
2819
     * @param string $type         Type of link expected
2820
     * @param int    $item_id      Learnpath item ID
2821
     * @param bool   $provided_toc
2822
     *
2823
     * @return string $provided_toc Link to the lp_item resource
2824
     */
2825
    public function get_link($type = 'http', $item_id = 0, $provided_toc = false)
2826
    {
2827
        $course_id = $this->get_course_int_id();
2828
        $item_id = (int) $item_id;
2829
2830
        if (empty($item_id)) {
2831
            $item_id = $this->get_current_item_id();
2832
2833
            if (empty($item_id)) {
2834
                //still empty, this means there was no item_id given and we are not in an object context or
2835
                //the object property is empty, return empty link
2836
                $this->first();
2837
2838
                return '';
2839
            }
2840
        }
2841
2842
        $file = '';
2843
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
2844
        $lp_item_table = Database::get_course_table(TABLE_LP_ITEM);
2845
        $lp_item_view_table = Database::get_course_table(TABLE_LP_ITEM_VIEW);
2846
2847
        $sql = "SELECT
2848
                    l.lp_type as ltype,
2849
                    l.path as lpath,
2850
                    li.item_type as litype,
2851
                    li.path as lipath,
2852
                    li.parameters as liparams
2853
        		FROM $lp_table l
2854
                INNER JOIN $lp_item_table li
2855
                ON (li.lp_id = l.iid)
2856
        		WHERE
2857
        		    li.iid = $item_id
2858
        		";
2859
        $res = Database::query($sql);
2860
        if (Database::num_rows($res) > 0) {
2861
            $row = Database::fetch_array($res);
2862
            $lp_type = $row['ltype'];
2863
            $lp_path = $row['lpath'];
2864
            $lp_item_type = $row['litype'];
2865
            $lp_item_path = $row['lipath'];
2866
            $lp_item_params = $row['liparams'];
2867
            if (empty($lp_item_params) && false !== strpos($lp_item_path, '?')) {
2868
                [$lp_item_path, $lp_item_params] = explode('?', $lp_item_path);
2869
            }
2870
            //$sys_course_path = api_get_path(SYS_COURSE_PATH).api_get_course_path();
2871
            if ('http' === $type) {
2872
                //web path
2873
                //$course_path = api_get_path(WEB_COURSE_PATH).api_get_course_path();
2874
            } else {
2875
                //$course_path = $sys_course_path; //system path
2876
            }
2877
2878
            // Fixed issue BT#1272 - If the item type is a Chamilo Item (quiz, link, etc),
2879
            // then change the lp type to thread it as a normal Chamilo LP not a SCO.
2880
            if (in_array(
2881
                $lp_item_type,
2882
                ['quiz', 'document', 'final_item', 'link', 'forum', 'thread', 'student_publication']
2883
            )
2884
            ) {
2885
                $lp_type = CLp::LP_TYPE;
2886
            }
2887
2888
            // Now go through the specific cases to get the end of the path
2889
            // @todo Use constants instead of int values.
2890
            switch ($lp_type) {
2891
                case CLp::LP_TYPE:
2892
                    $file = self::rl_get_resource_link_for_learnpath(
2893
                        $course_id,
2894
                        $this->get_id(),
2895
                        $item_id,
2896
                        $this->get_view_id()
2897
                    );
2898
                    switch ($lp_item_type) {
2899
                        case 'document':
2900
                            // Shows a button to download the file instead of just downloading the file directly.
2901
                            $documentPathInfo = pathinfo($file);
2902
                            if (isset($documentPathInfo['extension'])) {
2903
                                $parsed = parse_url($documentPathInfo['extension']);
2904
                                if (isset($parsed['path'])) {
2905
                                    $extension = $parsed['path'];
2906
                                    $extensionsToDownload = [
2907
                                        'zip',
2908
                                        'ppt',
2909
                                        'pptx',
2910
                                        'ods',
2911
                                        'xlsx',
2912
                                        'xls',
2913
                                        'csv',
2914
                                        'doc',
2915
                                        'docx',
2916
                                        'dot',
2917
                                    ];
2918
2919
                                    if (in_array($extension, $extensionsToDownload)) {
2920
                                        $file = api_get_path(WEB_CODE_PATH).
2921
                                            'lp/embed.php?type=download&source=file&lp_item_id='.$item_id.'&'.api_get_cidreq();
2922
                                    }
2923
                                }
2924
                            }
2925
                            break;
2926
                        case 'dir':
2927
                            $file = 'lp_content.php?type=dir';
2928
                            break;
2929
                        case 'link':
2930
                            if (Link::is_youtube_link($file)) {
2931
                                $src = Link::get_youtube_video_id($file);
2932
                                $file = api_get_path(WEB_CODE_PATH).'lp/embed.php?type=youtube&source='.$src;
2933
                            } elseif (Link::isVimeoLink($file)) {
2934
                                $src = Link::getVimeoLinkId($file);
2935
                                $file = api_get_path(WEB_CODE_PATH).'lp/embed.php?type=vimeo&source='.$src;
2936
                            } else {
2937
                                // If the current site is HTTPS and the link is
2938
                                // HTTP, browsers will refuse opening the link
2939
                                $urlId = api_get_current_access_url_id();
2940
                                $url = api_get_access_url($urlId, false);
2941
                                $protocol = substr($url['url'], 0, 5);
2942
                                if ('https' === $protocol) {
2943
                                    $linkProtocol = substr($file, 0, 5);
2944
                                    if ('http:' === $linkProtocol) {
2945
                                        //this is the special intervention case
2946
                                        $file = api_get_path(WEB_CODE_PATH).
2947
                                            'lp/embed.php?type=nonhttps&source='.urlencode($file);
2948
                                    }
2949
                                }
2950
                            }
2951
                            break;
2952
                        case 'quiz':
2953
                            // Check how much attempts of a exercise exits in lp
2954
                            $lp_item_id = $this->get_current_item_id();
2955
                            $lp_view_id = $this->get_view_id();
2956
2957
                            $prevent_reinit = null;
2958
                            if (isset($this->items[$this->current])) {
2959
                                $prevent_reinit = $this->items[$this->current]->get_prevent_reinit();
2960
                            }
2961
2962
                            if (empty($provided_toc)) {
2963
                                $list = $this->get_toc();
2964
                            } else {
2965
                                $list = $provided_toc;
2966
                            }
2967
2968
                            $type_quiz = false;
2969
                            foreach ($list as $toc) {
2970
                                if ($toc['id'] == $lp_item_id && 'quiz' === $toc['type']) {
2971
                                    $type_quiz = true;
2972
                                }
2973
                            }
2974
2975
                            if ($type_quiz) {
2976
                                $lp_item_id = (int) $lp_item_id;
2977
                                $lp_view_id = (int) $lp_view_id;
2978
                                $sql = "SELECT count(*) FROM $lp_item_view_table
2979
                                        WHERE
2980
                                            lp_item_id='".$lp_item_id."' AND
2981
                                            lp_view_id ='".$lp_view_id."' AND
2982
                                            status='completed'";
2983
                                $result = Database::query($sql);
2984
                                $row_count = Database:: fetch_row($result);
2985
                                $count_item_view = (int) $row_count[0];
2986
                                $not_multiple_attempt = 0;
2987
                                if (1 === $prevent_reinit && $count_item_view > 0) {
2988
                                    $not_multiple_attempt = 1;
2989
                                }
2990
                                $file .= '&not_multiple_attempt='.$not_multiple_attempt;
2991
                            }
2992
                            break;
2993
                    }
2994
2995
                    $tmp_array = explode('/', $file);
2996
                    $document_name = $tmp_array[count($tmp_array) - 1];
2997
                    if (strpos($document_name, '_DELETED_')) {
2998
                        $file = 'blank.php?error=document_deleted';
2999
                    }
3000
                    break;
3001
                case CLp::SCORM_TYPE:
3002
                    if ('dir' !== $lp_item_type) {
3003
                        // Quite complex here:
3004
                        // We want to make sure 'http://' (and similar) links can
3005
                        // be loaded as is (withouth the Chamilo path in front) but
3006
                        // some contents use this form: resource.htm?resource=http://blablabla
3007
                        // which means we have to find a protocol at the path's start, otherwise
3008
                        // it should not be considered as an external URL.
3009
                        // if ($this->prerequisites_match($item_id)) {
3010
                        if (0 != preg_match('#^[a-zA-Z]{2,5}://#', $lp_item_path)) {
3011
                            if ($this->debug > 2) {
3012
                                error_log('In learnpath::get_link() '.__LINE__.' - Found match for protocol in '.$lp_item_path, 0);
3013
                            }
3014
                            // Distant url, return as is.
3015
                            $file = $lp_item_path;
3016
                        } else {
3017
                            if ($this->debug > 2) {
3018
                                error_log('In learnpath::get_link() '.__LINE__.' - No starting protocol in '.$lp_item_path);
3019
                            }
3020
                            // Prevent getting untranslatable urls.
3021
                            $lp_item_path = preg_replace('/%2F/', '/', $lp_item_path);
3022
                            $lp_item_path = preg_replace('/%3A/', ':', $lp_item_path);
3023
3024
                            /*$asset = $this->getEntity()->getAsset();
3025
                            $folder = Container::getAssetRepository()->getFolder($asset);
3026
                            $hasFile = Container::getAssetRepository()->getFileSystem()->has($folder.$lp_item_path);
3027
                            $file = null;
3028
                            if ($hasFile) {
3029
                                $file = Container::getAssetRepository()->getAssetUrl($asset).'/'.$lp_item_path;
3030
                            }*/
3031
                            $file = $this->scormUrl.$lp_item_path;
3032
3033
                            // Prepare the path.
3034
                            /*$file = $course_path.'/scorm/'.$lp_path.'/'.$lp_item_path;
3035
                            // TODO: Fix this for urls with protocol header.
3036
                            $file = str_replace('//', '/', $file);
3037
                            $file = str_replace(':/', '://', $file);
3038
                            if ('/' === substr($lp_path, -1)) {
3039
                                $lp_path = substr($lp_path, 0, -1);
3040
                            }*/
3041
                            /*if (!$hasFile) {
3042
                                // if file not found.
3043
                                $decoded = html_entity_decode($lp_item_path);
3044
                                [$decoded] = explode('?', $decoded);
3045
                                if (!is_file(realpath($sys_course_path.'/scorm/'.$lp_path.'/'.$decoded))) {
3046
                                    $file = self::rl_get_resource_link_for_learnpath(
3047
                                        $course_id,
3048
                                        $this->get_id(),
3049
                                        $item_id,
3050
                                        $this->get_view_id()
3051
                                    );
3052
                                    if (empty($file)) {
3053
                                        $file = 'blank.php?error=document_not_found';
3054
                                    } else {
3055
                                        $tmp_array = explode('/', $file);
3056
                                        $document_name = $tmp_array[count($tmp_array) - 1];
3057
                                        if (strpos($document_name, '_DELETED_')) {
3058
                                            $file = 'blank.php?error=document_deleted';
3059
                                        } else {
3060
                                            $file = 'blank.php?error=document_not_found';
3061
                                        }
3062
                                    }
3063
                                } else {
3064
                                    $file = $course_path.'/scorm/'.$lp_path.'/'.$decoded;
3065
                                }
3066
                            }*/
3067
                        }
3068
3069
                        // We want to use parameters if they were defined in the imsmanifest
3070
                        if (false === strpos($file, 'blank.php')) {
3071
                            $lp_item_params = ltrim($lp_item_params, '?');
3072
                            $file .= (false === strstr($file, '?') ? '?' : '').$lp_item_params;
3073
                        }
3074
                    } else {
3075
                        $file = 'lp_content.php?type=dir';
3076
                    }
3077
                    break;
3078
                case CLp::AICC_TYPE:
3079
                    // Formatting AICC HACP append URL.
3080
                    $aicc_append = '?aicc_sid='.
3081
                        urlencode(session_id()).'&aicc_url='.urlencode(api_get_path(WEB_CODE_PATH).'lp/aicc_hacp.php').'&';
3082
                    if (!empty($lp_item_params)) {
3083
                        $aicc_append .= $lp_item_params.'&';
3084
                    }
3085
                    if ('dir' !== $lp_item_type) {
3086
                        // Quite complex here:
3087
                        // We want to make sure 'http://' (and similar) links can
3088
                        // be loaded as is (withouth the Chamilo path in front) but
3089
                        // some contents use this form: resource.htm?resource=http://blablabla
3090
                        // which means we have to find a protocol at the path's start, otherwise
3091
                        // it should not be considered as an external URL.
3092
                        if (0 != preg_match('#^[a-zA-Z]{2,5}://#', $lp_item_path)) {
3093
                            if ($this->debug > 2) {
3094
                                error_log('In learnpath::get_link() '.__LINE__.' - Found match for protocol in '.$lp_item_path, 0);
3095
                            }
3096
                            // Distant url, return as is.
3097
                            $file = $lp_item_path;
3098
                            // Enabled and modified by Ivan Tcholakov, 16-OCT-2008.
3099
                            /*
3100
                            if (stristr($file,'<servername>') !== false) {
3101
                                $file = str_replace('<servername>', $course_path.'/scorm/'.$lp_path.'/', $lp_item_path);
3102
                            }
3103
                            */
3104
                            if (false !== stripos($file, '<servername>')) {
3105
                                //$file = str_replace('<servername>',$course_path.'/scorm/'.$lp_path.'/',$lp_item_path);
3106
                                $web_course_path = str_replace('https://', '', str_replace('http://', '', $course_path));
3107
                                $file = str_replace('<servername>', $web_course_path.'/scorm/'.$lp_path, $lp_item_path);
3108
                            }
3109
3110
                            $file .= $aicc_append;
3111
                        } else {
3112
                            if ($this->debug > 2) {
3113
                                error_log('In learnpath::get_link() '.__LINE__.' - No starting protocol in '.$lp_item_path, 0);
3114
                            }
3115
                            // Prevent getting untranslatable urls.
3116
                            $lp_item_path = preg_replace('/%2F/', '/', $lp_item_path);
3117
                            $lp_item_path = preg_replace('/%3A/', ':', $lp_item_path);
3118
                            // Prepare the path - lp_path might be unusable because it includes the "aicc" subdir name.
3119
                            $file = $course_path.'/scorm/'.$lp_path.'/'.$lp_item_path;
3120
                            // TODO: Fix this for urls with protocol header.
3121
                            $file = str_replace('//', '/', $file);
3122
                            $file = str_replace(':/', '://', $file);
3123
                            $file .= $aicc_append;
3124
                        }
3125
                    } else {
3126
                        $file = 'lp_content.php?type=dir';
3127
                    }
3128
                    break;
3129
                case 4:
3130
                default:
3131
                    break;
3132
            }
3133
            // Replace &amp; by & because &amp; will break URL with params
3134
            $file = !empty($file) ? str_replace('&amp;', '&', $file) : '';
3135
        }
3136
        if ($this->debug > 2) {
3137
            error_log('In learnpath::get_link() - returning "'.$file.'" from get_link', 0);
3138
        }
3139
3140
        return $file;
3141
    }
3142
3143
    /**
3144
     * Gets the latest usable view or generate a new one.
3145
     *
3146
     * @param int $attempt_num Optional attempt number. If none given, takes the highest from the lp_view table
3147
     * @param int $userId      The user ID, as $this->get_user_id() is not always available
3148
     *
3149
     * @return int DB lp_view id
3150
     */
3151
    public function get_view($attempt_num = 0, $userId = null)
3152
    {
3153
        $search = '';
3154
        $attempt_num = (int) $attempt_num;
3155
        // Use $attempt_num to enable multi-views management (disabled so far).
3156
        if (!empty($attempt_num)) {
3157
            $search = 'AND view_count = '.$attempt_num;
3158
        }
3159
3160
        $course_id = api_get_course_int_id();
3161
        $sessionId = api_get_session_id();
3162
3163
        // Check user ID.
3164
        if (empty($userId)) {
3165
            if (empty($this->get_user_id())) {
3166
                $this->error = 'User ID is empty in learnpath::get_view()';
3167
3168
                return null;
3169
            } else {
3170
                $userId = $this->get_user_id();
3171
            }
3172
        }
3173
        $sessionCondition = api_get_session_condition($sessionId);
3174
3175
        // When missing $attempt_num, search for a unique lp_view record for this lp and user.
3176
        $table = Database::get_course_table(TABLE_LP_VIEW);
3177
        $sql = "SELECT iid FROM $table
3178
        		WHERE
3179
        		    c_id = $course_id AND
3180
        		    lp_id = ".$this->get_id()." AND
3181
        		    user_id = ".$userId."
3182
        		    $sessionCondition
3183
        		    $search
3184
                ORDER BY view_count DESC";
3185
        $res = Database::query($sql);
3186
        if (Database::num_rows($res) > 0) {
3187
            $row = Database::fetch_array($res);
3188
            $this->lp_view_id = $row['iid'];
3189
        } elseif (!api_is_invitee()) {
3190
            $params = [
3191
                'c_id' => $course_id,
3192
                'lp_id' => $this->get_id(),
3193
                'user_id' => $this->get_user_id(),
3194
                'view_count' => 1,
3195
                'last_item' => 0,
3196
            ];
3197
            if (!empty($sessionId)) {
3198
                $params['session_id']  = $sessionId;
3199
            }
3200
            $this->lp_view_id = Database::insert($table, $params);
3201
        }
3202
3203
        return $this->lp_view_id;
3204
    }
3205
3206
    /**
3207
     * Gets the current view id.
3208
     *
3209
     * @return int View ID (from lp_view)
3210
     */
3211
    public function get_view_id()
3212
    {
3213
        if (!empty($this->lp_view_id)) {
3214
            return (int) $this->lp_view_id;
3215
        }
3216
3217
        return 0;
3218
    }
3219
3220
    /**
3221
     * Gets the update queue.
3222
     *
3223
     * @return array Array containing IDs of items to be updated by JavaScript
3224
     */
3225
    public function get_update_queue()
3226
    {
3227
        return $this->update_queue;
3228
    }
3229
3230
    /**
3231
     * Gets the user ID.
3232
     *
3233
     * @return int User ID
3234
     */
3235
    public function get_user_id()
3236
    {
3237
        if (!empty($this->user_id)) {
3238
            return (int) $this->user_id;
3239
        }
3240
3241
        return false;
3242
    }
3243
3244
    /**
3245
     * Checks if any of the items has an audio element attached.
3246
     *
3247
     * @return bool True or false
3248
     */
3249
    public function has_audio()
3250
    {
3251
        $has = false;
3252
        foreach ($this->items as $i => $item) {
3253
            if (!empty($this->items[$i]->audio)) {
3254
                $has = true;
3255
                break;
3256
            }
3257
        }
3258
3259
        return $has;
3260
    }
3261
3262
    /**
3263
     * Moves an item up and down at its level.
3264
     *
3265
     * @param int    $id        Item to move up and down
3266
     * @param string $direction Direction 'up' or 'down'
3267
     *
3268
     * @return bool|int
3269
     */
3270
    public function move_item($id, $direction)
3271
    {
3272
        $course_id = api_get_course_int_id();
3273
        if (empty($id) || empty($direction)) {
3274
            return false;
3275
        }
3276
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
3277
        $sql_sel = "SELECT *
3278
                    FROM $tbl_lp_item
3279
                    WHERE
3280
                        iid = $id
3281
                    ";
3282
        $res_sel = Database::query($sql_sel);
3283
        // Check if elem exists.
3284
        if (Database::num_rows($res_sel) < 1) {
3285
            return false;
3286
        }
3287
        // Gather data.
3288
        $row = Database::fetch_array($res_sel);
3289
        $previous = $row['previous_item_id'];
3290
        $next = $row['next_item_id'];
3291
        $display = $row['display_order'];
3292
        $parent = $row['parent_item_id'];
3293
        $lp = $row['lp_id'];
3294
        // Update the item (switch with previous/next one).
3295
        switch ($direction) {
3296
            case 'up':
3297
                if ($display > 1) {
3298
                    $sql_sel2 = "SELECT * FROM $tbl_lp_item
3299
                                 WHERE iid = $previous";
3300
                    $res_sel2 = Database::query($sql_sel2);
3301
                    if (Database::num_rows($res_sel2) < 1) {
3302
                        $previous_previous = 0;
3303
                    }
3304
                    // Gather data.
3305
                    $row2 = Database::fetch_array($res_sel2);
3306
                    $previous_previous = $row2['previous_item_id'];
3307
                    // Update previous_previous item (switch "next" with current).
3308
                    if (0 != $previous_previous) {
3309
                        $sql_upd2 = "UPDATE $tbl_lp_item SET
3310
                                        next_item_id = $id
3311
                                    WHERE iid = $previous_previous";
3312
                        Database::query($sql_upd2);
3313
                    }
3314
                    // Update previous item (switch with current).
3315
                    if (0 != $previous) {
3316
                        $sql_upd2 = "UPDATE $tbl_lp_item SET
3317
                                    next_item_id = $next,
3318
                                    previous_item_id = $id,
3319
                                    display_order = display_order +1
3320
                                    WHERE iid = $previous";
3321
                        Database::query($sql_upd2);
3322
                    }
3323
3324
                    // Update current item (switch with previous).
3325
                    if (0 != $id) {
3326
                        $sql_upd2 = "UPDATE $tbl_lp_item SET
3327
                                        next_item_id = $previous,
3328
                                        previous_item_id = $previous_previous,
3329
                                        display_order = display_order-1
3330
                                    WHERE c_id = ".$course_id." AND id = $id";
3331
                        Database::query($sql_upd2);
3332
                    }
3333
                    // Update next item (new previous item).
3334
                    if (!empty($next)) {
3335
                        $sql_upd2 = "UPDATE $tbl_lp_item SET previous_item_id = $previous
3336
                                     WHERE iid = $next";
3337
                        Database::query($sql_upd2);
3338
                    }
3339
                    $display = $display - 1;
3340
                }
3341
                break;
3342
            case 'down':
3343
                if (0 != $next) {
3344
                    $sql_sel2 = "SELECT * FROM $tbl_lp_item
3345
                                 WHERE iid = $next";
3346
                    $res_sel2 = Database::query($sql_sel2);
3347
                    if (Database::num_rows($res_sel2) < 1) {
3348
                        $next_next = 0;
3349
                    }
3350
                    // Gather data.
3351
                    $row2 = Database::fetch_array($res_sel2);
3352
                    $next_next = $row2['next_item_id'];
3353
                    // Update previous item (switch with current).
3354
                    if (0 != $previous) {
3355
                        $sql_upd2 = "UPDATE $tbl_lp_item
3356
                                     SET next_item_id = $next
3357
                                     WHERE iid = $previous";
3358
                        Database::query($sql_upd2);
3359
                    }
3360
                    // Update current item (switch with previous).
3361
                    if (0 != $id) {
3362
                        $sql_upd2 = "UPDATE $tbl_lp_item SET
3363
                                     previous_item_id = $next,
3364
                                     next_item_id = $next_next,
3365
                                     display_order = display_order + 1
3366
                                     WHERE iid = $id";
3367
                        Database::query($sql_upd2);
3368
                    }
3369
3370
                    // Update next item (new previous item).
3371
                    if (0 != $next) {
3372
                        $sql_upd2 = "UPDATE $tbl_lp_item SET
3373
                                     previous_item_id = $previous,
3374
                                     next_item_id = $id,
3375
                                     display_order = display_order-1
3376
                                     WHERE iid = $next";
3377
                        Database::query($sql_upd2);
3378
                    }
3379
3380
                    // Update next_next item (switch "previous" with current).
3381
                    if (0 != $next_next) {
3382
                        $sql_upd2 = "UPDATE $tbl_lp_item SET
3383
                                     previous_item_id = $id
3384
                                     WHERE iid = $next_next";
3385
                        Database::query($sql_upd2);
3386
                    }
3387
                    $display = $display + 1;
3388
                }
3389
                break;
3390
            default:
3391
                return false;
3392
        }
3393
3394
        return $display;
3395
    }
3396
3397
    /**
3398
     * Move a LP up (display_order).
3399
     *
3400
     * @param int $lp_id      Learnpath ID
3401
     * @param int $categoryId Category ID
3402
     *
3403
     * @return bool
3404
     */
3405
    public static function move_up($lp_id, $categoryId = 0)
3406
    {
3407
        $courseId = api_get_course_int_id();
3408
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
3409
3410
        $categoryCondition = '';
3411
        if (!empty($categoryId)) {
3412
            $categoryId = (int) $categoryId;
3413
            $categoryCondition = " AND category_id = $categoryId";
3414
        }
3415
        $sql = "SELECT * FROM $lp_table
3416
                WHERE c_id = $courseId
3417
                $categoryCondition
3418
                ORDER BY display_order";
3419
        $res = Database::query($sql);
3420
        if (false === $res) {
3421
            return false;
3422
        }
3423
3424
        $lps = [];
3425
        $lp_order = [];
3426
        $num = Database::num_rows($res);
3427
        // First check the order is correct, globally (might be wrong because
3428
        // of versions < 1.8.4)
3429
        if ($num > 0) {
3430
            $i = 1;
3431
            while ($row = Database::fetch_array($res)) {
3432
                if ($row['display_order'] != $i) { // If we find a gap in the order, we need to fix it.
3433
                    $sql = "UPDATE $lp_table SET display_order = $i
3434
                            WHERE iid = ".$row['iid'];
3435
                    Database::query($sql);
3436
                }
3437
                $row['display_order'] = $i;
3438
                $lps[$row['iid']] = $row;
3439
                $lp_order[$i] = $row['iid'];
3440
                $i++;
3441
            }
3442
        }
3443
        if ($num > 1) { // If there's only one element, no need to sort.
3444
            $order = $lps[$lp_id]['display_order'];
3445
            if ($order > 1) { // If it's the first element, no need to move up.
3446
                $sql = "UPDATE $lp_table SET display_order = $order
3447
                        WHERE iid = ".$lp_order[$order - 1];
3448
                Database::query($sql);
3449
                $sql = "UPDATE $lp_table SET display_order = ".($order - 1)."
3450
                        WHERE iid = $lp_id";
3451
                Database::query($sql);
3452
            }
3453
        }
3454
3455
        return true;
3456
    }
3457
3458
    /**
3459
     * Move a learnpath down (display_order).
3460
     *
3461
     * @param int $lp_id      Learnpath ID
3462
     * @param int $categoryId Category ID
3463
     *
3464
     * @return bool
3465
     */
3466
    public static function move_down($lp_id, $categoryId = 0)
3467
    {
3468
        $courseId = api_get_course_int_id();
3469
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
3470
3471
        $categoryCondition = '';
3472
        if (!empty($categoryId)) {
3473
            $categoryId = (int) $categoryId;
3474
            $categoryCondition = " AND category_id = $categoryId";
3475
        }
3476
3477
        $sql = "SELECT * FROM $lp_table
3478
                WHERE c_id = $courseId
3479
                $categoryCondition
3480
                ORDER BY display_order";
3481
        $res = Database::query($sql);
3482
        if (false === $res) {
3483
            return false;
3484
        }
3485
        $lps = [];
3486
        $lp_order = [];
3487
        $num = Database::num_rows($res);
3488
        $max = 0;
3489
        // First check the order is correct, globally (might be wrong because
3490
        // of versions < 1.8.4).
3491
        if ($num > 0) {
3492
            $i = 1;
3493
            while ($row = Database::fetch_array($res)) {
3494
                $max = $i;
3495
                if ($row['display_order'] != $i) {
3496
                    // If we find a gap in the order, we need to fix it.
3497
                    $sql = "UPDATE $lp_table SET display_order = $i
3498
                              WHERE iid = ".$row['iid'];
3499
                    Database::query($sql);
3500
                }
3501
                $row['display_order'] = $i;
3502
                $lps[$row['iid']] = $row;
3503
                $lp_order[$i] = $row['iid'];
3504
                $i++;
3505
            }
3506
        }
3507
        if ($num > 1) { // If there's only one element, no need to sort.
3508
            $order = $lps[$lp_id]['display_order'];
3509
            if ($order < $max) { // If it's the first element, no need to move up.
3510
                $sql = "UPDATE $lp_table SET display_order = $order
3511
                        WHERE iid = ".$lp_order[$order + 1];
3512
                Database::query($sql);
3513
                $sql = "UPDATE $lp_table SET display_order = ".($order + 1)."
3514
                        WHERE iid = $lp_id";
3515
                Database::query($sql);
3516
            }
3517
        }
3518
3519
        return true;
3520
    }
3521
3522
    /**
3523
     * Updates learnpath attributes to point to the next element
3524
     * The last part is similar to set_current_item but processing the other way around.
3525
     */
3526
    public function next()
3527
    {
3528
        if ($this->debug > 0) {
3529
            error_log('In learnpath::next()', 0);
3530
        }
3531
        $this->last = $this->get_current_item_id();
3532
        $this->items[$this->last]->save(
3533
            false,
3534
            $this->prerequisites_match($this->last)
3535
        );
3536
        $this->autocomplete_parents($this->last);
3537
        $new_index = $this->get_next_index();
3538
        if ($this->debug > 2) {
3539
            error_log('New index: '.$new_index, 0);
3540
        }
3541
        $this->index = $new_index;
3542
        if ($this->debug > 2) {
3543
            error_log('Now having orderedlist['.$new_index.'] = '.$this->ordered_items[$new_index], 0);
3544
        }
3545
        $this->current = $this->ordered_items[$new_index];
3546
        if ($this->debug > 2) {
3547
            error_log('new item id is '.$this->current.'-'.$this->get_current_item_id(), 0);
3548
        }
3549
    }
3550
3551
    /**
3552
     * Open a resource = initialise all local variables relative to this resource. Depending on the child
3553
     * class, this might be redefined to allow several behaviours depending on the document type.
3554
     *
3555
     * @param int $id Resource ID
3556
     */
3557
    public function open($id)
3558
    {
3559
        // TODO:
3560
        // set the current resource attribute to this resource
3561
        // switch on element type (redefine in child class?)
3562
        // set status for this item to "opened"
3563
        // start timer
3564
        // initialise score
3565
        $this->index = 0; //or = the last item seen (see $this->last)
3566
    }
3567
3568
    /**
3569
     * Check that all prerequisites are fulfilled. Returns true and an
3570
     * empty string on success, returns false
3571
     * and the prerequisite string on error.
3572
     * This function is based on the rules for aicc_script language as
3573
     * described in the SCORM 1.2 CAM documentation page 108.
3574
     *
3575
     * @param int $itemId Optional item ID. If none given, uses the current open item.
3576
     *
3577
     * @return bool true if prerequisites are matched, false otherwise - Empty string if true returned, prerequisites
3578
     *              string otherwise
3579
     */
3580
    public function prerequisites_match($itemId = null)
3581
    {
3582
        $allow = api_get_configuration_value('allow_teachers_to_access_blocked_lp_by_prerequisite');
3583
        if ($allow) {
3584
            if (api_is_allowed_to_edit() ||
3585
                api_is_platform_admin(true) ||
3586
                api_is_drh() ||
3587
                api_is_coach(api_get_session_id(), api_get_course_int_id())
3588
            ) {
3589
                return true;
3590
            }
3591
        }
3592
3593
        $debug = $this->debug;
3594
        if ($debug > 0) {
3595
            error_log('In learnpath::prerequisites_match()');
3596
        }
3597
3598
        if (empty($itemId)) {
3599
            $itemId = $this->current;
3600
        }
3601
3602
        $currentItem = $this->getItem($itemId);
3603
3604
        if ($currentItem) {
3605
            if (2 == $this->type) {
3606
                // Getting prereq from scorm
3607
                $prereq_string = $this->get_scorm_prereq_string($itemId);
3608
            } else {
3609
                $prereq_string = $currentItem->get_prereq_string();
3610
            }
3611
3612
            if (empty($prereq_string)) {
3613
                if ($debug > 0) {
3614
                    error_log('Found prereq_string is empty return true');
3615
                }
3616
3617
                return true;
3618
            }
3619
3620
            // Clean spaces.
3621
            $prereq_string = str_replace(' ', '', $prereq_string);
3622
            if ($debug > 0) {
3623
                error_log('Found prereq_string: '.$prereq_string, 0);
3624
            }
3625
3626
            // Now send to the parse_prereq() function that will check this component's prerequisites.
3627
            $result = $currentItem->parse_prereq(
3628
                $prereq_string,
3629
                $this->items,
3630
                $this->refs_list,
3631
                $this->get_user_id()
3632
            );
3633
3634
            if (false === $result) {
3635
                $this->set_error_msg($currentItem->prereq_alert);
3636
            }
3637
        } else {
3638
            $result = true;
3639
            if ($debug > 1) {
3640
                error_log('$this->items['.$itemId.'] was not an object', 0);
3641
            }
3642
        }
3643
3644
        if ($debug > 1) {
3645
            error_log('End of prerequisites_match(). Error message is now '.$this->error, 0);
3646
        }
3647
3648
        return $result;
3649
    }
3650
3651
    /**
3652
     * Updates learnpath attributes to point to the previous element
3653
     * The last part is similar to set_current_item but processing the other way around.
3654
     */
3655
    public function previous()
3656
    {
3657
        $this->last = $this->get_current_item_id();
3658
        $this->items[$this->last]->save(
3659
            false,
3660
            $this->prerequisites_match($this->last)
3661
        );
3662
        $this->autocomplete_parents($this->last);
3663
        $new_index = $this->get_previous_index();
3664
        $this->index = $new_index;
3665
        $this->current = $this->ordered_items[$new_index];
3666
    }
3667
3668
    /**
3669
     * Publishes a learnpath. This basically means show or hide the learnpath
3670
     * to normal users.
3671
     * Can be used as abstract.
3672
     *
3673
     * @param int $id         Learnpath ID
3674
     * @param int $visibility New visibility (1 = visible/published, 0= invisible/draft)
3675
     *
3676
     * @return bool
3677
     */
3678
    public static function toggleVisibility($id, $visibility = 1)
3679
    {
3680
        $repo = Container::getLpRepository();
3681
        $lp = $repo->find($id);
3682
3683
        if (!$lp) {
3684
            return false;
3685
        }
3686
3687
        $visibility = (int) $visibility;
3688
3689
        if (1 === $visibility) {
3690
            $repo->setVisibilityPublished($lp);
3691
        } else {
3692
            $repo->setVisibilityDraft($lp);
3693
        }
3694
3695
        return true;
3696
    }
3697
3698
    /**
3699
     * Publishes a learnpath category.
3700
     * This basically means show or hide the learnpath category to normal users.
3701
     *
3702
     * @param int $id
3703
     * @param int $visibility
3704
     *
3705
     * @return bool
3706
     */
3707
    public static function toggleCategoryVisibility($id, $visibility = 1)
3708
    {
3709
        $repo = Container::getLpCategoryRepository();
3710
        $resource = $repo->find($id);
3711
3712
        if (!$resource) {
3713
            return false;
3714
        }
3715
3716
        $visibility = (int) $visibility;
3717
3718
        if (1 === $visibility) {
3719
            $repo->setVisibilityPublished($resource);
3720
        } else {
3721
            $repo->setVisibilityDraft($resource);
3722
            self::toggleCategoryPublish($id, 0);
3723
        }
3724
3725
        return false;
3726
    }
3727
3728
    /**
3729
     * Publishes a learnpath. This basically means show or hide the learnpath
3730
     * on the course homepage.
3731
     *
3732
     * @param int    $id            Learnpath id
3733
     * @param string $setVisibility New visibility (v/i - visible/invisible)
3734
     *
3735
     * @return bool
3736
     */
3737
    public static function togglePublish($id, $setVisibility = 'v')
3738
    {
3739
        $addShortcut = false;
3740
        if ('v' === $setVisibility) {
3741
            $addShortcut = true;
3742
        }
3743
        $repo = Container::getLpRepository();
3744
        /** @var CLp $lp */
3745
        $lp = $repo->find($id);
3746
        if (null === $lp) {
3747
            return false;
3748
        }
3749
        $repoShortcut = Container::getShortcutRepository();
3750
        $courseEntity = api_get_course_entity();
3751
3752
        if ($addShortcut) {
3753
            $repoShortcut->addShortCut($lp, $courseEntity, $courseEntity, api_get_session_entity());
3754
        } else {
3755
            $repoShortcut->removeShortCut($lp);
3756
        }
3757
3758
        return true;
3759
    }
3760
3761
    /**
3762
     * Show or hide the learnpath category on the course homepage.
3763
     *
3764
     * @param int $id
3765
     * @param int $setVisibility
3766
     *
3767
     * @return bool
3768
     */
3769
    public static function toggleCategoryPublish($id, $setVisibility = 1)
3770
    {
3771
        $setVisibility = (int) $setVisibility;
3772
        $addShortcut = false;
3773
        if (1 === $setVisibility) {
3774
            $addShortcut = true;
3775
        }
3776
3777
        $repo = Container::getLpCategoryRepository();
3778
        /** @var CLpCategory $lp */
3779
        $category = $repo->find($id);
3780
3781
        if (null === $category) {
3782
            return false;
3783
        }
3784
3785
        $repoShortcut = Container::getShortcutRepository();
3786
        if ($addShortcut) {
3787
            $courseEntity = api_get_course_entity(api_get_course_int_id());
3788
            $repoShortcut->addShortCut($category, $courseEntity, $courseEntity, api_get_session_entity());
3789
        } else {
3790
            $repoShortcut->removeShortCut($category);
3791
        }
3792
3793
        return true;
3794
    }
3795
3796
    /**
3797
     * Check if the learnpath category is visible for a user.
3798
     *
3799
     * @return bool
3800
     */
3801
    public static function categoryIsVisibleForStudent(
3802
        CLpCategory $category,
3803
        User $user,
3804
        Course $course,
3805
        Session $session = null
3806
    ) {
3807
        $isAllowedToEdit = api_is_allowed_to_edit(null, true);
3808
3809
        if ($isAllowedToEdit) {
3810
            return true;
3811
        }
3812
3813
        $categoryVisibility = $category->isVisible($course, $session);
3814
3815
        if (1 !== $categoryVisibility && -1 != $categoryVisibility) {
3816
            return false;
3817
        }
3818
3819
        $subscriptionSettings = self::getSubscriptionSettings();
3820
3821
        if (false == $subscriptionSettings['allow_add_users_to_lp_category']) {
3822
            return true;
3823
        }
3824
3825
        $noUserSubscribed = false;
3826
        $noGroupSubscribed = true;
3827
        $users = $category->getUsers();
3828
        if (empty($users) || !$users->count()) {
3829
            $noUserSubscribed = true;
3830
        } elseif ($category->hasUserAdded($user)) {
3831
            return true;
3832
        }
3833
3834
        $groups = GroupManager::getAllGroupPerUserSubscription($user->getId());
3835
        $em = Database::getManager();
3836
3837
        /** @var ItemPropertyRepository $itemRepo */
3838
        $itemRepo = $em->getRepository('ChamiloCourseBundle:CItemProperty');
3839
3840
        /** @var CourseRepository $courseRepo */
3841
        $courseRepo = $em->getRepository('ChamiloCoreBundle:Course');
3842
        $session = null;
3843
        if (!empty($sessionId)) {
3844
            $session = $em->getRepository('ChamiloCoreBundle:Session')->find($sessionId);
3845
        }
3846
3847
        $course = $courseRepo->find($courseId);
3848
3849
        if (0 != $courseId) {
3850
            // Subscribed groups to a LP
3851
            $subscribedGroupsInLp = $itemRepo->getGroupsSubscribedToItem(
3852
                    TOOL_LEARNPATH_CATEGORY,
3853
                    $category->getId(),
3854
                    $course,
3855
                    $session
3856
                );
3857
        }
3858
3859
        if (!empty($subscribedGroupsInLp)) {
3860
            $noGroupSubscribed = false;
3861
            if (!empty($groups)) {
3862
                $groups = array_column($groups, 'iid');
3863
                /** @var CItemProperty $item */
3864
                foreach ($subscribedGroupsInLp as $item) {
3865
                    if ($item->getGroup() &&
3866
                        in_array($item->getGroup()->getId(), $groups)
3867
                    ) {
3868
                        return true;
3869
                    }
3870
                }
3871
            }
3872
        }
3873
        $response = $noGroupSubscribed && $noUserSubscribed;
3874
3875
        return $response;
3876
    }
3877
3878
    /**
3879
     * Check if a learnpath category is published as course tool.
3880
     *
3881
     * @param int $courseId
3882
     *
3883
     * @return bool
3884
     */
3885
    public static function categoryIsPublished(CLpCategory $category, $courseId)
3886
    {
3887
        return false;
3888
        $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...
3889
        $em = Database::getManager();
3890
3891
        $tools = $em
3892
            ->createQuery("
3893
                SELECT t FROM ChamiloCourseBundle:CTool t
3894
                WHERE t.course = :course AND
3895
                    t.name = :name AND
3896
                    t.image LIKE 'lp_category.%' AND
3897
                    t.link LIKE :link
3898
            ")
3899
            ->setParameters([
3900
                'course' => $courseId,
3901
                'name' => strip_tags($category->getName()),
3902
                'link' => "$link%",
3903
            ])
3904
            ->getResult();
3905
3906
        /** @var CTool $tool */
3907
        $tool = current($tools);
3908
3909
        return $tool ? $tool->getVisibility() : false;
3910
    }
3911
3912
    /**
3913
     * Restart the whole learnpath. Return the URL of the first element.
3914
     * Make sure the results are saved with anoter method. This method should probably be redefined in children classes.
3915
     * To use a similar method  statically, use the create_new_attempt() method.
3916
     *
3917
     * @return bool
3918
     */
3919
    public function restart()
3920
    {
3921
        if ($this->debug > 0) {
3922
            error_log('In learnpath::restart()', 0);
3923
        }
3924
        // TODO
3925
        // Call autosave method to save the current progress.
3926
        //$this->index = 0;
3927
        if (api_is_invitee()) {
3928
            return false;
3929
        }
3930
        $session_id = api_get_session_id();
3931
        $course_id = api_get_course_int_id();
3932
        $lp_view_table = Database::get_course_table(TABLE_LP_VIEW);
3933
        $sql = "INSERT INTO $lp_view_table (c_id, lp_id, user_id, view_count, session_id)
3934
                VALUES ($course_id, ".$this->lp_id.",".$this->get_user_id().",".($this->attempt + 1).", $session_id)";
3935
        if ($this->debug > 2) {
3936
            error_log('Inserting new lp_view for restart: '.$sql, 0);
3937
        }
3938
        Database::query($sql);
3939
        $view_id = Database::insert_id();
3940
3941
        if ($view_id) {
3942
            $this->lp_view_id = $view_id;
3943
            $this->attempt = $this->attempt + 1;
3944
        } else {
3945
            $this->error = 'Could not insert into item_view table...';
3946
3947
            return false;
3948
        }
3949
        $this->autocomplete_parents($this->current);
3950
        foreach ($this->items as $index => $dummy) {
3951
            $this->items[$index]->restart();
3952
            $this->items[$index]->set_lp_view($this->lp_view_id);
3953
        }
3954
        $this->first();
3955
3956
        return true;
3957
    }
3958
3959
    /**
3960
     * Saves the current item.
3961
     *
3962
     * @return bool
3963
     */
3964
    public function save_current()
3965
    {
3966
        $debug = $this->debug;
3967
        // TODO: Do a better check on the index pointing to the right item (it is supposed to be working
3968
        // on $ordered_items[] but not sure it's always safe to use with $items[]).
3969
        if ($debug) {
3970
            error_log('save_current() saving item '.$this->current, 0);
3971
            error_log(''.print_r($this->items, true), 0);
3972
        }
3973
        if (isset($this->items[$this->current]) &&
3974
            is_object($this->items[$this->current])
3975
        ) {
3976
            if ($debug) {
3977
                error_log('Before save last_scorm_session_time: '.$this->items[$this->current]->getLastScormSessionTime());
3978
            }
3979
3980
            $res = $this->items[$this->current]->save(
3981
                false,
3982
                $this->prerequisites_match($this->current)
3983
            );
3984
            $this->autocomplete_parents($this->current);
3985
            $status = $this->items[$this->current]->get_status();
3986
            $this->update_queue[$this->current] = $status;
3987
3988
            if ($debug) {
3989
                error_log('After save last_scorm_session_time: '.$this->items[$this->current]->getLastScormSessionTime());
3990
            }
3991
3992
            return $res;
3993
        }
3994
3995
        return false;
3996
    }
3997
3998
    /**
3999
     * Saves the given item.
4000
     *
4001
     * @param int  $item_id      Optional (will take from $_REQUEST if null)
4002
     * @param bool $from_outside Save from url params (true) or from current attributes (false). Default true
4003
     *
4004
     * @return bool
4005
     */
4006
    public function save_item($item_id = null, $from_outside = true)
4007
    {
4008
        $debug = $this->debug;
4009
        if ($debug) {
4010
            error_log('In learnpath::save_item('.$item_id.','.intval($from_outside).')', 0);
4011
        }
4012
        // TODO: Do a better check on the index pointing to the right item (it is supposed to be working
4013
        // on $ordered_items[] but not sure it's always safe to use with $items[]).
4014
        if (empty($item_id)) {
4015
            $item_id = (int) $_REQUEST['id'];
4016
        }
4017
4018
        if (empty($item_id)) {
4019
            $item_id = $this->get_current_item_id();
4020
        }
4021
        if (isset($this->items[$item_id]) &&
4022
            is_object($this->items[$item_id])
4023
        ) {
4024
            if ($debug) {
4025
                error_log('Object exists');
4026
            }
4027
4028
            // Saving the item.
4029
            $res = $this->items[$item_id]->save(
4030
                $from_outside,
4031
                $this->prerequisites_match($item_id)
4032
            );
4033
4034
            if ($debug) {
4035
                error_log('update_queue before:');
4036
                error_log(print_r($this->update_queue, 1));
4037
            }
4038
            $this->autocomplete_parents($item_id);
4039
4040
            $status = $this->items[$item_id]->get_status();
4041
            $this->update_queue[$item_id] = $status;
4042
4043
            if ($debug) {
4044
                error_log('get_status(): '.$status);
4045
                error_log('update_queue after:');
4046
                error_log(print_r($this->update_queue, 1));
4047
            }
4048
4049
            return $res;
4050
        }
4051
4052
        return false;
4053
    }
4054
4055
    /**
4056
     * Saves the last item seen's ID only in case.
4057
     */
4058
    public function save_last()
4059
    {
4060
        $course_id = api_get_course_int_id();
4061
        $debug = $this->debug;
4062
        if ($debug) {
4063
            error_log('In learnpath::save_last()', 0);
4064
        }
4065
        $session_condition = api_get_session_condition(
4066
            api_get_session_id(),
4067
            true,
4068
            false
4069
        );
4070
        $table = Database::get_course_table(TABLE_LP_VIEW);
4071
4072
        $userId = $this->get_user_id();
4073
        if (empty($userId)) {
4074
            $userId = api_get_user_id();
4075
            if ($debug) {
4076
                error_log('$this->get_user_id() was empty, used api_get_user_id() instead in '.__FILE__.' line '.__LINE__);
4077
            }
4078
        }
4079
        if (isset($this->current) && !api_is_invitee()) {
4080
            if ($debug) {
4081
                error_log('Saving current item ('.$this->current.') for later review', 0);
4082
            }
4083
            $sql = "UPDATE $table SET
4084
                        last_item = ".$this->get_current_item_id()."
4085
                    WHERE
4086
                        c_id = $course_id AND
4087
                        lp_id = ".$this->get_id()." AND
4088
                        user_id = ".$userId." ".$session_condition;
4089
4090
            if ($debug) {
4091
                error_log('Saving last item seen : '.$sql, 0);
4092
            }
4093
            Database::query($sql);
4094
        }
4095
4096
        if (!api_is_invitee()) {
4097
            // Save progress.
4098
            [$progress] = $this->get_progress_bar_text('%');
4099
            $scoreAsProgressSetting = api_get_configuration_value('lp_score_as_progress_enable');
4100
            $scoreAsProgress = $this->getUseScoreAsProgress();
4101
            if ($scoreAsProgress && $scoreAsProgressSetting && (null === $score || empty($score) || -1 == $score)) {
4102
                if ($debug) {
4103
                    error_log("Return false: Dont save score: $score");
4104
                    error_log("progress: $progress");
4105
                }
4106
4107
                return false;
4108
            }
4109
4110
            if ($scoreAsProgress && $scoreAsProgressSetting) {
4111
                $storedProgress = self::getProgress(
4112
                    $this->get_id(),
4113
                    $userId,
4114
                    $course_id,
4115
                    $this->get_lp_session_id()
4116
                );
4117
4118
                // Check if the stored progress is higher than the new value
4119
                if ($storedProgress >= $progress) {
4120
                    if ($debug) {
4121
                        error_log("Return false: New progress value is lower than stored value - Current value: $storedProgress - New value: $progress [lp ".$this->get_id()." - user ".$userId."]");
4122
                    }
4123
4124
                    return false;
4125
                }
4126
            }
4127
            if ($progress >= 0 && $progress <= 100) {
4128
                $progress = (int) $progress;
4129
                $sql = "UPDATE $table SET
4130
                            progress = $progress
4131
                        WHERE
4132
                            c_id = $course_id AND
4133
                            lp_id = ".$this->get_id()." AND
4134
                            user_id = ".$userId." ".$session_condition;
4135
                // Ignore errors as some tables might not have the progress field just yet.
4136
                Database::query($sql);
4137
                $this->progress_db = $progress;
4138
            }
4139
        }
4140
    }
4141
4142
    /**
4143
     * Sets the current item ID (checks if valid and authorized first).
4144
     *
4145
     * @param int $item_id New item ID. If not given or not authorized, defaults to current
4146
     */
4147
    public function set_current_item($item_id = null)
4148
    {
4149
        $debug = $this->debug;
4150
        if ($debug) {
4151
            error_log('In learnpath::set_current_item('.$item_id.')', 0);
4152
        }
4153
        if (empty($item_id)) {
4154
            if ($debug) {
4155
                error_log('No new current item given, ignore...', 0);
4156
            }
4157
            // Do nothing.
4158
        } else {
4159
            if ($debug) {
4160
                error_log('New current item given is '.$item_id.'...', 0);
4161
            }
4162
            if (is_numeric($item_id)) {
4163
                $item_id = (int) $item_id;
4164
                // TODO: Check in database here.
4165
                $this->last = $this->current;
4166
                $this->current = $item_id;
4167
                // TODO: Update $this->index as well.
4168
                foreach ($this->ordered_items as $index => $item) {
4169
                    if ($item == $this->current) {
4170
                        $this->index = $index;
4171
                        break;
4172
                    }
4173
                }
4174
                if ($debug) {
4175
                    error_log('set_current_item('.$item_id.') done. Index is now : '.$this->index);
4176
                }
4177
            } else {
4178
                if ($debug) {
4179
                    error_log('set_current_item('.$item_id.') failed. Not a numeric value: ');
4180
                }
4181
            }
4182
        }
4183
    }
4184
4185
    /**
4186
     * Set index specified prefix terms for all items in this path.
4187
     *
4188
     * @param string $terms_string Comma-separated list of terms
4189
     * @param string $prefix       Xapian term prefix
4190
     *
4191
     * @return bool False on error, true otherwise
4192
     */
4193
    public function set_terms_by_prefix($terms_string, $prefix)
4194
    {
4195
        $course_id = api_get_course_int_id();
4196
        if ('true' !== api_get_setting('search_enabled')) {
4197
            return false;
4198
        }
4199
4200
        if (!extension_loaded('xapian')) {
4201
            return false;
4202
        }
4203
4204
        $terms_string = trim($terms_string);
4205
        $terms = explode(',', $terms_string);
4206
        array_walk($terms, 'trim_value');
4207
        $stored_terms = $this->get_common_index_terms_by_prefix($prefix);
4208
4209
        // Don't do anything if no change, verify only at DB, not the search engine.
4210
        if ((0 == count(array_diff($terms, $stored_terms))) && (0 == count(array_diff($stored_terms, $terms)))) {
4211
            return false;
4212
        }
4213
4214
        require_once 'xapian.php'; // TODO: Try catch every xapian use or make wrappers on API.
4215
        require_once api_get_path(LIBRARY_PATH).'search/xapian/XapianQuery.php';
4216
4217
        $items_table = Database::get_course_table(TABLE_LP_ITEM);
4218
        // TODO: Make query secure agains XSS : use member attr instead of post var.
4219
        $lp_id = (int) $_POST['lp_id'];
4220
        $sql = "SELECT * FROM $items_table WHERE c_id = $course_id AND lp_id = $lp_id";
4221
        $result = Database::query($sql);
4222
        $di = new ChamiloIndexer();
4223
4224
        while ($lp_item = Database::fetch_array($result)) {
4225
            // Get search_did.
4226
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
4227
            $sql = 'SELECT * FROM %s
4228
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
4229
                    LIMIT 1';
4230
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp_id, $lp_item['id']);
4231
4232
            //echo $sql; echo '<br>';
4233
            $res = Database::query($sql);
4234
            if (Database::num_rows($res) > 0) {
4235
                $se_ref = Database::fetch_array($res);
4236
                // Compare terms.
4237
                $doc = $di->get_document($se_ref['search_did']);
4238
                $xapian_terms = xapian_get_doc_terms($doc, $prefix);
4239
                $xterms = [];
4240
                foreach ($xapian_terms as $xapian_term) {
4241
                    $xterms[] = substr($xapian_term['name'], 1);
4242
                }
4243
4244
                $dterms = $terms;
4245
                $missing_terms = array_diff($dterms, $xterms);
4246
                $deprecated_terms = array_diff($xterms, $dterms);
4247
4248
                // Save it to search engine.
4249
                foreach ($missing_terms as $term) {
4250
                    $doc->add_term($prefix.$term, 1);
4251
                }
4252
                foreach ($deprecated_terms as $term) {
4253
                    $doc->remove_term($prefix.$term);
4254
                }
4255
                $di->getDb()->replace_document((int) $se_ref['search_did'], $doc);
4256
                $di->getDb()->flush();
4257
            }
4258
        }
4259
4260
        return true;
4261
    }
4262
4263
    /**
4264
     * Sets the previous item ID to a given ID. Generally, this should be set to the previous 'current' item.
4265
     *
4266
     * @param int $id DB ID of the item
4267
     */
4268
    public function set_previous_item($id)
4269
    {
4270
        if ($this->debug > 0) {
4271
            error_log('In learnpath::set_previous_item()', 0);
4272
        }
4273
        $this->last = $id;
4274
    }
4275
4276
    /**
4277
     * Sets and saves the expired_on date.
4278
     *
4279
     * @return bool Returns true if author's name is not empty
4280
     */
4281
    public function set_modified_on()
4282
    {
4283
        $this->modified_on = api_get_utc_datetime();
4284
        $table = Database::get_course_table(TABLE_LP_MAIN);
4285
        $lp_id = $this->get_id();
4286
        $sql = "UPDATE $table SET modified_on = '".$this->modified_on."'
4287
                WHERE iid = $lp_id";
4288
        Database::query($sql);
4289
4290
        return true;
4291
    }
4292
4293
    /**
4294
     * Sets the object's error message.
4295
     *
4296
     * @param string $error Error message. If empty, reinits the error string
4297
     */
4298
    public function set_error_msg($error = '')
4299
    {
4300
        if ($this->debug > 0) {
4301
            error_log('In learnpath::set_error_msg()', 0);
4302
        }
4303
        if (empty($error)) {
4304
            $this->error = '';
4305
        } else {
4306
            $this->error .= $error;
4307
        }
4308
    }
4309
4310
    /**
4311
     * Launches the current item if not 'sco'
4312
     * (starts timer and make sure there is a record ready in the DB).
4313
     *
4314
     * @param bool $allow_new_attempt Whether to allow a new attempt or not
4315
     *
4316
     * @return bool
4317
     */
4318
    public function start_current_item($allow_new_attempt = false)
4319
    {
4320
        $debug = $this->debug;
4321
        if ($debug) {
4322
            error_log('In learnpath::start_current_item()');
4323
            error_log('current: '.$this->current);
4324
        }
4325
        if (0 != $this->current && isset($this->items[$this->current]) &&
4326
            is_object($this->items[$this->current])
4327
        ) {
4328
            $type = $this->get_type();
4329
            $item_type = $this->items[$this->current]->get_type();
4330
            if ($debug) {
4331
                error_log('item type: '.$item_type);
4332
                error_log('lp type: '.$type);
4333
            }
4334
            if ((2 == $type && 'sco' !== $item_type) ||
4335
                (3 == $type && 'au' !== $item_type) ||
4336
                (1 == $type && TOOL_QUIZ != $item_type && TOOL_HOTPOTATOES != $item_type)
4337
            ) {
4338
                $this->items[$this->current]->open($allow_new_attempt);
4339
                $this->autocomplete_parents($this->current);
4340
                $prereq_check = $this->prerequisites_match($this->current);
4341
                if ($debug) {
4342
                    error_log('start_current_item will save item with prereq: '.$prereq_check);
4343
                }
4344
                $this->items[$this->current]->save(false, $prereq_check);
4345
            }
4346
            // If sco, then it is supposed to have been updated by some other call.
4347
            if ('sco' === $item_type) {
4348
                $this->items[$this->current]->restart();
4349
            }
4350
        }
4351
        if ($debug) {
4352
            error_log('lp_view_session_id');
4353
            error_log($this->lp_view_session_id);
4354
            error_log('api session id');
4355
            error_log(api_get_session_id());
4356
            error_log('End of learnpath::start_current_item()');
4357
        }
4358
4359
        return true;
4360
    }
4361
4362
    /**
4363
     * Stops the processing and counters for the old item (as held in $this->last).
4364
     *
4365
     * @return bool True/False
4366
     */
4367
    public function stop_previous_item()
4368
    {
4369
        $debug = $this->debug;
4370
        if ($debug) {
4371
            error_log('In learnpath::stop_previous_item()', 0);
4372
        }
4373
4374
        if (0 != $this->last && $this->last != $this->current &&
4375
            isset($this->items[$this->last]) && is_object($this->items[$this->last])
4376
        ) {
4377
            if ($debug) {
4378
                error_log('In learnpath::stop_previous_item() - '.$this->last.' is object');
4379
            }
4380
            switch ($this->get_type()) {
4381
                case '3':
4382
                    if ('au' != $this->items[$this->last]->get_type()) {
4383
                        if ($debug) {
4384
                            error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 3 is <> au');
4385
                        }
4386
                        $this->items[$this->last]->close();
4387
                    } else {
4388
                        if ($debug) {
4389
                            error_log('In learnpath::stop_previous_item() - Item is an AU, saving is managed by AICC signals');
4390
                        }
4391
                    }
4392
                    break;
4393
                case '2':
4394
                    if ('sco' != $this->items[$this->last]->get_type()) {
4395
                        if ($debug) {
4396
                            error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 2 is <> sco');
4397
                        }
4398
                        $this->items[$this->last]->close();
4399
                    } else {
4400
                        if ($debug) {
4401
                            error_log('In learnpath::stop_previous_item() - Item is a SCO, saving is managed by SCO signals');
4402
                        }
4403
                    }
4404
                    break;
4405
                case '1':
4406
                default:
4407
                    if ($debug) {
4408
                        error_log('In learnpath::stop_previous_item() - '.$this->last.' in lp_type 1 is asset');
4409
                    }
4410
                    $this->items[$this->last]->close();
4411
                    break;
4412
            }
4413
        } else {
4414
            if ($debug) {
4415
                error_log('In learnpath::stop_previous_item() - No previous element found, ignoring...');
4416
            }
4417
4418
            return false;
4419
        }
4420
4421
        return true;
4422
    }
4423
4424
    /**
4425
     * Updates the default view mode from fullscreen to embedded and inversely.
4426
     *
4427
     * @return string The current default view mode ('fullscreen' or 'embedded')
4428
     */
4429
    public function update_default_view_mode()
4430
    {
4431
        $table = Database::get_course_table(TABLE_LP_MAIN);
4432
        $sql = "SELECT * FROM $table
4433
                WHERE iid = ".$this->get_id();
4434
        $res = Database::query($sql);
4435
        if (Database::num_rows($res) > 0) {
4436
            $row = Database::fetch_array($res);
4437
            $default_view_mode = $row['default_view_mod'];
4438
            $view_mode = $default_view_mode;
4439
            switch ($default_view_mode) {
4440
                case 'fullscreen': // default with popup
4441
                    $view_mode = 'embedded';
4442
                    break;
4443
                case 'embedded': // default view with left menu
4444
                    $view_mode = 'embedframe';
4445
                    break;
4446
                case 'embedframe': //folded menu
4447
                    $view_mode = 'impress';
4448
                    break;
4449
                case 'impress':
4450
                    $view_mode = 'fullscreen';
4451
                    break;
4452
            }
4453
            $sql = "UPDATE $table SET default_view_mod = '$view_mode'
4454
                    WHERE iid = ".$this->get_id();
4455
            Database::query($sql);
4456
            $this->mode = $view_mode;
4457
4458
            return $view_mode;
4459
        }
4460
4461
        return -1;
4462
    }
4463
4464
    /**
4465
     * Updates the default behaviour about auto-commiting SCORM updates.
4466
     *
4467
     * @return bool True if auto-commit has been set to 'on', false otherwise
4468
     */
4469
    public function update_default_scorm_commit()
4470
    {
4471
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
4472
        $sql = "SELECT * FROM $lp_table
4473
                WHERE iid = ".$this->get_id();
4474
        $res = Database::query($sql);
4475
        if (Database::num_rows($res) > 0) {
4476
            $row = Database::fetch_array($res);
4477
            $force = $row['force_commit'];
4478
            if (1 == $force) {
4479
                $force = 0;
4480
                $force_return = false;
4481
            } elseif (0 == $force) {
4482
                $force = 1;
4483
                $force_return = true;
4484
            }
4485
            $sql = "UPDATE $lp_table SET force_commit = $force
4486
                    WHERE iid = ".$this->get_id();
4487
            Database::query($sql);
4488
            $this->force_commit = $force_return;
4489
4490
            return $force_return;
4491
        }
4492
4493
        return -1;
4494
    }
4495
4496
    /**
4497
     * Updates the order of learning paths (goes through all of them by order and fills the gaps).
4498
     *
4499
     * @return bool True on success, false on failure
4500
     */
4501
    public function update_display_order()
4502
    {
4503
        return;
4504
        $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...
4505
        $table = Database::get_course_table(TABLE_LP_MAIN);
4506
        $sql = "SELECT * FROM $table
4507
                WHERE c_id = $course_id
4508
                ORDER BY display_order";
4509
        $res = Database::query($sql);
4510
        if (false === $res) {
4511
            return false;
4512
        }
4513
4514
        $num = Database::num_rows($res);
4515
        // First check the order is correct, globally (might be wrong because
4516
        // of versions < 1.8.4).
4517
        if ($num > 0) {
4518
            $i = 1;
4519
            while ($row = Database::fetch_array($res)) {
4520
                if ($row['display_order'] != $i) {
4521
                    // If we find a gap in the order, we need to fix it.
4522
                    $sql = "UPDATE $table SET display_order = $i
4523
                            WHERE iid = ".$row['iid'];
4524
                    Database::query($sql);
4525
                }
4526
                $i++;
4527
            }
4528
        }
4529
4530
        return true;
4531
    }
4532
4533
    /**
4534
     * Updates the "prevent_reinit" value that enables control on reinitialising items on second view.
4535
     *
4536
     * @return bool True if prevent_reinit has been set to 'on', false otherwise (or 1 or 0 in this case)
4537
     */
4538
    public function update_reinit()
4539
    {
4540
        $force = $this->entity->getPreventReinit();
4541
        if (1 == $force) {
4542
            $force = 0;
4543
        } elseif (0 == $force) {
4544
            $force = 1;
4545
        }
4546
4547
        $table = Database::get_course_table(TABLE_LP_MAIN);
4548
        $sql = "UPDATE $table SET prevent_reinit = $force
4549
                WHERE iid = ".$this->get_id();
4550
        Database::query($sql);
4551
        $this->prevent_reinit = $force;
4552
4553
        return $force;
4554
    }
4555
4556
    /**
4557
     * Determine the attempt_mode thanks to prevent_reinit and seriousgame_mode db flag.
4558
     *
4559
     * @return string 'single', 'multi' or 'seriousgame'
4560
     *
4561
     * @author ndiechburg <[email protected]>
4562
     */
4563
    public function get_attempt_mode()
4564
    {
4565
        //Set default value for seriousgame_mode
4566
        if (!isset($this->seriousgame_mode)) {
4567
            $this->seriousgame_mode = 0;
4568
        }
4569
        // Set default value for prevent_reinit
4570
        if (!isset($this->prevent_reinit)) {
4571
            $this->prevent_reinit = 1;
4572
        }
4573
        if (1 == $this->seriousgame_mode && 1 == $this->prevent_reinit) {
4574
            return 'seriousgame';
4575
        }
4576
        if (0 == $this->seriousgame_mode && 1 == $this->prevent_reinit) {
4577
            return 'single';
4578
        }
4579
        if (0 == $this->seriousgame_mode && 0 == $this->prevent_reinit) {
4580
            return 'multiple';
4581
        }
4582
4583
        return 'single';
4584
    }
4585
4586
    /**
4587
     * Register the attempt mode into db thanks to flags prevent_reinit and seriousgame_mode flags.
4588
     *
4589
     * @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...
4590
     *
4591
     * @return bool
4592
     *
4593
     * @author ndiechburg <[email protected]>
4594
     */
4595
    public function set_attempt_mode($mode)
4596
    {
4597
        switch ($mode) {
4598
            case 'seriousgame':
4599
                $sg_mode = 1;
4600
                $prevent_reinit = 1;
4601
                break;
4602
            case 'single':
4603
                $sg_mode = 0;
4604
                $prevent_reinit = 1;
4605
                break;
4606
            case 'multiple':
4607
                $sg_mode = 0;
4608
                $prevent_reinit = 0;
4609
                break;
4610
            default:
4611
                $sg_mode = 0;
4612
                $prevent_reinit = 0;
4613
                break;
4614
        }
4615
        $this->prevent_reinit = $prevent_reinit;
4616
        $this->seriousgame_mode = $sg_mode;
4617
        $table = Database::get_course_table(TABLE_LP_MAIN);
4618
        $sql = "UPDATE $table SET
4619
                prevent_reinit = $prevent_reinit ,
4620
                seriousgame_mode = $sg_mode
4621
                WHERE iid = ".$this->get_id();
4622
        $res = Database::query($sql);
4623
        if ($res) {
0 ignored issues
show
introduced by
$res is of type Doctrine\DBAL\Driver\Statement, thus it always evaluated to true.
Loading history...
4624
            return true;
4625
        } else {
4626
            return false;
4627
        }
4628
    }
4629
4630
    /**
4631
     * Switch between multiple attempt, single attempt or serious_game mode (only for scorm).
4632
     *
4633
     * @author ndiechburg <[email protected]>
4634
     */
4635
    public function switch_attempt_mode()
4636
    {
4637
        $mode = $this->get_attempt_mode();
4638
        switch ($mode) {
4639
            case 'single':
4640
                $next_mode = 'multiple';
4641
                break;
4642
            case 'multiple':
4643
                $next_mode = 'seriousgame';
4644
                break;
4645
            case 'seriousgame':
4646
            default:
4647
                $next_mode = 'single';
4648
                break;
4649
        }
4650
        $this->set_attempt_mode($next_mode);
4651
    }
4652
4653
    /**
4654
     * Switch the lp in ktm mode. This is a special scorm mode with unique attempt
4655
     * but possibility to do again a completed item.
4656
     *
4657
     * @return bool true if seriousgame_mode has been set to 1, false otherwise
4658
     *
4659
     * @author ndiechburg <[email protected]>
4660
     */
4661
    public function set_seriousgame_mode()
4662
    {
4663
        $table = Database::get_course_table(TABLE_LP_MAIN);
4664
        $force = $this->entity->getSeriousgameMode();
4665
        if (1 == $force) {
4666
            $force = 0;
4667
        } elseif (0 == $force) {
4668
            $force = 1;
4669
        }
4670
        $sql = "UPDATE $table SET seriousgame_mode = $force
4671
                WHERE iid = ".$this->get_id();
4672
        Database::query($sql);
4673
        $this->seriousgame_mode = $force;
4674
4675
        return $force;
4676
    }
4677
4678
    /**
4679
     * Updates the "scorm_debug" value that shows or hide the debug window.
4680
     *
4681
     * @return bool True if scorm_debug has been set to 'on', false otherwise (or 1 or 0 in this case)
4682
     */
4683
    public function update_scorm_debug()
4684
    {
4685
        $table = Database::get_course_table(TABLE_LP_MAIN);
4686
        $force = $this->entity->getDebug();
4687
        if (1 == $force) {
4688
            $force = 0;
4689
        } elseif (0 == $force) {
4690
            $force = 1;
4691
        }
4692
        $sql = "UPDATE $table SET debug = $force
4693
                WHERE iid = ".$this->get_id();
4694
        Database::query($sql);
4695
        $this->scorm_debug = $force;
4696
4697
        return $force;
4698
    }
4699
4700
    /**
4701
     * Function that makes a call to the function sort_tree_array and create_tree_array.
4702
     *
4703
     * @author Kevin Van Den Haute
4704
     *
4705
     * @param  array
4706
     */
4707
    public function tree_array($array)
4708
    {
4709
        $array = $this->sort_tree_array($array);
4710
        $this->create_tree_array($array);
4711
    }
4712
4713
    /**
4714
     * Creates an array with the elements of the learning path tree in it.
4715
     *
4716
     * @author Kevin Van Den Haute
4717
     *
4718
     * @param array $array
4719
     * @param int   $parent
4720
     * @param int   $depth
4721
     * @param array $tmp
4722
     */
4723
    public function create_tree_array($array, $parent = 0, $depth = -1, $tmp = [])
4724
    {
4725
        if (is_array($array)) {
4726
            for ($i = 0; $i < count($array); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
4727
                if ($array[$i]['parent_item_id'] == $parent) {
4728
                    if (!in_array($array[$i]['parent_item_id'], $tmp)) {
4729
                        $tmp[] = $array[$i]['parent_item_id'];
4730
                        $depth++;
4731
                    }
4732
                    $preq = (empty($array[$i]['prerequisite']) ? '' : $array[$i]['prerequisite']);
4733
                    $audio = isset($array[$i]['audio']) ? $array[$i]['audio'] : null;
4734
                    $path = isset($array[$i]['path']) ? $array[$i]['path'] : null;
4735
                    $prerequisiteMinScore = isset($array[$i]['prerequisite_min_score']) ? $array[$i]['prerequisite_min_score'] : null;
4736
                    $prerequisiteMaxScore = isset($array[$i]['prerequisite_max_score']) ? $array[$i]['prerequisite_max_score'] : null;
4737
                    $ref = isset($array[$i]['ref']) ? $array[$i]['ref'] : '';
4738
                    $this->arrMenu[] = [
4739
                        'id' => $array[$i]['id'],
4740
                        'ref' => $ref,
4741
                        'item_type' => $array[$i]['item_type'],
4742
                        'title' => $array[$i]['title'],
4743
                        'title_raw' => $array[$i]['title_raw'],
4744
                        'path' => $path,
4745
                        'description' => $array[$i]['description'],
4746
                        'parent_item_id' => $array[$i]['parent_item_id'],
4747
                        'previous_item_id' => $array[$i]['previous_item_id'],
4748
                        'next_item_id' => $array[$i]['next_item_id'],
4749
                        'min_score' => $array[$i]['min_score'],
4750
                        'max_score' => $array[$i]['max_score'],
4751
                        'mastery_score' => $array[$i]['mastery_score'],
4752
                        'display_order' => $array[$i]['display_order'],
4753
                        'prerequisite' => $preq,
4754
                        'depth' => $depth,
4755
                        'audio' => $audio,
4756
                        'prerequisite_min_score' => $prerequisiteMinScore,
4757
                        'prerequisite_max_score' => $prerequisiteMaxScore,
4758
                    ];
4759
                    $this->create_tree_array($array, $array[$i]['id'], $depth, $tmp);
4760
                }
4761
            }
4762
        }
4763
    }
4764
4765
    /**
4766
     * Sorts a multi dimensional array by parent id and display order.
4767
     *
4768
     * @author Kevin Van Den Haute
4769
     *
4770
     * @param array $array (array with al the learning path items in it)
4771
     *
4772
     * @return array
4773
     */
4774
    public function sort_tree_array($array)
4775
    {
4776
        foreach ($array as $key => $row) {
4777
            $parent[$key] = $row['parent_item_id'];
4778
            $position[$key] = $row['display_order'];
4779
        }
4780
4781
        if (count($array) > 0) {
4782
            array_multisort($parent, SORT_ASC, $position, SORT_ASC, $array);
4783
        }
4784
4785
        return $array;
4786
    }
4787
4788
    /**
4789
     * Function that creates a html list of learning path items so that we can add audio files to them.
4790
     *
4791
     * @author Kevin Van Den Haute
4792
     *
4793
     * @return string
4794
     */
4795
    public function overview()
4796
    {
4797
        $return = '';
4798
        $update_audio = $_GET['updateaudio'] ?? null;
4799
4800
        // we need to start a form when we want to update all the mp3 files
4801
        if ('true' == $update_audio) {
4802
            $return .= '<form action="'.api_get_self().'?'.api_get_cidreq().'&updateaudio='.Security::remove_XSS(
4803
                    $_GET['updateaudio']
4804
                ).'&action='.Security::remove_XSS(
4805
                    $_GET['action']
4806
                ).'&lp_id='.$_SESSION['oLP']->lp_id.'" method="post" enctype="multipart/form-data" name="updatemp3" id="updatemp3">';
4807
        }
4808
        $return .= '<div id="message"></div>';
4809
        if (0 == count($this->items)) {
4810
            $return .= Display::return_message(
4811
                get_lang(
4812
                    'You should add some items to your learning path, otherwise you won\'t be able to attach audio files to them'
4813
                ),
4814
                'normal'
4815
            );
4816
        } else {
4817
            $return_audio = '<table class="table table-hover table-striped data_table">';
4818
            $return_audio .= '<tr>';
4819
            $return_audio .= '<th width="40%">'.get_lang('Title').'</th>';
4820
            $return_audio .= '<th>'.get_lang('Audio').'</th>';
4821
            $return_audio .= '</tr>';
4822
4823
            if ('true' != $update_audio) {
4824
                /*$return .= '<div class="col-md-12">';
4825
                $return .= self::return_new_tree($update_audio);
4826
                $return .= '</div>';*/
4827
                $return .= Display::div(
4828
                    Display::url(get_lang('Save'), '#', ['id' => 'listSubmit', 'class' => 'btn btn-primary']),
4829
                    ['style' => 'float:left; margin-top:15px;width:100%']
4830
                );
4831
            } else {
4832
                //$return_audio .= self::return_new_tree($update_audio);
4833
                $return .= $return_audio.'</table>';
4834
            }
4835
4836
            // We need to close the form when we are updating the mp3 files.
4837
            if ('true' == $update_audio) {
4838
                $return .= '<div class="footer-audio">';
4839
                $return .= Display::button(
4840
                    'save_audio',
4841
                    '<em class="fa fa-file-audio-o"></em> '.get_lang('Save audio and organization'),
4842
                    ['class' => 'btn btn-primary', 'type' => 'submit']
4843
                );
4844
                $return .= '</div>';
4845
            }
4846
        }
4847
4848
        // We need to close the form when we are updating the mp3 files.
4849
        if ('true' === $update_audio && isset($this->arrMenu) && 0 != count($this->arrMenu)) {
4850
            $return .= '</form>';
4851
        }
4852
4853
        return $return;
4854
    }
4855
4856
    public function showBuildSideBar($updateAudio = false, $dropElementHere = false, $type = null)
4857
    {
4858
        $sureToDelete = trim(get_lang('Are you sure to delete?'));
4859
        $ajax_url = api_get_path(WEB_AJAX_PATH).'lp.ajax.php?lp_id='.$this->get_id().'&'.api_get_cidreq();
4860
4861
        $content = '
4862
        <script>
4863
            /*
4864
            Script to manipulate Learning Path items with Drag and drop
4865
             */
4866
            $(function() {
4867
                function refreshTree() {
4868
                    var params = "&a=get_lp_item_tree";
4869
                    $.get(
4870
                        "'.$ajax_url.'",
4871
                        params,
4872
                        function(result) {
4873
                            serialized = [];
4874
                            $("#lp_item_list").html(result);
4875
                            nestedSortable();
4876
                        }
4877
                    );
4878
                }
4879
4880
                const nestedQuery = ".nested-sortable";
4881
                const identifier = "id";
4882
                const root = document.getElementById("lp_item_list");
4883
4884
                var serialized = [];
4885
                function serialize(sortable) {
4886
                  var children = [].slice.call(sortable.children);
4887
                  for (var i in children) {
4888
                    var nested = children[i].querySelector(nestedQuery);
4889
                    var parentId = $(children[i]).parent().parent().attr("id");
4890
                    var id = children[i].dataset[identifier];
4891
                    if (typeof id === "undefined") {
4892
                        return;
4893
                    }
4894
                    serialized.push({
4895
                      id: children[i].dataset[identifier],
4896
                      parent_id: parentId
4897
                    });
4898
4899
                    if (nested) {
4900
                        serialize(nested);
4901
                    }
4902
                  }
4903
4904
                  return serialized;
4905
                }
4906
4907
                function nestedSortable() {
4908
                    let left = document.getElementsByClassName("nested-sortable");
4909
                    Array.prototype.forEach.call(left, function(resource) {
4910
                        Sortable.create(resource, {
4911
                            group: "nested",
4912
                            put: ["nested-sortable", ".lp_resource", ".nested-source"],
4913
                            animation: 150,
4914
                            //fallbackOnBody: true,
4915
                            swapThreshold: 0.65,
4916
                            dataIdAttr: "data-id",
4917
                            store: {
4918
                                set: function (sortable) {
4919
                                    var order = sortable.toArray();
4920
                                    console.log(order);
4921
                                }
4922
                            },
4923
                            onEnd: function(evt) {
4924
                                console.log("onEnd");
4925
                                let list = serialize(root);
4926
                                let order = "&a=update_lp_item_order&new_order=" + JSON.stringify(list);
4927
                                $.get(
4928
                                    "'.$ajax_url.'",
4929
                                    order,
4930
                                    function(reponse) {
4931
                                        $("#message").html(reponse);
4932
                                        refreshTree();
4933
                                    }
4934
                                );
4935
                            },
4936
                        });
4937
                    });
4938
                }
4939
4940
                nestedSortable();
4941
4942
                let resources = document.getElementsByClassName("lp_resource");
4943
                Array.prototype.forEach.call(resources, function(resource) {
4944
                    Sortable.create(resource, {
4945
                        group: "nested",
4946
                        put: ["nested-sortable"],
4947
                        filter: ".disable_drag",
4948
                        animation: 150,
4949
                        fallbackOnBody: true,
4950
                        swapThreshold: 0.65,
4951
                        dataIdAttr: "data-id",
4952
                        onRemove: function(evt) {
4953
                            console.log("onRemove");
4954
                            var itemEl = evt.item;
4955
                            var newIndex = evt.newIndex;
4956
                            var id = $(itemEl).attr("id");
4957
                            var parent_id = $(itemEl).parent().parent().attr("id");
4958
                            var type =  $(itemEl).find(".link_with_id").attr("data_type");
4959
                            var title = $(itemEl).find(".link_with_id").text();
4960
4961
                            let previousId = 0;
4962
                            if (0 !== newIndex) {
4963
                                previousId = $(itemEl).prev().attr("id");
4964
                            }
4965
                            var params = {
4966
                                "a": "add_lp_item",
4967
                                "id": id,
4968
                                "parent_id": parent_id,
4969
                                "previous_id": previousId,
4970
                                "type": type,
4971
                                "title" : title
4972
                            };
4973
                            console.log(params);
4974
                            $.ajax({
4975
                                type: "GET",
4976
                                url: "'.$ajax_url.'",
4977
                                data: params,
4978
                                success: function(itemId) {
4979
                                    $(itemEl).attr("id", itemId);
4980
                                    $(itemEl).attr("data-id", itemId);
4981
                                    let list = serialize(root);
4982
                                    let listInString = JSON.stringify(list);
4983
                                    if (typeof listInString === "undefined") {
4984
                                        listInString = "";
4985
                                    }
4986
                                    let order = "&a=update_lp_item_order&new_order=" + listInString;
4987
                                    $.get(
4988
                                        "'.$ajax_url.'",
4989
                                        order,
4990
                                        function(reponse) {
4991
                                            $("#message").html(reponse);
4992
                                            refreshTree();
4993
                                        }
4994
                                    );
4995
                                }
4996
                            });
4997
                        },
4998
                    });
4999
                });
5000
5001
            });
5002
        </script>';
5003
5004
        $content .= "
5005
        <script>
5006
            function confirmation(name) {
5007
                if (confirm('$sureToDelete ' + name)) {
5008
                    return true;
5009
                } else {
5010
                    return false;
5011
                }
5012
            }
5013
            function refreshTree() {
5014
                var params = '&a=get_lp_item_tree';
5015
                $.get(
5016
                    '".$ajax_url."',
5017
                    params,
5018
                    function(result) {
5019
                        $('#lp_item_list').html(result);
5020
                    }
5021
                );
5022
            }
5023
5024
            $(function () {
5025
                //$('.scrollbar-inner').scrollbar();
5026
                $('#subtab').on('click', 'a:first', function() {
5027
                    window.location.reload();
5028
                });
5029
                $('#subtab ').on('click', 'a:first', function () {
5030
                    window.location.reload();
5031
                });
5032
                expandColumnToggle('#hide_bar_template', {
5033
                    selector: '#lp_sidebar'
5034
                }, {
5035
                    selector: '#doc_form'
5036
                });
5037
5038
                $('.lp-btn-associate-forum').on('click', function (e) {
5039
                    var associate = confirm('".get_lang('ConfirmAssociateForumToLPItem')."');
5040
                    if (!associate) {
5041
                        e.preventDefault();
5042
                    }
5043
                });
5044
5045
                $('.lp-btn-dissociate-forum').on('click', function (e) {
5046
                    var dissociate = confirm('".get_lang('ConfirmDissociateForumToLPItem')."');
5047
                    if (!dissociate) {
5048
                        e.preventDefault();
5049
                    }
5050
                });
5051
5052
                // hide the current template list for new documment until it tab clicked
5053
                $('#frmModel').hide();
5054
            });
5055
5056
            // document template for new document tab handler
5057
            $(document).on('shown.bs.tab', 'a[data-toggle=\"tab\"]', function (e) {
5058
                var id = e.target.id;
5059
                if (id == 'subtab2') {
5060
                    $('#frmModel').show();
5061
                } else {
5062
                    $('#frmModel').hide();
5063
                }
5064
            });
5065
5066
          function deleteItem(event) {
5067
          console.log($(event));
5068
            var id = $(event).attr('data-id');
5069
            var title = $(event).attr('data-title');
5070
            var params = '&a=delete_item&id=' + id;
5071
            if (confirmation(title)) {
5072
                $.get(
5073
                    '".$ajax_url."',
5074
                    params,
5075
                    function(result) {
5076
                        refreshTree();
5077
                    }
5078
                );
5079
            }
5080
        }
5081
        </script>";
5082
5083
        $content .= $this->return_new_tree($updateAudio, $dropElementHere);
5084
        $documentId = isset($_GET['path_item']) ? (int) $_GET['path_item'] : 0;
5085
5086
        $repo = Container::getDocumentRepository();
5087
        $document = $repo->find($documentId);
5088
        if ($document) {
5089
            // Show the template list
5090
            $content .= '<div id="frmModel" class="scrollbar-inner lp-add-item"></div>';
5091
        }
5092
5093
        // Show the template list.
5094
        if (('document' === $type || 'step' === $type) && !isset($_GET['file'])) {
5095
            // Show the template list.
5096
            $content .= '<div id="frmModel" class="scrollbar-inner lp-add-item"></div>';
5097
        }
5098
5099
        return $content;
5100
    }
5101
5102
    /**
5103
     * @param bool  $updateAudio
5104
     * @param bool   $dropElement
5105
     *
5106
     * @return string
5107
     */
5108
    public function return_new_tree($updateAudio = false, $dropElement = false)
5109
    {
5110
        /*$result = $this->processBuildMenuElements($update_audio);
5111
        $list = '<ul id="lp_item_list">';
5112
        $tree = $this->print_recursive(
5113
            $result['elements'],
5114
            $result['default_data'],
5115
            $result['default_content']
5116
        );
5117
5118
        if (!empty($tree)) {
5119
            $list .= $tree;
5120
        } else {
5121
            if ($drop_element_here) {
5122
                $list .= Display::return_message(get_lang('Drag and drop an element here'));
5123
            }
5124
        }
5125
        $list .= '</ul>';*/
5126
5127
        $list = $this->getBuildTree(false, $dropElement);
5128
5129
        $return = Display::panelCollapse(
5130
            $this->name,
5131
            $list,
5132
            'scorm-list',
5133
            null,
5134
            'scorm-list-accordion',
5135
            'scorm-list-collapse'
5136
        );
5137
5138
        if ($updateAudio) {
5139
            //$return = $result['return_audio'];
5140
        }
5141
5142
        return $return;
5143
    }
5144
5145
    public function getBuildTree($noWrapper = false, $dropElement = false)
5146
    {
5147
        $mainUrl = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?'.api_get_cidreq();
5148
5149
        $upIcon = Display::return_icon(
5150
            'up.png',
5151
            get_lang('Up'),
5152
            [],
5153
            ICON_SIZE_TINY
5154
        );
5155
5156
        $disableUpIcon = Display::return_icon(
5157
            'up_na.png',
5158
            get_lang('Up'),
5159
            [],
5160
            ICON_SIZE_TINY
5161
        );
5162
5163
        $downIcon = Display::return_icon(
5164
            'down.png',
5165
            get_lang('Down'),
5166
            [],
5167
            ICON_SIZE_TINY
5168
        );
5169
5170
        $previewImage = Display::return_icon(
5171
            'preview_view.png',
5172
            get_lang('Preview'),
5173
            [],
5174
            ICON_SIZE_TINY
5175
        );
5176
        $lpItemRepo = Container::getLpItemRepository();
5177
        $itemRoot = $lpItemRepo->getItemRoot($this->get_id());
5178
5179
        $options = [
5180
            'decorate' => true,
5181
            'rootOpen' => function($tree) use ($noWrapper) {
5182
                if ($tree[0]['lvl'] === 1) {
5183
                    if ($noWrapper) {
5184
                        return '';
5185
                    }
5186
                    return '<ul id="lp_item_list" class="list-group nested-sortable">';
5187
                }
5188
5189
                return '<ul class="list-group nested-sortable">';
5190
            },
5191
            'rootClose' => function($tree) use ($noWrapper, $dropElement)  {
5192
                if ($tree[0]['lvl'] === 1) {
5193
                    if ($dropElement) {
5194
                        //return Display::return_message(get_lang('Drag and drop an element here'));
5195
                        //return $this->getDropElementHtml();
5196
                    }
5197
                    if ($noWrapper) {
5198
                        return '';
5199
                    }
5200
                }
5201
5202
                return '</ul>';
5203
            },
5204
            'childOpen' => function($child) {
5205
                $id = $child['iid'];
5206
                return '<li
5207
                    id="'.$id.'"
5208
                    data-id="'.$id.'"
5209
                    class=" flex flex-col list-group-item nested-'.$child['lvl'].'">';
5210
            },
5211
            'childClose' => '',
5212
            'nodeDecorator' => function ($node) use ($mainUrl, $previewImage, $upIcon, $downIcon) {
5213
                $fullTitle = $node['title'];
5214
                $title = cut($fullTitle, self::MAX_LP_ITEM_TITLE_LENGTH);
5215
                $itemId = $node['iid'];
5216
                $type = $node['itemType'];
5217
                $lpId = $this->get_id();
5218
5219
                $moveIcon = '';
5220
                if (TOOL_LP_FINAL_ITEM !== $type) {
5221
                    $moveIcon .= '<a class="moved" href="#">';
5222
                    $moveIcon .= Display::return_icon(
5223
                        'move_everywhere.png',
5224
                        get_lang('Move'),
5225
                        [],
5226
                        ICON_SIZE_TINY
5227
                    );
5228
                    $moveIcon .= '</a>';
5229
                }
5230
5231
                $iconName = str_replace(' ', '', $type);
5232
                $icon = Display::return_icon(
5233
                    'lp_'.$iconName.'.png',
5234
                    '',
5235
                    [],
5236
                    ICON_SIZE_TINY
5237
                );
5238
5239
                $urlPreviewLink = $mainUrl.'&action=view_item&mode=preview_document&id='.$itemId.'&lp_id='.$lpId;
5240
                $previewIcon = Display::url(
5241
                    $previewImage,
5242
                    $urlPreviewLink,
5243
                    [
5244
                        'target' => '_blank',
5245
                        'class' => 'btn btn-default',
5246
                        'data-title' => $title,
5247
                        'title' => $title,
5248
                    ]
5249
                );
5250
                $url = $mainUrl.'&view=build&id='.$itemId.'&lp_id='.$lpId;
5251
5252
                $preRequisitiesIcon = Display::url(
5253
                    Display::return_icon(
5254
                        'accept.png',
5255
                        get_lang('Prerequisites'),
5256
                        [],
5257
                        ICON_SIZE_TINY
5258
                    ),
5259
                    $url.'&action=edit_item_prereq',
5260
                    ['class' => 'btn btn-default']
5261
                );
5262
5263
                //$editIcon = '';
5264
                $editIcon = '<a
5265
                    href="'.$mainUrl.'&action=edit_item&view=build&id='.$itemId.'&lp_id='.$lpId.'&path_item='.$node['path'].'"
5266
                    class="btn btn-default"
5267
                    >';
5268
                $editIcon .= Display::return_icon(
5269
                    'edit.png',
5270
                    get_lang('Edit section description/name'),
5271
                    [],
5272
                    ICON_SIZE_TINY
5273
                );
5274
                $editIcon .= '</a>';
5275
                $orderIcons = '';
5276
                /*if ('final_item' !== $type) {
5277
                    $orderIcons = Display::url(
5278
                        $upIcon,
5279
                        'javascript:void(0)',
5280
                        ['class' => 'btn btn-default order_items', 'data-dir' => 'up', 'data-id' => $itemId]
5281
                    );
5282
                    $orderIcons .= Display::url(
5283
                        $downIcon,
5284
                        'javascript:void(0)',
5285
                        ['class' => 'btn btn-default order_items', 'data-dir' => 'down', 'data-id' => $itemId]
5286
                    );
5287
                }*/
5288
5289
                $deleteIcon = ' <a
5290
                    data-id = '.$itemId.'
5291
                    data-title = \''.addslashes($title).'\'
5292
                    href="javascript:void(0);"
5293
                    onclick="return deleteItem(this);"
5294
                    class="btn btn-default">';
5295
                $deleteIcon .= Display::return_icon(
5296
                    'delete.png',
5297
                    get_lang('Delete section'),
5298
                    [],
5299
                    ICON_SIZE_TINY
5300
                );
5301
                $deleteIcon .= '</a>';
5302
                $extra = '';
5303
5304
                if ('dir' === $type && empty($node['__children'])) {
5305
                    $level = $node['lvl'] + 1;
5306
                    $extra = '<ul class="list-group nested-sortable">
5307
                                <li class="list-group-item list-group-item-empty nested-'.$level.'"></li>
5308
                              </ul>';
5309
                }
5310
                //return $title;
5311
                $buttons = Display::tag(
5312
                    'div',
5313
                    "<div class=\"btn-group btn-group-sm\">
5314
                                $editIcon
5315
                                $preRequisitiesIcon
5316
                                $orderIcons
5317
                                $deleteIcon
5318
                               </div>",
5319
                    ['class' => 'btn-toolbar button_actions']
5320
                );
5321
                //$buttons = '';
5322
                //$extra = '';
5323
                //Display::span($title, ['title' => $fullTitle])
5324
                //return $title.                $extra;
5325
                return
5326
                    "<div class='flex flex-row'> $moveIcon  $icon <div>$title </div></div>
5327
                    $extra
5328
                    $buttons
5329
                    "
5330
                    ;
5331
            },
5332
        ];
5333
        $tree = $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
5334
5335
        if (empty($tree) && $dropElement) {
5336
            return $this->getDropElementHtml($noWrapper);
5337
        }
5338
5339
        return $tree;
5340
    }
5341
5342
    public function getDropElementHtml($noWrapper = false)
5343
    {
5344
        $li = '<li class="list-group-item">'.
5345
            Display::return_message(get_lang('Drag and drop an element here')).
5346
            '</li>';
5347
        if ($noWrapper) {
5348
            return $li;
5349
        }
5350
5351
        return
5352
            '<ul id="lp_item_list" class="list-group nested-sortable">
5353
            '.$li.'
5354
            </ul>';
5355
    }
5356
5357
    /**
5358
     * @param array $elements
5359
     * @param array $default_data
5360
     * @param array $default_content
5361
     *
5362
     * @return string
5363
     */
5364
    public function print_recursive($elements, $default_data, $default_content)
5365
    {
5366
        $return = '';
5367
        foreach ($elements as $key => $item) {
5368
            if (isset($item['load_data']) || empty($item['data'])) {
5369
                $item['data'] = $default_data[$item['load_data']];
5370
                $item['type'] = $default_content[$item['load_data']]['item_type'];
5371
            }
5372
            $sub_list = '';
5373
            if (isset($item['type']) && 'dir' === $item['type']) {
5374
                // empty value
5375
                $sub_list = Display::tag('li', '', ['class' => 'sub_item empty']);
5376
            }
5377
            if (empty($item['children'])) {
5378
                $sub_list = Display::tag('ul', $sub_list, ['id' => 'UL_'.$key, 'class' => 'record li_container']);
5379
                $active = null;
5380
                if (isset($_REQUEST['id']) && $key == $_REQUEST['id']) {
5381
                    $active = 'active';
5382
                }
5383
                $return .= Display::tag(
5384
                    'li',
5385
                    Display::div($item['data'], ['class' => "item_data $active"]).$sub_list,
5386
                    ['id' => $key, 'class' => 'record li_container']
5387
                );
5388
            } else {
5389
                // Sections
5390
                $data = '';
5391
                if (isset($item['children'])) {
5392
                    $data = self::print_recursive($item['children'], $default_data, $default_content);
5393
                }
5394
                $sub_list = Display::tag('ul', $sub_list.$data, ['id' => 'UL_'.$key, 'class' => 'record li_container']);
5395
                $return .= Display::tag(
5396
                    'li',
5397
                    Display::div($item['data'], ['class' => 'item_data']).$sub_list,
5398
                    ['id' => $key, 'class' => 'record li_container']
5399
                );
5400
            }
5401
        }
5402
5403
        return $return;
5404
    }
5405
5406
    /**
5407
     * This function builds the action menu.
5408
     *
5409
     * @param bool   $returnString           Optional
5410
     * @param bool   $showRequirementButtons Optional. Allow show the requirements button
5411
     * @param bool   $isConfigPage           Optional. If is the config page, show the edit button
5412
     * @param bool   $allowExpand            Optional. Allow show the expand/contract button
5413
     * @param string $action
5414
     * @param array  $extraField
5415
     *
5416
     * @return string
5417
     */
5418
    public function build_action_menu(
5419
        $returnString = false,
5420
        $showRequirementButtons = true,
5421
        $isConfigPage = false,
5422
        $allowExpand = true,
5423
        $action = '',
5424
        $extraField = []
5425
    ) {
5426
        $actionsRight = '';
5427
        $lpId = $this->lp_id;
5428
        if (!isset($extraField['backTo']) && empty($extraField['backTo'])) {
5429
            $back = Display::url(
5430
                Display::return_icon(
5431
                    'back.png',
5432
                    get_lang('Back to learning paths'),
5433
                    '',
5434
                    ICON_SIZE_MEDIUM
5435
                ),
5436
                'lp_controller.php?'.api_get_cidreq()
5437
            );
5438
        } else {
5439
            $back = Display::url(
5440
                Display::return_icon(
5441
                    'back.png',
5442
                    get_lang('Back'),
5443
                    '',
5444
                    ICON_SIZE_MEDIUM
5445
                ),
5446
                $extraField['backTo']
5447
            );
5448
        }
5449
5450
        /*if ($backToBuild) {
5451
            $back = Display::url(
5452
                Display::return_icon(
5453
                    'back.png',
5454
                    get_lang('GoBack'),
5455
                    '',
5456
                    ICON_SIZE_MEDIUM
5457
                ),
5458
                "lp_controller.php?action=add_item&type=step&lp_id=$lpId&".api_get_cidreq()
5459
            );
5460
        }*/
5461
5462
        $actionsLeft = $back;
5463
5464
        $actionsLeft .= Display::url(
5465
            Display::return_icon(
5466
                'preview_view.png',
5467
                get_lang('Preview'),
5468
                '',
5469
                ICON_SIZE_MEDIUM
5470
            ),
5471
            'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
5472
                'action' => 'view',
5473
                'lp_id' => $lpId,
5474
                'isStudentView' => 'true',
5475
            ])
5476
        );
5477
5478
        /*$actionsLeft .= Display::url(
5479
            Display::return_icon(
5480
                'upload_audio.png',
5481
                get_lang('Add audio'),
5482
                '',
5483
                ICON_SIZE_MEDIUM
5484
            ),
5485
            'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
5486
                'action' => 'admin_view',
5487
                'lp_id' => $lpId,
5488
                'updateaudio' => 'true',
5489
            ])
5490
        );*/
5491
5492
        $subscriptionSettings = self::getSubscriptionSettings();
5493
5494
        $request = api_request_uri();
5495
        if (false === strpos($request, 'edit')) {
5496
            $actionsLeft .= Display::url(
5497
                Display::return_icon(
5498
                    'settings.png',
5499
                    get_lang('Course settings'),
5500
                    '',
5501
                    ICON_SIZE_MEDIUM
5502
                ),
5503
                'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
5504
                    'action' => 'edit',
5505
                    'lp_id' => $lpId,
5506
                ])
5507
            );
5508
        }
5509
5510
        if ((false === strpos($request, 'build') &&
5511
            false === strpos($request, 'add_item')) ||
5512
            in_array($action, ['add_audio'], true)
5513
        ) {
5514
            $actionsLeft .= Display::url(
5515
                Display::return_icon(
5516
                    'edit.png',
5517
                    get_lang('Edit'),
5518
                    '',
5519
                    ICON_SIZE_MEDIUM
5520
                ),
5521
                'lp_controller.php?'.http_build_query([
5522
                    'action' => 'build',
5523
                    'lp_id' => $lpId,
5524
                ]).'&'.api_get_cidreq()
5525
            );
5526
        }
5527
5528
        if (false === strpos(api_get_self(), 'lp_subscribe_users.php')) {
5529
            if (1 == $this->subscribeUsers &&
5530
                $subscriptionSettings['allow_add_users_to_lp']) {
5531
                $actionsLeft .= Display::url(
5532
                    Display::return_icon(
5533
                        'user.png',
5534
                        get_lang('Subscribe users to learning path'),
5535
                        '',
5536
                        ICON_SIZE_MEDIUM
5537
                    ),
5538
                    api_get_path(WEB_CODE_PATH)."lp/lp_subscribe_users.php?lp_id=$lpId&".api_get_cidreq()
5539
                );
5540
            }
5541
        }
5542
5543
        if ($allowExpand) {
5544
            /*$actionsLeft .= Display::url(
5545
                Display::return_icon(
5546
                    'expand.png',
5547
                    get_lang('Expand'),
5548
                    ['id' => 'expand'],
5549
                    ICON_SIZE_MEDIUM
5550
                ).
5551
                Display::return_icon(
5552
                    'contract.png',
5553
                    get_lang('Collapse'),
5554
                    ['id' => 'contract', 'class' => 'hide'],
5555
                    ICON_SIZE_MEDIUM
5556
                ),
5557
                '#',
5558
                ['role' => 'button', 'id' => 'hide_bar_template']
5559
            );*/
5560
        }
5561
5562
        if ($showRequirementButtons) {
5563
            $buttons = [
5564
                [
5565
                    'title' => get_lang('Set previous step as prerequisite for each step'),
5566
                    'href' => 'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
5567
                        'action' => 'set_previous_step_as_prerequisite',
5568
                        'lp_id' => $lpId,
5569
                    ]),
5570
                ],
5571
                [
5572
                    'title' => get_lang('Clear all prerequisites'),
5573
                    'href' => 'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
5574
                        'action' => 'clear_prerequisites',
5575
                        'lp_id' => $lpId,
5576
                    ]),
5577
                ],
5578
            ];
5579
            $actionsRight = Display::groupButtonWithDropDown(
5580
                get_lang('Prerequisites options'),
5581
                $buttons,
5582
                true
5583
            );
5584
        }
5585
5586
        if (api_is_platform_admin() && isset($extraField['authorlp'])) {
5587
            $actionsLeft .= Display::url(
5588
                Display::return_icon(
5589
                    'add-groups.png',
5590
                    get_lang('Author'),
5591
                    '',
5592
                    ICON_SIZE_MEDIUM
5593
                ),
5594
                'lp_controller.php?'.api_get_cidreq().'&'.http_build_query([
5595
                    'action' => 'author_view',
5596
                    'lp_id' => $lpId,
5597
                ])
5598
            );
5599
        }
5600
5601
        $toolbar = Display::toolbarAction('actions-lp-controller', [$actionsLeft, $actionsRight]);
5602
5603
        if ($returnString) {
5604
            return $toolbar;
5605
        }
5606
5607
        echo $toolbar;
5608
    }
5609
5610
    /**
5611
     * Creates the default learning path folder.
5612
     *
5613
     * @param array $course
5614
     * @param int   $creatorId
5615
     *
5616
     * @return CDocument
5617
     */
5618
    public static function generate_learning_path_folder($course, $creatorId = 0)
5619
    {
5620
        // Creating learning_path folder
5621
        $dir = 'learning_path';
5622
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
5623
5624
        return create_unexisting_directory(
5625
            $course,
5626
            $creatorId,
5627
            0,
5628
            null,
5629
            0,
5630
            '',
5631
            $dir,
5632
            get_lang('Learning paths'),
5633
            0
5634
        );
5635
    }
5636
5637
    /**
5638
     * @param array  $course
5639
     * @param string $lp_name
5640
     * @param int    $creatorId
5641
     *
5642
     * @return CDocument
5643
     */
5644
    public function generate_lp_folder($course, $lp_name = '', $creatorId = 0)
5645
    {
5646
        $filepath = '';
5647
        $dir = '/learning_path/';
5648
5649
        if (empty($lp_name)) {
5650
            $lp_name = $this->name;
5651
        }
5652
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
5653
        $parent = self::generate_learning_path_folder($course, $creatorId);
5654
5655
        // Limits title size
5656
        $title = api_substr(api_replace_dangerous_char($lp_name), 0, 80);
5657
        $dir = $dir.$title;
5658
5659
        // Creating LP folder
5660
        $documentId = null;
5661
        $folder = null;
5662
        if ($parent) {
5663
            $folder = create_unexisting_directory(
5664
                $course,
5665
                $creatorId,
5666
                0,
5667
                0,
5668
                0,
5669
                $filepath,
5670
                $dir,
5671
                $lp_name,
5672
                '',
5673
                false,
5674
                false,
5675
                $parent
5676
            );
5677
        }
5678
5679
        return $folder;
5680
    }
5681
5682
    /**
5683
     * Create a new document //still needs some finetuning.
5684
     *
5685
     * @param array  $courseInfo
5686
     * @param string $content
5687
     * @param string $title
5688
     * @param string $extension
5689
     * @param int    $parentId
5690
     * @param int    $creatorId  creator id
5691
     *
5692
     * @return int
5693
     */
5694
    public function create_document(
5695
        $courseInfo,
5696
        $content = '',
5697
        $title = '',
5698
        $extension = 'html',
5699
        $parentId = 0,
5700
        $creatorId = 0
5701
    ) {
5702
        $creatorId = empty($creatorId) ? api_get_user_id() : $creatorId;
5703
        $sessionId = api_get_session_id();
5704
5705
        // Generates folder
5706
        $this->generate_lp_folder($courseInfo);
5707
        // stripslashes() before calling api_replace_dangerous_char() because $_POST['title']
5708
        // is already escaped twice when it gets here.
5709
        $originalTitle = !empty($title) ? $title : $_POST['title'];
5710
        if (!empty($title)) {
5711
            $title = api_replace_dangerous_char(stripslashes($title));
5712
        } else {
5713
            $title = api_replace_dangerous_char(stripslashes($_POST['title']));
5714
        }
5715
5716
        $title = disable_dangerous_file($title);
5717
        $filename = $title;
5718
        $tmp_filename = $filename;
5719
        /*$i = 0;
5720
        while (file_exists($filepath.$tmp_filename.'.'.$extension)) {
5721
            $tmp_filename = $filename.'_'.++$i;
5722
        }*/
5723
        $filename = $tmp_filename.'.'.$extension;
5724
5725
        if ('html' === $extension) {
5726
            $content = stripslashes($content);
5727
            $content = str_replace(
5728
                api_get_path(WEB_COURSE_PATH),
5729
                api_get_path(REL_PATH).'courses/',
5730
                $content
5731
            );
5732
5733
            // Change the path of mp3 to absolute.
5734
            // The first regexp deals with :// urls.
5735
            /*$content = preg_replace(
5736
                "|(flashvars=\"file=)([^:/]+)/|",
5737
                "$1".api_get_path(
5738
                    REL_COURSE_PATH
5739
                ).$courseInfo['path'].'/document/',
5740
                $content
5741
            );*/
5742
            // The second regexp deals with audio/ urls.
5743
            /*$content = preg_replace(
5744
                "|(flashvars=\"file=)([^/]+)/|",
5745
                "$1".api_get_path(
5746
                    REL_COURSE_PATH
5747
                ).$courseInfo['path'].'/document/$2/',
5748
                $content
5749
            );*/
5750
            // For flv player: To prevent edition problem with firefox,
5751
            // we have to use a strange tip (don't blame me please).
5752
            $content = str_replace(
5753
                '</body>',
5754
                '<style type="text/css">body{}</style></body>',
5755
                $content
5756
            );
5757
        }
5758
5759
        $document = DocumentManager::addDocument(
5760
            $courseInfo,
5761
            null,
5762
            'file',
5763
            '',
5764
            $tmp_filename,
5765
            '',
5766
            0, //readonly
5767
            true,
5768
            null,
5769
            $sessionId,
5770
            $creatorId,
5771
            false,
5772
            $content,
5773
            $parentId
5774
        );
5775
5776
        $document_id = $document->getIid();
5777
        if ($document_id) {
5778
            $new_comment = isset($_POST['comment']) ? trim($_POST['comment']) : '';
5779
            $new_title = $originalTitle;
5780
5781
            if ($new_comment || $new_title) {
5782
                $tbl_doc = Database::get_course_table(TABLE_DOCUMENT);
5783
                $ct = '';
5784
                if ($new_comment) {
5785
                    $ct .= ", comment='".Database::escape_string($new_comment)."'";
5786
                }
5787
                if ($new_title) {
5788
                    $ct .= ", title='".Database::escape_string($new_title)."' ";
5789
                }
5790
5791
                $sql = "UPDATE $tbl_doc SET ".substr($ct, 1)."
5792
                        WHERE iid = $document_id ";
5793
                Database::query($sql);
5794
            }
5795
        }
5796
5797
        return $document_id;
5798
    }
5799
5800
    /**
5801
     * Edit a document based on $_POST and $_GET parameters 'dir' and 'path'.
5802
     */
5803
    public function edit_document()
5804
    {
5805
        $repo = Container::getDocumentRepository();
5806
        if (isset($_REQUEST['document_id']) && !empty($_REQUEST['document_id'])) {
5807
            $id = (int) $_REQUEST['document_id'];
5808
            /** @var CDocument $document */
5809
            $document = $repo->find($id);
5810
            if ($document->getResourceNode()->hasEditableTextContent()) {
5811
                $repo->updateResourceFileContent($document, $_REQUEST['content_lp']);
5812
                /*   $nodeRepo = Container::getDocumentRepository()->getResourceNodeRepository();
5813
                   $nodeRepo->update($document->getResourceNode());
5814
                   var_dump($document->getResourceNode()->getContent());
5815
                   exit;*/
5816
            }
5817
            $document->setTitle($_REQUEST['title']);
5818
            $repo->update($document);
5819
        }
5820
    }
5821
5822
    /**
5823
     * Displays the selected item, with a panel for manipulating the item.
5824
     *
5825
     * @param CLpItem $lpItem
5826
     * @param string  $msg
5827
     * @param bool    $show_actions
5828
     *
5829
     * @return string
5830
     */
5831
    public function display_item($lpItem, $msg = null, $show_actions = true)
5832
    {
5833
        $course_id = api_get_course_int_id();
5834
        $return = '';
5835
5836
        if (null === $lpItem) {
5837
            return '';
5838
        }
5839
        $item_id = $lpItem->getIid();
5840
        $itemType = $lpItem->getItemType();
5841
        $lpId = $lpItem->getLp()->getIid();
5842
        $path = $lpItem->getPath();
5843
5844
        Session::write('parent_item_id', 'dir' === $itemType ? $item_id : 0);
5845
5846
        // Prevents wrong parent selection for document, see Bug#1251.
5847
        if ('dir' !== $itemType) {
5848
            Session::write('parent_item_id', $lpItem->getParentItemId());
5849
        }
5850
5851
        if ($show_actions) {
5852
            $return .= $this->displayItemMenu($lpItem);
5853
        }
5854
        $return .= '<div style="padding:10px;">';
5855
5856
        if ('' != $msg) {
5857
            $return .= $msg;
5858
        }
5859
5860
        $return .= '<h3>'.$lpItem->getTitle().'</h3>';
5861
5862
        switch ($itemType) {
5863
            case TOOL_THREAD:
5864
                $link = $this->rl_get_resource_link_for_learnpath(
5865
                    $course_id,
5866
                    $lpId,
5867
                    $item_id,
5868
                    0
5869
                );
5870
                $return .= Display::url(
5871
                    get_lang('Go to thread'),
5872
                    $link,
5873
                    ['class' => 'btn btn-primary']
5874
                );
5875
                break;
5876
            case TOOL_FORUM:
5877
                $return .= Display::url(
5878
                    get_lang('Go to the forum'),
5879
                    api_get_path(WEB_CODE_PATH).'forum/viewforum.php?'.api_get_cidreq().'&forum='.$path,
5880
                    ['class' => 'btn btn-primary']
5881
                );
5882
                break;
5883
            case TOOL_QUIZ:
5884
                if (!empty($path)) {
5885
                    $exercise = new Exercise();
5886
                    $exercise->read($path);
5887
                    $return .= $exercise->description.'<br />';
5888
                    $return .= Display::url(
5889
                        get_lang('Go to exercise'),
5890
                        api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$exercise->id,
5891
                        ['class' => 'btn btn-primary']
5892
                    );
5893
                }
5894
                break;
5895
            case TOOL_LP_FINAL_ITEM:
5896
                $return .= $this->getSavedFinalItem();
5897
                break;
5898
            case TOOL_DOCUMENT:
5899
            case TOOL_READOUT_TEXT:
5900
                $repo = Container::getDocumentRepository();
5901
                /** @var CDocument $document */
5902
                $document = $repo->find($lpItem->getPath());
5903
                $return .= $this->display_document($document, true, true);
5904
                break;
5905
            case TOOL_HOTPOTATOES:
5906
                $return .= $this->display_document($document, false, true);
5907
                break;
5908
        }
5909
        $return .= '</div>';
5910
5911
        return $return;
5912
    }
5913
5914
    /**
5915
     * Shows the needed forms for editing a specific item.
5916
     *
5917
     * @param CLpItem $lpItem
5918
     *
5919
     * @throws Exception
5920
     *
5921
     *
5922
     * @return string
5923
     */
5924
    public function display_edit_item($lpItem, $excludeExtraFields = [])
5925
    {
5926
        $return = '';
5927
        if (empty($lpItem)) {
5928
            return '';
5929
        }
5930
        $itemType = $lpItem->getItemType();
5931
        $path = $lpItem->getPath();
5932
5933
        switch ($itemType) {
5934
            case 'dir':
5935
            case 'asset':
5936
            case 'sco':
5937
                if (isset($_GET['view']) && 'build' === $_GET['view']) {
5938
                    $return .= $this->displayItemMenu($lpItem);
5939
                    $return .= $this->display_item_form($lpItem, 'edit');
5940
                } else {
5941
                    $return .= $this->display_item_form($lpItem, 'edit_item');
5942
                }
5943
                break;
5944
            case TOOL_LP_FINAL_ITEM:
5945
            case TOOL_DOCUMENT:
5946
            case TOOL_READOUT_TEXT:
5947
                $return .= $this->displayItemMenu($lpItem);
5948
                $return .= $this->displayDocumentForm('edit', $lpItem);
5949
                break;
5950
            case TOOL_LINK:
5951
                $link = null;
5952
                if (!empty($path)) {
5953
                    $repo = Container::getLinkRepository();
5954
                    $link = $repo->find($path);
5955
                }
5956
                $return .= $this->displayItemMenu($lpItem);
5957
                $return .= $this->display_link_form('edit', $lpItem, $link);
5958
5959
                break;
5960
            case TOOL_QUIZ:
5961
                if (!empty($path)) {
5962
                    $repo = Container::getQuizRepository();
5963
                    $resource = $repo->find($path);
5964
                }
5965
                $return .= $this->displayItemMenu($lpItem);
5966
                $return .= $this->display_quiz_form('edit', $lpItem, $resource);
5967
                break;
5968
            /*case TOOL_HOTPOTATOES:
5969
                $return .= $this->displayItemMenu($lpItem);
5970
                $return .= $this->display_hotpotatoes_form('edit', $item_id, $row);
5971
                break;*/
5972
            case TOOL_STUDENTPUBLICATION:
5973
                if (!empty($path)) {
5974
                    $repo = Container::getStudentPublicationRepository();
5975
                    $resource = $repo->find($path);
5976
                }
5977
                $return .= $this->displayItemMenu($lpItem);
5978
                $return .= $this->display_student_publication_form('edit', $lpItem, $resource);
5979
                break;
5980
            case TOOL_FORUM:
5981
                if (!empty($path)) {
5982
                    $repo = Container::getForumRepository();
5983
                    $resource = $repo->find($path);
5984
                }
5985
                $return .= $this->displayItemMenu($lpItem);
5986
                $return .= $this->display_forum_form('edit', $lpItem, $resource);
5987
                break;
5988
            case TOOL_THREAD:
5989
                if (!empty($path)) {
5990
                    $repo = Container::getForumPostRepository();
5991
                    $resource = $repo->find($path);
5992
                }
5993
                $return .= $this->displayItemMenu($lpItem);
5994
                $return .= $this->display_thread_form('edit', $lpItem, $resource);
5995
                break;
5996
        }
5997
5998
        return $return;
5999
    }
6000
6001
    /**
6002
     * Function that displays a list with al the resources that
6003
     * could be added to the learning path.
6004
     *
6005
     * @throws Exception
6006
     */
6007
    public function displayResources(): string
6008
    {
6009
        // Get all the docs.
6010
        $documents = $this->get_documents(true);
6011
6012
        // Get all the exercises.
6013
        $exercises = $this->get_exercises();
6014
6015
        // Get all the links.
6016
        $links = $this->get_links();
6017
6018
        // Get all the student publications.
6019
        $works = $this->get_student_publications();
6020
6021
        // Get all the forums.
6022
        $forums = $this->get_forums();
6023
6024
        // Get the final item form (see BT#11048) .
6025
        $finish = $this->getFinalItemForm();
6026
        $size = ICON_SIZE_MEDIUM; //ICON_SIZE_BIG
6027
        $headers = [
6028
            Display::return_icon('folder_document.png', get_lang('Documents'), [], $size),
6029
            Display::return_icon('quiz.png', get_lang('Tests'), [], $size),
6030
            Display::return_icon('links.png', get_lang('Links'), [], $size),
6031
            Display::return_icon('works.png', get_lang('Assignments'), [], $size),
6032
            Display::return_icon('forum.png', get_lang('Forums'), [], $size),
6033
            Display::return_icon('add_learnpath_section.png', get_lang('Add section'), [], $size),
6034
            Display::return_icon('certificate.png', get_lang('Certificate'), [], $size),
6035
        ];
6036
        $content = '';
6037
        /*$content = Display::return_message(
6038
            get_lang('Click on the [Learner view] button to see your learning path'),
6039
            'normal'
6040
        );*/
6041
        $section = $this->displayNewSectionForm();
6042
        $selected = isset($_REQUEST['lp_build_selected']) ? (int) $_REQUEST['lp_build_selected'] : 0;
6043
6044
        return Display::tabs(
6045
            $headers,
6046
            [
6047
                $documents,
6048
                $exercises,
6049
                $links,
6050
                $works,
6051
                $forums,
6052
                $section,
6053
                $finish,
6054
            ],
6055
            'resource_tab',
6056
            [],
6057
            [],
6058
            $selected
6059
        );
6060
    }
6061
6062
    /**
6063
     * Returns the extension of a document.
6064
     *
6065
     * @param string $filename
6066
     *
6067
     * @return string Extension (part after the last dot)
6068
     */
6069
    public function get_extension($filename)
6070
    {
6071
        $explode = explode('.', $filename);
6072
6073
        return $explode[count($explode) - 1];
6074
    }
6075
6076
    /**
6077
     * @return string
6078
     */
6079
    public function getCurrentBuildingModeURL()
6080
    {
6081
        $pathItem = isset($_GET['path_item']) ? (int) $_GET['path_item'] : '';
6082
        $action = isset($_GET['action']) ? Security::remove_XSS($_GET['action']) : '';
6083
        $id = isset($_GET['id']) ? (int) $_GET['id'] : '';
6084
        $view = isset($_GET['view']) ? Security::remove_XSS($_GET['view']) : '';
6085
6086
        $currentUrl = api_get_self().'?'.api_get_cidreq().
6087
            '&action='.$action.'&lp_id='.$this->lp_id.'&path_item='.$pathItem.'&view='.$view.'&id='.$id;
6088
6089
        return $currentUrl;
6090
    }
6091
6092
    /**
6093
     * Displays a document by id.
6094
     *
6095
     * @param CDocument $document
6096
     * @param bool      $show_title
6097
     * @param bool      $iframe
6098
     * @param bool      $edit_link
6099
     *
6100
     * @return string
6101
     */
6102
    public function display_document($document, $show_title = false, $iframe = true, $edit_link = false)
6103
    {
6104
        $return = '';
6105
        if (!$document) {
6106
            return '';
6107
        }
6108
6109
        $repo = Container::getDocumentRepository();
6110
6111
        // TODO: Add a path filter.
6112
        if ($iframe) {
6113
            //$url = api_get_path(WEB_COURSE_PATH).$_course['path'].'/document'.str_replace('%2F', '/', urlencode($row_doc['path'])).'?'.api_get_cidreq();
6114
            $url = $repo->getResourceFileUrl($document);
6115
6116
            $return .= '<iframe
6117
                id="learnpath_preview_frame"
6118
                frameborder="0"
6119
                height="400"
6120
                width="100%"
6121
                scrolling="auto"
6122
                src="'.$url.'"></iframe>';
6123
        } else {
6124
            $return = $repo->getResourceFileContent($document);
6125
        }
6126
6127
        return $return;
6128
    }
6129
6130
    /**
6131
     * Return HTML form to add/edit a link item.
6132
     *
6133
     * @param string  $action (add/edit)
6134
     * @param CLpItem $lpItem
6135
     * @param CLink   $link
6136
     *
6137
     * @throws Exception
6138
     *
6139
     *
6140
     * @return string HTML form
6141
     */
6142
    public function display_link_form($action, $lpItem, $link)
6143
    {
6144
        $item_url = '';
6145
        if ($link) {
6146
            $item_url = stripslashes($link->getUrl());
6147
        }
6148
        $form = new FormValidator(
6149
            'edit_link',
6150
            'POST',
6151
            $this->getCurrentBuildingModeURL()
6152
        );
6153
6154
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
6155
6156
        $urlAttributes = ['class' => 'learnpath_item_form'];
6157
        $urlAttributes['disabled'] = 'disabled';
6158
        $form->addElement('url', 'url', get_lang('URL'), $urlAttributes);
6159
        $form->setDefault('url', $item_url);
6160
6161
        $form->addButtonSave(get_lang('Save'), 'submit_button');
6162
6163
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
6164
    }
6165
6166
    /**
6167
     * Return HTML form to add/edit a quiz.
6168
     *
6169
     * @param string  $action   Action (add/edit)
6170
     * @param CLpItem $lpItem   Item ID if already exists
6171
     * @param CQuiz   $exercise Extra information (quiz ID if integer)
6172
     *
6173
     * @throws Exception
6174
     *
6175
     * @return string HTML form
6176
     */
6177
    public function display_quiz_form($action, $lpItem, $exercise)
6178
    {
6179
        $form = new FormValidator(
6180
            'quiz_form',
6181
            'POST',
6182
            $this->getCurrentBuildingModeURL()
6183
        );
6184
6185
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
6186
6187
        $form->addButtonSave(get_lang('Save'), 'submit_button');
6188
6189
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
6190
    }
6191
6192
    /**
6193
     * Return the form to display the forum edit/add option.
6194
     *
6195
     * @param CLpItem $lpItem
6196
     *
6197
     * @throws Exception
6198
     *
6199
     * @return string HTML form
6200
     */
6201
    public function display_forum_form($action, $lpItem, $resource)
6202
    {
6203
        $form = new FormValidator(
6204
            'forum_form',
6205
            'POST',
6206
            $this->getCurrentBuildingModeURL()
6207
        );
6208
        LearnPathItemForm::setForm($form, $action, $this, $lpItem);
6209
6210
        if ('add' === $action) {
6211
            $form->addButtonSave(get_lang('Add forum to course'), 'submit_button');
6212
        } else {
6213
            $form->addButtonSave(get_lang('Edit the current forum'), 'submit_button');
6214
        }
6215
6216
        return '<div class="sectioncomment">'.$form->returnForm().'</div>';
6217
    }
6218
6219
    /**
6220
     * Return HTML form to add/edit forum threads.
6221
     *
6222
     * @param string  $action
6223
     * @param CLpItem $lpItem
6224
     * @param string  $resource
6225
     *
6226
     * @throws Exception
6227
     *
6228
     * @return string HTML form
6229
     */
6230
    public function display_thread_form($action, $lpItem, $resource)
6231
    {
6232
        $form = new FormValidator(
6233
            'thread_form',
6234
            'POST',
6235
            $this->getCurrentBuildingModeURL()
6236
        );
6237
6238
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
6239
6240
        $form->addButtonSave(get_lang('Save'), 'submit_button');
6241
6242
        return $form->returnForm();
6243
    }
6244
6245
    /**
6246
     * Return the HTML form to display an item (generally a dir item).
6247
     *
6248
     * @param CLpItem $lpItem
6249
     * @param string  $action
6250
     *
6251
     * @throws Exception
6252
     *
6253
     *
6254
     * @return string HTML form
6255
     */
6256
    public function display_item_form(
6257
        $lpItem,
6258
        $action = 'add_item'
6259
    ) {
6260
        $item_type = $lpItem->getItemType();
6261
6262
        $url = api_get_self().'?'.api_get_cidreq().'&action='.$action.'&type='.$item_type.'&lp_id='.$this->lp_id;
6263
6264
        $form = new FormValidator('form_'.$item_type, 'POST', $url);
6265
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
6266
6267
        $form->addButtonSave(get_lang('Save section'), 'submit_button');
6268
6269
        return $form->returnForm();
6270
    }
6271
6272
    /**
6273
     * Return HTML form to add/edit a student publication (work).
6274
     *
6275
     * @param string              $action
6276
     * @param CStudentPublication $resource
6277
     *
6278
     * @throws Exception
6279
     *
6280
     * @return string HTML form
6281
     */
6282
    public function display_student_publication_form($action, CLpItem $lpItem, $resource)
6283
    {
6284
        $form = new FormValidator('frm_student_publication', 'post', '#');
6285
        LearnPathItemForm::setForm($form, 'edit', $this, $lpItem);
6286
6287
        $form->addButtonSave(get_lang('Save'), 'submit_button');
6288
6289
        $return = '<div class="sectioncomment">';
6290
        $return .= $form->returnForm();
6291
        $return .= '</div>';
6292
6293
        return $return;
6294
    }
6295
6296
    public function displayNewSectionForm()
6297
    {
6298
        $action = 'add_item';
6299
        $item_type = 'dir';
6300
6301
        $lpItem = new CLpItem();
6302
        $lpItem->setTitle('dir');
6303
        $lpItem->setItemType('dir');
6304
6305
        $url = api_get_self().'?'.api_get_cidreq().'&action='.$action.'&type='.$item_type.'&lp_id='.$this->lp_id;
6306
6307
        $form = new FormValidator('form_'.$item_type, 'POST', $url);
6308
        LearnPathItemForm::setForm($form, 'add', $this, $lpItem);
6309
6310
        $form->addButtonSave(get_lang('Save section'), 'submit_button');
6311
        $form->addElement('hidden', 'type', 'dir');
6312
6313
        return $form->returnForm();
6314
    }
6315
6316
    /**
6317
     * Returns the form to update or create a document.
6318
     *
6319
     * @param string  $action (add/edit)
6320
     * @param CLpItem $lpItem
6321
     *
6322
     *
6323
     * @throws Exception
6324
     *
6325
     * @return string HTML form
6326
     */
6327
    public function displayDocumentForm($action = 'add', $lpItem = null)
6328
    {
6329
        $courseInfo = api_get_course_info();
6330
6331
        $form = new FormValidator(
6332
            'form',
6333
            'POST',
6334
            $this->getCurrentBuildingModeURL(),
6335
            '',
6336
            ['enctype' => 'multipart/form-data']
6337
        );
6338
6339
        $data = $this->generate_lp_folder($courseInfo);
6340
6341
        if (null !== $lpItem) {
6342
            LearnPathItemForm::setForm($form, $action, $this, $lpItem);
6343
        }
6344
6345
        switch ($action) {
6346
            case 'add':
6347
                $folders = DocumentManager::get_all_document_folders(
6348
                    $courseInfo,
6349
                    0,
6350
                    true
6351
                );
6352
                DocumentManager::build_directory_selector(
6353
                    $folders,
6354
                    '',
6355
                    [],
6356
                    true,
6357
                    $form,
6358
                    'directory_parent_id'
6359
                );
6360
6361
                if ($data) {
6362
                    $defaults['directory_parent_id'] = $data->getIid();
6363
                }
6364
6365
                break;
6366
        }
6367
6368
        $form->addButtonSave(get_lang('Save'), 'submit_button');
6369
6370
        return $form->returnForm();
6371
    }
6372
6373
    /**
6374
     * @param array  $courseInfo
6375
     * @param string $content
6376
     * @param string $title
6377
     * @param int    $parentId
6378
     *
6379
     * @return int
6380
     */
6381
    public function createReadOutText($courseInfo, $content = '', $title = '', $parentId = 0)
6382
    {
6383
        $creatorId = api_get_user_id();
6384
        $sessionId = api_get_session_id();
6385
6386
        // Generates folder
6387
        $result = $this->generate_lp_folder($courseInfo);
6388
        $dir = $result['dir'];
6389
6390
        if (empty($parentId) || '/' == $parentId) {
6391
            $postDir = isset($_POST['dir']) ? $_POST['dir'] : $dir;
6392
            $dir = isset($_GET['dir']) ? $_GET['dir'] : $postDir; // Please, do not modify this dirname formatting.
6393
6394
            if ('/' === $parentId) {
6395
                $dir = '/';
6396
            }
6397
6398
            // Please, do not modify this dirname formatting.
6399
            if (strstr($dir, '..')) {
6400
                $dir = '/';
6401
            }
6402
6403
            if (!empty($dir[0]) && '.' == $dir[0]) {
6404
                $dir = substr($dir, 1);
6405
            }
6406
            if (!empty($dir[0]) && '/' != $dir[0]) {
6407
                $dir = '/'.$dir;
6408
            }
6409
            if (isset($dir[strlen($dir) - 1]) && '/' != $dir[strlen($dir) - 1]) {
6410
                $dir .= '/';
6411
            }
6412
        } else {
6413
            $parentInfo = DocumentManager::get_document_data_by_id(
6414
                $parentId,
6415
                $courseInfo['code']
6416
            );
6417
            if (!empty($parentInfo)) {
6418
                $dir = $parentInfo['path'].'/';
6419
            }
6420
        }
6421
6422
        $filepath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/'.$dir;
6423
6424
        if (!is_dir($filepath)) {
6425
            $dir = '/';
6426
            $filepath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/'.$dir;
6427
        }
6428
6429
        $originalTitle = !empty($title) ? $title : $_POST['title'];
6430
6431
        if (!empty($title)) {
6432
            $title = api_replace_dangerous_char(stripslashes($title));
6433
        } else {
6434
            $title = api_replace_dangerous_char(stripslashes($_POST['title']));
6435
        }
6436
6437
        $title = disable_dangerous_file($title);
6438
        $filename = $title;
6439
        $content = !empty($content) ? $content : $_POST['content_lp'];
6440
        $tmpFileName = $filename;
6441
6442
        $i = 0;
6443
        while (file_exists($filepath.$tmpFileName.'.html')) {
6444
            $tmpFileName = $filename.'_'.++$i;
6445
        }
6446
6447
        $filename = $tmpFileName.'.html';
6448
        $content = stripslashes($content);
6449
6450
        if (file_exists($filepath.$filename)) {
6451
            return 0;
6452
        }
6453
6454
        $putContent = file_put_contents($filepath.$filename, $content);
6455
6456
        if (false === $putContent) {
6457
            return 0;
6458
        }
6459
6460
        $fileSize = filesize($filepath.$filename);
6461
        $saveFilePath = $dir.$filename;
6462
6463
        $document = DocumentManager::addDocument(
6464
            $courseInfo,
6465
            $saveFilePath,
6466
            'file',
6467
            $fileSize,
6468
            $tmpFileName,
6469
            '',
6470
            0, //readonly
6471
            true,
6472
            null,
6473
            $sessionId,
6474
            $creatorId
6475
        );
6476
6477
        $documentId = $document->getId();
6478
6479
        if (!$document) {
6480
            return 0;
6481
        }
6482
6483
        $newComment = isset($_POST['comment']) ? trim($_POST['comment']) : '';
6484
        $newTitle = $originalTitle;
6485
6486
        if ($newComment || $newTitle) {
6487
            $em = Database::getManager();
6488
6489
            if ($newComment) {
6490
                $document->setComment($newComment);
6491
            }
6492
6493
            if ($newTitle) {
6494
                $document->setTitle($newTitle);
6495
            }
6496
6497
            $em->persist($document);
6498
            $em->flush();
6499
        }
6500
6501
        return $documentId;
6502
    }
6503
6504
    /**
6505
     * Displays the menu for manipulating a step.
6506
     *
6507
     * @return string
6508
     */
6509
    public function displayItemMenu(CLpItem $lpItem)
6510
    {
6511
        $item_id = $lpItem->getIid();
6512
        $audio = $lpItem->getAudio();
6513
        $itemType = $lpItem->getItemType();
6514
        $path = $lpItem->getPath();
6515
6516
        $return = '';
6517
        $audio_player = null;
6518
        // We display an audio player if needed.
6519
        if (!empty($audio)) {
6520
            /*$webAudioPath = '../..'.api_get_path(REL_COURSE_PATH).$_course['path'].'/document/audio/'.$row['audio'];
6521
            $audio_player .= '<div class="lp_mediaplayer" id="container">'
6522
                .'<audio src="'.$webAudioPath.'" controls>'
6523
                .'</div><br>';*/
6524
        }
6525
6526
        $url = api_get_self().'?'.api_get_cidreq().'&view=build&id='.$item_id.'&lp_id='.$this->lp_id;
6527
6528
        if (TOOL_LP_FINAL_ITEM !== $itemType) {
6529
            $return .= Display::url(
6530
                Display::return_icon(
6531
                    'edit.png',
6532
                    get_lang('Edit'),
6533
                    [],
6534
                    ICON_SIZE_SMALL
6535
                ),
6536
                $url.'&action=edit_item&path_item='.$path
6537
            );
6538
6539
            /*$return .= Display::url(
6540
                Display::return_icon(
6541
                    'move.png',
6542
                    get_lang('Move'),
6543
                    [],
6544
                    ICON_SIZE_SMALL
6545
                ),
6546
                $url.'&action=move_item'
6547
            );*/
6548
        }
6549
6550
        // Commented for now as prerequisites cannot be added to chapters.
6551
        if ('dir' !== $itemType) {
6552
            $return .= Display::url(
6553
                Display::return_icon(
6554
                    'accept.png',
6555
                    get_lang('Prerequisites'),
6556
                    [],
6557
                    ICON_SIZE_SMALL
6558
                ),
6559
                $url.'&action=edit_item_prereq'
6560
            );
6561
        }
6562
        $return .= Display::url(
6563
            Display::return_icon(
6564
                'delete.png',
6565
                get_lang('Delete'),
6566
                [],
6567
                ICON_SIZE_SMALL
6568
            ),
6569
            $url.'&action=delete_item'
6570
        );
6571
6572
        /*if (in_array($itemType, [TOOL_DOCUMENT, TOOL_LP_FINAL_ITEM, TOOL_HOTPOTATOES])) {
6573
            $documentData = DocumentManager::get_document_data_by_id($path, $course_code);
6574
            if (empty($documentData)) {
6575
                // Try with iid
6576
                $table = Database::get_course_table(TABLE_DOCUMENT);
6577
                $sql = "SELECT path FROM $table
6578
                        WHERE
6579
                              c_id = ".api_get_course_int_id()." AND
6580
                              iid = ".$path." AND
6581
                              path NOT LIKE '%_DELETED_%'";
6582
                $result = Database::query($sql);
6583
                $documentData = Database::fetch_array($result);
6584
                if ($documentData) {
6585
                    $documentData['absolute_path_from_document'] = '/document'.$documentData['path'];
6586
                }
6587
            }
6588
            if (isset($documentData['absolute_path_from_document'])) {
6589
                $return .= get_lang('File').': '.$documentData['absolute_path_from_document'];
6590
            }
6591
        }*/
6592
6593
        if (!empty($audio_player)) {
6594
            $return .= $audio_player;
6595
        }
6596
6597
        return Display::toolbarAction('lp_item', [$return]);
6598
    }
6599
6600
    /**
6601
     * Creates the javascript needed for filling up the checkboxes without page reload.
6602
     *
6603
     * @return string
6604
     */
6605
    public function get_js_dropdown_array()
6606
    {
6607
        $course_id = api_get_course_int_id();
6608
        $return = 'var child_name = new Array();'."\n";
6609
        $return .= 'var child_value = new Array();'."\n\n";
6610
        $return .= 'child_name[0] = new Array();'."\n";
6611
        $return .= 'child_value[0] = new Array();'."\n\n";
6612
6613
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
6614
        $sql = "SELECT * FROM ".$tbl_lp_item."
6615
                WHERE
6616
                    lp_id = ".$this->lp_id." AND
6617
                    parent_item_id = 0
6618
                ORDER BY display_order ASC";
6619
        Database::query($sql);
6620
        $i = 0;
6621
6622
        $list = $this->getItemsForForm(true);
6623
6624
        foreach ($list as $row_zero) {
6625
            if (TOOL_LP_FINAL_ITEM !== $row_zero['item_type']) {
6626
                if (TOOL_QUIZ == $row_zero['item_type']) {
6627
                    $row_zero['title'] = Exercise::get_formated_title_variable($row_zero['title']);
6628
                }
6629
                $js_var = json_encode(get_lang('After').' '.$row_zero['title']);
6630
                $return .= 'child_name[0]['.$i.'] = '.$js_var.' ;'."\n";
6631
                $return .= 'child_value[0]['.$i++.'] = "'.$row_zero['iid'].'";'."\n";
6632
            }
6633
        }
6634
6635
        $return .= "\n";
6636
        $sql = "SELECT * FROM $tbl_lp_item
6637
                WHERE lp_id = ".$this->lp_id;
6638
        $res = Database::query($sql);
6639
        while ($row = Database::fetch_array($res)) {
6640
            $sql_parent = "SELECT * FROM ".$tbl_lp_item."
6641
                           WHERE
6642
                                parent_item_id = ".$row['iid']."
6643
                           ORDER BY display_order ASC";
6644
            $res_parent = Database::query($sql_parent);
6645
            $i = 0;
6646
            $return .= 'child_name['.$row['iid'].'] = new Array();'."\n";
6647
            $return .= 'child_value['.$row['iid'].'] = new Array();'."\n\n";
6648
6649
            while ($row_parent = Database::fetch_array($res_parent)) {
6650
                $js_var = json_encode(get_lang('After').' '.$this->cleanItemTitle($row_parent['title']));
6651
                $return .= 'child_name['.$row['iid'].']['.$i.'] =   '.$js_var.' ;'."\n";
6652
                $return .= 'child_value['.$row['iid'].']['.$i++.'] = "'.$row_parent['iid'].'";'."\n";
6653
            }
6654
            $return .= "\n";
6655
        }
6656
6657
        $return .= "
6658
            function load_cbo(id) {
6659
                if (!id) {
6660
                    return false;
6661
                }
6662
6663
                var cbo = document.getElementById('previous');
6664
                for(var i = cbo.length - 1; i > 0; i--) {
6665
                    cbo.options[i] = null;
6666
                }
6667
6668
                var k=0;
6669
                for(var i = 1; i <= child_name[id].length; i++){
6670
                    var option = new Option(child_name[id][i - 1], child_value[id][i - 1]);
6671
                    option.style.paddingLeft = '40px';
6672
                    cbo.options[i] = option;
6673
                    k = i;
6674
                }
6675
6676
                cbo.options[k].selected = true;
6677
                //$('#previous').selectpicker('refresh');
6678
            }";
6679
6680
        return $return;
6681
    }
6682
6683
    /**
6684
     * Display the form to allow moving an item.
6685
     *
6686
     * @param CLpItem $lpItem
6687
     *
6688
     * @throws Exception
6689
     *
6690
     *
6691
     * @return string HTML form
6692
     */
6693
    public function display_move_item($lpItem)
6694
    {
6695
        $return = '';
6696
        $path = $lpItem->getPath();
6697
6698
        if ($lpItem) {
6699
            $itemType = $lpItem->getItemType();
6700
            switch ($itemType) {
6701
                case 'dir':
6702
                case 'asset':
6703
                    $return .= $this->displayItemMenu($lpItem);
6704
                    $return .= $this->display_item_form(
6705
                        $lpItem,
6706
                        get_lang('Move the current section'),
6707
                        'move',
6708
                        $row
6709
                    );
6710
                    break;
6711
                case TOOL_DOCUMENT:
6712
                    $return .= $this->displayItemMenu($lpItem);
6713
                    $return .= $this->displayDocumentForm('move', $lpItem);
6714
                    break;
6715
                case TOOL_LINK:
6716
                    $link = null;
6717
                    if (!empty($path)) {
6718
                        $repo = Container::getLinkRepository();
6719
                        $link = $repo->find($path);
6720
                    }
6721
                    $return .= $this->displayItemMenu($lpItem);
6722
                    $return .= $this->display_link_form('move', $lpItem, $link);
6723
                    break;
6724
                case TOOL_HOTPOTATOES:
6725
                    $return .= $this->displayItemMenu($lpItem);
6726
                    $return .= $this->display_link_form('move', $lpItem, $row);
6727
                    break;
6728
                case TOOL_QUIZ:
6729
                    $return .= $this->displayItemMenu($lpItem);
6730
                    $return .= $this->display_quiz_form('move', $lpItem, $row);
6731
                    break;
6732
                case TOOL_STUDENTPUBLICATION:
6733
                    $return .= $this->displayItemMenu($lpItem);
6734
                    $return .= $this->display_student_publication_form('move', $lpItem, $row);
6735
                    break;
6736
                case TOOL_FORUM:
6737
                    $return .= $this->displayItemMenu($lpItem);
6738
                    $return .= $this->display_forum_form('move', $lpItem, $row);
6739
                    break;
6740
                case TOOL_THREAD:
6741
                    $return .= $this->displayItemMenu($lpItem);
6742
                    $return .= $this->display_forum_form('move', $lpItem, $row);
6743
                    break;
6744
            }
6745
        }
6746
6747
        return $return;
6748
    }
6749
6750
    /**
6751
     * Return HTML form to allow prerequisites selection.
6752
     *
6753
     * @todo use FormValidator
6754
     *
6755
     * @return string HTML form
6756
     */
6757
    public function display_item_prerequisites_form(CLpItem $lpItem)
6758
    {
6759
        $course_id = api_get_course_int_id();
6760
        $preRequisiteId = $lpItem->getPrerequisite();
6761
        $itemId = $lpItem->getIid();
6762
6763
        $return = Display::page_header(get_lang('Add/edit prerequisites').' '.$lpItem->getTitle());
6764
6765
        $return .= '<form method="POST">';
6766
        $return .= '<div class="table-responsive">';
6767
        $return .= '<table class="table table-hover">';
6768
        $return .= '<thead>';
6769
        $return .= '<tr>';
6770
        $return .= '<th>'.get_lang('Prerequisites').'</th>';
6771
        $return .= '<th width="140">'.get_lang('minimum').'</th>';
6772
        $return .= '<th width="140">'.get_lang('maximum').'</th>';
6773
        $return .= '</tr>';
6774
        $return .= '</thead>';
6775
6776
        // Adding the none option to the prerequisites see http://www.chamilo.org/es/node/146
6777
        $return .= '<tbody>';
6778
        $return .= '<tr>';
6779
        $return .= '<td colspan="3">';
6780
        $return .= '<div class="radio learnpath"><label for="idnone">';
6781
        $return .= '<input checked="checked" id="idnone" name="prerequisites" type="radio" />';
6782
        $return .= get_lang('none').'</label>';
6783
        $return .= '</div>';
6784
        $return .= '</tr>';
6785
6786
        // @todo use entitites
6787
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
6788
        $sql = "SELECT * FROM $tbl_lp_item
6789
                WHERE lp_id = ".$this->lp_id;
6790
        $result = Database::query($sql);
6791
6792
        $selectedMinScore = [];
6793
        $selectedMaxScore = [];
6794
        $masteryScore = [];
6795
        while ($row = Database::fetch_array($result)) {
6796
            if ($row['iid'] == $itemId) {
6797
                $selectedMinScore[$row['prerequisite']] = $row['prerequisite_min_score'];
6798
                $selectedMaxScore[$row['prerequisite']] = $row['prerequisite_max_score'];
6799
            }
6800
            $masteryScore[$row['iid']] = $row['mastery_score'];
6801
        }
6802
6803
        $displayOrder = $lpItem->getDisplayOrder();
6804
        $lpItemRepo = Container::getLpItemRepository();
6805
        $itemRoot = $lpItemRepo->getItemRoot($this->get_id());
6806
        $em = Database::getManager();
6807
6808
        $currentItemId = $itemId;
6809
        $options = [
6810
            'decorate' => true,
6811
            'rootOpen' => function() {
6812
                return '';
6813
            },
6814
            'rootClose' => function() {
6815
                return '';
6816
            },
6817
            'childOpen' => function() {
6818
                return '';
6819
            },
6820
            'childClose' => '',
6821
            'nodeDecorator' => function ($item) use (
6822
                $currentItemId,
6823
                $preRequisiteId,
6824
                $course_id,
6825
                $selectedMaxScore,
6826
                $selectedMinScore,
6827
                $displayOrder,
6828
                $lpItemRepo,
6829
                $em
6830
            ) {
6831
                $mainUrl = '';
6832
                $fullTitle = $item['title'];
6833
                $title = cut($fullTitle, self::MAX_LP_ITEM_TITLE_LENGTH);
6834
                $itemId = $item['iid'];
6835
                $type = $item['itemType'];
6836
                $lpId = $this->get_id();
6837
                $iconName = str_replace(' ', '', $type);
6838
                $icon = Display::return_icon(
6839
                    'lp_'.$iconName.'.png',
6840
                    '',
6841
                    [],
6842
                    ICON_SIZE_TINY
6843
                );
6844
                $url = $mainUrl.'&view=build&id='.$itemId.'&lp_id='.$lpId;
6845
6846
                if ($itemId == $currentItemId) {
6847
                    return '';
6848
                }
6849
6850
                if ($displayOrder < $item['displayOrder']) {
6851
                    return '';
6852
                }
6853
6854
                $selectedMaxScoreValue = isset($selectedMaxScore[$itemId]) ? $selectedMaxScore[$itemId] : $item['maxScore'];
6855
                $selectedMinScoreValue = $selectedMinScore[$itemId] ?? 0;
6856
                $masteryScoreAsMinValue = $masteryScore[$itemId] ?? 0;
6857
6858
                $return = '<tr>';
6859
                $return .= '<td '.((TOOL_QUIZ != $type && TOOL_HOTPOTATOES != $type) ? ' colspan="3"' : '').'>';
6860
                $return .= '<div style="margin-left:'.($item['lvl'] * 20).'px;" class="radio learnpath">';
6861
                $return .= '<label for="id'.$itemId.'">';
6862
6863
                $checked = '';
6864
                if (null !== $preRequisiteId) {
6865
                    $checked = in_array($preRequisiteId, [$itemId, $item['ref']]) ? ' checked="checked" ' : '';
6866
                }
6867
6868
                $disabled = 'dir' === $type ? ' disabled="disabled" ' : '';
6869
6870
                $return .= '<input
6871
                    '.$checked.' '.$disabled.'
6872
                    id="id'.$itemId.'"
6873
                    name="prerequisites"
6874
                    type="radio"
6875
                    value="'.$itemId.'" />';
6876
6877
                $return .= $icon.'&nbsp;&nbsp;'.$item['title'].'</label>';
6878
                $return .= '</div>';
6879
                $return .= '</td>';
6880
6881
                if (TOOL_QUIZ == $type) {
6882
                    // lets update max_score Tests information depending of the Tests Advanced properties
6883
                    $exercise = new Exercise($course_id);
6884
                    /** @var CLpItem $itemEntity */
6885
                    $itemEntity = $lpItemRepo->find($itemId);
6886
                    $exercise->read($item['path']);
6887
                    $itemEntity->setMaxScore($exercise->get_max_score());
6888
                    $em->persist($itemEntity);
6889
                    $em->flush($itemEntity);
6890
6891
                    $item['maxScore'] = $exercise->get_max_score();
6892
6893
                    if (empty($selectedMinScoreValue) && !empty($masteryScoreAsMinValue)) {
6894
                        // Backwards compatibility with 1.9.x use mastery_score as min value
6895
                        $selectedMinScoreValue = $masteryScoreAsMinValue;
6896
                    }
6897
                    $return .= '<td>';
6898
                    $return .= '<input
6899
                        class="form-control"
6900
                        size="4" maxlength="3"
6901
                        name="min_'.$itemId.'"
6902
                        type="number"
6903
                        min="0"
6904
                        step="any"
6905
                        max="'.$item['maxScore'].'"
6906
                        value="'.$selectedMinScoreValue.'"
6907
                    />';
6908
                    $return .= '</td>';
6909
                    $return .= '<td>';
6910
                    $return .= '<input
6911
                        class="form-control"
6912
                        size="4"
6913
                        maxlength="3"
6914
                        name="max_'.$itemId.'"
6915
                        type="number"
6916
                        min="0"
6917
                        step="any"
6918
                        max="'.$item['maxScore'].'"
6919
                        value="'.$selectedMaxScoreValue.'"
6920
                    />';
6921
                        $return .= '</td>';
6922
                    }
6923
6924
                if (TOOL_HOTPOTATOES == $type) {
6925
                    $return .= '<td>';
6926
                    $return .= '<input
6927
                        size="4"
6928
                        maxlength="3"
6929
                        name="min_'.$itemId.'"
6930
                        type="number"
6931
                        min="0"
6932
                        step="any"
6933
                        max="'.$item['maxScore'].'"
6934
                        value="'.$selectedMinScoreValue.'"
6935
                    />';
6936
                        $return .= '</td>';
6937
                        $return .= '<td>';
6938
                        $return .= '<input
6939
                        size="4"
6940
                        maxlength="3"
6941
                        name="max_'.$itemId.'"
6942
                        type="number"
6943
                        min="0"
6944
                        step="any"
6945
                        max="'.$item['maxScore'].'"
6946
                        value="'.$selectedMaxScoreValue.'"
6947
                    />';
6948
                    $return .= '</td>';
6949
                }
6950
                $return .= '</tr>';
6951
6952
                return $return;
6953
            },
6954
        ];
6955
6956
        $tree = $lpItemRepo->childrenHierarchy($itemRoot, false, $options);
6957
        $return .= $tree;
6958
        $return .= '</tbody>';
6959
        $return .= '</table>';
6960
        $return .= '</div>';
6961
        $return .= '<div class="form-group">';
6962
        $return .= '<button class="btn btn-primary" name="submit_button" type="submit">'.
6963
            get_lang('Save prerequisites settings').'</button>';
6964
        $return .= '</form>';
6965
6966
        return $return;
6967
    }
6968
6969
    /**
6970
     * Return HTML list to allow prerequisites selection for lp.
6971
     */
6972
    public function display_lp_prerequisites_list(FormValidator $form)
6973
    {
6974
        $lp_id = $this->lp_id;
6975
        $prerequisiteId = $this->entity->getPrerequisite();
6976
6977
        $repo = Container::getLpRepository();
6978
        $qb = $repo->findAllByCourse(api_get_course_entity(), api_get_session_entity());
6979
        /** @var CLp[] $lps */
6980
        $lps = $qb->getQuery()->getResult();
6981
6982
        //$session_id = api_get_session_id();
6983
        /*$session_condition = api_get_session_condition($session_id, true, true);
6984
        $sql = "SELECT * FROM $tbl_lp
6985
                WHERE c_id = $course_id $session_condition
6986
                ORDER BY display_order ";
6987
        $rs = Database::query($sql);*/
6988
6989
        $items = [get_lang('none')];
6990
        foreach ($lps as $lp) {
6991
            $myLpId = $lp->getIid();
6992
            if ($myLpId == $lp_id) {
6993
                continue;
6994
            }
6995
            $items[$myLpId] = $lp->getName();
6996
            /*$return .= '<option
6997
                value="'.$myLpId.'" '.(($myLpId == $prerequisiteId) ? ' selected ' : '').'>'.
6998
                $lp->getName().
6999
                '</option>';*/
7000
        }
7001
7002
        $select = $form->addSelect('prerequisites', get_lang('Prerequisites'), $items);
7003
        $select->setSelected($prerequisiteId);
7004
    }
7005
7006
    /**
7007
     * Creates a list with all the documents in it.
7008
     *
7009
     * @param bool $showInvisibleFiles
7010
     *
7011
     * @throws Exception
7012
     *
7013
     *
7014
     * @return string
7015
     */
7016
    public function get_documents($showInvisibleFiles = false)
7017
    {
7018
        $course_info = api_get_course_info();
7019
        $sessionId = api_get_session_id();
7020
        $documentTree = DocumentManager::get_document_preview(
7021
            $course_info,
7022
            $this->lp_id,
7023
            null,
7024
            $sessionId,
7025
            true,
7026
            null,
7027
            null,
7028
            $showInvisibleFiles,
7029
            true
7030
        );
7031
7032
        $form = new FormValidator(
7033
            'form_upload',
7034
            'POST',
7035
            $this->getCurrentBuildingModeURL(),
7036
            '',
7037
            ['enctype' => 'multipart/form-data']
7038
        );
7039
7040
        $folders = DocumentManager::get_all_document_folders(
7041
            api_get_course_info(),
7042
            0,
7043
            true
7044
        );
7045
7046
        $folder = $this->generate_lp_folder(api_get_course_info());
7047
7048
        DocumentManager::build_directory_selector(
7049
            $folders,
7050
            $folder->getIid(),
7051
            [],
7052
            true,
7053
            $form,
7054
            'directory_parent_id'
7055
        );
7056
7057
        $group = [
7058
            $form->createElement(
7059
                'radio',
7060
                'if_exists',
7061
                get_lang('If file exists:'),
7062
                get_lang('Do nothing'),
7063
                'nothing'
7064
            ),
7065
            $form->createElement(
7066
                'radio',
7067
                'if_exists',
7068
                null,
7069
                get_lang('Overwrite the existing file'),
7070
                'overwrite'
7071
            ),
7072
            $form->createElement(
7073
                'radio',
7074
                'if_exists',
7075
                null,
7076
                get_lang('Rename the uploaded file if it exists'),
7077
                'rename'
7078
            ),
7079
        ];
7080
        $form->addGroup($group, null, get_lang('If file exists:'));
7081
7082
        $fileExistsOption = api_get_setting('document_if_file_exists_option');
7083
        $defaultFileExistsOption = 'rename';
7084
        if (!empty($fileExistsOption)) {
7085
            $defaultFileExistsOption = $fileExistsOption;
7086
        }
7087
        $form->setDefaults(['if_exists' => $defaultFileExistsOption]);
7088
7089
        // Check box options
7090
        $form->addElement(
7091
            'checkbox',
7092
            'unzip',
7093
            get_lang('Options'),
7094
            get_lang('Uncompress zip')
7095
        );
7096
7097
        $url = api_get_path(WEB_AJAX_PATH).'document.ajax.php?'.api_get_cidreq().'&a=upload_file&curdirpath=';
7098
        $form->addMultipleUpload($url);
7099
7100
        $lpItem = new CLpItem();
7101
        $lpItem->setTitle('');
7102
        $lpItem->setItemType(TOOL_DOCUMENT);
7103
        $new = $this->displayDocumentForm('add', $lpItem);
7104
7105
        /*$lpItem = new CLpItem();
7106
        $lpItem->setItemType(TOOL_READOUT_TEXT);
7107
        $frmReadOutText = $this->displayDocumentForm('add');*/
7108
7109
        $headers = [
7110
            get_lang('Files'),
7111
            get_lang('Create a new document'),
7112
            //get_lang('Create read-out text'),
7113
            get_lang('Upload'),
7114
        ];
7115
7116
        return Display::tabs(
7117
            $headers,
7118
            [$documentTree, $new, $form->returnForm()],
7119
            'subtab'
7120
        );
7121
    }
7122
7123
    /**
7124
     * Creates a list with all the exercises (quiz) in it.
7125
     *
7126
     * @return string
7127
     */
7128
    public function get_exercises()
7129
    {
7130
        $course_id = api_get_course_int_id();
7131
        $session_id = api_get_session_id();
7132
        $setting = 'true' === api_get_setting('lp.show_invisible_exercise_in_lp_toc');
7133
7134
        //$activeCondition = ' active <> -1 ';
7135
        $active = 2;
7136
        if ($setting) {
7137
            $active = 1;
7138
            //$activeCondition = ' active = 1 ';
7139
        }
7140
7141
        $categoryCondition = '';
7142
7143
        $keyword = $_REQUEST['keyword'] ?? null;
7144
        $categoryId = $_REQUEST['category_id'] ?? null;
7145
        /*if (api_get_configuration_value('allow_exercise_categories') && !empty($categoryId)) {
7146
            $categoryCondition = " AND exercise_category_id = $categoryId ";
7147
        }
7148
7149
        $keywordCondition = '';
7150
7151
        if (!empty($keyword)) {
7152
            $keyword = Database::escape_string($keyword);
7153
            $keywordCondition = " AND title LIKE '%$keyword%' ";
7154
        }
7155
        */
7156
        $course = api_get_course_entity($course_id);
7157
        $session = api_get_session_entity($session_id);
7158
7159
        $qb = Container::getQuizRepository()->findAllByCourse($course, $session, $keyword, $active, false, $categoryId);
7160
        /** @var CQuiz[] $exercises */
7161
        $exercises = $qb->getQuery()->getResult();
7162
7163
        /*$sql_quiz = "SELECT * FROM $tbl_quiz
7164
                     WHERE
7165
                            c_id = $course_id AND
7166
                            $activeCondition
7167
                            $condition_session
7168
                            $categoryCondition
7169
                            $keywordCondition
7170
                     ORDER BY title ASC";
7171
        $res_quiz = Database::query($sql_quiz);*/
7172
7173
        $currentUrl = api_get_self().'?'.api_get_cidreq().'&action=add_item&type=step&lp_id='.$this->lp_id.'#resource_tab-2';
7174
7175
        // Create a search-box
7176
        /*$form = new FormValidator('search_simple', 'get', $currentUrl);
7177
        $form->addHidden('action', 'add_item');
7178
        $form->addHidden('type', 'step');
7179
        $form->addHidden('lp_id', $this->lp_id);
7180
        $form->addHidden('lp_build_selected', '2');
7181
7182
        $form->addCourseHiddenParams();
7183
        $form->addText(
7184
            'keyword',
7185
            get_lang('Search'),
7186
            false,
7187
            [
7188
                'aria-label' => get_lang('Search'),
7189
            ]
7190
        );
7191
7192
        if (api_get_configuration_value('allow_exercise_categories')) {
7193
            $manager = new ExerciseCategoryManager();
7194
            $options = $manager->getCategoriesForSelect(api_get_course_int_id());
7195
            if (!empty($options)) {
7196
                $form->addSelect(
7197
                    'category_id',
7198
                    get_lang('Category'),
7199
                    $options,
7200
                    ['placeholder' => get_lang('Please select an option')]
7201
                );
7202
            }
7203
        }
7204
7205
        $form->addButtonSearch(get_lang('Search'));
7206
        $return = $form->returnForm();*/
7207
7208
        $return = '<ul class = "list-group lp_resource">';
7209
        $return .= '<li class="list-group-item lp_resource_element disable_drag">';
7210
        $return .= Display::return_icon('new_exercice.png');
7211
        $return .= '<a
7212
            href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise_admin.php?'.api_get_cidreq().'&lp_id='.$this->lp_id.'">'.
7213
            get_lang('New test').'</a>';
7214
        $return .= '</li>';
7215
7216
        $previewIcon = Display::return_icon(
7217
            'preview_view.png',
7218
            get_lang('Preview')
7219
        );
7220
        $quizIcon = Display::return_icon('quiz.png', '', [], ICON_SIZE_TINY);
7221
        $moveIcon = Display::return_icon('move_everywhere.png', get_lang('Move'), [], ICON_SIZE_TINY);
7222
        $exerciseUrl = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq();
7223
        foreach ($exercises as $exercise) {
7224
            $exerciseId = $exercise->getIid();
7225
            $title = strip_tags(api_html_entity_decode($exercise->getTitle()));
7226
            $visibility = $exercise->isVisible($course, $session);
7227
7228
            $link = Display::url(
7229
                $previewIcon,
7230
                $exerciseUrl.'&exerciseId='.$exerciseId,
7231
                ['target' => '_blank']
7232
            );
7233
            $return .= '<li
7234
                class="list-group-item lp_resource_element"
7235
                id="'.$exerciseId.'"
7236
                data-id="'.$exerciseId.'"
7237
                title="'.$title.'">';
7238
            $return .= Display::url($moveIcon, '#', ['class' => 'moved']);
7239
            $return .= $quizIcon;
7240
            $sessionStar = '';
7241
            /*$sessionStar = api_get_session_image(
7242
                $row_quiz['session_id'],
7243
                $userInfo['status']
7244
            );*/
7245
            $return .= Display::url(
7246
                Security::remove_XSS(cut($title, 80)).$link.$sessionStar,
7247
                api_get_self().'?'.
7248
                    api_get_cidreq().'&action=add_item&type='.TOOL_QUIZ.'&file='.$exerciseId.'&lp_id='.$this->lp_id,
7249
                [
7250
                    'class' => false === $visibility ? 'moved text-muted ' : 'moved link_with_id',
7251
                    'data_type' => 'quiz',
7252
                    'data-id' => $exerciseId,
7253
                ]
7254
            );
7255
            $return .= '</li>';
7256
        }
7257
7258
        $return .= '</ul>';
7259
7260
        return $return;
7261
    }
7262
7263
    /**
7264
     * Creates a list with all the links in it.
7265
     *
7266
     * @return string
7267
     */
7268
    public function get_links()
7269
    {
7270
        $sessionId = api_get_session_id();
7271
        $repo = Container::getLinkRepository();
7272
7273
        $course = api_get_course_entity();
7274
        $session = api_get_session_entity($sessionId);
7275
        $qb = $repo->getResourcesByCourse($course, $session);
7276
        /** @var CLink[] $links */
7277
        $links = $qb->getQuery()->getResult();
7278
7279
        $selfUrl = api_get_self();
7280
        $courseIdReq = api_get_cidreq();
7281
        $userInfo = api_get_user_info();
7282
7283
        $moveEverywhereIcon = Display::return_icon(
7284
            'move_everywhere.png',
7285
            get_lang('Move'),
7286
            [],
7287
            ICON_SIZE_TINY
7288
        );
7289
7290
        $categorizedLinks = [];
7291
        $categories = [];
7292
7293
        foreach ($links as $link) {
7294
            $categoryId = null !== $link->getCategory() ? $link->getCategory()->getIid() : 0;
7295
            if (empty($categoryId)) {
7296
                $categories[0] = get_lang('Uncategorized');
7297
            } else {
7298
                $category = $link->getCategory();
7299
                $categories[$categoryId] = $category->getCategoryTitle();
7300
            }
7301
            $categorizedLinks[$categoryId][$link->getIid()] = $link;
7302
        }
7303
7304
        $linksHtmlCode =
7305
            '<script>
7306
            function toggle_tool(tool, id) {
7307
                if(document.getElementById(tool+"_"+id+"_content").style.display == "none"){
7308
                    document.getElementById(tool+"_"+id+"_content").style.display = "block";
7309
                    document.getElementById(tool+"_"+id+"_opener").src = "'.Display::returnIconPath('remove.gif').'";
7310
                } else {
7311
                    document.getElementById(tool+"_"+id+"_content").style.display = "none";
7312
                    document.getElementById(tool+"_"+id+"_opener").src = "'.Display::returnIconPath('add.png').'";
7313
                }
7314
            }
7315
        </script>
7316
7317
        <ul class="list-group lp_resource">
7318
            <li class="list-group-item lp_resource_element disable_drag ">
7319
                '.Display::return_icon('linksnew.gif').'
7320
                <a
7321
                href="'.api_get_path(WEB_CODE_PATH).'link/link.php?'.$courseIdReq.'&action=addlink&lp_id='.$this->lp_id.'"
7322
                title="'.get_lang('Add a link').'">'.
7323
                get_lang('Add a link').'
7324
                </a>
7325
            </li>';
7326
        $linkIcon = Display::return_icon('links.png', '', [], ICON_SIZE_TINY);
7327
        foreach ($categorizedLinks as $categoryId => $links) {
7328
            $linkNodes = null;
7329
            /** @var CLink $link */
7330
            foreach ($links as $key => $link) {
7331
                $title = $link->getTitle();
7332
                $id = $link->getIid();
7333
                $linkUrl = Display::url(
7334
                    Display::return_icon('preview_view.png', get_lang('Preview')),
7335
                    api_get_path(WEB_CODE_PATH).'link/link_goto.php?'.api_get_cidreq().'&link_id='.$key,
7336
                    ['target' => '_blank']
7337
                );
7338
7339
                if ($link->isVisible($course, $session)) {
7340
                    //$sessionStar = api_get_session_image($linkSessionId, $userInfo['status']);
7341
                    $sessionStar = '';
7342
                    $url = $selfUrl.'?'.$courseIdReq.'&action=add_item&type='.TOOL_LINK.'&file='.$key.'&lp_id='.$this->lp_id;
7343
                    $link = Display::url(
7344
                        Security::remove_XSS($title).$sessionStar.$linkUrl,
7345
                        $url,
7346
                        [
7347
                            'class' => 'moved link_with_id',
7348
                            'data-id' => $key,
7349
                            'data_type' => TOOL_LINK,
7350
                            'title' => $title,
7351
                        ]
7352
                    );
7353
                    $linkNodes .=
7354
                        "<li
7355
                            class='list-group-item lp_resource_element'
7356
                            id= $id
7357
                            data-id= $id
7358
                            >
7359
                         <a class='moved' href='#'>
7360
                            $moveEverywhereIcon
7361
                        </a>
7362
                        $linkIcon $link
7363
                        </li>";
7364
                }
7365
            }
7366
            $linksHtmlCode .=
7367
                '<li class="list-group-item disable_drag">
7368
                    <a style="cursor:hand" onclick="javascript: toggle_tool(\''.TOOL_LINK.'\','.$categoryId.')" >
7369
                        <img src="'.Display::returnIconPath('add.png').'" id="'.TOOL_LINK.'_'.$categoryId.'_opener"
7370
                        align="absbottom" />
7371
                    </a>
7372
                    <span style="vertical-align:middle">'.Security::remove_XSS($categories[$categoryId]).'</span>
7373
                </li>
7374
            '.
7375
                $linkNodes.
7376
            '';
7377
            //<div style="display:none" id="'.TOOL_LINK.'_'.$categoryId.'_content">'.
7378
        }
7379
        $linksHtmlCode .= '</ul>';
7380
7381
        return $linksHtmlCode;
7382
    }
7383
7384
    /**
7385
     * Creates a list with all the student publications in it.
7386
     *
7387
     * @return string
7388
     */
7389
    public function get_student_publications()
7390
    {
7391
        $return = '<ul class="list-group lp_resource">';
7392
        $return .= '<li class="list-group-item lp_resource_element">';
7393
        $works = getWorkListTeacher(0, 100, null, null, null);
7394
        if (!empty($works)) {
7395
            $icon = Display::return_icon('works.png', '', [], ICON_SIZE_TINY);
7396
            foreach ($works as $work) {
7397
                $workId = $work['iid'];
7398
                $link = Display::url(
7399
                    Display::return_icon('preview_view.png', get_lang('Preview')),
7400
                    api_get_path(WEB_CODE_PATH).'work/work_list_all.php?'.api_get_cidreq().'&id='.$workId,
7401
                    ['target' => '_blank']
7402
                );
7403
7404
                $return .= '<li
7405
                    class="list-group-item lp_resource_element"
7406
                    id="'.$workId.'"
7407
                    data-id="'.$workId.'"
7408
                    >';
7409
                $return .= '<a class="moved" href="#">';
7410
                $return .= Display::return_icon(
7411
                    'move_everywhere.png',
7412
                    get_lang('Move'),
7413
                    [],
7414
                    ICON_SIZE_TINY
7415
                );
7416
                $return .= '</a> ';
7417
7418
                $return .= $icon;
7419
                $return .= Display::url(
7420
                    Security::remove_XSS(cut(strip_tags($work['title']), 80)).' '.$link,
7421
                    api_get_self().'?'.
7422
                    api_get_cidreq().'&action=add_item&type='.TOOL_STUDENTPUBLICATION.'&file='.$work['iid'].'&lp_id='.$this->lp_id,
7423
                    [
7424
                        'class' => 'moved link_with_id',
7425
                        'data-id' => $work['iid'],
7426
                        'data_type' => TOOL_STUDENTPUBLICATION,
7427
                        'title' => Security::remove_XSS(cut(strip_tags($work['title']), 80)),
7428
                    ]
7429
                );
7430
                $return .= '</li>';
7431
            }
7432
        }
7433
7434
        $return .= '</ul>';
7435
7436
        return $return;
7437
    }
7438
7439
    /**
7440
     * Creates a list with all the forums in it.
7441
     *
7442
     * @return string
7443
     */
7444
    public function get_forums()
7445
    {
7446
        $forumCategories = get_forum_categories();
7447
        $forumsInNoCategory = get_forums_in_category(0);
7448
        if (!empty($forumsInNoCategory)) {
7449
            $forumCategories = array_merge(
7450
                $forumCategories,
7451
                [
7452
                    [
7453
                        'cat_id' => 0,
7454
                        'session_id' => 0,
7455
                        'visibility' => 1,
7456
                        'cat_comment' => null,
7457
                    ],
7458
                ]
7459
            );
7460
        }
7461
7462
        $a_forums = [];
7463
        $courseEntity = api_get_course_entity(api_get_course_int_id());
7464
        $sessionEntity = api_get_session_entity(api_get_session_id());
7465
7466
        foreach ($forumCategories as $forumCategory) {
7467
            // The forums in this category.
7468
            $forumsInCategory = get_forums_in_category($forumCategory->getIid());
7469
            if (!empty($forumsInCategory)) {
7470
                foreach ($forumsInCategory as $forum) {
7471
                    if ($forum->isVisible($courseEntity, $sessionEntity)) {
7472
                        $a_forums[] = $forum;
7473
                    }
7474
                }
7475
            }
7476
        }
7477
7478
        $return = '<ul class="list-group lp_resource">';
7479
7480
        // First add link
7481
        $return .= '<li class="list-group-item lp_resource_element disable_drag">';
7482
        $return .= Display::return_icon('new_forum.png');
7483
        $return .= Display::url(
7484
            get_lang('Create a new forum'),
7485
            api_get_path(WEB_CODE_PATH).'forum/index.php?'.api_get_cidreq().'&'.http_build_query([
7486
                'action' => 'add',
7487
                'content' => 'forum',
7488
                'lp_id' => $this->lp_id,
7489
            ]),
7490
            ['title' => get_lang('Create a new forum')]
7491
        );
7492
        $return .= '</li>';
7493
7494
        $return .= '<script>
7495
            function toggle_forum(forum_id) {
7496
                if (document.getElementById("forum_"+forum_id+"_content").style.display == "none") {
7497
                    document.getElementById("forum_"+forum_id+"_content").style.display = "block";
7498
                    document.getElementById("forum_"+forum_id+"_opener").src = "'.Display::returnIconPath('remove.gif').'";
7499
                } else {
7500
                    document.getElementById("forum_"+forum_id+"_content").style.display = "none";
7501
                    document.getElementById("forum_"+forum_id+"_opener").src = "'.Display::returnIconPath('add.png').'";
7502
                }
7503
            }
7504
        </script>';
7505
        $moveIcon = Display::return_icon('move_everywhere.png', get_lang('Move'), [], ICON_SIZE_TINY);
7506
        foreach ($a_forums as $forum) {
7507
            $forumId = $forum->getIid();
7508
            $title = Security::remove_XSS($forum->getForumTitle());
7509
            $link = Display::url(
7510
                Display::return_icon('preview_view.png', get_lang('Preview')),
7511
                api_get_path(WEB_CODE_PATH).'forum/viewforum.php?'.api_get_cidreq().'&forum='.$forumId,
7512
                ['target' => '_blank']
7513
            );
7514
7515
            $return .= '<li
7516
                    class="list-group-item lp_resource_element"
7517
                    id="'.$forumId.'"
7518
                    data-id="'.$forumId.'"
7519
                    >';
7520
            $return .= '<a class="moved" href="#">';
7521
            $return .= $moveIcon;
7522
            $return .= ' </a>';
7523
            $return .= Display::return_icon('forum.png', '', [], ICON_SIZE_TINY);
7524
7525
            $moveLink = Display::url(
7526
                $title.' '.$link,
7527
                api_get_self().'?'.
7528
                api_get_cidreq().'&action=add_item&type='.TOOL_FORUM.'&forum_id='.$forumId.'&lp_id='.$this->lp_id,
7529
                [
7530
                    'class' => 'moved link_with_id',
7531
                    'data-id' => $forumId,
7532
                    'data_type' => TOOL_FORUM,
7533
                    'title' => $title,
7534
                    'style' => 'vertical-align:middle',
7535
                ]
7536
            );
7537
            $return .= '<a onclick="javascript:toggle_forum('.$forumId.');" style="cursor:hand; vertical-align:middle">
7538
                            <img
7539
                                src="'.Display::returnIconPath('add.png').'"
7540
                                id="forum_'.$forumId.'_opener" align="absbottom"
7541
                             />
7542
                        </a>
7543
                        '.$moveLink;
7544
            $return .= '</li>';
7545
7546
            $return .= '<div style="display:none" id="forum_'.$forumId.'_content">';
7547
            $threads = get_threads($forumId);
7548
            if (is_array($threads)) {
7549
                foreach ($threads as $thread) {
7550
                    $threadId = $thread->getIid();
7551
                    $link = Display::url(
7552
                        Display::return_icon('preview_view.png', get_lang('Preview')),
7553
                        api_get_path(WEB_CODE_PATH).
7554
                        'forum/viewthread.php?'.api_get_cidreq().'&forum='.$forumId.'&thread='.$threadId,
7555
                        ['target' => '_blank']
7556
                    );
7557
7558
                    $return .= '<li
7559
                        class="list-group-item lp_resource_element"
7560
                      id="'.$threadId.'"
7561
                        data-id="'.$threadId.'"
7562
                    >';
7563
                    $return .= '&nbsp;<a class="moved" href="#">';
7564
                    $return .= $moveIcon;
7565
                    $return .= ' </a>';
7566
                    $return .= Display::return_icon('forumthread.png', get_lang('Thread'), [], ICON_SIZE_TINY);
7567
                    $return .= '<a
7568
                        class="moved link_with_id"
7569
                        data-id="'.$threadId.'"
7570
                        data_type="'.TOOL_THREAD.'"
7571
                        title="'.$thread->getThreadTitle().'"
7572
                        href="'.api_get_self().'?'.api_get_cidreq().'&action=add_item&type='.TOOL_THREAD.'&thread_id='.$threadId.'&lp_id='.$this->lp_id.'"
7573
                        >'.
7574
                        Security::remove_XSS($thread->getThreadTitle()).' '.$link.'</a>';
7575
                    $return .= '</li>';
7576
                }
7577
            }
7578
            $return .= '</div>';
7579
        }
7580
        $return .= '</ul>';
7581
7582
        return $return;
7583
    }
7584
7585
    /**
7586
     * // TODO: The output encoding should be equal to the system encoding.
7587
     *
7588
     * Exports the learning path as a SCORM package. This is the main function that
7589
     * gathers the content, transforms it, writes the imsmanifest.xml file, zips the
7590
     * whole thing and returns the zip.
7591
     *
7592
     * This method needs to be called in PHP5, as it will fail with non-adequate
7593
     * XML package (like the ones for PHP4), and it is *not* a static method, so
7594
     * you need to call it on a learnpath object.
7595
     *
7596
     * @TODO The method might be redefined later on in the scorm class itself to avoid
7597
     * creating a SCORM structure if there is one already. However, if the initial SCORM
7598
     * path has been modified, it should use the generic method here below.
7599
     *
7600
     * @return string Returns the zip package string, or null if error
7601
     */
7602
    public function scormExport()
7603
    {
7604
        api_set_more_memory_and_time_limits();
7605
7606
        $_course = api_get_course_info();
7607
        $course_id = $_course['real_id'];
7608
        // Create the zip handler (this will remain available throughout the method).
7609
        $archivePath = api_get_path(SYS_ARCHIVE_PATH);
7610
        $sys_course_path = api_get_path(SYS_COURSE_PATH);
7611
        $temp_dir_short = uniqid('scorm_export', true);
7612
        $temp_zip_dir = $archivePath.'/'.$temp_dir_short;
7613
        $temp_zip_file = $temp_zip_dir.'/'.md5(time()).'.zip';
7614
        $zip_folder = new PclZip($temp_zip_file);
7615
        $current_course_path = api_get_path(SYS_COURSE_PATH).api_get_course_path();
7616
        $root_path = $main_path = api_get_path(SYS_PATH);
7617
        $files_cleanup = [];
7618
7619
        // Place to temporarily stash the zip file.
7620
        // create the temp dir if it doesn't exist
7621
        // or do a cleanup before creating the zip file.
7622
        if (!is_dir($temp_zip_dir)) {
7623
            mkdir($temp_zip_dir, api_get_permissions_for_new_directories());
7624
        } else {
7625
            // Cleanup: Check the temp dir for old files and delete them.
7626
            $handle = opendir($temp_zip_dir);
7627
            while (false !== ($file = readdir($handle))) {
7628
                if ('.' != $file && '..' != $file) {
7629
                    unlink("$temp_zip_dir/$file");
7630
                }
7631
            }
7632
            closedir($handle);
7633
        }
7634
        $zip_files = $zip_files_abs = $zip_files_dist = [];
7635
        if (is_dir($current_course_path.'/scorm/'.$this->path) &&
7636
            is_file($current_course_path.'/scorm/'.$this->path.'/imsmanifest.xml')
7637
        ) {
7638
            // Remove the possible . at the end of the path.
7639
            $dest_path_to_lp = '.' == substr($this->path, -1) ? substr($this->path, 0, -1) : $this->path;
7640
            $dest_path_to_scorm_folder = str_replace('//', '/', $temp_zip_dir.'/scorm/'.$dest_path_to_lp);
7641
            mkdir(
7642
                $dest_path_to_scorm_folder,
7643
                api_get_permissions_for_new_directories(),
7644
                true
7645
            );
7646
            copyr(
7647
                $current_course_path.'/scorm/'.$this->path,
7648
                $dest_path_to_scorm_folder,
7649
                ['imsmanifest'],
7650
                $zip_files
7651
            );
7652
        }
7653
7654
        // Build a dummy imsmanifest structure.
7655
        // Do not add to the zip yet (we still need it).
7656
        // This structure is developed following regulations for SCORM 1.2 packaging in the SCORM 1.2 Content
7657
        // Aggregation Model official document, section "2.3 Content Packaging".
7658
        // We are going to build a UTF-8 encoded manifest.
7659
        // Later we will recode it to the desired (and supported) encoding.
7660
        $xmldoc = new DOMDocument('1.0');
7661
        $root = $xmldoc->createElement('manifest');
7662
        $root->setAttribute('identifier', 'SingleCourseManifest');
7663
        $root->setAttribute('version', '1.1');
7664
        $root->setAttribute('xmlns', 'http://www.imsproject.org/xsd/imscp_rootv1p1p2');
7665
        $root->setAttribute('xmlns:adlcp', 'http://www.adlnet.org/xsd/adlcp_rootv1p2');
7666
        $root->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
7667
        $root->setAttribute(
7668
            'xsi:schemaLocation',
7669
            'http://www.imsproject.org/xsd/imscp_rootv1p1p2 imscp_rootv1p1p2.xsd http://www.imsglobal.org/xsd/imsmd_rootv1p2p1 imsmd_rootv1p2p1.xsd http://www.adlnet.org/xsd/adlcp_rootv1p2 adlcp_rootv1p2.xsd'
7670
        );
7671
        // Build mandatory sub-root container elements.
7672
        $metadata = $xmldoc->createElement('metadata');
7673
        $md_schema = $xmldoc->createElement('schema', 'ADL SCORM');
7674
        $metadata->appendChild($md_schema);
7675
        $md_schemaversion = $xmldoc->createElement('schemaversion', '1.2');
7676
        $metadata->appendChild($md_schemaversion);
7677
        $root->appendChild($metadata);
7678
7679
        $organizations = $xmldoc->createElement('organizations');
7680
        $resources = $xmldoc->createElement('resources');
7681
7682
        // Build the only organization we will use in building our learnpaths.
7683
        $organizations->setAttribute('default', 'chamilo_scorm_export');
7684
        $organization = $xmldoc->createElement('organization');
7685
        $organization->setAttribute('identifier', 'chamilo_scorm_export');
7686
        // To set the title of the SCORM entity (=organization), we take the name given
7687
        // in Chamilo and convert it to HTML entities using the Chamilo charset (not the
7688
        // learning path charset) as it is the encoding that defines how it is stored
7689
        // in the database. Then we convert it to HTML entities again as the "&" character
7690
        // alone is not authorized in XML (must be &amp;).
7691
        // The title is then decoded twice when extracting (see scorm::parse_manifest).
7692
        $org_title = $xmldoc->createElement('title', api_utf8_encode($this->get_name()));
7693
        $organization->appendChild($org_title);
7694
        $folder_name = 'document';
7695
7696
        // Removes the learning_path/scorm_folder path when exporting see #4841
7697
        $path_to_remove = '';
7698
        $path_to_replace = '';
7699
        $result = $this->generate_lp_folder($_course);
7700
        if (isset($result['dir']) && strpos($result['dir'], 'learning_path')) {
7701
            $path_to_remove = 'document'.$result['dir'];
7702
            $path_to_replace = $folder_name.'/';
7703
        }
7704
7705
        // Fixes chamilo scorm exports
7706
        if ('chamilo_scorm_export' === $this->ref) {
7707
            $path_to_remove = 'scorm/'.$this->path.'/document/';
7708
        }
7709
7710
        // For each element, add it to the imsmanifest structure, then add it to the zip.
7711
        $link_updates = [];
7712
        $links_to_create = [];
7713
        foreach ($this->ordered_items as $index => $itemId) {
7714
            /** @var learnpathItem $item */
7715
            $item = $this->items[$itemId];
7716
            if (!in_array($item->type, [TOOL_QUIZ, TOOL_FORUM, TOOL_THREAD, TOOL_LINK, TOOL_STUDENTPUBLICATION])) {
7717
                // Get included documents from this item.
7718
                if ('sco' === $item->type) {
7719
                    $inc_docs = $item->get_resources_from_source(
7720
                        null,
7721
                        $current_course_path.'/scorm/'.$this->path.'/'.$item->get_path()
7722
                    );
7723
                } else {
7724
                    $inc_docs = $item->get_resources_from_source();
7725
                }
7726
7727
                // Give a child element <item> to the <organization> element.
7728
                $my_item_id = $item->get_id();
7729
                $my_item = $xmldoc->createElement('item');
7730
                $my_item->setAttribute('identifier', 'ITEM_'.$my_item_id);
7731
                $my_item->setAttribute('identifierref', 'RESOURCE_'.$my_item_id);
7732
                $my_item->setAttribute('isvisible', 'true');
7733
                // Give a child element <title> to the <item> element.
7734
                $my_title = $xmldoc->createElement(
7735
                    'title',
7736
                    htmlspecialchars(
7737
                        api_utf8_encode($item->get_title()),
7738
                        ENT_QUOTES,
7739
                        'UTF-8'
7740
                    )
7741
                );
7742
                $my_item->appendChild($my_title);
7743
                // Give a child element <adlcp:prerequisites> to the <item> element.
7744
                $my_prereqs = $xmldoc->createElement(
7745
                    'adlcp:prerequisites',
7746
                    $this->get_scorm_prereq_string($my_item_id)
7747
                );
7748
                $my_prereqs->setAttribute('type', 'aicc_script');
7749
                $my_item->appendChild($my_prereqs);
7750
                // Give a child element <adlcp:maxtimeallowed> to the <item> element - not yet supported.
7751
                //$xmldoc->createElement('adlcp:maxtimeallowed','');
7752
                // Give a child element <adlcp:timelimitaction> to the <item> element - not yet supported.
7753
                //$xmldoc->createElement('adlcp:timelimitaction','');
7754
                // Give a child element <adlcp:datafromlms> to the <item> element - not yet supported.
7755
                //$xmldoc->createElement('adlcp:datafromlms','');
7756
                // Give a child element <adlcp:masteryscore> to the <item> element.
7757
                $my_masteryscore = $xmldoc->createElement('adlcp:masteryscore', $item->get_mastery_score());
7758
                $my_item->appendChild($my_masteryscore);
7759
7760
                // Attach this item to the organization element or hits parent if there is one.
7761
                if (!empty($item->parent) && 0 != $item->parent) {
7762
                    $children = $organization->childNodes;
7763
                    $possible_parent = $this->get_scorm_xml_node($children, 'ITEM_'.$item->parent);
7764
                    if (is_object($possible_parent)) {
7765
                        $possible_parent->appendChild($my_item);
7766
                    } else {
7767
                        if ($this->debug > 0) {
7768
                            error_log('Parent ITEM_'.$item->parent.' of item ITEM_'.$my_item_id.' not found');
7769
                        }
7770
                    }
7771
                } else {
7772
                    if ($this->debug > 0) {
7773
                        error_log('No parent');
7774
                    }
7775
                    $organization->appendChild($my_item);
7776
                }
7777
7778
                // Get the path of the file(s) from the course directory root.
7779
                $my_file_path = $item->get_file_path('scorm/'.$this->path.'/');
7780
                $my_xml_file_path = $my_file_path;
7781
                if (!empty($path_to_remove)) {
7782
                    // From docs
7783
                    $my_xml_file_path = str_replace($path_to_remove, $path_to_replace, $my_file_path);
7784
7785
                    // From quiz
7786
                    if ('chamilo_scorm_export' === $this->ref) {
7787
                        $path_to_remove = 'scorm/'.$this->path.'/';
7788
                        $my_xml_file_path = str_replace($path_to_remove, '', $my_file_path);
7789
                    }
7790
                }
7791
7792
                $my_sub_dir = dirname($my_file_path);
7793
                $my_sub_dir = str_replace('\\', '/', $my_sub_dir);
7794
                $my_xml_sub_dir = $my_sub_dir;
7795
                // Give a <resource> child to the <resources> element
7796
                $my_resource = $xmldoc->createElement('resource');
7797
                $my_resource->setAttribute('identifier', 'RESOURCE_'.$item->get_id());
7798
                $my_resource->setAttribute('type', 'webcontent');
7799
                $my_resource->setAttribute('href', $my_xml_file_path);
7800
                // adlcp:scormtype can be either 'sco' or 'asset'.
7801
                if ('sco' === $item->type) {
7802
                    $my_resource->setAttribute('adlcp:scormtype', 'sco');
7803
                } else {
7804
                    $my_resource->setAttribute('adlcp:scormtype', 'asset');
7805
                }
7806
                // xml:base is the base directory to find the files declared in this resource.
7807
                $my_resource->setAttribute('xml:base', '');
7808
                // Give a <file> child to the <resource> element.
7809
                $my_file = $xmldoc->createElement('file');
7810
                $my_file->setAttribute('href', $my_xml_file_path);
7811
                $my_resource->appendChild($my_file);
7812
7813
                // Dependency to other files - not yet supported.
7814
                $i = 1;
7815
                if ($inc_docs) {
7816
                    foreach ($inc_docs as $doc_info) {
7817
                        if (count($doc_info) < 1 || empty($doc_info[0])) {
7818
                            continue;
7819
                        }
7820
                        $my_dep = $xmldoc->createElement('resource');
7821
                        $res_id = 'RESOURCE_'.$item->get_id().'_'.$i;
7822
                        $my_dep->setAttribute('identifier', $res_id);
7823
                        $my_dep->setAttribute('type', 'webcontent');
7824
                        $my_dep->setAttribute('adlcp:scormtype', 'asset');
7825
                        $my_dep_file = $xmldoc->createElement('file');
7826
                        // Check type of URL.
7827
                        if ('remote' == $doc_info[1]) {
7828
                            // Remote file. Save url as is.
7829
                            $my_dep_file->setAttribute('href', $doc_info[0]);
7830
                            $my_dep->setAttribute('xml:base', '');
7831
                        } elseif ('local' === $doc_info[1]) {
7832
                            switch ($doc_info[2]) {
7833
                                case 'url':
7834
                                    // Local URL - save path as url for now, don't zip file.
7835
                                    $abs_path = api_get_path(SYS_PATH).
7836
                                        str_replace(api_get_path(WEB_PATH), '', $doc_info[0]);
7837
                                    $current_dir = dirname($abs_path);
7838
                                    $current_dir = str_replace('\\', '/', $current_dir);
7839
                                    $file_path = realpath($abs_path);
7840
                                    $file_path = str_replace('\\', '/', $file_path);
7841
                                    $my_dep_file->setAttribute('href', $file_path);
7842
                                    $my_dep->setAttribute('xml:base', '');
7843
                                    if (false !== strstr($file_path, $main_path)) {
7844
                                        // The calculated real path is really inside Chamilo's root path.
7845
                                        // Reduce file path to what's under the DocumentRoot.
7846
                                        $replace = $file_path;
7847
                                        $file_path = substr($file_path, strlen($root_path) - 1);
7848
                                        $destinationFile = $file_path;
7849
7850
                                        if (false !== strstr($file_path, 'upload/users')) {
7851
                                            $pos = strpos($file_path, 'my_files/');
7852
                                            if (false !== $pos) {
7853
                                                $onlyDirectory = str_replace(
7854
                                                    'upload/users/',
7855
                                                    '',
7856
                                                    substr($file_path, $pos, strlen($file_path))
7857
                                                );
7858
                                            }
7859
                                            $replace = $onlyDirectory;
7860
                                            $destinationFile = $replace;
7861
                                        }
7862
                                        $zip_files_abs[] = $file_path;
7863
                                        $link_updates[$my_file_path][] = [
7864
                                            'orig' => $doc_info[0],
7865
                                            'dest' => $destinationFile,
7866
                                            'replace' => $replace,
7867
                                        ];
7868
                                        $my_dep_file->setAttribute('href', $file_path);
7869
                                        $my_dep->setAttribute('xml:base', '');
7870
                                    } elseif (empty($file_path)) {
7871
                                        $file_path = $_SERVER['DOCUMENT_ROOT'].$abs_path;
7872
                                        $file_path = str_replace('//', '/', $file_path);
7873
                                        if (file_exists($file_path)) {
7874
                                            // We get the relative path.
7875
                                            $file_path = substr($file_path, strlen($current_dir));
7876
                                            $zip_files[] = $my_sub_dir.'/'.$file_path;
7877
                                            $link_updates[$my_file_path][] = [
7878
                                                'orig' => $doc_info[0],
7879
                                                'dest' => $file_path,
7880
                                            ];
7881
                                            $my_dep_file->setAttribute('href', $file_path);
7882
                                            $my_dep->setAttribute('xml:base', '');
7883
                                        }
7884
                                    }
7885
                                    break;
7886
                                case 'abs':
7887
                                    // Absolute path from DocumentRoot. Save file and leave path as is in the zip.
7888
                                    $my_dep_file->setAttribute('href', $doc_info[0]);
7889
                                    $my_dep->setAttribute('xml:base', '');
7890
7891
                                    // The next lines fix a bug when using the "subdir" mode of Chamilo, whereas
7892
                                    // an image path would be constructed as /var/www/subdir/subdir/img/foo.bar
7893
                                    $abs_img_path_without_subdir = $doc_info[0];
7894
                                    $relp = api_get_path(REL_PATH); // The url-append config param.
7895
                                    $pos = strpos($abs_img_path_without_subdir, $relp);
7896
                                    if (0 === $pos) {
7897
                                        $abs_img_path_without_subdir = trim('/'.substr($abs_img_path_without_subdir, strlen($relp)));
7898
                                    }
7899
7900
                                    $file_path = realpath(api_get_path(SYS_APP_PATH).$abs_img_path_without_subdir);
7901
                                    $file_path = str_replace(['\\', '//'], '/', $file_path);
7902
7903
                                    // Prepare the current directory path (until just under 'document') with a trailing slash.
7904
                                    $cur_path = '/' == substr($current_course_path, -1) ? $current_course_path : $current_course_path.'/';
7905
                                    // Check if the current document is in that path.
7906
                                    if (false !== strstr($file_path, $cur_path)) {
7907
                                        $destinationFile = substr($file_path, strlen($cur_path));
7908
                                        $filePathNoCoursePart = substr($file_path, strlen($cur_path));
7909
7910
                                        $fileToTest = $cur_path.$my_file_path;
7911
                                        if (!empty($path_to_remove)) {
7912
                                            $fileToTest = str_replace(
7913
                                                $path_to_remove.'/',
7914
                                                $path_to_replace,
7915
                                                $cur_path.$my_file_path
7916
                                            );
7917
                                        }
7918
7919
                                        $relative_path = api_get_relative_path($fileToTest, $file_path);
7920
7921
                                        // Put the current document in the zip (this array is the array
7922
                                        // that will manage documents already in the course folder - relative).
7923
                                        $zip_files[] = $filePathNoCoursePart;
7924
                                        // Update the links to the current document in the
7925
                                        // containing document (make them relative).
7926
                                        $link_updates[$my_file_path][] = [
7927
                                            'orig' => $doc_info[0],
7928
                                            'dest' => $destinationFile,
7929
                                            'replace' => $relative_path,
7930
                                        ];
7931
7932
                                        $my_dep_file->setAttribute('href', $file_path);
7933
                                        $my_dep->setAttribute('xml:base', '');
7934
                                    } elseif (false !== strstr($file_path, $main_path)) {
7935
                                        // The calculated real path is really inside Chamilo's root path.
7936
                                        // Reduce file path to what's under the DocumentRoot.
7937
                                        $file_path = substr($file_path, strlen($root_path));
7938
                                        $zip_files_abs[] = $file_path;
7939
                                        $link_updates[$my_file_path][] = ['orig' => $doc_info[0], 'dest' => $file_path];
7940
                                        $my_dep_file->setAttribute('href', 'document/'.$file_path);
7941
                                        $my_dep->setAttribute('xml:base', '');
7942
                                    } elseif (empty($file_path)) {
7943
                                        // Probably this is an image inside "/main" directory
7944
                                        $file_path = api_get_path(SYS_PATH).$abs_img_path_without_subdir;
7945
                                        $abs_path = api_get_path(SYS_PATH).str_replace(api_get_path(WEB_PATH), '', $doc_info[0]);
7946
7947
                                        if (file_exists($file_path)) {
7948
                                            if (false !== strstr($file_path, 'main/default_course_document')) {
7949
                                                // We get the relative path.
7950
                                                $pos = strpos($file_path, 'main/default_course_document/');
7951
                                                if (false !== $pos) {
7952
                                                    $onlyDirectory = str_replace(
7953
                                                        'main/default_course_document/',
7954
                                                        '',
7955
                                                        substr($file_path, $pos, strlen($file_path))
7956
                                                    );
7957
                                                }
7958
7959
                                                $destinationFile = 'default_course_document/'.$onlyDirectory;
7960
                                                $fileAbs = substr($file_path, strlen(api_get_path(SYS_PATH)));
7961
                                                $zip_files_abs[] = $fileAbs;
7962
                                                $link_updates[$my_file_path][] = [
7963
                                                    'orig' => $doc_info[0],
7964
                                                    'dest' => $destinationFile,
7965
                                                ];
7966
                                                $my_dep_file->setAttribute('href', 'document/'.$file_path);
7967
                                                $my_dep->setAttribute('xml:base', '');
7968
                                            }
7969
                                        }
7970
                                    }
7971
                                    break;
7972
                                case 'rel':
7973
                                    // Path relative to the current document.
7974
                                    // Save xml:base as current document's directory and save file in zip as subdir.file_path
7975
                                    if ('..' === substr($doc_info[0], 0, 2)) {
7976
                                        // Relative path going up.
7977
                                        $current_dir = dirname($current_course_path.'/'.$item->get_file_path()).'/';
7978
                                        $current_dir = str_replace('\\', '/', $current_dir);
7979
                                        $file_path = realpath($current_dir.$doc_info[0]);
7980
                                        $file_path = str_replace('\\', '/', $file_path);
7981
                                        if (false !== strstr($file_path, $main_path)) {
7982
                                            // The calculated real path is really inside Chamilo's root path.
7983
                                            // Reduce file path to what's under the DocumentRoot.
7984
                                            $file_path = substr($file_path, strlen($root_path));
7985
                                            $zip_files_abs[] = $file_path;
7986
                                            $link_updates[$my_file_path][] = ['orig' => $doc_info[0], 'dest' => $file_path];
7987
                                            $my_dep_file->setAttribute('href', 'document/'.$file_path);
7988
                                            $my_dep->setAttribute('xml:base', '');
7989
                                        }
7990
                                    } else {
7991
                                        $zip_files[] = $my_sub_dir.'/'.$doc_info[0];
7992
                                        $my_dep_file->setAttribute('href', $doc_info[0]);
7993
                                        $my_dep->setAttribute('xml:base', $my_xml_sub_dir);
7994
                                    }
7995
                                    break;
7996
                                default:
7997
                                    $my_dep_file->setAttribute('href', $doc_info[0]);
7998
                                    $my_dep->setAttribute('xml:base', '');
7999
                                    break;
8000
                            }
8001
                        }
8002
                        $my_dep->appendChild($my_dep_file);
8003
                        $resources->appendChild($my_dep);
8004
                        $dependency = $xmldoc->createElement('dependency');
8005
                        $dependency->setAttribute('identifierref', $res_id);
8006
                        $my_resource->appendChild($dependency);
8007
                        $i++;
8008
                    }
8009
                }
8010
                $resources->appendChild($my_resource);
8011
                $zip_files[] = $my_file_path;
8012
            } else {
8013
                // If the item is a quiz or a link or whatever non-exportable, we include a step indicating it.
8014
                switch ($item->type) {
8015
                    case TOOL_LINK:
8016
                        $my_item = $xmldoc->createElement('item');
8017
                        $my_item->setAttribute('identifier', 'ITEM_'.$item->get_id());
8018
                        $my_item->setAttribute('identifierref', 'RESOURCE_'.$item->get_id());
8019
                        $my_item->setAttribute('isvisible', 'true');
8020
                        // Give a child element <title> to the <item> element.
8021
                        $my_title = $xmldoc->createElement(
8022
                            'title',
8023
                            htmlspecialchars(
8024
                                api_utf8_encode($item->get_title()),
8025
                                ENT_QUOTES,
8026
                                'UTF-8'
8027
                            )
8028
                        );
8029
                        $my_item->appendChild($my_title);
8030
                        // Give a child element <adlcp:prerequisites> to the <item> element.
8031
                        $my_prereqs = $xmldoc->createElement('adlcp:prerequisites', $item->get_prereq_string());
8032
                        $my_prereqs->setAttribute('type', 'aicc_script');
8033
                        $my_item->appendChild($my_prereqs);
8034
                        // Give a child element <adlcp:maxtimeallowed> to the <item> element - not yet supported.
8035
                        //$xmldoc->createElement('adlcp:maxtimeallowed', '');
8036
                        // Give a child element <adlcp:timelimitaction> to the <item> element - not yet supported.
8037
                        //$xmldoc->createElement('adlcp:timelimitaction', '');
8038
                        // Give a child element <adlcp:datafromlms> to the <item> element - not yet supported.
8039
                        //$xmldoc->createElement('adlcp:datafromlms', '');
8040
                        // Give a child element <adlcp:masteryscore> to the <item> element.
8041
                        $my_masteryscore = $xmldoc->createElement('adlcp:masteryscore', $item->get_mastery_score());
8042
                        $my_item->appendChild($my_masteryscore);
8043
8044
                        // Attach this item to the organization element or its parent if there is one.
8045
                        if (!empty($item->parent) && 0 != $item->parent) {
8046
                            $children = $organization->childNodes;
8047
                            for ($i = 0; $i < $children->length; $i++) {
8048
                                $item_temp = $children->item($i);
8049
                                if ('item' == $item_temp->nodeName) {
8050
                                    if ($item_temp->getAttribute('identifier') == 'ITEM_'.$item->parent) {
8051
                                        $item_temp->appendChild($my_item);
8052
                                    }
8053
                                }
8054
                            }
8055
                        } else {
8056
                            $organization->appendChild($my_item);
8057
                        }
8058
8059
                        $my_file_path = 'link_'.$item->get_id().'.html';
8060
                        $sql = 'SELECT url, title FROM '.Database::get_course_table(TABLE_LINK).'
8061
                                WHERE c_id = '.$course_id.' AND id = '.$item->path;
8062
                        $rs = Database::query($sql);
8063
                        if ($link = Database::fetch_array($rs)) {
8064
                            $url = $link['url'];
8065
                            $title = stripslashes($link['title']);
8066
                            $links_to_create[$my_file_path] = ['title' => $title, 'url' => $url];
8067
                            $my_xml_file_path = $my_file_path;
8068
                            $my_sub_dir = dirname($my_file_path);
8069
                            $my_sub_dir = str_replace('\\', '/', $my_sub_dir);
8070
                            $my_xml_sub_dir = $my_sub_dir;
8071
                            // Give a <resource> child to the <resources> element.
8072
                            $my_resource = $xmldoc->createElement('resource');
8073
                            $my_resource->setAttribute('identifier', 'RESOURCE_'.$item->get_id());
8074
                            $my_resource->setAttribute('type', 'webcontent');
8075
                            $my_resource->setAttribute('href', $my_xml_file_path);
8076
                            // adlcp:scormtype can be either 'sco' or 'asset'.
8077
                            $my_resource->setAttribute('adlcp:scormtype', 'asset');
8078
                            // xml:base is the base directory to find the files declared in this resource.
8079
                            $my_resource->setAttribute('xml:base', '');
8080
                            // give a <file> child to the <resource> element.
8081
                            $my_file = $xmldoc->createElement('file');
8082
                            $my_file->setAttribute('href', $my_xml_file_path);
8083
                            $my_resource->appendChild($my_file);
8084
                            $resources->appendChild($my_resource);
8085
                        }
8086
                        break;
8087
                    case TOOL_QUIZ:
8088
                        $exe_id = $item->path;
8089
                        // Should be using ref when everything will be cleaned up in this regard.
8090
                        $exe = new Exercise();
8091
                        $exe->read($exe_id);
8092
                        $my_item = $xmldoc->createElement('item');
8093
                        $my_item->setAttribute('identifier', 'ITEM_'.$item->get_id());
8094
                        $my_item->setAttribute('identifierref', 'RESOURCE_'.$item->get_id());
8095
                        $my_item->setAttribute('isvisible', 'true');
8096
                        // Give a child element <title> to the <item> element.
8097
                        $my_title = $xmldoc->createElement(
8098
                            'title',
8099
                            htmlspecialchars(
8100
                                api_utf8_encode($item->get_title()),
8101
                                ENT_QUOTES,
8102
                                'UTF-8'
8103
                            )
8104
                        );
8105
                        $my_item->appendChild($my_title);
8106
                        $my_max_score = $xmldoc->createElement('max_score', $item->get_max());
8107
                        $my_item->appendChild($my_max_score);
8108
                        // Give a child element <adlcp:prerequisites> to the <item> element.
8109
                        $my_prereqs = $xmldoc->createElement('adlcp:prerequisites', $item->get_prereq_string());
8110
                        $my_prereqs->setAttribute('type', 'aicc_script');
8111
                        $my_item->appendChild($my_prereqs);
8112
                        // Give a child element <adlcp:masteryscore> to the <item> element.
8113
                        $my_masteryscore = $xmldoc->createElement('adlcp:masteryscore', $item->get_mastery_score());
8114
                        $my_item->appendChild($my_masteryscore);
8115
8116
                        // Attach this item to the organization element or hits parent if there is one.
8117
                        if (!empty($item->parent) && 0 != $item->parent) {
8118
                            $children = $organization->childNodes;
8119
                            $possible_parent = $this->get_scorm_xml_node($children, 'ITEM_'.$item->parent);
8120
                            if ($possible_parent) {
8121
                                if ($possible_parent->getAttribute('identifier') === 'ITEM_'.$item->parent) {
8122
                                    $possible_parent->appendChild($my_item);
8123
                                }
8124
                            }
8125
                        } else {
8126
                            $organization->appendChild($my_item);
8127
                        }
8128
8129
                        // Get the path of the file(s) from the course directory root
8130
                        //$my_file_path = $item->get_file_path('scorm/'.$this->path.'/');
8131
                        $my_file_path = 'quiz_'.$item->get_id().'.html';
8132
                        // Write the contents of the exported exercise into a (big) html file
8133
                        // to later pack it into the exported SCORM. The file will be removed afterwards.
8134
                        $scormExercise = new ScormExercise($exe, true);
8135
                        $contents = $scormExercise->export();
8136
8137
                        $tmp_file_path = $archivePath.$temp_dir_short.'/'.$my_file_path;
8138
                        $res = file_put_contents($tmp_file_path, $contents);
8139
                        if (false === $res) {
8140
                            error_log('Could not write into file '.$tmp_file_path.' '.__FILE__.' '.__LINE__, 0);
8141
                        }
8142
                        $files_cleanup[] = $tmp_file_path;
8143
                        $my_xml_file_path = $my_file_path;
8144
                        $my_sub_dir = dirname($my_file_path);
8145
                        $my_sub_dir = str_replace('\\', '/', $my_sub_dir);
8146
                        $my_xml_sub_dir = $my_sub_dir;
8147
                        // Give a <resource> child to the <resources> element.
8148
                        $my_resource = $xmldoc->createElement('resource');
8149
                        $my_resource->setAttribute('identifier', 'RESOURCE_'.$item->get_id());
8150
                        $my_resource->setAttribute('type', 'webcontent');
8151
                        $my_resource->setAttribute('href', $my_xml_file_path);
8152
                        // adlcp:scormtype can be either 'sco' or 'asset'.
8153
                        $my_resource->setAttribute('adlcp:scormtype', 'sco');
8154
                        // xml:base is the base directory to find the files declared in this resource.
8155
                        $my_resource->setAttribute('xml:base', '');
8156
                        // Give a <file> child to the <resource> element.
8157
                        $my_file = $xmldoc->createElement('file');
8158
                        $my_file->setAttribute('href', $my_xml_file_path);
8159
                        $my_resource->appendChild($my_file);
8160
8161
                        // Get included docs.
8162
                        $inc_docs = $item->get_resources_from_source(null, $tmp_file_path);
8163
8164
                        // Dependency to other files - not yet supported.
8165
                        $i = 1;
8166
                        foreach ($inc_docs as $doc_info) {
8167
                            if (count($doc_info) < 1 || empty($doc_info[0])) {
8168
                                continue;
8169
                            }
8170
                            $my_dep = $xmldoc->createElement('resource');
8171
                            $res_id = 'RESOURCE_'.$item->get_id().'_'.$i;
8172
                            $my_dep->setAttribute('identifier', $res_id);
8173
                            $my_dep->setAttribute('type', 'webcontent');
8174
                            $my_dep->setAttribute('adlcp:scormtype', 'asset');
8175
                            $my_dep_file = $xmldoc->createElement('file');
8176
                            // Check type of URL.
8177
                            if ('remote' == $doc_info[1]) {
8178
                                // Remote file. Save url as is.
8179
                                $my_dep_file->setAttribute('href', $doc_info[0]);
8180
                                $my_dep->setAttribute('xml:base', '');
8181
                            } elseif ('local' == $doc_info[1]) {
8182
                                switch ($doc_info[2]) {
8183
                                    case 'url': // Local URL - save path as url for now, don't zip file.
8184
                                        // Save file but as local file (retrieve from URL).
8185
                                        $abs_path = api_get_path(SYS_PATH).
8186
                                            str_replace(api_get_path(WEB_PATH), '', $doc_info[0]);
8187
                                        $current_dir = dirname($abs_path);
8188
                                        $current_dir = str_replace('\\', '/', $current_dir);
8189
                                        $file_path = realpath($abs_path);
8190
                                        $file_path = str_replace('\\', '/', $file_path);
8191
                                        $my_dep_file->setAttribute('href', 'document/'.$file_path);
8192
                                        $my_dep->setAttribute('xml:base', '');
8193
                                        if (false !== strstr($file_path, $main_path)) {
8194
                                            // The calculated real path is really inside the chamilo root path.
8195
                                            // Reduce file path to what's under the DocumentRoot.
8196
                                            $file_path = substr($file_path, strlen($root_path));
8197
                                            $zip_files_abs[] = $file_path;
8198
                                            $link_updates[$my_file_path][] = [
8199
                                                'orig' => $doc_info[0],
8200
                                                'dest' => 'document/'.$file_path,
8201
                                            ];
8202
                                            $my_dep_file->setAttribute('href', 'document/'.$file_path);
8203
                                            $my_dep->setAttribute('xml:base', '');
8204
                                        } elseif (empty($file_path)) {
8205
                                            $file_path = $_SERVER['DOCUMENT_ROOT'].$abs_path;
8206
                                            $file_path = str_replace('//', '/', $file_path);
8207
                                            if (file_exists($file_path)) {
8208
                                                $file_path = substr($file_path, strlen($current_dir));
8209
                                                // We get the relative path.
8210
                                                $zip_files[] = $my_sub_dir.'/'.$file_path;
8211
                                                $link_updates[$my_file_path][] = [
8212
                                                    'orig' => $doc_info[0],
8213
                                                    'dest' => 'document/'.$file_path,
8214
                                                ];
8215
                                                $my_dep_file->setAttribute('href', 'document/'.$file_path);
8216
                                                $my_dep->setAttribute('xml:base', '');
8217
                                            }
8218
                                        }
8219
                                        break;
8220
                                    case 'abs':
8221
                                        // Absolute path from DocumentRoot. Save file and leave path as is in the zip.
8222
                                        $current_dir = dirname($current_course_path.'/'.$item->get_file_path()).'/';
8223
                                        $current_dir = str_replace('\\', '/', $current_dir);
8224
                                        $file_path = realpath($doc_info[0]);
8225
                                        $file_path = str_replace('\\', '/', $file_path);
8226
                                        $my_dep_file->setAttribute('href', $file_path);
8227
                                        $my_dep->setAttribute('xml:base', '');
8228
8229
                                        if (false !== strstr($file_path, $main_path)) {
8230
                                            // The calculated real path is really inside the chamilo root path.
8231
                                            // Reduce file path to what's under the DocumentRoot.
8232
                                            $file_path = substr($file_path, strlen($root_path));
8233
                                            $zip_files_abs[] = $file_path;
8234
                                            $link_updates[$my_file_path][] = [
8235
                                                'orig' => $doc_info[0],
8236
                                                'dest' => $file_path,
8237
                                            ];
8238
                                            $my_dep_file->setAttribute('href', 'document/'.$file_path);
8239
                                            $my_dep->setAttribute('xml:base', '');
8240
                                        } elseif (empty($file_path)) {
8241
                                            $docSysPartPath = str_replace(
8242
                                                api_get_path(REL_COURSE_PATH),
8243
                                                '',
8244
                                                $doc_info[0]
8245
                                            );
8246
8247
                                            $docSysPartPathNoCourseCode = str_replace(
8248
                                                $_course['directory'].'/',
8249
                                                '',
8250
                                                $docSysPartPath
8251
                                            );
8252
8253
                                            $docSysPath = api_get_path(SYS_COURSE_PATH).$docSysPartPath;
8254
                                            if (file_exists($docSysPath)) {
8255
                                                $file_path = $docSysPartPathNoCourseCode;
8256
                                                $zip_files[] = $my_sub_dir.'/'.$file_path;
8257
                                                $link_updates[$my_file_path][] = [
8258
                                                    'orig' => $doc_info[0],
8259
                                                    'dest' => $file_path,
8260
                                                ];
8261
                                                $my_dep_file->setAttribute('href', 'document/'.$file_path);
8262
                                                $my_dep->setAttribute('xml:base', '');
8263
                                            }
8264
                                        }
8265
                                        break;
8266
                                    case 'rel':
8267
                                        // Path relative to the current document. Save xml:base as current document's
8268
                                        // directory and save file in zip as subdir.file_path
8269
                                        if ('..' === substr($doc_info[0], 0, 2)) {
8270
                                            // Relative path going up.
8271
                                            $current_dir = dirname($current_course_path.'/'.$item->get_file_path()).'/';
8272
                                            $current_dir = str_replace('\\', '/', $current_dir);
8273
                                            $file_path = realpath($current_dir.$doc_info[0]);
8274
                                            $file_path = str_replace('\\', '/', $file_path);
8275
                                            if (false !== strstr($file_path, $main_path)) {
8276
                                                // The calculated real path is really inside Chamilo's root path.
8277
                                                // Reduce file path to what's under the DocumentRoot.
8278
8279
                                                $file_path = substr($file_path, strlen($root_path));
8280
                                                $file_path_dest = $file_path;
8281
8282
                                                // File path is courses/CHAMILO/document/....
8283
                                                $info_file_path = explode('/', $file_path);
8284
                                                if ('courses' == $info_file_path[0]) {
8285
                                                    // Add character "/" in file path.
8286
                                                    $file_path_dest = 'document/'.$file_path;
8287
                                                }
8288
                                                $zip_files_abs[] = $file_path;
8289
8290
                                                $link_updates[$my_file_path][] = [
8291
                                                    'orig' => $doc_info[0],
8292
                                                    'dest' => $file_path_dest,
8293
                                                ];
8294
                                                $my_dep_file->setAttribute('href', 'document/'.$file_path);
8295
                                                $my_dep->setAttribute('xml:base', '');
8296
                                            }
8297
                                        } else {
8298
                                            $zip_files[] = $my_sub_dir.'/'.$doc_info[0];
8299
                                            $my_dep_file->setAttribute('href', $doc_info[0]);
8300
                                            $my_dep->setAttribute('xml:base', $my_xml_sub_dir);
8301
                                        }
8302
                                        break;
8303
                                    default:
8304
                                        $my_dep_file->setAttribute('href', $doc_info[0]); // ../../courses/
8305
                                        $my_dep->setAttribute('xml:base', '');
8306
                                        break;
8307
                                }
8308
                            }
8309
                            $my_dep->appendChild($my_dep_file);
8310
                            $resources->appendChild($my_dep);
8311
                            $dependency = $xmldoc->createElement('dependency');
8312
                            $dependency->setAttribute('identifierref', $res_id);
8313
                            $my_resource->appendChild($dependency);
8314
                            $i++;
8315
                        }
8316
                        $resources->appendChild($my_resource);
8317
                        $zip_files[] = $my_file_path;
8318
                        break;
8319
                    default:
8320
                        // Get the path of the file(s) from the course directory root
8321
                        $my_file_path = 'non_exportable.html';
8322
                        //$my_xml_file_path = api_htmlentities(api_utf8_encode($my_file_path), ENT_COMPAT, 'UTF-8');
8323
                        $my_xml_file_path = $my_file_path;
8324
                        $my_sub_dir = dirname($my_file_path);
8325
                        $my_sub_dir = str_replace('\\', '/', $my_sub_dir);
8326
                        //$my_xml_sub_dir = api_htmlentities(api_utf8_encode($my_sub_dir), ENT_COMPAT, 'UTF-8');
8327
                        $my_xml_sub_dir = $my_sub_dir;
8328
                        // Give a <resource> child to the <resources> element.
8329
                        $my_resource = $xmldoc->createElement('resource');
8330
                        $my_resource->setAttribute('identifier', 'RESOURCE_'.$item->get_id());
8331
                        $my_resource->setAttribute('type', 'webcontent');
8332
                        $my_resource->setAttribute('href', $folder_name.'/'.$my_xml_file_path);
8333
                        // adlcp:scormtype can be either 'sco' or 'asset'.
8334
                        $my_resource->setAttribute('adlcp:scormtype', 'asset');
8335
                        // xml:base is the base directory to find the files declared in this resource.
8336
                        $my_resource->setAttribute('xml:base', '');
8337
                        // Give a <file> child to the <resource> element.
8338
                        $my_file = $xmldoc->createElement('file');
8339
                        $my_file->setAttribute('href', 'document/'.$my_xml_file_path);
8340
                        $my_resource->appendChild($my_file);
8341
                        $resources->appendChild($my_resource);
8342
                        break;
8343
                }
8344
            }
8345
        }
8346
        $organizations->appendChild($organization);
8347
        $root->appendChild($organizations);
8348
        $root->appendChild($resources);
8349
        $xmldoc->appendChild($root);
8350
8351
        $copyAll = api_get_configuration_value('add_all_files_in_lp_export');
8352
8353
        // then add the file to the zip, then destroy the file (this is done automatically).
8354
        // http://www.reload.ac.uk/scormplayer.html - once done, don't forget to close FS#138
8355
        foreach ($zip_files as $file_path) {
8356
            if (empty($file_path)) {
8357
                continue;
8358
            }
8359
8360
            $filePath = $sys_course_path.$_course['path'].'/'.$file_path;
8361
            $dest_file = $archivePath.$temp_dir_short.'/'.$file_path;
8362
8363
            if (!empty($path_to_remove) && !empty($path_to_replace)) {
8364
                $dest_file = str_replace($path_to_remove, $path_to_replace, $dest_file);
8365
            }
8366
8367
            $this->create_path($dest_file);
8368
            @copy($filePath, $dest_file);
8369
8370
            // Check if the file needs a link update.
8371
            if (in_array($file_path, array_keys($link_updates))) {
8372
                $string = file_get_contents($dest_file);
8373
                unlink($dest_file);
8374
                foreach ($link_updates[$file_path] as $old_new) {
8375
                    // This is an ugly hack that allows .flv files to be found by the flv player that
8376
                    // will be added in document/main/inc/lib/flv_player/flv_player.swf and that needs
8377
                    // to find the flv to play in document/main/, so we replace main/ in the flv path by
8378
                    // ../../.. to return from inc/lib/flv_player to the document/main path.
8379
                    if ('flv' === substr($old_new['dest'], -3) &&
8380
                        'main/' === substr($old_new['dest'], 0, 5)
8381
                    ) {
8382
                        $old_new['dest'] = str_replace('main/', '../../../', $old_new['dest']);
8383
                    } elseif ('flv' === substr($old_new['dest'], -3) &&
8384
                        'video/' === substr($old_new['dest'], 0, 6)
8385
                    ) {
8386
                        $old_new['dest'] = str_replace('video/', '../../../../video/', $old_new['dest']);
8387
                    }
8388
8389
                    // Fix to avoid problems with default_course_document
8390
                    if (false === strpos('main/default_course_document', $old_new['dest'])) {
8391
                        $newDestination = $old_new['dest'];
8392
                        if (isset($old_new['replace']) && !empty($old_new['replace'])) {
8393
                            $newDestination = $old_new['replace'];
8394
                        }
8395
                    } else {
8396
                        $newDestination = str_replace('document/', '', $old_new['dest']);
8397
                    }
8398
                    $string = str_replace($old_new['orig'], $newDestination, $string);
8399
8400
                    // Add files inside the HTMLs
8401
                    $new_path = str_replace(api_get_path(REL_COURSE_PATH), '', $old_new['orig']);
8402
                    $destinationFile = $archivePath.$temp_dir_short.'/'.$old_new['dest'];
8403
                    if (file_exists($sys_course_path.$new_path) && is_file($sys_course_path.$new_path)) {
8404
                        copy(
8405
                            $sys_course_path.$new_path,
8406
                            $destinationFile
8407
                        );
8408
                    }
8409
                }
8410
                file_put_contents($dest_file, $string);
8411
            }
8412
8413
            if (file_exists($filePath) && $copyAll) {
8414
                $extension = $this->get_extension($filePath);
8415
                if (in_array($extension, ['html', 'html'])) {
8416
                    $containerOrigin = dirname($filePath);
8417
                    $containerDestination = dirname($dest_file);
8418
8419
                    $finder = new Finder();
8420
                    $finder->files()->in($containerOrigin)
8421
                        ->notName('*_DELETED_*')
8422
                        ->exclude('share_folder')
8423
                        ->exclude('chat_files')
8424
                        ->exclude('certificates')
8425
                    ;
8426
8427
                    if (is_dir($containerOrigin) &&
8428
                        is_dir($containerDestination)
8429
                    ) {
8430
                        $fs = new Filesystem();
8431
                        $fs->mirror(
8432
                            $containerOrigin,
8433
                            $containerDestination,
8434
                            $finder
8435
                        );
8436
                    }
8437
                }
8438
            }
8439
        }
8440
8441
        foreach ($zip_files_abs as $file_path) {
8442
            if (empty($file_path)) {
8443
                continue;
8444
            }
8445
8446
            if (!is_file($main_path.$file_path) || !is_readable($main_path.$file_path)) {
8447
                continue;
8448
            }
8449
8450
            $dest_file = $archivePath.$temp_dir_short.'/document/'.$file_path;
8451
            if (false !== strstr($file_path, 'upload/users')) {
8452
                $pos = strpos($file_path, 'my_files/');
8453
                if (false !== $pos) {
8454
                    $onlyDirectory = str_replace(
8455
                        'upload/users/',
8456
                        '',
8457
                        substr($file_path, $pos, strlen($file_path))
8458
                    );
8459
                    $dest_file = $archivePath.$temp_dir_short.'/document/'.$onlyDirectory;
8460
                }
8461
            }
8462
8463
            if (false !== strstr($file_path, 'default_course_document/')) {
8464
                $replace = str_replace('/main', '', $file_path);
8465
                $dest_file = $archivePath.$temp_dir_short.'/document/'.$replace;
8466
            }
8467
8468
            if (empty($dest_file)) {
8469
                continue;
8470
            }
8471
8472
            $this->create_path($dest_file);
8473
            copy($main_path.$file_path, $dest_file);
8474
            // Check if the file needs a link update.
8475
            if (in_array($file_path, array_keys($link_updates))) {
8476
                $string = file_get_contents($dest_file);
8477
                unlink($dest_file);
8478
                foreach ($link_updates[$file_path] as $old_new) {
8479
                    // This is an ugly hack that allows .flv files to be found by the flv player that
8480
                    // will be added in document/main/inc/lib/flv_player/flv_player.swf and that needs
8481
                    // to find the flv to play in document/main/, so we replace main/ in the flv path by
8482
                    // ../../.. to return from inc/lib/flv_player to the document/main path.
8483
                    if ('flv' == substr($old_new['dest'], -3) &&
8484
                        'main/' == substr($old_new['dest'], 0, 5)
8485
                    ) {
8486
                        $old_new['dest'] = str_replace('main/', '../../../', $old_new['dest']);
8487
                    }
8488
                    $string = str_replace($old_new['orig'], $old_new['dest'], $string);
8489
                }
8490
                file_put_contents($dest_file, $string);
8491
            }
8492
        }
8493
8494
        if (is_array($links_to_create)) {
8495
            foreach ($links_to_create as $file => $link) {
8496
                $content = '<!DOCTYPE html><head>
8497
                            <meta charset="'.api_get_language_isocode().'" />
8498
                            <title>'.$link['title'].'</title>
8499
                            </head>
8500
                            <body dir="'.api_get_text_direction().'">
8501
                            <div style="text-align:center">
8502
                            <a href="'.$link['url'].'">'.$link['title'].'</a></div>
8503
                            </body>
8504
                            </html>';
8505
                file_put_contents($archivePath.$temp_dir_short.'/'.$file, $content);
8506
            }
8507
        }
8508
8509
        // Add non exportable message explanation.
8510
        $lang_not_exportable = get_lang('This learning object or activity is not SCORM compliant. That\'s why it is not exportable.');
8511
        $file_content = '<!DOCTYPE html><head>
8512
                        <meta charset="'.api_get_language_isocode().'" />
8513
                        <title>'.$lang_not_exportable.'</title>
8514
                        <meta http-equiv="Content-Type" content="text/html; charset='.api_get_system_encoding().'" />
8515
                        </head>
8516
                        <body dir="'.api_get_text_direction().'">';
8517
        $file_content .=
8518
            <<<EOD
8519
                    <style>
8520
            .error-message {
8521
                font-family: arial, verdana, helvetica, sans-serif;
8522
                border-width: 1px;
8523
                border-style: solid;
8524
                left: 50%;
8525
                margin: 10px auto;
8526
                min-height: 30px;
8527
                padding: 5px;
8528
                right: 50%;
8529
                width: 500px;
8530
                background-color: #FFD1D1;
8531
                border-color: #FF0000;
8532
                color: #000;
8533
            }
8534
        </style>
8535
    <body>
8536
        <div class="error-message">
8537
            $lang_not_exportable
8538
        </div>
8539
    </body>
8540
</html>
8541
EOD;
8542
        if (!is_dir($archivePath.$temp_dir_short.'/document')) {
8543
            @mkdir($archivePath.$temp_dir_short.'/document', api_get_permissions_for_new_directories());
8544
        }
8545
        file_put_contents($archivePath.$temp_dir_short.'/document/non_exportable.html', $file_content);
8546
8547
        // Add the extra files that go along with a SCORM package.
8548
        $main_code_path = api_get_path(SYS_CODE_PATH).'lp/packaging/';
8549
8550
        $fs = new Filesystem();
8551
        $fs->mirror($main_code_path, $archivePath.$temp_dir_short);
8552
8553
        // Finalize the imsmanifest structure, add to the zip, then return the zip.
8554
        $manifest = @$xmldoc->saveXML();
8555
        $manifest = api_utf8_decode_xml($manifest); // The manifest gets the system encoding now.
8556
        file_put_contents($archivePath.'/'.$temp_dir_short.'/imsmanifest.xml', $manifest);
8557
        $zip_folder->add(
8558
            $archivePath.'/'.$temp_dir_short,
8559
            PCLZIP_OPT_REMOVE_PATH,
8560
            $archivePath.'/'.$temp_dir_short.'/'
8561
        );
8562
8563
        // Clean possible temporary files.
8564
        foreach ($files_cleanup as $file) {
8565
            $res = unlink($file);
8566
            if (false === $res) {
8567
                error_log(
8568
                    'Could not delete temp file '.$file.' '.__FILE__.' '.__LINE__,
8569
                    0
8570
                );
8571
            }
8572
        }
8573
        $name = api_replace_dangerous_char($this->get_name()).'.zip';
8574
        DocumentManager::file_send_for_download($temp_zip_file, true, $name);
8575
    }
8576
8577
    /**
8578
     * @param int $lp_id
8579
     *
8580
     * @return bool
8581
     */
8582
    public function scorm_export_to_pdf($lp_id)
8583
    {
8584
        $lp_id = (int) $lp_id;
8585
        $files_to_export = [];
8586
8587
        $sessionId = api_get_session_id();
8588
        $course_data = api_get_course_info($this->cc);
8589
8590
        $lp = Container::getLpRepository()->find($lp_id);
8591
        if (!empty($course_data)) {
8592
            $scorm_path = api_get_path(SYS_COURSE_PATH).$course_data['path'].'/scorm/'.$this->path;
8593
            $list = self::get_flat_ordered_items_list($lp);
8594
            if (!empty($list)) {
8595
                foreach ($list as $item_id) {
8596
                    $item = $this->items[$item_id];
8597
                    switch ($item->type) {
8598
                        case 'document':
8599
                            // Getting documents from a LP with chamilo documents
8600
                            $file_data = DocumentManager::get_document_data_by_id($item->path, $this->cc);
8601
                            // Try loading document from the base course.
8602
                            if (empty($file_data) && !empty($sessionId)) {
8603
                                $file_data = DocumentManager::get_document_data_by_id(
8604
                                    $item->path,
8605
                                    $this->cc,
8606
                                    false,
8607
                                    0
8608
                                );
8609
                            }
8610
                            $file_path = api_get_path(SYS_COURSE_PATH).$course_data['path'].'/document'.$file_data['path'];
8611
                            if (file_exists($file_path)) {
8612
                                $files_to_export[] = [
8613
                                    'title' => $item->get_title(),
8614
                                    'path' => $file_path,
8615
                                ];
8616
                            }
8617
                            break;
8618
                        case 'asset': //commes from a scorm package generated by chamilo
8619
                        case 'sco':
8620
                            $file_path = $scorm_path.'/'.$item->path;
8621
                            if (file_exists($file_path)) {
8622
                                $files_to_export[] = [
8623
                                    'title' => $item->get_title(),
8624
                                    'path' => $file_path,
8625
                                ];
8626
                            }
8627
                            break;
8628
                        case 'dir':
8629
                            $files_to_export[] = [
8630
                                'title' => $item->get_title(),
8631
                                'path' => null,
8632
                            ];
8633
                            break;
8634
                    }
8635
                }
8636
            }
8637
8638
            $pdf = new PDF();
8639
            $result = $pdf->html_to_pdf(
8640
                $files_to_export,
8641
                $this->name,
8642
                $this->cc,
8643
                true,
8644
                true,
8645
                true,
8646
                $this->get_name()
8647
            );
8648
8649
            return $result;
8650
        }
8651
8652
        return false;
8653
    }
8654
8655
    /**
8656
     * Temp function to be moved in main_api or the best place around for this.
8657
     * Creates a file path if it doesn't exist.
8658
     *
8659
     * @param string $path
8660
     */
8661
    public function create_path($path)
8662
    {
8663
        $path_bits = explode('/', dirname($path));
8664
8665
        // IS_WINDOWS_OS has been defined in main_api.lib.php
8666
        $path_built = IS_WINDOWS_OS ? '' : '/';
8667
        foreach ($path_bits as $bit) {
8668
            if (!empty($bit)) {
8669
                $new_path = $path_built.$bit;
8670
                if (is_dir($new_path)) {
8671
                    $path_built = $new_path.'/';
8672
                } else {
8673
                    mkdir($new_path, api_get_permissions_for_new_directories());
8674
                    $path_built = $new_path.'/';
8675
                }
8676
            }
8677
        }
8678
    }
8679
8680
    /**
8681
     * @param int    $lp_id
8682
     * @param string $status
8683
     */
8684
    public function set_autolaunch($lp_id, $status)
8685
    {
8686
        $course_id = api_get_course_int_id();
8687
        $lp_id = (int) $lp_id;
8688
        $status = (int) $status;
8689
        $lp_table = Database::get_course_table(TABLE_LP_MAIN);
8690
8691
        // Setting everything to autolaunch = 0
8692
        $attributes['autolaunch'] = 0;
8693
        $where = [
8694
            'session_id = ? AND c_id = ? ' => [
8695
                api_get_session_id(),
8696
                $course_id,
8697
            ],
8698
        ];
8699
        Database::update($lp_table, $attributes, $where);
8700
        if (1 == $status) {
8701
            //Setting my lp_id to autolaunch = 1
8702
            $attributes['autolaunch'] = 1;
8703
            $where = [
8704
                'iid = ? AND session_id = ? AND c_id = ?' => [
8705
                    $lp_id,
8706
                    api_get_session_id(),
8707
                    $course_id,
8708
                ],
8709
            ];
8710
            Database::update($lp_table, $attributes, $where);
8711
        }
8712
    }
8713
8714
    /**
8715
     * Gets previous_item_id for the next element of the lp_item table.
8716
     *
8717
     * @author Isaac flores paz
8718
     *
8719
     * @return int Previous item ID
8720
     */
8721
    public function select_previous_item_id()
8722
    {
8723
        $course_id = api_get_course_int_id();
8724
        $table_lp_item = Database::get_course_table(TABLE_LP_ITEM);
8725
8726
        // Get the max order of the items
8727
        $sql = "SELECT max(display_order) AS display_order FROM $table_lp_item
8728
                WHERE c_id = $course_id AND lp_id = ".$this->lp_id;
8729
        $rs_max_order = Database::query($sql);
8730
        $row_max_order = Database::fetch_object($rs_max_order);
8731
        $max_order = $row_max_order->display_order;
8732
        // Get the previous item ID
8733
        $sql = "SELECT iid as previous FROM $table_lp_item
8734
                WHERE
8735
                    c_id = $course_id AND
8736
                    lp_id = ".$this->lp_id." AND
8737
                    display_order = '$max_order' ";
8738
        $rs_max = Database::query($sql);
8739
        $row_max = Database::fetch_object($rs_max);
8740
8741
        // Return the previous item ID
8742
        return $row_max->previous;
8743
    }
8744
8745
    /**
8746
     * Copies an LP.
8747
     */
8748
    public function copy()
8749
    {
8750
        // Course builder
8751
        $cb = new CourseBuilder();
8752
8753
        //Setting tools that will be copied
8754
        $cb->set_tools_to_build(['learnpaths']);
8755
8756
        //Setting elements that will be copied
8757
        $cb->set_tools_specific_id_list(
8758
            ['learnpaths' => [$this->lp_id]]
8759
        );
8760
8761
        $course = $cb->build();
8762
8763
        //Course restorer
8764
        $course_restorer = new CourseRestorer($course);
8765
        $course_restorer->set_add_text_in_items(true);
8766
        $course_restorer->set_tool_copy_settings(
8767
            ['learnpaths' => ['reset_dates' => true]]
8768
        );
8769
        $course_restorer->restore(
8770
            api_get_course_id(),
8771
            api_get_session_id(),
8772
            false,
8773
            false
8774
        );
8775
    }
8776
8777
    /**
8778
     * Verify document size.
8779
     *
8780
     * @param string $s
8781
     *
8782
     * @return bool
8783
     */
8784
    public static function verify_document_size($s)
8785
    {
8786
        $post_max = ini_get('post_max_size');
8787
        if ('M' == substr($post_max, -1, 1)) {
8788
            $post_max = intval(substr($post_max, 0, -1)) * 1024 * 1024;
8789
        } elseif ('G' == substr($post_max, -1, 1)) {
8790
            $post_max = intval(substr($post_max, 0, -1)) * 1024 * 1024 * 1024;
8791
        }
8792
        $upl_max = ini_get('upload_max_filesize');
8793
        if ('M' == substr($upl_max, -1, 1)) {
8794
            $upl_max = intval(substr($upl_max, 0, -1)) * 1024 * 1024;
8795
        } elseif ('G' == substr($upl_max, -1, 1)) {
8796
            $upl_max = intval(substr($upl_max, 0, -1)) * 1024 * 1024 * 1024;
8797
        }
8798
8799
        $repo = Container::getDocumentRepository();
8800
        $documents_total_space = $repo->getTotalSpace(api_get_course_int_id());
8801
8802
        $course_max_space = DocumentManager::get_course_quota();
8803
        $total_size = filesize($s) + $documents_total_space;
8804
        if (filesize($s) > $post_max || filesize($s) > $upl_max || $total_size > $course_max_space) {
8805
            return true;
8806
        }
8807
8808
        return false;
8809
    }
8810
8811
    /**
8812
     * Clear LP prerequisites.
8813
     */
8814
    public function clearPrerequisites()
8815
    {
8816
        $course_id = $this->get_course_int_id();
8817
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
8818
        $lp_id = $this->get_id();
8819
        // Cleaning prerequisites
8820
        $sql = "UPDATE $tbl_lp_item SET prerequisite = ''
8821
                WHERE lp_id = $lp_id";
8822
        Database::query($sql);
8823
8824
        // Cleaning mastery score for exercises
8825
        $sql = "UPDATE $tbl_lp_item SET mastery_score = ''
8826
                WHERE lp_id = $lp_id AND item_type = 'quiz'";
8827
        Database::query($sql);
8828
    }
8829
8830
    public function set_previous_step_as_prerequisite_for_all_items()
8831
    {
8832
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
8833
        $course_id = $this->get_course_int_id();
8834
        $lp_id = $this->get_id();
8835
8836
        if (!empty($this->items)) {
8837
            $previous_item_id = null;
8838
            $previous_item_max = 0;
8839
            $previous_item_type = null;
8840
            $last_item_not_dir = null;
8841
            $last_item_not_dir_type = null;
8842
            $last_item_not_dir_max = null;
8843
8844
            foreach ($this->ordered_items as $itemId) {
8845
                $item = $this->getItem($itemId);
8846
                // if there was a previous item... (otherwise jump to set it)
8847
                if (!empty($previous_item_id)) {
8848
                    $current_item_id = $item->get_id(); //save current id
8849
                    if ('dir' != $item->get_type()) {
8850
                        // Current item is not a folder, so it qualifies to get a prerequisites
8851
                        if ('quiz' == $last_item_not_dir_type) {
8852
                            // if previous is quiz, mark its max score as default score to be achieved
8853
                            $sql = "UPDATE $tbl_lp_item SET mastery_score = '$last_item_not_dir_max'
8854
                                    WHERE c_id = $course_id AND lp_id = $lp_id AND iid = $last_item_not_dir";
8855
                            Database::query($sql);
8856
                        }
8857
                        // now simply update the prerequisite to set it to the last non-chapter item
8858
                        $sql = "UPDATE $tbl_lp_item SET prerequisite = '$last_item_not_dir'
8859
                                WHERE lp_id = $lp_id AND iid = $current_item_id";
8860
                        Database::query($sql);
8861
                        // record item as 'non-chapter' reference
8862
                        $last_item_not_dir = $item->get_id();
8863
                        $last_item_not_dir_type = $item->get_type();
8864
                        $last_item_not_dir_max = $item->get_max();
8865
                    }
8866
                } else {
8867
                    if ('dir' != $item->get_type()) {
8868
                        // Current item is not a folder (but it is the first item) so record as last "non-chapter" item
8869
                        $last_item_not_dir = $item->get_id();
8870
                        $last_item_not_dir_type = $item->get_type();
8871
                        $last_item_not_dir_max = $item->get_max();
8872
                    }
8873
                }
8874
                // Saving the item as "previous item" for the next loop
8875
                $previous_item_id = $item->get_id();
8876
                $previous_item_max = $item->get_max();
8877
                $previous_item_type = $item->get_type();
8878
            }
8879
        }
8880
    }
8881
8882
    /**
8883
     * @param array $params
8884
     *
8885
     * @return int
8886
     */
8887
    public static function createCategory($params)
8888
    {
8889
        $courseEntity = api_get_course_entity(api_get_course_int_id());
8890
8891
        $item = new CLpCategory();
8892
        $item
8893
            ->setName($params['name'])
8894
            ->setParent($courseEntity)
8895
            ->addCourseLink($courseEntity, api_get_session_entity())
8896
        ;
8897
8898
        $repo = Container::getLpCategoryRepository();
8899
        $repo->create($item);
8900
8901
        return $item->getIid();
8902
    }
8903
8904
    /**
8905
     * @param array $params
8906
     */
8907
    public static function updateCategory($params)
8908
    {
8909
        $em = Database::getManager();
8910
        /** @var CLpCategory $item */
8911
        $item = $em->find(CLpCategory::class, $params['id']);
8912
        if ($item) {
8913
            $item->setName($params['name']);
8914
            $em->persist($item);
8915
            $em->flush();
8916
        }
8917
    }
8918
8919
    /**
8920
     * @param int $id
8921
     */
8922
    public static function moveUpCategory($id)
8923
    {
8924
        $id = (int) $id;
8925
        $em = Database::getManager();
8926
        /** @var CLpCategory $item */
8927
        $item = $em->find(CLpCategory::class, $id);
8928
        if ($item) {
8929
            $position = $item->getPosition() - 1;
8930
            $item->setPosition($position);
8931
            $em->persist($item);
8932
            $em->flush();
8933
        }
8934
    }
8935
8936
    /**
8937
     * @param int $id
8938
     */
8939
    public static function moveDownCategory($id)
8940
    {
8941
        $id = (int) $id;
8942
        $em = Database::getManager();
8943
        /** @var CLpCategory $item */
8944
        $item = $em->find(CLpCategory::class, $id);
8945
        if ($item) {
8946
            $position = $item->getPosition() + 1;
8947
            $item->setPosition($position);
8948
            $em->persist($item);
8949
            $em->flush();
8950
        }
8951
    }
8952
8953
    /**
8954
     * @param int $courseId
8955
     *
8956
     * @return int
8957
     */
8958
    public static function getCountCategories($courseId)
8959
    {
8960
        if (empty($courseId)) {
8961
            return 0;
8962
        }
8963
        $repo = Container::getLpCategoryRepository();
8964
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId));
8965
        $qb->addSelect('count(resource)');
8966
8967
        return (int) $qb->getQuery()->getSingleScalarResult();
8968
    }
8969
8970
    /**
8971
     * @param int $courseId
8972
     *
8973
     * @return CLpCategory[]
8974
     */
8975
    public static function getCategories($courseId)
8976
    {
8977
        // Using doctrine extensions
8978
        $repo = Container::getLpCategoryRepository();
8979
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId));
8980
8981
        return $qb->getQuery()->getResult();
8982
    }
8983
8984
    public static function getCategorySessionId($id)
8985
    {
8986
        if (false === api_get_configuration_value('allow_session_lp_category')) {
8987
            return 0;
8988
        }
8989
8990
        $table = Database::get_course_table(TABLE_LP_CATEGORY);
8991
        $id = (int) $id;
8992
8993
        $sql = "SELECT session_id FROM $table WHERE iid = $id";
8994
        $result = Database::query($sql);
8995
        $result = Database::fetch_array($result, 'ASSOC');
8996
8997
        if ($result) {
8998
            return (int) $result['session_id'];
8999
        }
9000
9001
        return 0;
9002
    }
9003
9004
    /**
9005
     * @param int $id
9006
     */
9007
    public static function deleteCategory($id): bool
9008
    {
9009
        $repo = Container::getLpCategoryRepository();
9010
        /** @var CLpCategory $category */
9011
        $category = $repo->find($id);
9012
        if ($category) {
9013
            $em = Database::getManager();
9014
            $lps = $category->getLps();
9015
9016
            foreach ($lps as $lp) {
9017
                $lp->setCategory(null);
9018
                $em->persist($lp);
9019
            }
9020
9021
            // Removing category.
9022
            $em->remove($category);
9023
            $em->flush();
9024
9025
            return true;
9026
        }
9027
9028
        return false;
9029
    }
9030
9031
    /**
9032
     * @param int  $courseId
9033
     * @param bool $addSelectOption
9034
     *
9035
     * @return array
9036
     */
9037
    public static function getCategoryFromCourseIntoSelect($courseId, $addSelectOption = false)
9038
    {
9039
        $repo = Container::getLpCategoryRepository();
9040
        $qb = $repo->getResourcesByCourse(api_get_course_entity($courseId));
9041
        $items = $qb->getQuery()->getResult();
9042
9043
        $cats = [];
9044
        if ($addSelectOption) {
9045
            $cats = [get_lang('Select a category')];
9046
        }
9047
9048
        if (!empty($items)) {
9049
            foreach ($items as $cat) {
9050
                $cats[$cat->getIid()] = $cat->getName();
9051
            }
9052
        }
9053
9054
        return $cats;
9055
    }
9056
9057
    /**
9058
     * @param string $courseCode
9059
     * @param int    $lpId
9060
     * @param int    $user_id
9061
     *
9062
     * @return learnpath
9063
     */
9064
    public static function getLpFromSession($courseCode, $lpId, $user_id)
9065
    {
9066
        $debug = 0;
9067
        $learnPath = null;
9068
        $lpObject = Session::read('lpobject');
9069
9070
        $repo = Container::getLpRepository();
9071
        $lp = $repo->find($lpId);
9072
        if (null !== $lpObject) {
9073
            /** @var learnpath $learnPath */
9074
            $learnPath = UnserializeApi::unserialize('lp', $lpObject);
9075
            $learnPath->entity = $lp;
9076
            if ($debug) {
9077
                error_log('getLpFromSession: unserialize');
9078
                error_log('------getLpFromSession------');
9079
                error_log('------unserialize------');
9080
                error_log("lp_view_session_id: ".$learnPath->lp_view_session_id);
9081
                error_log("api_get_sessionid: ".api_get_session_id());
9082
            }
9083
        }
9084
9085
        if (!is_object($learnPath)) {
9086
            $learnPath = new learnpath($lp, api_get_course_info($courseCode), $user_id);
9087
            if ($debug) {
9088
                error_log('------getLpFromSession------');
9089
                error_log('getLpFromSession: create new learnpath');
9090
                error_log("create new LP with $courseCode - $lpId - $user_id");
9091
                error_log("lp_view_session_id: ".$learnPath->lp_view_session_id);
9092
                error_log("api_get_sessionid: ".api_get_session_id());
9093
            }
9094
        }
9095
9096
        return $learnPath;
9097
    }
9098
9099
    /**
9100
     * @param int $itemId
9101
     *
9102
     * @return learnpathItem|false
9103
     */
9104
    public function getItem($itemId)
9105
    {
9106
        if (isset($this->items[$itemId]) && is_object($this->items[$itemId])) {
9107
            return $this->items[$itemId];
9108
        }
9109
9110
        return false;
9111
    }
9112
9113
    /**
9114
     * @return int
9115
     */
9116
    public function getCurrentAttempt()
9117
    {
9118
        $attempt = $this->getItem($this->get_current_item_id());
9119
        if ($attempt) {
9120
            return $attempt->get_attempt_id();
9121
        }
9122
9123
        return 0;
9124
    }
9125
9126
    /**
9127
     * @return int
9128
     */
9129
    public function getCategoryId()
9130
    {
9131
        return (int) $this->categoryId;
9132
    }
9133
9134
    /**
9135
     * Get whether this is a learning path with the possibility to subscribe
9136
     * users or not.
9137
     *
9138
     * @return int
9139
     */
9140
    public function getSubscribeUsers()
9141
    {
9142
        return $this->subscribeUsers;
9143
    }
9144
9145
    /**
9146
     * Calculate the count of stars for a user in this LP
9147
     * This calculation is based on the following rules:
9148
     * - the student gets one star when he gets to 50% of the learning path
9149
     * - the student gets a second star when the average score of all tests inside the learning path >= 50%
9150
     * - the student gets a third star when the average score of all tests inside the learning path >= 80%
9151
     * - the student gets the final star when the score for the *last* test is >= 80%.
9152
     *
9153
     * @param int $sessionId Optional. The session ID
9154
     *
9155
     * @return int The count of stars
9156
     */
9157
    public function getCalculateStars($sessionId = 0)
9158
    {
9159
        $stars = 0;
9160
        $progress = self::getProgress(
9161
            $this->lp_id,
9162
            $this->user_id,
9163
            $this->course_int_id,
9164
            $sessionId
9165
        );
9166
9167
        if ($progress >= 50) {
9168
            $stars++;
9169
        }
9170
9171
        // Calculate stars chapters evaluation
9172
        $exercisesItems = $this->getExercisesItems();
9173
9174
        if (!empty($exercisesItems)) {
9175
            $totalResult = 0;
9176
9177
            foreach ($exercisesItems as $exerciseItem) {
9178
                $exerciseResultInfo = Event::getExerciseResultsByUser(
9179
                    $this->user_id,
9180
                    $exerciseItem->path,
9181
                    $this->course_int_id,
9182
                    $sessionId,
9183
                    $this->lp_id,
9184
                    $exerciseItem->db_id
9185
                );
9186
9187
                $exerciseResultInfo = end($exerciseResultInfo);
9188
9189
                if (!$exerciseResultInfo) {
9190
                    continue;
9191
                }
9192
9193
                if (!empty($exerciseResultInfo['max_score'])) {
9194
                    $exerciseResult = $exerciseResultInfo['score'] * 100 / $exerciseResultInfo['max_score'];
9195
                } else {
9196
                    $exerciseResult = 0;
9197
                }
9198
                $totalResult += $exerciseResult;
9199
            }
9200
9201
            $totalExerciseAverage = $totalResult / (count($exercisesItems) > 0 ? count($exercisesItems) : 1);
9202
9203
            if ($totalExerciseAverage >= 50) {
9204
                $stars++;
9205
            }
9206
9207
            if ($totalExerciseAverage >= 80) {
9208
                $stars++;
9209
            }
9210
        }
9211
9212
        // Calculate star for final evaluation
9213
        $finalEvaluationItem = $this->getFinalEvaluationItem();
9214
9215
        if (!empty($finalEvaluationItem)) {
9216
            $evaluationResultInfo = Event::getExerciseResultsByUser(
9217
                $this->user_id,
9218
                $finalEvaluationItem->path,
9219
                $this->course_int_id,
9220
                $sessionId,
9221
                $this->lp_id,
9222
                $finalEvaluationItem->db_id
9223
            );
9224
9225
            $evaluationResultInfo = end($evaluationResultInfo);
9226
9227
            if ($evaluationResultInfo) {
9228
                $evaluationResult = $evaluationResultInfo['score'] * 100 / $evaluationResultInfo['max_score'];
9229
                if ($evaluationResult >= 80) {
9230
                    $stars++;
9231
                }
9232
            }
9233
        }
9234
9235
        return $stars;
9236
    }
9237
9238
    /**
9239
     * Get the items of exercise type.
9240
     *
9241
     * @return array The items. Otherwise return false
9242
     */
9243
    public function getExercisesItems()
9244
    {
9245
        $exercises = [];
9246
        foreach ($this->items as $item) {
9247
            if ('quiz' !== $item->type) {
9248
                continue;
9249
            }
9250
            $exercises[] = $item;
9251
        }
9252
9253
        array_pop($exercises);
9254
9255
        return $exercises;
9256
    }
9257
9258
    /**
9259
     * Get the item of exercise type (evaluation type).
9260
     *
9261
     * @return array The final evaluation. Otherwise return false
9262
     */
9263
    public function getFinalEvaluationItem()
9264
    {
9265
        $exercises = [];
9266
        foreach ($this->items as $item) {
9267
            if (TOOL_QUIZ !== $item->type) {
9268
                continue;
9269
            }
9270
9271
            $exercises[] = $item;
9272
        }
9273
9274
        return array_pop($exercises);
9275
    }
9276
9277
    /**
9278
     * Calculate the total points achieved for the current user in this learning path.
9279
     *
9280
     * @param int $sessionId Optional. The session Id
9281
     *
9282
     * @return int
9283
     */
9284
    public function getCalculateScore($sessionId = 0)
9285
    {
9286
        // Calculate stars chapters evaluation
9287
        $exercisesItems = $this->getExercisesItems();
9288
        $finalEvaluationItem = $this->getFinalEvaluationItem();
9289
        $totalExercisesResult = 0;
9290
        $totalEvaluationResult = 0;
9291
9292
        if (false !== $exercisesItems) {
9293
            foreach ($exercisesItems as $exerciseItem) {
9294
                $exerciseResultInfo = Event::getExerciseResultsByUser(
9295
                    $this->user_id,
9296
                    $exerciseItem->path,
9297
                    $this->course_int_id,
9298
                    $sessionId,
9299
                    $this->lp_id,
9300
                    $exerciseItem->db_id
9301
                );
9302
9303
                $exerciseResultInfo = end($exerciseResultInfo);
9304
9305
                if (!$exerciseResultInfo) {
9306
                    continue;
9307
                }
9308
9309
                $totalExercisesResult += $exerciseResultInfo['score'];
9310
            }
9311
        }
9312
9313
        if (!empty($finalEvaluationItem)) {
9314
            $evaluationResultInfo = Event::getExerciseResultsByUser(
9315
                $this->user_id,
9316
                $finalEvaluationItem->path,
9317
                $this->course_int_id,
9318
                $sessionId,
9319
                $this->lp_id,
9320
                $finalEvaluationItem->db_id
9321
            );
9322
9323
            $evaluationResultInfo = end($evaluationResultInfo);
9324
9325
            if ($evaluationResultInfo) {
9326
                $totalEvaluationResult += $evaluationResultInfo['score'];
9327
            }
9328
        }
9329
9330
        return $totalExercisesResult + $totalEvaluationResult;
9331
    }
9332
9333
    /**
9334
     * Check if URL is not allowed to be show in a iframe.
9335
     *
9336
     * @param string $src
9337
     *
9338
     * @return string
9339
     */
9340
    public function fixBlockedLinks($src)
9341
    {
9342
        $urlInfo = parse_url($src);
9343
9344
        $platformProtocol = 'https';
9345
        if (false === strpos(api_get_path(WEB_CODE_PATH), 'https')) {
9346
            $platformProtocol = 'http';
9347
        }
9348
9349
        $protocolFixApplied = false;
9350
        //Scheme validation to avoid "Notices" when the lesson doesn't contain a valid scheme
9351
        $scheme = isset($urlInfo['scheme']) ? $urlInfo['scheme'] : null;
9352
        $host = isset($urlInfo['host']) ? $urlInfo['host'] : null;
9353
9354
        if ($platformProtocol != $scheme) {
9355
            Session::write('x_frame_source', $src);
9356
            $src = 'blank.php?error=x_frames_options';
9357
            $protocolFixApplied = true;
9358
        }
9359
9360
        if (false == $protocolFixApplied) {
9361
            if (false === strpos(api_get_path(WEB_PATH), $host)) {
9362
                // Check X-Frame-Options
9363
                $ch = curl_init();
9364
                $options = [
9365
                    CURLOPT_URL => $src,
9366
                    CURLOPT_RETURNTRANSFER => true,
9367
                    CURLOPT_HEADER => true,
9368
                    CURLOPT_FOLLOWLOCATION => true,
9369
                    CURLOPT_ENCODING => "",
9370
                    CURLOPT_AUTOREFERER => true,
9371
                    CURLOPT_CONNECTTIMEOUT => 120,
9372
                    CURLOPT_TIMEOUT => 120,
9373
                    CURLOPT_MAXREDIRS => 10,
9374
                ];
9375
9376
                $proxySettings = api_get_configuration_value('proxy_settings');
9377
                if (!empty($proxySettings) &&
9378
                    isset($proxySettings['curl_setopt_array'])
9379
                ) {
9380
                    $options[CURLOPT_PROXY] = $proxySettings['curl_setopt_array']['CURLOPT_PROXY'];
9381
                    $options[CURLOPT_PROXYPORT] = $proxySettings['curl_setopt_array']['CURLOPT_PROXYPORT'];
9382
                }
9383
9384
                curl_setopt_array($ch, $options);
9385
                $response = curl_exec($ch);
9386
                $httpCode = curl_getinfo($ch);
9387
                $headers = substr($response, 0, $httpCode['header_size']);
9388
9389
                $error = false;
9390
                if (stripos($headers, 'X-Frame-Options: DENY') > -1
9391
                    //|| stripos($headers, 'X-Frame-Options: SAMEORIGIN') > -1
9392
                ) {
9393
                    $error = true;
9394
                }
9395
9396
                if ($error) {
9397
                    Session::write('x_frame_source', $src);
9398
                    $src = 'blank.php?error=x_frames_options';
9399
                }
9400
            }
9401
        }
9402
9403
        return $src;
9404
    }
9405
9406
    /**
9407
     * Check if this LP has a created forum in the basis course.
9408
     *
9409
     * @deprecated
9410
     *
9411
     * @return bool
9412
     */
9413
    public function lpHasForum()
9414
    {
9415
        $forumTable = Database::get_course_table(TABLE_FORUM);
9416
        $itemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
9417
9418
        $fakeFrom = "
9419
            $forumTable f
9420
            INNER JOIN $itemProperty ip
9421
            ON (f.forum_id = ip.ref AND f.c_id = ip.c_id)
9422
        ";
9423
9424
        $resultData = Database::select(
9425
            'COUNT(f.iid) AS qty',
9426
            $fakeFrom,
9427
            [
9428
                'where' => [
9429
                    'ip.visibility != ? AND ' => 2,
9430
                    'ip.tool = ? AND ' => TOOL_FORUM,
9431
                    'f.c_id = ? AND ' => intval($this->course_int_id),
9432
                    'f.lp_id = ?' => intval($this->lp_id),
9433
                ],
9434
            ],
9435
            'first'
9436
        );
9437
9438
        return $resultData['qty'] > 0;
9439
    }
9440
9441
    /**
9442
     * Get the forum for this learning path.
9443
     *
9444
     * @param int $sessionId
9445
     *
9446
     * @return array
9447
     */
9448
    public function getForum($sessionId = 0)
9449
    {
9450
        $repo = Container::getForumRepository();
9451
9452
        $course = api_get_course_entity();
9453
        $session = api_get_session_entity($sessionId);
9454
        $qb = $repo->getResourcesByCourse($course, $session);
9455
9456
        return $qb->getQuery()->getResult();
9457
    }
9458
9459
    /**
9460
     * Create a forum for this learning path.
9461
     *
9462
     * @return int The forum ID if was created. Otherwise return false
9463
     */
9464
    public function createForum(CForumCategory $forumCategory)
9465
    {
9466
        return store_forum(
9467
            [
9468
                'lp_id' => $this->lp_id,
9469
                'forum_title' => $this->name,
9470
                'forum_comment' => null,
9471
                'forum_category' => $forumCategory->getIid(),
9472
                'students_can_edit_group' => ['students_can_edit' => 0],
9473
                'allow_new_threads_group' => ['allow_new_threads' => 0],
9474
                'default_view_type_group' => ['default_view_type' => 'flat'],
9475
                'group_forum' => 0,
9476
                'public_private_group_forum_group' => ['public_private_group_forum' => 'public'],
9477
            ],
9478
            [],
9479
            true
9480
        );
9481
    }
9482
9483
    /**
9484
     * Get the LP Final Item form.
9485
     *
9486
     * @throws Exception
9487
     *
9488
     *
9489
     * @return string
9490
     */
9491
    public function getFinalItemForm()
9492
    {
9493
        $finalItem = $this->getFinalItem();
9494
        $title = '';
9495
9496
        if ($finalItem) {
9497
            $title = $finalItem->get_title();
9498
            $buttonText = get_lang('Save');
9499
            $content = $this->getSavedFinalItem();
9500
        } else {
9501
            $buttonText = get_lang('Add this document to the course');
9502
            $content = $this->getFinalItemTemplate();
9503
        }
9504
9505
        $editorConfig = [
9506
            'ToolbarSet' => 'LearningPathDocuments',
9507
            'Width' => '100%',
9508
            'Height' => '500',
9509
            'FullPage' => true,
9510
//            'CreateDocumentDir' => $relative_prefix,
9511
    //            'CreateDocumentWebDir' => api_get_path(WEB_COURSE_PATH).api_get_course_path().'/document/',
9512
  //          'BaseHref' => api_get_path(WEB_COURSE_PATH).api_get_course_path().'/document/'.$relative_path,
9513
        ];
9514
9515
        $url = api_get_self().'?'.api_get_cidreq().'&'.http_build_query([
9516
            'type' => 'document',
9517
            'lp_id' => $this->lp_id,
9518
        ]);
9519
9520
        $form = new FormValidator('final_item', 'POST', $url);
9521
        $form->addText('title', get_lang('Title'));
9522
        $form->addButtonSave($buttonText);
9523
        $form->addHtml(
9524
            Display::return_message(
9525
                'Variables :</br></br> <b>((certificate))</b> </br> <b>((skill))</b>',
9526
                'normal',
9527
                false
9528
            )
9529
        );
9530
9531
        $renderer = $form->defaultRenderer();
9532
        $renderer->setElementTemplate('&nbsp;{label}{element}', 'content_lp_certificate');
9533
9534
        $form->addHtmlEditor(
9535
            'content_lp_certificate',
9536
            null,
9537
            true,
9538
            false,
9539
            $editorConfig,
9540
            true
9541
        );
9542
        $form->addHidden('action', 'add_final_item');
9543
        $form->addHidden('path', Session::read('pathItem'));
9544
        $form->addHidden('previous', $this->get_last());
9545
        $form->setDefaults(
9546
            ['title' => $title, 'content_lp_certificate' => $content]
9547
        );
9548
9549
        if ($form->validate()) {
9550
            $values = $form->exportValues();
9551
            $lastItemId = $this->getLastInFirstLevel();
9552
9553
            if (!$finalItem) {
9554
                $documentId = $this->create_document(
9555
                    $this->course_info,
9556
                    $values['content_lp_certificate'],
9557
                    $values['title']
9558
                );
9559
                $this->add_item(
9560
                    0,
9561
                    $lastItemId,
9562
                    'final_item',
9563
                    $documentId,
9564
                    $values['title'],
9565
                    ''
9566
                );
9567
9568
                Display::addFlash(
9569
                    Display::return_message(get_lang('Added'))
9570
                );
9571
            } else {
9572
                $this->edit_document($this->course_info);
9573
            }
9574
        }
9575
9576
        return $form->returnForm();
9577
    }
9578
9579
    /**
9580
     * Check if the current lp item is first, both, last or none from lp list.
9581
     *
9582
     * @param int $currentItemId
9583
     *
9584
     * @return string
9585
     */
9586
    public function isFirstOrLastItem($currentItemId)
9587
    {
9588
        $lpItemId = [];
9589
        $typeListNotToVerify = self::getChapterTypes();
9590
9591
        // Using get_toc() function instead $this->items because returns the correct order of the items
9592
        foreach ($this->get_toc() as $item) {
9593
            if (!in_array($item['type'], $typeListNotToVerify)) {
9594
                $lpItemId[] = $item['id'];
9595
            }
9596
        }
9597
9598
        $lastLpItemIndex = count($lpItemId) - 1;
9599
        $position = array_search($currentItemId, $lpItemId);
9600
9601
        switch ($position) {
9602
            case 0:
9603
                if (!$lastLpItemIndex) {
9604
                    $answer = 'both';
9605
                    break;
9606
                }
9607
9608
                $answer = 'first';
9609
                break;
9610
            case $lastLpItemIndex:
9611
                $answer = 'last';
9612
                break;
9613
            default:
9614
                $answer = 'none';
9615
        }
9616
9617
        return $answer;
9618
    }
9619
9620
    /**
9621
     * Get whether this is a learning path with the accumulated SCORM time or not.
9622
     *
9623
     * @return int
9624
     */
9625
    public function getAccumulateScormTime()
9626
    {
9627
        return $this->accumulateScormTime;
9628
    }
9629
9630
    /**
9631
     * Returns an HTML-formatted link to a resource, to incorporate directly into
9632
     * the new learning path tool.
9633
     *
9634
     * The function is a big switch on tool type.
9635
     * In each case, we query the corresponding table for information and build the link
9636
     * with that information.
9637
     *
9638
     * @author Yannick Warnier <[email protected]> - rebranding based on
9639
     * previous work (display_addedresource_link_in_learnpath())
9640
     *
9641
     * @param int $course_id      Course code
9642
     * @param int $learningPathId The learning path ID (in lp table)
9643
     * @param int $id_in_path     the unique index in the items table
9644
     * @param int $lpViewId
9645
     *
9646
     * @return string
9647
     */
9648
    public static function rl_get_resource_link_for_learnpath(
9649
        $course_id,
9650
        $learningPathId,
9651
        $id_in_path,
9652
        $lpViewId
9653
    ) {
9654
        $session_id = api_get_session_id();
9655
9656
        $learningPathId = (int) $learningPathId;
9657
        $id_in_path = (int) $id_in_path;
9658
        $lpViewId = (int) $lpViewId;
9659
9660
        $em = Database::getManager();
9661
        $lpItemRepo = $em->getRepository(CLpItem::class);
9662
9663
        /** @var CLpItem $rowItem */
9664
        $rowItem = $lpItemRepo->findOneBy([
9665
            'lp' => $learningPathId,
9666
            'iid' => $id_in_path,
9667
        ]);
9668
        $type = $rowItem->getItemType();
9669
        $id = empty($rowItem->getPath()) ? '0' : $rowItem->getPath();
9670
        $main_dir_path = api_get_path(WEB_CODE_PATH);
9671
        $link = '';
9672
        $extraParams = api_get_cidreq(true, true, 'learnpath').'&sid='.$session_id;
9673
9674
        switch ($type) {
9675
            case 'dir':
9676
                return $main_dir_path.'lp/blank.php';
9677
            case TOOL_CALENDAR_EVENT:
9678
                return $main_dir_path.'calendar/agenda.php?agenda_id='.$id.'&'.$extraParams;
9679
            case TOOL_ANNOUNCEMENT:
9680
                return $main_dir_path.'announcements/announcements.php?ann_id='.$id.'&'.$extraParams;
9681
            case TOOL_LINK:
9682
                $linkInfo = Link::getLinkInfo($id);
9683
                if (isset($linkInfo['url'])) {
9684
                    return $linkInfo['url'];
9685
                }
9686
9687
                return '';
9688
            case TOOL_QUIZ:
9689
                if (empty($id)) {
9690
                    return '';
9691
                }
9692
9693
                // Get the lp_item_view with the highest view_count.
9694
                $learnpathItemViewResult = $em
9695
                    ->getRepository('ChamiloCourseBundle:CLpItemView')
9696
                    ->findBy(
9697
                        ['item' => $rowItem->getIid(), 'view' => $lpViewId],
9698
                        ['viewCount' => 'DESC'],
9699
                        1
9700
                    );
9701
                /** @var CLpItemView $learnpathItemViewData */
9702
                $learnpathItemViewData = current($learnpathItemViewResult);
9703
                $learnpathItemViewId = $learnpathItemViewData ? $learnpathItemViewData->getIid() : 0;
9704
9705
                return $main_dir_path.'exercise/overview.php?'.$extraParams.'&'
9706
                    .http_build_query([
9707
                        'lp_init' => 1,
9708
                        'learnpath_item_view_id' => $learnpathItemViewId,
9709
                        'learnpath_id' => $learningPathId,
9710
                        'learnpath_item_id' => $id_in_path,
9711
                        'exerciseId' => $id,
9712
                    ]);
9713
            case TOOL_HOTPOTATOES: //lowercase because of strtolower above
9714
                /*$TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
9715
                $result = Database::query("SELECT * FROM ".$TBL_DOCUMENT." WHERE c_id = $course_id AND iid=$id");
9716
                $myrow = Database::fetch_array($result);
9717
                $path = $myrow['path'];
9718
9719
                return $main_dir_path.'exercise/showinframes.php?file='.$path.'&cid='.$course_code.'&uid='
9720
                    .api_get_user_id().'&learnpath_id='.$learningPathId.'&learnpath_item_id='.$id_in_path
9721
                    .'&lp_view_id='.$lpViewId.'&'.$extraParams;*/
9722
            case TOOL_FORUM:
9723
                return $main_dir_path.'forum/viewforum.php?forum='.$id.'&lp=true&'.$extraParams;
9724
            case TOOL_THREAD:
9725
                // forum post
9726
                $tbl_topics = Database::get_course_table(TABLE_FORUM_THREAD);
9727
                if (empty($id)) {
9728
                    return '';
9729
                }
9730
                $sql = "SELECT * FROM $tbl_topics WHERE c_id = $course_id AND iid=$id";
9731
                $result = Database::query($sql);
9732
                $myrow = Database::fetch_array($result);
9733
9734
                return $main_dir_path.'forum/viewthread.php?thread='.$id.'&forum='.$myrow['forum_id'].'&lp=true&'
9735
                    .$extraParams;
9736
            case TOOL_POST:
9737
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
9738
                $result = Database::query("SELECT * FROM $tbl_post WHERE c_id = $course_id AND post_id=$id");
9739
                $myrow = Database::fetch_array($result);
9740
9741
                return $main_dir_path.'forum/viewthread.php?post='.$id.'&thread='.$myrow['thread_id'].'&forum='
9742
                    .$myrow['forum_id'].'&lp=true&'.$extraParams;
9743
            case TOOL_READOUT_TEXT:
9744
                return api_get_path(WEB_CODE_PATH).
9745
                    'lp/readout_text.php?&id='.$id.'&lp_id='.$learningPathId.'&'.$extraParams;
9746
            case TOOL_DOCUMENT:
9747
                $repo = Container::getDocumentRepository();
9748
                $document = $repo->find($rowItem->getPath());
9749
                if ($document) {
9750
                    $params = [
9751
                        'cid' => $course_id,
9752
                        'sid' => $session_id,
9753
                    ];
9754
9755
                    return $repo->getResourceFileUrl($document, $params, UrlGeneratorInterface::ABSOLUTE_URL);
9756
                }
9757
9758
                return null;
9759
9760
                $documentPathInfo = pathinfo($document->getPath());
0 ignored issues
show
Unused Code introduced by
$documentPathInfo = path...o($document->getPath()) 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...
9761
                $mediaSupportedFiles = ['mp3', 'mp4', 'ogv', 'ogg', 'flv', 'm4v'];
9762
                $extension = isset($documentPathInfo['extension']) ? $documentPathInfo['extension'] : '';
9763
                $showDirectUrl = !in_array($extension, $mediaSupportedFiles);
9764
9765
                $openmethod = 2;
9766
                $officedoc = false;
9767
                Session::write('openmethod', $openmethod);
9768
                Session::write('officedoc', $officedoc);
9769
9770
                if ($showDirectUrl) {
9771
                    $file = $main_course_path.'document'.$document->getPath().'?'.$extraParams;
9772
                    if (api_get_configuration_value('allow_pdf_viewerjs_in_lp')) {
9773
                        if (Link::isPdfLink($file)) {
9774
                            $pdfUrl = api_get_path(WEB_LIBRARY_PATH).'javascript/ViewerJS/index.html#'.$file;
9775
9776
                            return $pdfUrl;
9777
                        }
9778
                    }
9779
9780
                    return $file;
9781
                }
9782
9783
                return api_get_path(WEB_CODE_PATH).'document/showinframes.php?id='.$id.'&'.$extraParams;
9784
            case TOOL_LP_FINAL_ITEM:
9785
                return api_get_path(WEB_CODE_PATH).'lp/lp_final_item.php?&id='.$id.'&lp_id='.$learningPathId.'&'
9786
                    .$extraParams;
9787
            case 'assignments':
9788
                return $main_dir_path.'work/work.php?'.$extraParams;
9789
            case TOOL_DROPBOX:
9790
                return $main_dir_path.'dropbox/index.php?'.$extraParams;
9791
            case 'introduction_text': //DEPRECATED
9792
                return '';
9793
            case TOOL_COURSE_DESCRIPTION:
9794
                return $main_dir_path.'course_description?'.$extraParams;
9795
            case TOOL_GROUP:
9796
                return $main_dir_path.'group/group.php?'.$extraParams;
9797
            case TOOL_USER:
9798
                return $main_dir_path.'user/user.php?'.$extraParams;
9799
            case TOOL_STUDENTPUBLICATION:
9800
                if (!empty($rowItem->getPath())) {
9801
                    return $main_dir_path.'work/work_list.php?id='.$rowItem->getPath().'&'.$extraParams;
9802
                }
9803
9804
                return $main_dir_path.'work/work.php?'.api_get_cidreq().'&id='.$rowItem->getPath().'&'.$extraParams;
9805
        }
9806
9807
        return $link;
9808
    }
9809
9810
    /**
9811
     * Gets the name of a resource (generally used in learnpath when no name is provided).
9812
     *
9813
     * @author Yannick Warnier <[email protected]>
9814
     *
9815
     * @param string $course_code    Course code
9816
     * @param int    $learningPathId
9817
     * @param int    $id_in_path     The resource ID
9818
     *
9819
     * @return string
9820
     */
9821
    public static function rl_get_resource_name($course_code, $learningPathId, $id_in_path)
9822
    {
9823
        $_course = api_get_course_info($course_code);
9824
        if (empty($_course)) {
9825
            return '';
9826
        }
9827
        $course_id = $_course['real_id'];
9828
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
9829
        $learningPathId = (int) $learningPathId;
9830
        $id_in_path = (int) $id_in_path;
9831
9832
        $sql = "SELECT item_type, title, ref
9833
                FROM $tbl_lp_item
9834
                WHERE c_id = $course_id AND lp_id = $learningPathId AND iid = $id_in_path";
9835
        $res_item = Database::query($sql);
9836
9837
        if (Database::num_rows($res_item) < 1) {
9838
            return '';
9839
        }
9840
        $row_item = Database::fetch_array($res_item);
9841
        $type = strtolower($row_item['item_type']);
9842
        $id = $row_item['ref'];
9843
        $output = '';
9844
9845
        switch ($type) {
9846
            case TOOL_CALENDAR_EVENT:
9847
                $TABLEAGENDA = Database::get_course_table(TABLE_AGENDA);
9848
                $result = Database::query("SELECT * FROM $TABLEAGENDA WHERE c_id = $course_id AND id=$id");
9849
                $myrow = Database::fetch_array($result);
9850
                $output = $myrow['title'];
9851
                break;
9852
            case TOOL_ANNOUNCEMENT:
9853
                $tbl_announcement = Database::get_course_table(TABLE_ANNOUNCEMENT);
9854
                $result = Database::query("SELECT * FROM $tbl_announcement WHERE c_id = $course_id AND id=$id");
9855
                $myrow = Database::fetch_array($result);
9856
                $output = $myrow['title'];
9857
                break;
9858
            case TOOL_LINK:
9859
                // Doesn't take $target into account.
9860
                $TABLETOOLLINK = Database::get_course_table(TABLE_LINK);
9861
                $result = Database::query("SELECT * FROM $TABLETOOLLINK WHERE c_id = $course_id AND id=$id");
9862
                $myrow = Database::fetch_array($result);
9863
                $output = $myrow['title'];
9864
                break;
9865
            case TOOL_QUIZ:
9866
                $TBL_EXERCICES = Database::get_course_table(TABLE_QUIZ_TEST);
9867
                $result = Database::query("SELECT * FROM $TBL_EXERCICES WHERE c_id = $course_id AND id = $id");
9868
                $myrow = Database::fetch_array($result);
9869
                $output = $myrow['title'];
9870
                break;
9871
            case TOOL_FORUM:
9872
                $TBL_FORUMS = Database::get_course_table(TABLE_FORUM);
9873
                $result = Database::query("SELECT * FROM $TBL_FORUMS WHERE c_id = $course_id AND forum_id = $id");
9874
                $myrow = Database::fetch_array($result);
9875
                $output = $myrow['forum_name'];
9876
                break;
9877
            case TOOL_THREAD:
9878
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
9879
                // Grabbing the title of the post.
9880
                $sql_title = "SELECT * FROM $tbl_post WHERE c_id = $course_id AND post_id=".$id;
9881
                $result_title = Database::query($sql_title);
9882
                $myrow_title = Database::fetch_array($result_title);
9883
                $output = $myrow_title['post_title'];
9884
                break;
9885
            case TOOL_POST:
9886
                $tbl_post = Database::get_course_table(TABLE_FORUM_POST);
9887
                $sql = "SELECT * FROM $tbl_post p WHERE c_id = $course_id AND p.post_id = $id";
9888
                $result = Database::query($sql);
9889
                $post = Database::fetch_array($result);
9890
                $output = $post['post_title'];
9891
                break;
9892
            case 'dir':
9893
            case TOOL_DOCUMENT:
9894
                $title = $row_item['title'];
9895
                $output = '-';
9896
                if (!empty($title)) {
9897
                    $output = $title;
9898
                }
9899
                break;
9900
            case 'hotpotatoes':
9901
                $tbl_doc = Database::get_course_table(TABLE_DOCUMENT);
9902
                $result = Database::query("SELECT * FROM $tbl_doc WHERE c_id = $course_id AND iid = $id");
9903
                $myrow = Database::fetch_array($result);
9904
                $pathname = explode('/', $myrow['path']); // Making a correct name for the link.
9905
                $last = count($pathname) - 1; // Making a correct name for the link.
9906
                $filename = $pathname[$last]; // Making a correct name for the link.
9907
                $myrow['path'] = rawurlencode($myrow['path']);
9908
                $output = $filename;
9909
                break;
9910
        }
9911
9912
        return stripslashes($output);
9913
    }
9914
9915
    /**
9916
     * Get the parent names for the current item.
9917
     *
9918
     * @param int $newItemId Optional. The item ID
9919
     *
9920
     * @return array
9921
     */
9922
    public function getCurrentItemParentNames($newItemId = 0)
9923
    {
9924
        $newItemId = $newItemId ?: $this->get_current_item_id();
9925
        $return = [];
9926
        $item = $this->getItem($newItemId);
9927
        $parent = $this->getItem($item->get_parent());
9928
9929
        while ($parent) {
9930
            $return[] = $parent->get_title();
9931
            $parent = $this->getItem($parent->get_parent());
9932
        }
9933
9934
        return array_reverse($return);
9935
    }
9936
9937
    /**
9938
     * Reads and process "lp_subscription_settings" setting.
9939
     *
9940
     * @return array
9941
     */
9942
    public static function getSubscriptionSettings()
9943
    {
9944
        $subscriptionSettings = api_get_configuration_value('lp_subscription_settings');
9945
        if (empty($subscriptionSettings)) {
9946
            // By default allow both settings
9947
            $subscriptionSettings = [
9948
                'allow_add_users_to_lp' => true,
9949
                'allow_add_users_to_lp_category' => true,
9950
            ];
9951
        } else {
9952
            $subscriptionSettings = $subscriptionSettings['options'];
9953
        }
9954
9955
        return $subscriptionSettings;
9956
    }
9957
9958
    /**
9959
     * Exports a LP to a courseBuilder zip file. It adds the documents related to the LP.
9960
     */
9961
    public function exportToCourseBuildFormat()
9962
    {
9963
        if (!api_is_allowed_to_edit()) {
9964
            return false;
9965
        }
9966
9967
        $courseBuilder = new CourseBuilder();
9968
        $itemList = [];
9969
        /** @var learnpathItem $item */
9970
        foreach ($this->items as $item) {
9971
            $itemList[$item->get_type()][] = $item->get_path();
9972
        }
9973
9974
        if (empty($itemList)) {
9975
            return false;
9976
        }
9977
9978
        if (isset($itemList['document'])) {
9979
            // Get parents
9980
            foreach ($itemList['document'] as $documentId) {
9981
                $documentInfo = DocumentManager::get_document_data_by_id($documentId, api_get_course_id(), true);
9982
                if (!empty($documentInfo['parents'])) {
9983
                    foreach ($documentInfo['parents'] as $parentInfo) {
9984
                        if (in_array($parentInfo['iid'], $itemList['document'])) {
9985
                            continue;
9986
                        }
9987
                        $itemList['document'][] = $parentInfo['iid'];
9988
                    }
9989
                }
9990
            }
9991
9992
            $courseInfo = api_get_course_info();
9993
            foreach ($itemList['document'] as $documentId) {
9994
                $documentInfo = DocumentManager::get_document_data_by_id($documentId, api_get_course_id());
9995
                $items = DocumentManager::get_resources_from_source_html(
9996
                    $documentInfo['absolute_path'],
9997
                    true,
9998
                    TOOL_DOCUMENT
9999
                );
10000
10001
                if (!empty($items)) {
10002
                    foreach ($items as $item) {
10003
                        // Get information about source url
10004
                        $url = $item[0]; // url
10005
                        $scope = $item[1]; // scope (local, remote)
10006
                        $type = $item[2]; // type (rel, abs, url)
10007
10008
                        $origParseUrl = parse_url($url);
10009
                        $realOrigPath = isset($origParseUrl['path']) ? $origParseUrl['path'] : null;
10010
10011
                        if ('local' === $scope) {
10012
                            if ('abs' === $type || 'rel' === $type) {
10013
                                $documentFile = strstr($realOrigPath, 'document');
10014
                                if (false !== strpos($realOrigPath, $documentFile)) {
10015
                                    $documentFile = str_replace('document', '', $documentFile);
10016
                                    $itemDocumentId = DocumentManager::get_document_id($courseInfo, $documentFile);
10017
                                    // Document found! Add it to the list
10018
                                    if ($itemDocumentId) {
10019
                                        $itemList['document'][] = $itemDocumentId;
10020
                                    }
10021
                                }
10022
                            }
10023
                        }
10024
                    }
10025
                }
10026
            }
10027
10028
            $courseBuilder->build_documents(
10029
                api_get_session_id(),
10030
                $this->get_course_int_id(),
10031
                true,
10032
                $itemList['document']
10033
            );
10034
        }
10035
10036
        if (isset($itemList['quiz'])) {
10037
            $courseBuilder->build_quizzes(
10038
                api_get_session_id(),
10039
                $this->get_course_int_id(),
10040
                true,
10041
                $itemList['quiz']
10042
            );
10043
        }
10044
10045
        /*if (!empty($itemList['thread'])) {
10046
            $postList = [];
10047
            foreach ($itemList['thread'] as $postId) {
10048
                $post = get_post_information($postId);
10049
                if ($post) {
10050
                    if (!isset($itemList['forum'])) {
10051
                        $itemList['forum'] = [];
10052
                    }
10053
                    $itemList['forum'][] = $post['forum_id'];
10054
                    $postList[] = $postId;
10055
                }
10056
            }
10057
10058
            if (!empty($postList)) {
10059
                $courseBuilder->build_forum_posts(
10060
                    $this->get_course_int_id(),
10061
                    null,
10062
                    null,
10063
                    $postList
10064
                );
10065
            }
10066
        }*/
10067
10068
        if (!empty($itemList['thread'])) {
10069
            $threadList = [];
10070
            $em = Database::getManager();
10071
            $repo = $em->getRepository('ChamiloCourseBundle:CForumThread');
10072
            foreach ($itemList['thread'] as $threadId) {
10073
                /** @var CForumThread $thread */
10074
                $thread = $repo->find($threadId);
10075
                if ($thread) {
10076
                    $itemList['forum'][] = $thread->getForum() ? $thread->getForum()->getIid() : 0;
10077
                    $threadList[] = $thread->getIid();
10078
                }
10079
            }
10080
10081
            if (!empty($threadList)) {
10082
                $courseBuilder->build_forum_topics(
10083
                    api_get_session_id(),
10084
                    $this->get_course_int_id(),
10085
                    null,
10086
                    $threadList
10087
                );
10088
            }
10089
        }
10090
10091
        $forumCategoryList = [];
10092
        if (isset($itemList['forum'])) {
10093
            foreach ($itemList['forum'] as $forumId) {
10094
                $forumInfo = get_forums($forumId);
10095
                $forumCategoryList[] = $forumInfo['forum_category'];
10096
            }
10097
        }
10098
10099
        if (!empty($forumCategoryList)) {
10100
            $courseBuilder->build_forum_category(
10101
                api_get_session_id(),
10102
                $this->get_course_int_id(),
10103
                true,
10104
                $forumCategoryList
10105
            );
10106
        }
10107
10108
        if (!empty($itemList['forum'])) {
10109
            $courseBuilder->build_forums(
10110
                api_get_session_id(),
10111
                $this->get_course_int_id(),
10112
                true,
10113
                $itemList['forum']
10114
            );
10115
        }
10116
10117
        if (isset($itemList['link'])) {
10118
            $courseBuilder->build_links(
10119
                api_get_session_id(),
10120
                $this->get_course_int_id(),
10121
                true,
10122
                $itemList['link']
10123
            );
10124
        }
10125
10126
        if (!empty($itemList['student_publication'])) {
10127
            $courseBuilder->build_works(
10128
                api_get_session_id(),
10129
                $this->get_course_int_id(),
10130
                true,
10131
                $itemList['student_publication']
10132
            );
10133
        }
10134
10135
        $courseBuilder->build_learnpaths(
10136
            api_get_session_id(),
10137
            $this->get_course_int_id(),
10138
            true,
10139
            [$this->get_id()],
10140
            false
10141
        );
10142
10143
        $courseBuilder->restoreDocumentsFromList();
10144
10145
        $zipFile = CourseArchiver::createBackup($courseBuilder->course);
10146
        $zipPath = CourseArchiver::getBackupDir().$zipFile;
10147
        $result = DocumentManager::file_send_for_download(
10148
            $zipPath,
10149
            true,
10150
            $this->get_name().'.zip'
10151
        );
10152
10153
        if ($result) {
10154
            api_not_allowed();
10155
        }
10156
10157
        return true;
10158
    }
10159
10160
    /**
10161
     * Get whether this is a learning path with the accumulated work time or not.
10162
     *
10163
     * @return int
10164
     */
10165
    public function getAccumulateWorkTime()
10166
    {
10167
        return (int) $this->accumulateWorkTime;
10168
    }
10169
10170
    /**
10171
     * Get whether this is a learning path with the accumulated work time or not.
10172
     *
10173
     * @return int
10174
     */
10175
    public function getAccumulateWorkTimeTotalCourse()
10176
    {
10177
        $table = Database::get_course_table(TABLE_LP_MAIN);
10178
        $sql = "SELECT SUM(accumulate_work_time) AS total
10179
                FROM $table
10180
                WHERE c_id = ".$this->course_int_id;
10181
        $result = Database::query($sql);
10182
        $row = Database::fetch_array($result);
10183
10184
        return (int) $row['total'];
10185
    }
10186
10187
    /**
10188
     * @param int $lpId
10189
     * @param int $courseId
10190
     *
10191
     * @return mixed
10192
     */
10193
    public static function getAccumulateWorkTimePrerequisite($lpId, $courseId)
10194
    {
10195
        $lpId = (int) $lpId;
10196
        $table = Database::get_course_table(TABLE_LP_MAIN);
10197
        $sql = "SELECT accumulate_work_time
10198
                FROM $table
10199
                WHERE iid = $lpId";
10200
        $result = Database::query($sql);
10201
        $row = Database::fetch_array($result);
10202
10203
        return $row['accumulate_work_time'];
10204
    }
10205
10206
    /**
10207
     * @param int $courseId
10208
     *
10209
     * @return int
10210
     */
10211
    public static function getAccumulateWorkTimeTotal($courseId)
10212
    {
10213
        $table = Database::get_course_table(TABLE_LP_MAIN);
10214
        $courseId = (int) $courseId;
10215
        $sql = "SELECT SUM(accumulate_work_time) AS total
10216
                FROM $table
10217
                WHERE c_id = $courseId";
10218
        $result = Database::query($sql);
10219
        $row = Database::fetch_array($result);
10220
10221
        return (int) $row['total'];
10222
    }
10223
10224
    /**
10225
     * In order to use the lp icon option you need to create the "lp_icon" LP extra field
10226
     * and put the images in.
10227
     *
10228
     * @return array
10229
     */
10230
    public static function getIconSelect()
10231
    {
10232
        $theme = api_get_visual_theme();
10233
        $path = api_get_path(SYS_PUBLIC_PATH).'css/themes/'.$theme.'/lp_icons/';
10234
        $icons = ['' => get_lang('Please select an option')];
10235
10236
        if (is_dir($path)) {
10237
            $finder = new Finder();
10238
            $finder->files()->in($path);
10239
            $allowedExtensions = ['jpeg', 'jpg', 'png'];
10240
            /** @var SplFileInfo $file */
10241
            foreach ($finder as $file) {
10242
                if (in_array(strtolower($file->getExtension()), $allowedExtensions)) {
10243
                    $icons[$file->getFilename()] = $file->getFilename();
10244
                }
10245
            }
10246
        }
10247
10248
        return $icons;
10249
    }
10250
10251
    /**
10252
     * @param int $lpId
10253
     *
10254
     * @return string
10255
     */
10256
    public static function getSelectedIcon($lpId)
10257
    {
10258
        $extraFieldValue = new ExtraFieldValue('lp');
10259
        $lpIcon = $extraFieldValue->get_values_by_handler_and_field_variable($lpId, 'lp_icon');
10260
        $icon = '';
10261
        if (!empty($lpIcon) && isset($lpIcon['value'])) {
10262
            $icon = $lpIcon['value'];
10263
        }
10264
10265
        return $icon;
10266
    }
10267
10268
    /**
10269
     * @param int $lpId
10270
     *
10271
     * @return string
10272
     */
10273
    public static function getSelectedIconHtml($lpId)
10274
    {
10275
        $icon = self::getSelectedIcon($lpId);
10276
10277
        if (empty($icon)) {
10278
            return '';
10279
        }
10280
10281
        $theme = api_get_visual_theme();
10282
        $path = api_get_path(WEB_PUBLIC_PATH).'css/themes/'.$theme.'/lp_icons/'.$icon;
10283
10284
        return Display::img($path);
10285
    }
10286
10287
    /**
10288
     * @param string $value
10289
     *
10290
     * @return string
10291
     */
10292
    public function cleanItemTitle($value)
10293
    {
10294
        $value = Security::remove_XSS(strip_tags($value));
10295
10296
        return $value;
10297
    }
10298
10299
    public function setItemTitle(FormValidator $form)
10300
    {
10301
        if (api_get_configuration_value('save_titles_as_html')) {
10302
            $form->addHtmlEditor(
10303
                'title',
10304
                get_lang('Title'),
10305
                true,
10306
                false,
10307
                ['ToolbarSet' => 'TitleAsHtml', 'id' => uniqid('editor')]
10308
            );
10309
        } else {
10310
            $form->addText('title', get_lang('Title'), true, ['id' => 'idTitle', 'class' => 'learnpath_item_form']);
10311
            $form->applyFilter('title', 'trim');
10312
            $form->applyFilter('title', 'html_filter');
10313
        }
10314
    }
10315
10316
    /**
10317
     * @return array
10318
     */
10319
    public function getItemsForForm($addParentCondition = false)
10320
    {
10321
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
10322
10323
        $sql = "SELECT * FROM $tbl_lp_item
10324
                WHERE path <> 'root' AND lp_id = ".$this->lp_id;
10325
10326
        if ($addParentCondition) {
10327
            $sql .= ' AND parent_item_id IS NULL ';
10328
        }
10329
        $sql .= ' ORDER BY display_order ASC';
10330
10331
        $result = Database::query($sql);
10332
        $arrLP = [];
10333
        while ($row = Database::fetch_array($result)) {
10334
            $arrLP[] = [
10335
                'iid' => $row['iid'],
10336
                'id' => $row['iid'],
10337
                'item_type' => $row['item_type'],
10338
                'title' => $this->cleanItemTitle($row['title']),
10339
                'title_raw' => $row['title'],
10340
                'path' => $row['path'],
10341
                'description' => Security::remove_XSS($row['description']),
10342
                'parent_item_id' => $row['parent_item_id'],
10343
                'previous_item_id' => $row['previous_item_id'],
10344
                'next_item_id' => $row['next_item_id'],
10345
                'display_order' => $row['display_order'],
10346
                'max_score' => $row['max_score'],
10347
                'min_score' => $row['min_score'],
10348
                'mastery_score' => $row['mastery_score'],
10349
                'prerequisite' => $row['prerequisite'],
10350
                'max_time_allowed' => $row['max_time_allowed'],
10351
                'prerequisite_min_score' => $row['prerequisite_min_score'],
10352
                'prerequisite_max_score' => $row['prerequisite_max_score'],
10353
            ];
10354
        }
10355
10356
        return $arrLP;
10357
    }
10358
10359
    /**
10360
     * Gets whether this SCORM learning path has been marked to use the score
10361
     * as progress. Takes into account whether the learnpath matches (SCORM
10362
     * content + less than 2 items).
10363
     *
10364
     * @return bool True if the score should be used as progress, false otherwise
10365
     */
10366
    public function getUseScoreAsProgress()
10367
    {
10368
        // If not a SCORM, we don't care about the setting
10369
        if (2 != $this->get_type()) {
10370
            return false;
10371
        }
10372
        // If more than one step in the SCORM, we don't care about the setting
10373
        if ($this->get_total_items_count() > 1) {
10374
            return false;
10375
        }
10376
        $extraFieldValue = new ExtraFieldValue('lp');
10377
        $doUseScore = false;
10378
        $useScore = $extraFieldValue->get_values_by_handler_and_field_variable(
10379
            $this->get_id(),
10380
            'use_score_as_progress'
10381
        );
10382
        if (!empty($useScore) && isset($useScore['value'])) {
10383
            $doUseScore = $useScore['value'];
10384
        }
10385
10386
        return $doUseScore;
10387
    }
10388
10389
    /**
10390
     * Get the user identifier (user_id or username
10391
     * Depends on scorm_api_username_as_student_id in app/config/configuration.php.
10392
     *
10393
     * @return string User ID or username, depending on configuration setting
10394
     */
10395
    public static function getUserIdentifierForExternalServices()
10396
    {
10397
        if (api_get_configuration_value('scorm_api_username_as_student_id')) {
10398
            return api_get_user_info(api_get_user_id())['username'];
10399
        } elseif (null != api_get_configuration_value('scorm_api_extrafield_to_use_as_student_id')) {
10400
            $extraFieldValue = new ExtraFieldValue('user');
10401
            $extrafield = $extraFieldValue->get_values_by_handler_and_field_variable(
10402
                api_get_user_id(),
10403
                api_get_configuration_value('scorm_api_extrafield_to_use_as_student_id')
10404
            );
10405
10406
            return $extrafield['value'];
10407
        } else {
10408
            return api_get_user_id();
10409
        }
10410
    }
10411
10412
    /**
10413
     * Save the new order for learning path items.
10414
     *
10415
     * We have to update parent_item_id, previous_item_id, next_item_id, display_order in the database.
10416
     *
10417
     * @param array $orderList A associative array with item ID as key and parent ID as value.
10418
     */
10419
    public function sortItemByOrderList(array $orderList = [])
10420
    {
10421
        if (empty($orderList)) {
10422
            return true;
10423
        }
10424
        //echo '<pre>';        var_dump($orderList);
10425
        $lpItemRepo = Container::getLpItemRepository();
10426
        $rootParent = $lpItemRepo->getItemRoot($this->get_id());
10427
10428
        /*$previous = 2;
10429
        $next = 0;
10430
        $rootParent->setPreviousItemId(1);
10431
        $last = $this->updateList($orderList, $rootParent, $previous, $next);
10432
        $rootParent->setNextItemId($last + 1);
10433
*/
10434
        $em = Database::getManager();
10435
        //echo '<pre>';
10436
        //var_dump($orderList);
10437
//        $em->persist($rootParent);
10438
10439
        /*$node = new \Tree\Node\Node($rootParent->getIid());
10440
        $parentList[$rootParent->getIid()] = $node;
10441
10442
        foreach ($orderList as $item) {
10443
            $itemId = $item->id ?? 0;
10444
            if (empty($itemId)) {
10445
                continue;
10446
            }
10447
            $parentId = $item->parent_id ?? 0;
10448
10449
            if (empty($parentId)) {
10450
                $parent = $rootParent;
10451
                $parentId = $rootParent->getIid();
10452
            } else {
10453
                //$parent = $lpItemRepo->find($parentId);
10454
            }
10455
            //$parentList[$itemId] = ;
10456
10457
            $child = new \Tree\Node\Node($itemId);
10458
            $parentList[$itemId] = $child;
10459
            $parentList[$parentId]->addChild($child);
10460
        }
10461
10462
        $builder = new Tree\Builder\NodeBuilder;
10463
        echo '<pre>';
10464
        foreach ($node->getChildren() as $child) {
10465
            var_dump($child->getValue().'-'.$child->getDepth().'-'.$child->getSize());
10466
10467
            var_dump("---");
10468
            foreach ($child->getNeighbors() as $neighbor) {
10469
10470
                var_dump($neighbor->getValue(), $child->getSize(), $neighbor->getDepth());
10471
            }
10472
        }
10473
exit;*/
10474
10475
        $counter = 2;
10476
        /*$rootParent->setPreviousItemId(1);
10477
        $rootParent->setDisplayOrder(0);
10478
        $rootParent->setLaunchData(1);*/
10479
        //echo '<pre>';
10480
        $rootParent->setDisplayOrder(1);
10481
        //$rootParent->setNextItemId(null);
10482
        //$rootParent->setPreviousItemId(null);
10483
        foreach ($orderList as $item) {
10484
            $itemId  = $item->id ?? 0;
10485
            if (empty($itemId)) {
10486
                continue;
10487
            }
10488
            $parentId = $item->parent_id ?? 0;
10489
            $parent = $rootParent;
10490
            if (!empty($parentId)) {
10491
                $parentExists = $lpItemRepo->find($parentId);
10492
                if (null !== $parentExists) {
10493
                    $parent = $parentExists;
10494
                }
10495
            }
10496
10497
            if (isset($parentOrder[$parent->getIid()])) {
10498
                $parentOrder[$parent->getIid()]++;
10499
            } else {
10500
                $parentOrder[$parent->getIid()] = 0;
10501
            }
10502
            /** @var CLpItem $item */
10503
            $item = $lpItemRepo->find($itemId);
10504
10505
            $previousId = 2;
10506
            if (isset($previous[$counter - 1])) {
10507
                $previousId = $previous[$counter - 1];
10508
            }
10509
            $previous[$counter] = $counter + 2;
10510
10511
            $item->setParent($parent);
10512
            //$item->setPreviousItemId($previousId);
10513
            //$item->setNextItemId($counter + 1);
10514
            //$item->setDisplayOrder($parentOrder[$parent->getIid()]);
10515
            //var_dump($item->getIid().'-'.$counter);
10516
            //$item->setPreviousItemId(null);
10517
            //$item->setNextItemId(null);
10518
            //$item->setPreviousItemId($previousId);
10519
            $item->setDisplayOrder($counter);
10520
            //var_dump($parent->getIid(), $parent->getTitle());
10521
//            $lpItemRepo->persistAsLastChildOf($item, $parent);
10522
            //$lpItemRepo->persistAs();
10523
            //$lpItemRepo->persistAsFirstChildOf($item, $parent);
10524
            //$em->flush();
10525
            //$lpItemRepo->moveDown($item, true);
10526
            $em->persist($item);
10527
            $counter++;
10528
        }
10529
10530
        //$rootParent->setNextItemId($counter+1);
10531
        $em->persist($rootParent);
10532
        $em->flush();
10533
        //var_dump($lpItemRepo->verify());
10534
        //$lpItemRepo->reorder($rootParent, 'displayOrder', 'ASC', false);
10535
        //$lpItemRepo->reorder($rootParent);
10536
        //$em->flush();
10537
        //var_dump($lpItemRepo->verify());
10538
        $lpItemRepo->recoverNode($rootParent, 'displayOrder');
10539
        $em->flush();
10540
        //var_dump($lpItemRepo->verify());
10541
10542
        return true;
10543
        /*
10544
        $lpItemRepo->recoverNode($rootParent);
10545
        var_dump($lpItemRepo->verify());
10546
10547
        $em->flush();
10548
        exit;*/
10549
    }
10550
10551
    /**
10552
     * Get the depth level of LP item.
10553
     *
10554
     * @param array $items
10555
     * @param int   $currentItemId
10556
     *
10557
     * @return int
10558
     */
10559
    private static function get_level_for_item($items, $currentItemId)
10560
    {
10561
        $parentItemId = 0;
10562
        if (isset($items[$currentItemId])) {
10563
            $parentItemId = $items[$currentItemId]->parent;
10564
        }
10565
10566
        if (0 == $parentItemId) {
10567
            return 0;
10568
        }
10569
10570
        return self::get_level_for_item($items, $parentItemId) + 1;
10571
    }
10572
10573
    /**
10574
     * Generate the link for a learnpath category as course tool.
10575
     *
10576
     * @param int $categoryId
10577
     *
10578
     * @return string
10579
     */
10580
    private static function getCategoryLinkForTool($categoryId)
10581
    {
10582
        $categoryId = (int) $categoryId;
10583
        return 'lp/lp_controller.php?'.api_get_cidreq().'&'
10584
            .http_build_query(
10585
                [
10586
                    'action' => 'view_category',
10587
                    'id' => $categoryId,
10588
                ]
10589
            );
10590
    }
10591
10592
    /**
10593
     * Return the scorm item type object with spaces replaced with _
10594
     * The return result is use to build a css classname like scorm_type_$return.
10595
     *
10596
     * @param $in_type
10597
     *
10598
     * @return mixed
10599
     */
10600
    private static function format_scorm_type_item($in_type)
10601
    {
10602
        return str_replace(' ', '_', $in_type);
10603
    }
10604
10605
    /**
10606
     * Check and obtain the lp final item if exist.
10607
     *
10608
     * @return learnpathItem
10609
     */
10610
    private function getFinalItem()
10611
    {
10612
        if (empty($this->items)) {
10613
            return null;
10614
        }
10615
10616
        foreach ($this->items as $item) {
10617
            if ('final_item' !== $item->type) {
10618
                continue;
10619
            }
10620
10621
            return $item;
10622
        }
10623
    }
10624
10625
    /**
10626
     * Get the LP Final Item Template.
10627
     *
10628
     * @return string
10629
     */
10630
    private function getFinalItemTemplate()
10631
    {
10632
        return file_get_contents(api_get_path(SYS_CODE_PATH).'lp/final_item_template/template.html');
10633
    }
10634
10635
    /**
10636
     * Get the LP Final Item Url.
10637
     *
10638
     * @return string
10639
     */
10640
    private function getSavedFinalItem()
10641
    {
10642
        $finalItem = $this->getFinalItem();
10643
10644
        $repo = Container::getDocumentRepository();
10645
        /** @var CDocument $document */
10646
        $document = $repo->find($finalItem->path);
10647
10648
        if ($document && $document->getResourceNode()->hasResourceFile()) {
10649
            return $repo->getResourceFileContent($document);
10650
        }
10651
10652
        return '';
10653
    }
10654
}
10655