Passed
Push — master ( 14e06d...463e9a )
by Julito
09:07
created

learnpath::display_student_publication_form()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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