Passed
Push — master ( 65453d...b1f8c2 )
by
unknown
16:36 queued 07:54
created

learnpath   F

Complexity

Total Complexity 1090

Size/Duplication

Total Lines 8835
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4275
dl 0
loc 8835
rs 0.8
c 1
b 0
f 0
wmc 1090

189 Methods

Rating   Name   Duplication   Size   Complexity  
A getDropElementHtml() 0 12 2
A next() 0 22 5
B get_iv_objectives_array() 0 38 6
A get_update_queue() 0 3 1
F get_link() 0 316 46
D prerequisites_match() 0 69 16
A previous() 0 11 1
A open() 0 9 1
A get_type() 0 8 3
A get_type_static() 0 16 3
A get_items_status_list() 0 10 2
A get_items_details_as_js() 0 8 2
A get_user_id() 0 7 2
A getChapterTypes() 0 4 1
B get_iv_interactions_array() 0 54 8
A getNameNoTags() 0 3 1
A get_interactions_count_from_db() 0 16 2
B get_scorm_prereq_string() 0 73 11
B get_scorm_xml_node() 0 19 7
A has_audio() 0 11 3
B get_view() 0 53 7
B get_flat_ordered_items_list() 0 48 8
A get_objectives_count_from_db() 0 16 2
A getListArrayToc() 0 9 1
A get_view_id() 0 7 2
A get_toc() 0 19 2
A get_name() 0 7 2
A getStatusCSSClassName() 0 7 2
A get_teacher_toc_buttons() 0 24 4
B update_default_view_mode() 0 33 6
D display_edit_item() 0 71 20
A switch_attempt_mode() 0 16 4
C create_document() 0 104 11
A update_default_scorm_commit() 0 25 4
A set_error_msg() 0 9 3
D stop_previous_item() 0 55 18
C start_current_item() 0 42 16
B set_current_item() 0 33 10
A set_modified_on() 0 10 1
D getBuildTree() 0 156 15
A set_seriousgame_mode() 0 15 3
B set_terms_by_prefix() 0 68 10
A set_previous_item() 0 6 2
A set_attempt_mode() 0 32 5
A update_display_order() 0 30 5
A generate_lp_folder() 0 35 4
C display_item() 0 78 13
B overview() 0 59 8
F build_action_menu() 0 135 15
B showBuildSideBar() 0 243 6
A edit_document() 0 12 4
A update_scorm_debug() 0 15 3
A update_reinit() 0 16 3
A generate_learning_path_folder() 0 16 2
B get_attempt_mode() 0 21 9
A return_new_tree() 0 17 2
A getProgressBar() 0 5 1
A togglePublish() 0 20 4
A get_total_items_count() 0 3 1
A display_thread_form() 0 13 1
A set_autolaunch() 0 26 4
B getCalculateScore() 0 47 6
F save_last() 0 85 22
A get_first_item_id() 0 8 2
A isUserSubscribedToLp() 0 31 4
A copy() 0 26 1
A displayDocumentForm() 0 44 4
A getCategoryFromCourseIntoSelect() 0 18 4
A getAccumulateScormTime() 0 3 1
A get_course_int_id() 0 3 1
A createCategory() 0 15 1
A get_exercises() 0 130 4
C fixBlockedLinks() 0 64 11
B edit_item() 0 52 7
A getCategorySessionId() 0 17 4
B set_previous_step_as_prerequisite_for_all_items() 0 48 7
A updateCategory() 0 9 2
A displayItemMenu() 0 69 5
A getUserIdentifierForExternalServices() 0 15 4
A getAccumulateWorkTime() 0 3 1
F displayItemPrerequisitesForm() 0 210 19
A display_lp_prerequisites_list() 0 33 3
A categoryIsPublished() 0 25 2
A get_last() 0 10 2
A display_link_form() 0 22 2
A getForum() 0 9 1
A get_progress_bar() 0 8 1
B getFinalItemForm() 0 81 4
A getExercisesItems() 0 13 3
A getSelectedIcon() 0 10 3
B sortItemByOrderList() 0 54 11
A set_course_int_id() 0 3 1
A getCountCategories() 0 10 2
F createReadOutText() 0 121 27
A moveDownCategory() 0 15 3
A getAccumulateWorkTimePrerequisite() 0 11 1
A getSelectedIconHtml() 0 11 2
A get_theme() 0 7 2
A select_previous_item_id() 0 22 1
A display_item_form() 0 14 1
A get_previous_index() 0 16 5
B add_item() 0 108 8
A getItemsForForm() 0 38 3
A isGroupSubscribedToLp() 0 29 6
C isBlockedByPrerequisite() 0 64 12
B get_progress_bar_text() 0 56 11
A get_js_lib() 0 8 2
A getItem() 0 7 3
F first() 0 72 20
A getCategoryId() 0 3 1
B get_links() 0 109 7
A get_progress_bar_mode() 0 7 2
A create_path() 0 14 5
A get_current_item_id() 0 8 2
A getCurrentBuildingModeURL() 0 11 5
A display_document() 0 25 3
A save_current() 0 32 6
A getLastInFirstLevel() 0 13 2
F __construct() 0 252 41
A displayNewSectionForm() 0 19 1
C rl_get_resource_name() 0 92 14
A getSavedFinalItem() 0 9 2
B restart() 0 38 6
A cleanItemTitle() 0 5 1
A getCurrentItemParentNames() 0 17 4
A getSubscribeUsers() 0 3 1
A close() 0 13 2
A deleteCategory() 0 23 3
A getAccumulateWorkTimeTotalCourse() 0 10 1
A display_quiz_form() 0 12 1
C get_mediaplayer() 0 79 12
A moveUpCategory() 0 15 3
A getProgressFromLpList() 0 32 4
F autocomplete_parents() 0 101 17
B isFirstOrLastItem() 0 32 6
A isForumFromBaseCourse() 0 20 4
C getCalculateStars() 0 79 12
C display_move_item() 0 55 12
B edit_item_prereq() 0 36 7
A get_id() 0 7 2
B get_js_dropdown_array() 0 76 6
A getFinalEvaluationItem() 0 12 3
A setItemTitle() 0 14 2
A get_next_item_id() 0 10 3
F is_lp_visible_for_student() 0 114 23
A getSurveys() 0 36 3
A getProgress() 0 23 2
A getCategoryLinkForTool() 0 8 1
A getSubscriptionSettings() 0 14 2
B verify_document_size() 0 25 8
A get_complete_items_count() 0 24 5
C get_forums() 0 142 9
B delete_item() 0 48 6
B get_documents() 0 104 2
A getTotalItemsCountWithoutDirs() 0 11 3
A getCurrentAttempt() 0 8 2
A display_forum_form() 0 16 2
A toggleCategoryVisibility() 0 22 3
A getUseScoreAsProgress() 0 21 5
C add_lp() 0 90 11
A getFinalItem() 0 12 4
A displayResources() 0 57 2
A getFinalItemTemplate() 0 3 1
B recalculateResultsForLp() 0 52 8
A move() 0 21 5
F rl_get_resource_link_for_learnpath() 0 161 30
A get_extension() 0 5 1
A getAccumulateWorkTimeTotal() 0 11 1
A getLpFromSession() 0 33 5
A toggleCategoryPublish() 0 25 4
B get_navigation_bar() 0 77 9
A getCategories() 0 7 1
B get_next_index() 0 23 7
D getPackageType() 0 90 20
A toggleVisibility() 0 21 3
A get_lp_session_id() 0 16 5
A get_common_index_terms_by_prefix() 0 16 3
A getIconSelect() 0 24 4
F exportToCourseBuildFormat() 0 173 31
A get_previous_item_id() 0 5 1
A get_level_for_item() 0 12 3
B categoryIsVisibleForStudent() 0 36 8
B delete() 0 42 7
A clearPrerequisites() 0 14 1
A get_student_publications() 0 43 3
A lpHasForum() 0 26 1
A display_student_publication_form() 0 12 1
A delete_children_items() 0 22 4
B save_item() 0 47 9

How to fix   Complexity   

Complex Class

Complex classes like learnpath often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use learnpath, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\Course;
6
use Chamilo\CoreBundle\Entity\ResourceLink;
7
use Chamilo\CoreBundle\Entity\TrackEExercise;
8
use Chamilo\CoreBundle\Entity\User;
9
use Chamilo\CoreBundle\Entity\Session as SessionEntity;
10
use Chamilo\CoreBundle\Event\Events;
11
use Chamilo\CoreBundle\Event\LearningPathEndedEvent;
12
use Chamilo\CoreBundle\Repository\TrackEDefaultRepository;
13
use Chamilo\CoreBundle\ServiceHelper\ThemeHelper;
14
use Chamilo\CourseBundle\Entity\CLpRelUser;
15
use Chamilo\CoreBundle\Framework\Container;
16
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
17
use Chamilo\CourseBundle\Entity\CSurvey;
18
use Chamilo\CourseBundle\Repository\CLpRelUserRepository;
19
use Chamilo\CourseBundle\Component\CourseCopy\CourseArchiver;
20
use Chamilo\CourseBundle\Component\CourseCopy\CourseBuilder;
21
use Chamilo\CourseBundle\Component\CourseCopy\CourseRestorer;
22
use Chamilo\CourseBundle\Entity\CDocument;
23
use Chamilo\CourseBundle\Entity\CForumCategory;
24
use Chamilo\CourseBundle\Entity\CForumThread;
25
use Chamilo\CourseBundle\Entity\CLink;
26
use Chamilo\CourseBundle\Entity\CLp;
27
use Chamilo\CourseBundle\Entity\CLpCategory;
28
use Chamilo\CourseBundle\Entity\CLpItem;
29
use Chamilo\CourseBundle\Entity\CLpItemView;
30
use Chamilo\CourseBundle\Entity\CQuiz;
31
use Chamilo\CourseBundle\Entity\CStudentPublication;
32
use Chamilo\CourseBundle\Entity\CTool;
33
use \Chamilo\CoreBundle\Entity\ResourceNode;
34
use ChamiloSession as Session;
35
use Doctrine\Common\Collections\Criteria;
36
use PhpZip\ZipFile;
37
use Symfony\Component\Finder\Finder;
38
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
39
use Chamilo\CoreBundle\Component\Utils\ObjectIcon;
40
41
/**
42
 * Class learnpath
43
 * This class defines the parent attributes and methods for Chamilo learnpaths
44
 * and SCORM learnpaths. It is used by the scorm class.
45
 *
46
 * @todo decouple class
47
 *
48
 * @author  Yannick Warnier <[email protected]>
49
 * @author  Julio Montoya   <[email protected]> Several improvements and fixes
50
 */
51
class learnpath
52
{
53
    public const MAX_LP_ITEM_TITLE_LENGTH = 36;
54
    public const STATUS_CSS_CLASS_NAME = [
55
        'not attempted' => 'scorm_not_attempted',
56
        'incomplete' => 'scorm_not_attempted',
57
        'failed' => 'scorm_failed',
58
        'completed' => 'scorm_completed',
59
        'passed' => 'scorm_completed',
60
        'succeeded' => 'scorm_completed',
61
        'browsed' => 'scorm_completed',
62
    ];
63
64
    public $attempt = 0; // The number for the current ID view.
65
    public $cc; // Course (code) this learnpath is located in. @todo change name for something more comprensible ...
66
    public $current; // Id of the current item the user is viewing.
67
    public $current_score; // The score of the current item.
68
    public $current_time_start; // The time the user loaded this resource (this does not mean he can see it yet).
69
    public $current_time_stop; // The time the user closed this resource.
70
    public $default_status = 'not attempted';
71
    public $encoding = 'UTF-8';
72
    public $error = '';
73
    public $force_commit = false; // For SCORM only- if true will send a scorm LMSCommit() request on each LMSSetValue()
74
    public $index; // The index of the active learnpath_item in $ordered_items array.
75
    /** @var learnpathItem[] */
76
    public $items = [];
77
    public $last; // item_id of last item viewed in the learning path.
78
    public $last_item_seen = 0; // In case we have already come in this lp, reuse the last item seen if authorized.
79
    public $license; // Which license this course has been given - not used yet on 20060522.
80
    public $lp_id; // DB iid for this learnpath.
81
    public $lp_view_id; // DB ID for lp_view
82
    public $maker; // Which maker has conceived the content (ENI, Articulate, ...).
83
    public $message = '';
84
    public $mode = 'embedded'; // Holds the video display mode (fullscreen or embedded).
85
    public $name; // Learnpath name (they generally have one).
86
    public $ordered_items = []; // List of the learnpath items in the order they are to be read.
87
    public $path = ''; // Path inside the scorm directory (if scorm).
88
    public $theme; // The current theme of the learning path.
89
    public $accumulateScormTime; // Flag to decide whether to accumulate SCORM time or not
90
    public $accumulateWorkTime; // The min time of learnpath
91
92
    // Tells if all the items of the learnpath can be tried again. Defaults to "no" (=1).
93
    public $prevent_reinit = 1;
94
95
    // Describes the mode of progress bar display.
96
    public $seriousgame_mode = 0;
97
    public $progress_bar_mode = '%';
98
99
    // Percentage progress as saved in the db.
100
    public $progress_db = 0;
101
    public $proximity; // Wether the content is distant or local or unknown.
102
    public $refs_list = []; //list of items by ref => db_id. Used only for prerequisites match.
103
    // !!!This array (refs_list) is built differently depending on the nature of the LP.
104
    // If SCORM, uses ref, if Chamilo, uses id to keep a unique value.
105
    public $type; //type of learnpath. Could be 'chamilo', 'scorm', 'scorm2004', 'aicc', ...
106
    // TODO: Check if this type variable is useful here (instead of just in the controller script).
107
    public $user_id; //ID of the user that is viewing/using the course
108
    public $update_queue = [];
109
    public $scorm_debug = 0;
110
    public $arrMenu = []; // Array for the menu items.
111
    public $debug = 0; // Logging level.
112
    public $lp_session_id = 0;
113
    public $lp_view_session_id = 0; // The specific view might be bound to a session.
114
    public $prerequisite = 0;
115
    public $use_max_score = 1; // 1 or 0
116
    public $subscribeUsers = 0; // Subscribe users or not
117
    public $created_on = '';
118
    public $modified_on = '';
119
    public $published_on = '';
120
    public $expired_on = '';
121
    public $ref;
122
    public $course_int_id;
123
    public $course_info;
124
    public $categoryId;
125
    public $scormUrl;
126
    public $entity;
127
128
    public function __construct(CLp $entity = null, $course_info, $user_id)
129
    {
130
        $debug = $this->debug;
131
        $user_id = (int) $user_id;
132
        $this->encoding = api_get_system_encoding();
133
        $lp_id = 0;
134
        if (null !== $entity) {
135
            $lp_id = $entity->getIid();
136
        }
137
        $course_info = empty($course_info) ? api_get_course_info() : $course_info;
138
        $course_id = (int) $course_info['real_id'];
139
        $this->course_info = $course_info;
140
        $this->set_course_int_id($course_id);
141
        if (empty($lp_id) || empty($course_id)) {
142
            $this->error = "Parameter is empty: LpId:'$lp_id', courseId: '$lp_id'";
143
        } else {
144
            //$this->entity = $entity;
145
            $this->lp_id = $lp_id;
146
            $this->type = $entity->getLpType();
147
            $this->name = stripslashes($entity->getTitle());
148
            $this->proximity = $entity->getContentLocal();
149
            $this->theme = $entity->getTheme();
150
            $this->maker = $entity->getContentLocal();
151
            $this->prevent_reinit = $entity->getPreventReinit();
152
            $this->seriousgame_mode = $entity->getSeriousgameMode();
153
            $this->license = $entity->getContentLicense();
154
            $this->scorm_debug = $entity->getDebug();
155
            $this->js_lib = $entity->getJsLib();
156
            $this->path = $entity->getPath();
157
            $this->author = $entity->getAuthor();
158
            $this->hide_toc_frame = $entity->getHideTocFrame();
159
            //$this->lp_session_id = $entity->getSessionId();
160
            $this->use_max_score = $entity->getUseMaxScore();
161
            $this->subscribeUsers = $entity->getSubscribeUsers();
162
            $this->created_on = $entity->getCreatedOn()->format('Y-m-d H:i:s');
163
            $this->modified_on = $entity->getModifiedOn()->format('Y-m-d H:i:s');
164
            $this->ref = $entity->getRef();
165
            $this->categoryId = 0;
166
            if ($entity->getCategory()) {
167
                $this->categoryId = $entity->getCategory()->getIid();
168
            }
169
170
            if ($entity->hasAsset()) {
171
                $asset = $entity->getAsset();
172
                $this->scormUrl = Container::getAssetRepository()->getAssetUrl($asset).'/'.$entity->getPath().'/';
173
            }
174
175
            $this->accumulateScormTime = $entity->getAccumulateWorkTime();
176
177
            if (!empty($entity->getPublishedOn())) {
178
                $this->published_on = $entity->getPublishedOn()->format('Y-m-d H:i:s');
179
            }
180
181
            if (!empty($entity->getExpiredOn())) {
182
                $this->expired_on = $entity->getExpiredOn()->format('Y-m-d H:i:s');
183
            }
184
            if (2 == $this->type) {
185
                if (1 == $entity->getForceCommit()) {
186
                    $this->force_commit = true;
187
                }
188
            }
189
            $this->mode = $entity->getDefaultViewMod();
190
191
            // Check user ID.
192
            if (empty($user_id)) {
193
                $this->error = 'User ID is empty';
194
            } else {
195
                $this->user_id = $user_id;
196
            }
197
198
            // End of variables checking.
199
            $session_id = api_get_session_id();
200
            //  Get the session condition for learning paths of the base + session.
201
            $session = api_get_session_condition($session_id);
202
            // Now get the latest attempt from this user on this LP, if available, otherwise create a new one.
203
            $lp_table = Database::get_course_table(TABLE_LP_VIEW);
204
205
            // Selecting by view_count descending allows to get the highest view_count first.
206
            $sql = "SELECT * FROM $lp_table
207
                    WHERE
208
                        c_id = $course_id AND
209
                        lp_id = $lp_id AND
210
                        user_id = $user_id
211
                        $session
212
                    ORDER BY view_count DESC";
213
            $res = Database::query($sql);
214
215
            if (Database::num_rows($res) > 0) {
216
                $row = Database::fetch_array($res);
217
                $this->attempt = $row['view_count'];
218
                $this->lp_view_id = $row['iid'];
219
                $this->last_item_seen = $row['last_item'];
220
                $this->progress_db = $row['progress'];
221
                $this->lp_view_session_id = $row['session_id'];
222
            } elseif (!api_is_invitee()) {
223
                $this->attempt = 1;
224
                $params = [
225
                    'c_id' => $course_id,
226
                    'lp_id' => $lp_id,
227
                    'user_id' => $user_id,
228
                    'view_count' => 1,
229
                    //'session_id' => $session_id,
230
                    'last_item' => 0,
231
                ];
232
                if (!empty($session_id)) {
233
                    $params['session_id'] = $session_id;
234
                }
235
                $this->last_item_seen = 0;
236
                $this->lp_view_session_id = $session_id;
237
                $this->lp_view_id = Database::insert($lp_table, $params);
238
            }
239
240
            $criteria = new Criteria();
241
            $criteria
242
                ->where($criteria->expr()->neq('path', 'root'))
243
                ->orderBy(
244
                    [
245
                        'parent' => Criteria::ASC,
246
                        'displayOrder' => Criteria::ASC,
247
                    ]
248
                );
249
            $items = $entity->getItems()->matching($criteria);
250
            $lp_item_id_list = [];
251
            foreach ($items as $item) {
252
                $itemId = $item->getIid();
253
                $lp_item_id_list[] = $itemId;
254
255
                switch ($this->type) {
256
                    case CLp::AICC_TYPE:
257
                        $oItem = new aiccItem('db', $itemId, $course_id);
258
                        if (is_object($oItem)) {
259
                            $oItem->set_lp_view($this->lp_view_id);
260
                            $oItem->set_prevent_reinit($this->prevent_reinit);
261
                            // Don't use reference here as the next loop will make the pointed object change.
262
                            $this->items[$itemId] = $oItem;
263
                            $this->refs_list[$oItem->ref] = $itemId;
264
                        }
265
                        break;
266
                    case CLp::SCORM_TYPE:
267
                        $oItem = new scormItem('db', $itemId);
268
                        if (is_object($oItem)) {
269
                            $oItem->set_lp_view($this->lp_view_id);
270
                            $oItem->set_prevent_reinit($this->prevent_reinit);
271
                            // Don't use reference here as the next loop will make the pointed object change.
272
                            $this->items[$itemId] = $oItem;
273
                            $this->refs_list[$oItem->ref] = $itemId;
274
                        }
275
                        break;
276
                    case CLp::LP_TYPE:
277
                    default:
278
                        $oItem = new learnpathItem(null, $item);
279
                        if (is_object($oItem)) {
280
                            // Moved down to when we are sure the item_view exists.
281
                            //$oItem->set_lp_view($this->lp_view_id);
282
                            $oItem->set_prevent_reinit($this->prevent_reinit);
283
                            // Don't use reference here as the next loop will make the pointed object change.
284
                            $this->items[$itemId] = $oItem;
285
                            $this->refs_list[$itemId] = $itemId;
286
                        }
287
                        break;
288
                }
289
290
                // Setting the object level with variable $this->items[$i][parent]
291
                foreach ($this->items as $itemLPObject) {
292
                    $level = self::get_level_for_item($this->items, $itemLPObject->db_id);
293
                    $itemLPObject->level = $level;
294
                }
295
296
                // Setting the view in the item object.
297
                if (isset($this->items[$itemId]) && is_object($this->items[$itemId])) {
298
                    $this->items[$itemId]->set_lp_view($this->lp_view_id);
299
                    if (TOOL_HOTPOTATOES == $this->items[$itemId]->get_type()) {
300
                        $this->items[$itemId]->current_start_time = 0;
301
                        $this->items[$itemId]->current_stop_time = 0;
302
                    }
303
                }
304
            }
305
306
            if (!empty($lp_item_id_list)) {
307
                $lp_item_id_list_to_string = implode("','", $lp_item_id_list);
308
                if (!empty($lp_item_id_list_to_string)) {
309
                    // Get last viewing vars.
310
                    $itemViewTable = Database::get_course_table(TABLE_LP_ITEM_VIEW);
311
                    // This query should only return one or zero result.
312
                    $sql = "SELECT lp_item_id, status
313
                            FROM $itemViewTable
314
                            WHERE
315
                                lp_view_id = ".$this->get_view_id()." AND
316
                                lp_item_id IN ('".$lp_item_id_list_to_string."')
317
                            ORDER BY view_count DESC ";
318
                    $status_list = [];
319
                    $res = Database::query($sql);
320
                    while ($row = Database:: fetch_array($res)) {
321
                        $status_list[$row['lp_item_id']] = $row['status'];
322
                    }
323
324
                    foreach ($lp_item_id_list as $item_id) {
325
                        if (isset($status_list[$item_id])) {
326
                            $status = $status_list[$item_id];
327
328
                            if (is_object($this->items[$item_id])) {
329
                                $this->items[$item_id]->set_status($status);
330
                                if (empty($status)) {
331
                                    $this->items[$item_id]->set_status(
332
                                        $this->default_status
333
                                    );
334
                                }
335
                            }
336
                        } else {
337
                            if (!api_is_invitee()) {
338
                                if (isset($this->items[$item_id]) && is_object($this->items[$item_id])) {
339
                                    $this->items[$item_id]->set_status(
340
                                        $this->default_status
341
                                    );
342
                                }
343
344
                                if (!empty($this->lp_view_id)) {
345
                                    // Add that row to the lp_item_view table so that
346
                                    // we have something to show in the stats page.
347
                                    $params = [
348
                                        'lp_item_id' => $item_id,
349
                                        'lp_view_id' => $this->lp_view_id,
350
                                        'view_count' => 1,
351
                                        'status' => 'not attempted',
352
                                        'start_time' => time(),
353
                                        'total_time' => 0,
354
                                        'score' => 0,
355
                                    ];
356
                                    Database::insert($itemViewTable, $params);
357
358
                                    $this->items[$item_id]->set_lp_view(
359
                                        $this->lp_view_id
360
                                    );
361
                                }
362
                            }
363
                        }
364
                    }
365
                }
366
            }
367
368
            $this->ordered_items = self::get_flat_ordered_items_list($entity, null);
369
            $this->max_ordered_items = 0;
370
            foreach ($this->ordered_items as $index => $dummy) {
371
                if ($index > $this->max_ordered_items && !empty($dummy)) {
372
                    $this->max_ordered_items = $index;
373
                }
374
            }
375
            // TODO: Define the current item better.
376
            $this->first();
377
            if ($debug) {
378
                error_log('lp_view_session_id '.$this->lp_view_session_id);
379
                error_log('End of learnpath constructor for learnpath '.$this->get_id());
380
            }
381
        }
382
    }
383
384
    /**
385
     * @return int
386
     */
387
    public function get_course_int_id()
388
    {
389
        return $this->course_int_id ?? api_get_course_int_id();
390
    }
391
392
    /**
393
     * @param $course_id
394
     *
395
     * @return int
396
     */
397
    public function set_course_int_id($course_id)
398
    {
399
        return $this->course_int_id = (int) $course_id;
400
    }
401
402
    /**
403
     * Function rewritten based on old_add_item() from Yannick Warnier.
404
     * Due the fact that users can decide where the item should come, I had to overlook this function and
405
     * I found it better to rewrite it. Old function is still available.
406
     * Added also the possibility to add a description.
407
     *
408
     * @param CLpItem $parent
409
     * @param int     $previousId
410
     * @param string  $type
411
     * @param int     $id resource ID (ref)
412
     * @param string  $title
413
     * @param string  $description
414
     * @param int     $prerequisites
415
     * @param int     $maxTimeAllowed
416
     * @param int     $userId
417
     *
418
     * @return int
419
     */
420
    public function add_item(
421
        ?CLpItem $parent,
422
        $previousId,
423
        $type,
424
        $id,
425
        $title,
426
        $description = '',
427
        $prerequisites = 0,
428
        $maxTimeAllowed = 0
429
    ) {
430
        $type = empty($type) ? 'dir' : $type;
431
        $course_id = $this->course_info['real_id'];
432
        if (empty($course_id)) {
433
            // Sometimes Oogie doesn't catch the course info but sets $this->cc
434
            $this->course_info = api_get_course_info($this->cc);
435
            $course_id = $this->course_info['real_id'];
436
        }
437
        $id = (int) $id;
438
        $maxTimeAllowed = (int) $maxTimeAllowed;
439
        if (empty($maxTimeAllowed)) {
440
            $maxTimeAllowed = 0;
441
        }
442
        $maxScore = 100;
443
        if ('quiz' === $type && $id) {
444
            // Disabling the exercise if we add it inside a LP
445
            $exercise = new Exercise($course_id);
446
            $exercise->read($id);
447
            $maxScore = $exercise->getMaxScore();
448
449
            $exercise->disable();
450
            $exercise->save();
451
            $title = $exercise->get_formated_title();
452
        }
453
454
        $lpItem = (new CLpItem())
455
            ->setTitle($title)
456
            ->setDescription($description)
457
            ->setPath($id)
458
            ->setLp(api_get_lp_entity($this->get_id()))
459
            ->setItemType($type)
460
            ->setMaxScore($maxScore)
461
            ->setMaxTimeAllowed($maxTimeAllowed)
462
            ->setPrerequisite($prerequisites)
463
            //->setDisplayOrder($display_order + 1)
464
            //->setNextItemId((int) $next)
465
            //->setPreviousItemId($previous)
466
        ;
467
468
        if (!empty($parent))  {
469
            $lpItem->setParent($parent);
470
        }
471
        $em = Database::getManager();
472
        $em->persist($lpItem);
473
        $em->flush();
474
475
        $new_item_id = $lpItem->getIid();
476
        if ($new_item_id) {
477
            // @todo fix upload audio.
478
            // Upload audio.
479
            /*if (!empty($_FILES['mp3']['name'])) {
480
                // Create the audio folder if it does not exist yet.
481
                $filepath = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document/';
482
                if (!is_dir($filepath.'audio')) {
483
                    mkdir(
484
                        $filepath.'audio',
485
                        api_get_permissions_for_new_directories()
486
                    );
487
                    DocumentManager::addDocument(
488
                        $_course,
489
                        '/audio',
490
                        'folder',
491
                        0,
492
                        'audio',
493
                        '',
494
                        0,
495
                        true,
496
                        null,
497
                        $sessionId,
498
                        $userId
499
                    );
500
                }
501
502
                $file_path = handle_uploaded_document(
503
                    $_course,
504
                    $_FILES['mp3'],
505
                    api_get_path(SYS_COURSE_PATH).$_course['path'].'/document',
506
                    '/audio',
507
                    $userId,
508
                    '',
509
                    '',
510
                    '',
511
                    '',
512
                    false
513
                );
514
515
                // Getting the filename only.
516
                $file_components = explode('/', $file_path);
517
                $file = $file_components[count($file_components) - 1];
518
519
                // Store the mp3 file in the lp_item table.
520
                $sql = "UPDATE $tbl_lp_item SET
521
                          audio = '".Database::escape_string($file)."'
522
                        WHERE iid = '".intval($new_item_id)."'";
523
                Database::query($sql);
524
            }*/
525
        }
526
527
        return $new_item_id;
528
    }
529
530
    /**
531
     * Static admin function allowing addition of a learnpath to a course.
532
     *
533
     * @param string $courseCode
534
     * @param string $name
535
     * @param string $description
536
     * @param string $learnpath
537
     * @param string $origin
538
     * @param string $zipname       Zip file containing the learnpath or directory containing the learnpath
539
     * @param string $published_on
540
     * @param string $expired_on
541
     * @param int    $categoryId
542
     * @param int    $userId
543
     *
544
     * @return CLp
545
     */
546
    public static function add_lp(
547
        $courseCode,
548
        $name,
549
        $description = '',
550
        $learnpath = 'guess',
551
        $origin = 'zip',
552
        $zipname = '',
553
        $published_on = '',
554
        $expired_on = '',
555
        $categoryId = 0,
556
        $userId = 0
557
    ) {
558
        global $charset;
559
560
        if (!empty($courseCode)) {
561
            $courseInfo = api_get_course_info($courseCode);
562
            $course_id = $courseInfo['real_id'];
563
        } else {
564
            $course_id = api_get_course_int_id();
565
            $courseInfo = api_get_course_info();
566
        }
567
568
        $categoryId = (int) $categoryId;
569
570
        if (empty($published_on)) {
571
            $published_on = null;
572
        } else {
573
            $published_on = api_get_utc_datetime($published_on, true, true);
574
        }
575
576
        if (empty($expired_on)) {
577
            $expired_on = null;
578
        } else {
579
            $expired_on = api_get_utc_datetime($expired_on, true, true);
580
        }
581
582
        $description = Database::escape_string(api_htmlentities($description, ENT_QUOTES));
583
        $type = 1;
584
        switch ($learnpath) {
585
            case 'guess':
586
            case 'aicc':
587
                break;
588
            case 'dokeos':
589
            case 'chamilo':
590
                $type = 1;
591
                break;
592
        }
593
594
        $sessionEntity = api_get_session_entity();
595
        $courseEntity = api_get_course_entity($courseInfo['real_id']);
596
        $lp = null;
597
        switch ($origin) {
598
            case 'zip':
599
                // Check zip name string. If empty, we are currently creating a new Chamilo learnpath.
600
                break;
601
            case 'manual':
602
            default:
603
                /*$get_max = "SELECT MAX(display_order)
604
                            FROM $tbl_lp WHERE c_id = $course_id";
605
                $res_max = Database::query($get_max);
606
                if (Database::num_rows($res_max) < 1) {
607
                    $dsp = 1;
608
                } else {
609
                    $row = Database::fetch_array($res_max);
610
                    $dsp = $row[0] + 1;
611
                }*/
612
613
                $category = null;
614
                if (!empty($categoryId)) {
615
                    $category = Container::getLpCategoryRepository()->find($categoryId);
616
                }
617
618
                $lpRepo = Container::getLpRepository();
619
620
                $lp = (new CLp())
621
                    ->setLpType($type)
622
                    ->setTitle($name)
623
                    ->setDescription($description)
624
                    ->setCategory($category)
625
                    ->setPublishedOn($published_on)
626
                    ->setExpiredOn($expired_on)
627
                    ->setParent($courseEntity)
628
                    ->addCourseLink($courseEntity, $sessionEntity)
629
                ;
630
                $lpRepo->createLp($lp);
631
632
                break;
633
        }
634
635
        return $lp;
636
    }
637
638
    /**
639
     * Auto completes the parents of an item in case it's been completed or passed.
640
     *
641
     * @param int $item Optional ID of the item from which to look for parents
642
     */
643
    public function autocomplete_parents($item)
644
    {
645
        $debug = $this->debug;
646
647
        if (empty($item)) {
648
            $item = $this->current;
649
        }
650
651
        $currentItem = $this->getItem($item);
652
        if ($currentItem) {
653
            $parent_id = $currentItem->get_parent();
654
            $parent = $this->getItem($parent_id);
655
            if ($parent) {
656
                // if $item points to an object and there is a parent.
657
                if ($debug) {
658
                    error_log(
659
                        'Autocompleting parent of item '.$item.' '.
660
                        $currentItem->get_title().'" (item '.$parent_id.' "'.$parent->get_title().'") ',
661
                        0
662
                    );
663
                }
664
665
                // New experiment including failed and browsed in completed status.
666
                //$current_status = $currentItem->get_status();
667
                //if ($currentItem->is_done() || $current_status == 'browsed' || $current_status == 'failed') {
668
                // Fixes chapter auto complete
669
                if (true) {
670
                    // If the current item is completed or passes or succeeded.
671
                    $updateParentStatus = true;
672
                    if ($debug) {
673
                        error_log('Status of current item is alright');
674
                    }
675
676
                    foreach ($parent->get_children() as $childItemId) {
677
                        $childItem = $this->getItem($childItemId);
678
679
                        // If children was not set try to get the info
680
                        if (empty($childItem->db_item_view_id)) {
681
                            $childItem->set_lp_view($this->lp_view_id);
682
                        }
683
684
                        // Check all his brothers (parent's children) for completion status.
685
                        if ($childItemId != $item) {
686
                            if ($debug) {
687
                                error_log(
688
                                    'Looking at brother #'.$childItemId.' "'.$childItem->get_title().'", status is '.$childItem->get_status(),
689
                                    0
690
                                );
691
                            }
692
                            // Trying completing parents of failed and browsed items as well.
693
                            if ($childItem->status_is(
694
                                [
695
                                    'completed',
696
                                    'passed',
697
                                    'succeeded',
698
                                    'browsed',
699
                                    'failed',
700
                                ]
701
                            )
702
                            ) {
703
                                // Keep completion status to true.
704
                                continue;
705
                            } else {
706
                                if ($debug > 2) {
707
                                    error_log(
708
                                        '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,
709
                                        0
710
                                    );
711
                                }
712
                                $updateParentStatus = false;
713
                                break;
714
                            }
715
                        }
716
                    }
717
718
                    if ($updateParentStatus) {
719
                        // If all the children were completed:
720
                        $parent->set_status('completed');
721
                        $parent->save(false, $this->prerequisites_match($parent->get_id()));
722
                        // Force the status to "completed"
723
                        //$this->update_queue[$parent->get_id()] = $parent->get_status();
724
                        $this->update_queue[$parent->get_id()] = 'completed';
725
                        if ($debug) {
726
                            error_log(
727
                                'Added parent #'.$parent->get_id().' "'.$parent->get_title().'" to update queue status: completed '.
728
                                print_r($this->update_queue, 1),
729
                                0
730
                            );
731
                        }
732
                        // Recursive call.
733
                        $this->autocomplete_parents($parent->get_id());
734
                    }
735
                }
736
            } else {
737
                if ($debug) {
738
                    error_log("Parent #$parent_id does not exists");
739
                }
740
            }
741
        } else {
742
            if ($debug) {
743
                error_log("#$item is an item that doesn't have parents");
744
            }
745
        }
746
    }
747
748
    /**
749
     * Closes the current resource.
750
     *
751
     * Stops the timer
752
     * Saves into the database if required
753
     * Clears the current resource data from this object
754
     *
755
     * @return bool True on success, false on failure
756
     */
757
    public function close()
758
    {
759
        if (empty($this->lp_id)) {
760
            $this->error = 'Trying to close this learnpath but no ID is set';
761
762
            return false;
763
        }
764
        $this->current_time_stop = time();
765
        $this->ordered_items = [];
766
        $this->index = 0;
767
        unset($this->lp_id);
768
        //unset other stuff
769
        return true;
770
    }
771
772
    /**
773
     * Static admin function allowing removal of a learnpath.
774
     *
775
     * @param array  $courseInfo
776
     * @param int    $id         Learnpath ID
777
     * @param string $delete     Whether to delete data or keep it (default: 'keep', others: 'remove')
778
     *
779
     * @return bool True on success, false on failure (might change that to return number of elements deleted)
780
     */
781
    public function delete($courseInfo = null, $id = null, $delete = 'keep')
782
    {
783
        $course_id = api_get_course_int_id();
784
        if (!empty($courseInfo)) {
785
            $course_id = isset($courseInfo['real_id']) ? $courseInfo['real_id'] : $course_id;
786
        }
787
788
        // TODO: Implement a way of getting this to work when the current object is not set.
789
        // In clear: implement this in the item class as well (abstract class) and use the given ID in queries.
790
        // If an ID is specifically given and the current LP is not the same, prevent delete.
791
        if (!empty($id) && ($id != $this->lp_id)) {
792
            return false;
793
        }
794
795
        $course = api_get_course_entity();
796
        $session = api_get_session_entity();
797
        $lp = Container::getLpRepository()->find($this->lp_id);
798
799
        Database::getManager()
800
            ->getRepository(ResourceLink::class)
801
            ->removeByResourceInContext($lp, $course, $session);
802
803
        $link_info = GradebookUtils::isResourceInCourseGradebook(
804
            api_get_course_int_id(),
805
            4,
806
            $id,
807
            api_get_session_id()
808
        );
809
810
        if (!empty($link_info)) {
811
            GradebookUtils::remove_resource_from_course_gradebook($link_info['id']);
812
        }
813
814
        $trackRepo     = Container::$container->get(TrackEDefaultRepository::class);
815
        $resourceNode  = $lp->getResourceNode();
816
        if ($resourceNode) {
817
            $trackRepo->registerResourceEvent(
818
                $resourceNode,
819
                'deletion',
820
                api_get_user_id(),
821
                api_get_course_int_id(),
822
                api_get_session_id()
823
            );
824
        }
825
    }
826
827
    /**
828
     * Removes all the children of one item - dangerous!
829
     *
830
     * @param int $id Element ID of which children have to be removed
831
     *
832
     * @return int Total number of children removed
833
     */
834
    public function delete_children_items($id)
835
    {
836
        $course_id = $this->course_info['real_id'];
837
838
        $num = 0;
839
        $id = (int) $id;
840
        if (empty($id) || empty($course_id)) {
841
            return false;
842
        }
843
        $lp_item = Database::get_course_table(TABLE_LP_ITEM);
844
        $sql = "SELECT * FROM $lp_item
845
                WHERE parent_item_id = $id";
846
        $res = Database::query($sql);
847
        while ($row = Database::fetch_array($res)) {
848
            $num += $this->delete_children_items($row['iid']);
849
            $sql = "DELETE FROM $lp_item
850
                    WHERE iid = ".$row['iid'];
851
            Database::query($sql);
852
            $num++;
853
        }
854
855
        return $num;
856
    }
857
858
    /**
859
     * Removes an item from the current learnpath.
860
     *
861
     * @param int $id Elem ID (0 if first)
862
     *
863
     * @return int Number of elements moved
864
     *
865
     * @todo implement resource removal
866
     */
867
    public function delete_item($id)
868
    {
869
        $course_id = api_get_course_int_id();
870
        $id = (int) $id;
871
        // TODO: Implement the resource removal.
872
        if (empty($id) || empty($course_id)) {
873
            return false;
874
        }
875
876
        $repo = Container::getLpItemRepository();
877
        $item = $repo->find($id);
878
        if (null === $item) {
879
            return false;
880
        }
881
882
        $em = Database::getManager();
883
        $repo->removeFromTree($item);
884
        $em->flush();
885
        $lp_item = Database::get_course_table(TABLE_LP_ITEM);
886
887
        //Removing prerequisites since the item will not longer exist
888
        $sql_all = "UPDATE $lp_item SET prerequisite = ''
889
                    WHERE prerequisite = '$id'";
890
        Database::query($sql_all);
891
892
        $sql = "UPDATE $lp_item
893
                SET previous_item_id = ".$this->getLastInFirstLevel()."
894
                WHERE lp_id = {$this->lp_id} AND item_type = '".TOOL_LP_FINAL_ITEM."'";
895
        Database::query($sql);
896
897
        // Remove from search engine if enabled.
898
        if ('true' === api_get_setting('search_enabled')) {
899
            $tbl_se_ref = Database::get_main_table(TABLE_MAIN_SEARCH_ENGINE_REF);
900
            $sql = 'SELECT * FROM %s
901
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
902
                    LIMIT 1';
903
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp, $id);
904
            $res = Database::query($sql);
905
            if (Database::num_rows($res) > 0) {
906
                $row2 = Database::fetch_array($res);
907
                $di = new ChamiloIndexer();
908
                $di->remove_document($row2['search_did']);
909
            }
910
            $sql = 'DELETE FROM %s
911
                    WHERE course_code=\'%s\' AND tool_id=\'%s\' AND ref_id_high_level=%s AND ref_id_second_level=%d
912
                    LIMIT 1';
913
            $sql = sprintf($sql, $tbl_se_ref, $this->cc, TOOL_LEARNPATH, $lp, $id);
914
            Database::query($sql);
915
        }
916
    }
917
918
    /**
919
     * Updates an item's content in place.
920
     *
921
     * @param int    $id               Element ID
922
     * @param int    $parent           Parent item ID
923
     * @param int    $previous         Previous item ID
924
     * @param string $title            Item title
925
     * @param string $description      Item description
926
     * @param string $prerequisites    Prerequisites (optional)
927
     * @param array  $audio            The array resulting of the $_FILES[mp3] element
928
     * @param int    $max_time_allowed
929
     * @param string $url
930
     *
931
     * @return bool True on success, false on error
932
     */
933
    public function edit_item(
934
        $id,
935
        $parent,
936
        $previous,
937
        $title,
938
        $description,
939
        $prerequisites = '0',
940
        $audio = [],
941
        $max_time_allowed = 0,
942
        $url = ''
943
    ) {
944
        $_course = api_get_course_info();
945
        $id = (int) $id;
946
947
        if (empty($id) || empty($_course)) {
948
            return false;
949
        }
950
        $repo = Container::getLpItemRepository();
951
        /** @var CLpItem $item */
952
        $item = $repo->find($id);
953
        if (null === $item) {
954
            return false;
955
        }
956
957
        $item
958
            ->setTitle($title)
959
            ->setDescription($description)
960
            ->setPrerequisite($prerequisites)
961
            ->setMaxTimeAllowed((int) $max_time_allowed)
962
        ;
963
964
        $em = Database::getManager();
965
        if (!empty($parent)) {
966
            $parent = $repo->find($parent);
967
            $item->setParent($parent);
968
        } else {
969
            $item->setParent(null);
970
        }
971
972
        if (!empty($previous)) {
973
            $previous = $repo->find($previous);
974
            $repo->persistAsNextSiblingOf( $item, $previous);
975
        } else {
976
            $em->persist($item);
977
        }
978
979
        $em->flush();
980
981
        if ('link' === $item->getItemType()) {
982
            $link = new Link();
983
            $linkId = $item->getPath();
984
            $link->updateLink($linkId, $url);
985
        }
986
    }
987
988
    /**
989
     * Updates an item's prereq in place.
990
     *
991
     * @param int    $id              Element ID
992
     * @param string $prerequisite_id Prerequisite Element ID
993
     * @param int    $minScore        Prerequisite min score
994
     * @param int    $maxScore        Prerequisite max score
995
     *
996
     * @return bool True on success, false on error
997
     */
998
    public function edit_item_prereq($id, $prerequisite_id, $minScore = 0, $maxScore = 100)
999
    {
1000
        $id = (int) $id;
1001
1002
        if (empty($id)) {
1003
            return false;
1004
        }
1005
        $prerequisite_id = (int) $prerequisite_id;
1006
1007
        if (empty($minScore) || $minScore < 0) {
1008
            $minScore = 0;
1009
        }
1010
1011
        if (empty($maxScore) || $maxScore < 0) {
1012
            $maxScore = 100;
1013
        }
1014
1015
        $minScore = (float) $minScore;
1016
        $maxScore = (float) $maxScore;
1017
1018
        if (empty($prerequisite_id)) {
1019
            $prerequisite_id = 'NULL';
1020
            $minScore = 0;
1021
            $maxScore = 100;
1022
        }
1023
1024
        $table = Database::get_course_table(TABLE_LP_ITEM);
1025
        $sql = " UPDATE $table
1026
                 SET
1027
                    prerequisite = $prerequisite_id ,
1028
                    prerequisite_min_score = $minScore ,
1029
                    prerequisite_max_score = $maxScore
1030
                 WHERE iid = $id";
1031
        Database::query($sql);
1032
1033
        return true;
1034
    }
1035
1036
    /**
1037
     * Get the specific prefix index terms of this learning path.
1038
     *
1039
     * @param string $prefix
1040
     *
1041
     * @return array Array of terms
1042
     */
1043
    public function get_common_index_terms_by_prefix($prefix)
1044
    {
1045
        $terms = get_specific_field_values_list_by_prefix(
1046
            $prefix,
1047
            $this->cc,
1048
            TOOL_LEARNPATH,
1049
            $this->lp_id
1050
        );
1051
        $prefix_terms = [];
1052
        if (!empty($terms)) {
1053
            foreach ($terms as $term) {
1054
                $prefix_terms[] = $term['value'];
1055
            }
1056
        }
1057
1058
        return $prefix_terms;
1059
    }
1060
1061
    /**
1062
     * Gets the number of items currently completed.
1063
     *
1064
     * @param bool Flag to determine the failed status is not considered progressed
1065
     *
1066
     * @return int The number of items currently completed
1067
     */
1068
    public function get_complete_items_count(bool $failedStatusException = false): int
1069
    {
1070
        $i = 0;
1071
        $completedStatusList = [
1072
            'completed',
1073
            'passed',
1074
            'succeeded',
1075
            'browsed',
1076
        ];
1077
1078
        if (!$failedStatusException) {
1079
            $completedStatusList[] = 'failed';
1080
        }
1081
1082
        foreach ($this->items as $id => $dummy) {
1083
            // Trying failed and browsed considered "progressed" as well.
1084
            if ($this->items[$id]->status_is($completedStatusList) &&
1085
                'dir' !== $this->items[$id]->get_type()
1086
            ) {
1087
                $i++;
1088
            }
1089
        }
1090
1091
        return $i;
1092
    }
1093
1094
    /**
1095
     * Gets the current item ID.
1096
     *
1097
     * @return int The current learnpath item id
1098
     */
1099
    public function get_current_item_id()
1100
    {
1101
        $current = 0;
1102
        if (!empty($this->current)) {
1103
            $current = (int) $this->current;
1104
        }
1105
1106
        return $current;
1107
    }
1108
1109
    /**
1110
     * Force to get the first learnpath item id.
1111
     *
1112
     * @return int The current learnpath item id
1113
     */
1114
    public function get_first_item_id()
1115
    {
1116
        $current = 0;
1117
        if (is_array($this->ordered_items)) {
1118
            $current = $this->ordered_items[0];
1119
        }
1120
1121
        return $current;
1122
    }
1123
1124
    /**
1125
     * Gets the total number of items available for viewing in this SCORM.
1126
     *
1127
     * @return int The total number of items
1128
     */
1129
    public function get_total_items_count()
1130
    {
1131
        return count($this->items);
1132
    }
1133
1134
    /**
1135
     * Gets the total number of items available for viewing in this SCORM but without chapters.
1136
     *
1137
     * @return int The total no-chapters number of items
1138
     */
1139
    public function getTotalItemsCountWithoutDirs()
1140
    {
1141
        $total = 0;
1142
        $typeListNotToCount = self::getChapterTypes();
1143
        foreach ($this->items as $temp2) {
1144
            if (!in_array($temp2->get_type(), $typeListNotToCount)) {
1145
                $total++;
1146
            }
1147
        }
1148
1149
        return $total;
1150
    }
1151
1152
    /**
1153
     *  Sets the first element URL.
1154
     */
1155
    public function first()
1156
    {
1157
        if ($this->debug > 0) {
1158
            error_log('In learnpath::first()', 0);
1159
            error_log('$this->last_item_seen '.$this->last_item_seen);
1160
        }
1161
1162
        // Test if the last_item_seen exists and is not a dir.
1163
        if (0 == count($this->ordered_items)) {
1164
            $this->index = 0;
1165
        }
1166
1167
        if (!empty($this->last_item_seen) &&
1168
            !empty($this->items[$this->last_item_seen]) &&
1169
            'dir' !== $this->items[$this->last_item_seen]->get_type()
1170
            //with this change (below) the LP will NOT go to the next item, it will take lp item we left
1171
            //&& !$this->items[$this->last_item_seen]->is_done()
1172
        ) {
1173
            if ($this->debug > 2) {
1174
                error_log(
1175
                    'In learnpath::first() - Last item seen is '.$this->last_item_seen.' of type '.
1176
                    $this->items[$this->last_item_seen]->get_type()
1177
                );
1178
            }
1179
            $index = -1;
1180
            foreach ($this->ordered_items as $myindex => $item_id) {
1181
                if ($item_id == $this->last_item_seen) {
1182
                    $index = $myindex;
1183
                    break;
1184
                }
1185
            }
1186
            if (-1 == $index) {
1187
                // Index hasn't changed, so item not found - panic (this shouldn't happen).
1188
                if ($this->debug > 2) {
1189
                    error_log('Last item ('.$this->last_item_seen.') was found in items but not in ordered_items, panic!', 0);
1190
                }
1191
1192
                return false;
1193
            } else {
1194
                $this->last = $this->last_item_seen;
1195
                $this->current = $this->last_item_seen;
1196
                $this->index = $index;
1197
            }
1198
        } else {
1199
            if ($this->debug > 2) {
1200
                error_log('In learnpath::first() - No last item seen', 0);
1201
            }
1202
            $index = 0;
1203
            // Loop through all ordered items and stop at the first item that is
1204
            // not a directory *and* that has not been completed yet.
1205
            while (!empty($this->ordered_items[$index]) &&
1206
                is_a($this->items[$this->ordered_items[$index]], 'learnpathItem') &&
1207
                (
1208
                    'dir' === $this->items[$this->ordered_items[$index]]->get_type() ||
1209
                    true === $this->items[$this->ordered_items[$index]]->is_done()
1210
                ) && $index < $this->max_ordered_items
1211
            ) {
1212
                $index++;
1213
            }
1214
1215
            $this->last = $this->current;
1216
            // current is
1217
            $this->current = isset($this->ordered_items[$index]) ? $this->ordered_items[$index] : null;
1218
            $this->index = $index;
1219
            if ($this->debug > 2) {
1220
                error_log('$index '.$index);
1221
                error_log('In learnpath::first() - No last item seen');
1222
                error_log('New last = '.$this->last.'('.$this->ordered_items[$index].')');
1223
            }
1224
        }
1225
        if ($this->debug > 2) {
1226
            error_log('In learnpath::first() - First item is '.$this->get_current_item_id());
1227
        }
1228
    }
1229
1230
    /**
1231
     * Gets the js library from the database.
1232
     *
1233
     * @return string The name of the javascript library to be used
1234
     */
1235
    public function get_js_lib()
1236
    {
1237
        $lib = '';
1238
        if (!empty($this->js_lib)) {
1239
            $lib = $this->js_lib;
1240
        }
1241
1242
        return $lib;
1243
    }
1244
1245
    /**
1246
     * Gets the learnpath database ID.
1247
     *
1248
     * @return int Learnpath ID in the lp table
1249
     */
1250
    public function get_id()
1251
    {
1252
        if (!empty($this->lp_id)) {
1253
            return (int) $this->lp_id;
1254
        }
1255
1256
        return 0;
1257
    }
1258
1259
    /**
1260
     * Gets the last element URL.
1261
     *
1262
     * @return string URL to load into the viewer
1263
     */
1264
    public function get_last()
1265
    {
1266
        // This is just in case the lesson doesn't cointain a valid scheme, just to avoid "Notices"
1267
        if (count($this->ordered_items) > 0) {
1268
            $this->index = count($this->ordered_items) - 1;
1269
1270
            return $this->ordered_items[$this->index];
1271
        }
1272
1273
        return false;
1274
    }
1275
1276
    /**
1277
     * Get the last element in the first level.
1278
     * Unlike learnpath::get_last this function doesn't consider the subsection' elements.
1279
     *
1280
     * @return mixed
1281
     */
1282
    public function getLastInFirstLevel()
1283
    {
1284
        try {
1285
            $lastId = Database::getManager()
1286
                ->createQuery('SELECT i.iid FROM ChamiloCourseBundle:CLpItem i
1287
                WHERE i.lp = :lp AND i.parent IS NULL AND i.itemType != :type ORDER BY i.displayOrder DESC')
1288
                ->setMaxResults(1)
1289
                ->setParameters(['lp' => $this->lp_id, 'type' => TOOL_LP_FINAL_ITEM])
1290
                ->getSingleScalarResult();
1291
1292
            return $lastId;
1293
        } catch (Exception $exception) {
1294
            return 0;
1295
        }
1296
    }
1297
1298
    /**
1299
     * Gets the navigation bar for the learnpath display screen.
1300
     *
1301
     * @param string $barId
1302
     *
1303
     * @return string The HTML string to use as a navigation bar
1304
     */
1305
    public function get_navigation_bar($barId = '')
1306
    {
1307
        if (empty($barId)) {
1308
            $barId = 'control-top';
1309
        }
1310
        $lpId = $this->lp_id;
1311
        $mycurrentitemid = $this->get_current_item_id();
1312
        $reportingText = get_lang('Reporting');
1313
        $previousText = get_lang('Previous');
1314
        $nextText = get_lang('Next');
1315
        $fullScreenText = get_lang('Back to normal screen');
1316
1317
        $settings = api_get_setting('lp.lp_view_settings', true);
1318
        $display = $settings['display'] ?? false;
1319
        $icon = Display::getMdiIcon('information');
1320
1321
        $reportingIcon = '
1322
            <a class="icon-toolbar"
1323
                id="stats_link"
1324
                href="lp_controller.php?action=stats&'.api_get_cidreq(true).'&lp_id='.$lpId.'"
1325
                onclick="window.parent.API.save_asset(); return true;"
1326
                target="content_name" title="'.$reportingText.'">
1327
                '.$icon.'<span class="sr-only">'.$reportingText.'</span>
1328
            </a>';
1329
1330
        if (!empty($display)) {
1331
            $showReporting = isset($display['show_reporting_icon']) ? $display['show_reporting_icon'] : true;
1332
            if (false === $showReporting) {
1333
                $reportingIcon = '';
1334
            }
1335
        }
1336
1337
        $hideArrows = false;
1338
        if (isset($settings['display']) && isset($settings['display']['hide_lp_arrow_navigation'])) {
1339
            $hideArrows = $settings['display']['hide_lp_arrow_navigation'];
1340
        }
1341
1342
        $previousIcon = '';
1343
        $nextIcon = '';
1344
        if (false === $hideArrows) {
1345
            $icon = Display::getMdiIcon('chevron-left');
1346
            $previousIcon = '
1347
                <button class="icon-toolbar" id="scorm-previous" type="button"
1348
                    onclick="switch_item('.$mycurrentitemid.',\'previous\');return false;" title="'.$previousText.'">
1349
                    '.$icon.'<span class="sr-only">'.$previousText.'</span>
1350
                </button>';
1351
1352
            $icon = Display::getMdiIcon('chevron-right');
1353
            $nextIcon = '
1354
                <button class="icon-toolbar" id="scorm-next" type="button"
1355
                    onclick="switch_item('.$mycurrentitemid.',\'next\');return false;" title="'.$nextText.'">
1356
                    '.$icon.'<span class="sr-only">'.$nextText.'</span>
1357
                </button>';
1358
        }
1359
1360
        if ('fullscreen' === $this->mode) {
1361
            $icon = Display::getMdiIcon('view-column');
1362
            $navbar = '
1363
                  <span id="'.$barId.'" class="buttons">
1364
                    '.$reportingIcon.'
1365
                    '.$previousIcon.'
1366
                    '.$nextIcon.'
1367
                    <a class="icon-toolbar" id="view-embedded"
1368
                        href="lp_controller.php?action=mode&mode=embedded" target="_top" title="'.$fullScreenText.'">
1369
                        '.$icon.'<span class="sr-only">'.$fullScreenText.'</span>
1370
                    </a>
1371
                  </span>';
1372
        } else {
1373
            $navbar = '
1374
                 <span id="'.$barId.'" class="buttons text-right">
1375
                    '.$reportingIcon.'
1376
                    '.$previousIcon.'
1377
                    '.$nextIcon.'
1378
                </span>';
1379
        }
1380
1381
        return $navbar;
1382
    }
1383
1384
    /**
1385
     * Gets the next resource in queue (url).
1386
     *
1387
     * @return string URL to load into the viewer
1388
     */
1389
    public function get_next_index()
1390
    {
1391
        // TODO
1392
        $index = $this->index;
1393
        $index++;
1394
        while (
1395
            !empty($this->ordered_items[$index]) && ('dir' == $this->items[$this->ordered_items[$index]]->get_type()) &&
1396
            $index < $this->max_ordered_items
1397
        ) {
1398
            $index++;
1399
            if ($index == $this->max_ordered_items) {
1400
                if ('dir' === $this->items[$this->ordered_items[$index]]->get_type()) {
1401
                    return $this->index;
1402
                }
1403
1404
                return $index;
1405
            }
1406
        }
1407
        if (empty($this->ordered_items[$index])) {
1408
            return $this->index;
1409
        }
1410
1411
        return $index;
1412
    }
1413
1414
    /**
1415
     * Gets item_id for the next element.
1416
     *
1417
     * @return int Next item (DB) ID
1418
     */
1419
    public function get_next_item_id()
1420
    {
1421
        $new_index = $this->get_next_index();
1422
        if (!empty($new_index)) {
1423
            if (isset($this->ordered_items[$new_index])) {
1424
                return $this->ordered_items[$new_index];
1425
            }
1426
        }
1427
1428
        return 0;
1429
    }
1430
1431
    /**
1432
     * Returns the package type ('scorm','aicc','scorm2004','ppt'...).
1433
     *
1434
     * Generally, the package provided is in the form of a zip file, so the function
1435
     * has been written to test a zip file. If not a zip, the function will return the
1436
     * default return value: ''
1437
     *
1438
     * @param string $filePath the path to the file
1439
     * @param string $file_name the original name of the file
1440
     *
1441
     * @return string 'scorm','aicc','scorm2004','error-empty-package'
1442
     *                if the package is empty, or '' if the package cannot be recognized
1443
     */
1444
    public static function getPackageType($filePath, $file_name)
1445
    {
1446
        // Get name of the zip file without the extension.
1447
        $file_info = pathinfo($file_name);
1448
        $extension = $file_info['extension']; // Extension only.
1449
        if (!empty($_POST['ppt2lp']) && !in_array(strtolower($extension), [
1450
                'dll',
1451
                'exe',
1452
            ])) {
1453
            return 'oogie';
1454
        }
1455
        if (!empty($_POST['woogie']) && !in_array(strtolower($extension), [
1456
                'dll',
1457
                'exe',
1458
            ])) {
1459
            return 'woogie';
1460
        }
1461
1462
        $zipFile = new ZipFile();
1463
        $zipFile->openFile($filePath);
1464
        $zipContentArray = $zipFile->getEntries();
1465
        $package_type = '';
1466
        $manifest = '';
1467
        $aicc_match_crs = 0;
1468
        $aicc_match_au = 0;
1469
        $aicc_match_des = 0;
1470
        $aicc_match_cst = 0;
1471
        $countItems = 0;
1472
        // The following loop should be stopped as soon as we found the right imsmanifest.xml (how to recognize it?).
1473
        if ($zipContentArray) {
1474
            $countItems = count($zipContentArray);
1475
            if ($countItems > 0) {
1476
                foreach ($zipContentArray as $thisContent) {
1477
                    $fileName = basename($thisContent->getName());
1478
                    if (preg_match('~.(php.*|phtml)$~i', $fileName)) {
1479
                        // New behaviour: Don't do anything. These files will be removed in scorm::import_package.
1480
                    } elseif (false !== stristr($fileName, 'imsmanifest.xml')) {
1481
                        $manifest = $fileName; // Just the relative directory inside scorm/
1482
                        $package_type = 'scorm';
1483
                        break; // Exit the foreach loop.
1484
                    } elseif (
1485
                        preg_match('/aicc\//i', $fileName) ||
1486
                        in_array(
1487
                            strtolower(pathinfo($fileName, PATHINFO_EXTENSION)),
1488
                            ['crs', 'au', 'des', 'cst']
1489
                        )
1490
                    ) {
1491
                        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
1492
                        switch ($ext) {
1493
                            case 'crs':
1494
                                $aicc_match_crs = 1;
1495
                                break;
1496
                            case 'au':
1497
                                $aicc_match_au = 1;
1498
                                break;
1499
                            case 'des':
1500
                                $aicc_match_des = 1;
1501
                                break;
1502
                            case 'cst':
1503
                                $aicc_match_cst = 1;
1504
                                break;
1505
                            default:
1506
                                break;
1507
                        }
1508
                        //break; // Don't exit the loop, because if we find an imsmanifest afterwards, we want it, not the AICC.
1509
                    } else {
1510
                        $package_type = '';
1511
                    }
1512
                }
1513
            }
1514
        }
1515
1516
        if (empty($package_type) && 4 == ($aicc_match_crs + $aicc_match_au + $aicc_match_des + $aicc_match_cst)) {
1517
            // If found an aicc directory... (!= false means it cannot be false (error) or 0 (no match)).
1518
            $package_type = 'aicc';
1519
        }
1520
1521
        // Try with chamilo course builder
1522
        if (empty($package_type)) {
1523
            // Sometimes users will try to upload an empty zip, or a zip with
1524
            // only a folder. Catch that and make the calling function aware.
1525
            // If the single file was the imsmanifest.xml, then $package_type
1526
            // would be 'scorm' and we wouldn't be here.
1527
            if ($countItems < 2) {
1528
                return 'error-empty-package';
1529
            }
1530
            $package_type = 'chamilo';
1531
        }
1532
1533
        return $package_type;
1534
    }
1535
1536
    /**
1537
     * Gets the previous resource in queue (url). Also initialises time values for this viewing.
1538
     *
1539
     * @return string URL to load into the viewer
1540
     */
1541
    public function get_previous_index()
1542
    {
1543
        $index = $this->index;
1544
        if (isset($this->ordered_items[$index - 1])) {
1545
            $index--;
1546
            while (isset($this->ordered_items[$index]) &&
1547
                ('dir' === $this->items[$this->ordered_items[$index]]->get_type())
1548
            ) {
1549
                $index--;
1550
                if ($index < 0) {
1551
                    return $this->index;
1552
                }
1553
            }
1554
        }
1555
1556
        return $index;
1557
    }
1558
1559
    /**
1560
     * Gets item_id for the next element.
1561
     *
1562
     * @return int Previous item (DB) ID
1563
     */
1564
    public function get_previous_item_id()
1565
    {
1566
        $index = $this->get_previous_index();
1567
1568
        return $this->ordered_items[$index];
1569
    }
1570
1571
    /**
1572
     * Returns the HTML necessary to print a mediaplayer block inside a page.
1573
     *
1574
     * @param int    $lpItemId
1575
     * @param string $autostart
1576
     *
1577
     * @return string The mediaplayer HTML
1578
     */
1579
    public function get_mediaplayer($lpItemId, $autostart = 'true')
1580
    {
1581
        $courseInfo = api_get_course_info();
1582
        $lpItemId = (int) $lpItemId;
1583
1584
        if (empty($courseInfo) || empty($lpItemId)) {
1585
            return '';
1586
        }
1587
        $item = $this->items[$lpItemId] ?? null;
1588
1589
        if (empty($item)) {
1590
            return '';
1591
        }
1592
1593
        $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
1594
        $tbl_lp_item_view = Database::get_course_table(TABLE_LP_ITEM_VIEW);
1595
        $itemViewId = (int) $item->db_item_view_id;
1596
1597
        // Getting all the information about the item.
1598
        $sql = "SELECT lp_view.status
1599
                FROM $tbl_lp_item as lpi
1600
                INNER JOIN $tbl_lp_item_view as lp_view
1601
                ON (lpi.iid = lp_view.lp_item_id)
1602
                WHERE
1603
                    lp_view.iid = $itemViewId AND
1604
                    lpi.iid = $lpItemId
1605
                ";
1606
        $result = Database::query($sql);
1607
        $row = Database::fetch_assoc($result);
1608
        $output = '';
1609
        $audio = $item->audio;
1610
1611
        if (!empty($audio)) {
1612
            $list = $_SESSION['oLP']->get_toc();
1613
1614
            switch ($item->get_type()) {
1615
                case 'quiz':
1616
                    $type_quiz = false;
1617
                    foreach ($list as $toc) {
1618
                        if ($toc['id'] == $_SESSION['oLP']->current) {
1619
                            $type_quiz = true;
1620
                        }
1621
                    }
1622
1623
                    if ($type_quiz) {
1624
                        if (1 == $_SESSION['oLP']->prevent_reinit) {
1625
                            $autostart_audio = 'completed' === $row['status'] ? 'false' : 'true';
1626
                        } else {
1627
                            $autostart_audio = $autostart;
1628
                        }
1629
                    }
1630
                    break;
1631
                case TOOL_READOUT_TEXT:
1632
                    $autostart_audio = 'false';
1633
                    break;
1634
                default:
1635
                    $autostart_audio = 'true';
1636
            }
1637
1638
            $file = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document'.$audio;
1639
            $url = api_get_path(WEB_COURSE_PATH).$courseInfo['path'].'/document'.$audio.'?'.api_get_cidreq();
1640
1641
            $player = Display::getMediaPlayer(
1642
                $file,
1643
                [
1644
                    'id' => 'lp_audio_media_player',
1645
                    'url' => $url,
1646
                    'autoplay' => $autostart_audio,
1647
                    'width' => '100%',
1648
                ]
1649
            );
1650
1651
            // The mp3 player.
1652
            $output = '<div id="container">';
1653
            $output .= $player;
1654
            $output .= '</div>';
1655
        }
1656
1657
        return $output;
1658
    }
1659
1660
    /**
1661
     * @param int    $studentId
1662
     * @param int    $prerequisite
1663
     * @param Course $course
1664
     * @param int    $sessionId
1665
     *
1666
     * @return bool
1667
     */
1668
    public static function isBlockedByPrerequisite(
1669
        $studentId,
1670
        $prerequisite,
1671
        Course $course,
1672
        $sessionId
1673
    ) {
1674
        $courseId = $course->getId();
1675
1676
        $allow = ('true' === api_get_setting('lp.allow_teachers_to_access_blocked_lp_by_prerequisite'));
1677
        if ($allow) {
1678
            if (api_is_allowed_to_edit() ||
1679
                api_is_platform_admin(true) ||
1680
                api_is_drh() ||
1681
                api_is_coach($sessionId, $courseId, false)
1682
            ) {
1683
                return false;
1684
            }
1685
        }
1686
1687
        $isBlocked = false;
1688
        if (!empty($prerequisite)) {
1689
            $progress = self::getProgress(
1690
                $prerequisite,
1691
                $studentId,
1692
                $courseId,
1693
                $sessionId
1694
            );
1695
            if ($progress < 100) {
1696
                $isBlocked = true;
1697
            }
1698
1699
            if (Tracking::minimumTimeAvailable($sessionId, $courseId)) {
1700
                // Block if it does not exceed minimum time
1701
                // Minimum time (in minutes) to pass the learning path
1702
                $accumulateWorkTime = self::getAccumulateWorkTimePrerequisite($prerequisite, $courseId);
1703
1704
                if ($accumulateWorkTime > 0) {
1705
                    // Total time in course (sum of times in learning paths from course)
1706
                    $accumulateWorkTimeTotal = self::getAccumulateWorkTimeTotal($courseId);
1707
1708
                    // Connect with the plugin_licences_course_session table
1709
                    // which indicates what percentage of the time applies
1710
                    // Minimum connection percentage
1711
                    $perc = 100;
1712
                    // Time from the course
1713
                    $tc = $accumulateWorkTimeTotal;
1714
1715
                    // Percentage of the learning paths
1716
                    $pl = $accumulateWorkTime / $accumulateWorkTimeTotal;
1717
                    // Minimum time for each learning path
1718
                    $accumulateWorkTime = ($pl * $tc * $perc / 100);
1719
1720
                    // Spent time (in seconds) so far in the learning path
1721
                    $lpTimeList = Tracking::getCalculateTime($studentId, $courseId, $sessionId);
1722
                    $lpTime = isset($lpTimeList[TOOL_LEARNPATH][$prerequisite]) ? $lpTimeList[TOOL_LEARNPATH][$prerequisite] : 0;
1723
1724
                    if ($lpTime < ($accumulateWorkTime * 60)) {
1725
                        $isBlocked = true;
1726
                    }
1727
                }
1728
            }
1729
        }
1730
1731
        return $isBlocked;
1732
    }
1733
1734
    /**
1735
     * Checks if the learning path is visible for student after the progress
1736
     * of its prerequisite is completed, considering the time availability and
1737
     * the LP visibility.
1738
     */
1739
    public static function is_lp_visible_for_student(CLp $lp, $student_id, Course $course, SessionEntity $session = null): bool
1740
    {
1741
        $sessionId = $session ? $session->getId() : 0;
1742
        $courseId = $course->getId();
1743
        $visibility = $lp->isVisible($course, $session);
1744
1745
        // If the item was deleted.
1746
        if (false === $visibility) {
1747
            return false;
1748
        }
1749
1750
        $now = time();
1751
        if ($lp->hasCategory()) {
1752
            $category = $lp->getCategory();
1753
1754
            if (false === self::categoryIsVisibleForStudent(
1755
                    $category,
1756
                    api_get_user_entity($student_id),
1757
                    $course,
1758
                    $session
1759
                )) {
1760
                return false;
1761
            }
1762
1763
            $prerequisite = $lp->getPrerequisite();
1764
            $is_visible = true;
1765
1766
            $isBlocked = self::isBlockedByPrerequisite(
1767
                $student_id,
1768
                $prerequisite,
1769
                $course,
1770
                $sessionId
1771
            );
1772
1773
            if ($isBlocked) {
1774
                $is_visible = false;
1775
            }
1776
1777
            // Also check the time availability of the LP
1778
            if ($is_visible) {
1779
                // Adding visibility restrictions
1780
                if (null !== $lp->getPublishedOn()) {
1781
                    if ($now < $lp->getPublishedOn()->getTimestamp()) {
1782
                        $is_visible = false;
1783
                    }
1784
                }
1785
                // Blocking empty start times see BT#2800
1786
                global $_custom;
1787
                if (isset($_custom['lps_hidden_when_no_start_date']) &&
1788
                    $_custom['lps_hidden_when_no_start_date']
1789
                ) {
1790
                    if (null !== $lp->getPublishedOn()) {
1791
                        $is_visible = false;
1792
                    }
1793
                }
1794
1795
                if (null !== $lp->getExpiredOn()) {
1796
                    if ($now > $lp->getExpiredOn()->getTimestamp()) {
1797
                        $is_visible = false;
1798
                    }
1799
                }
1800
            }
1801
1802
            if ($is_visible) {
1803
                $subscriptionSettings = self::getSubscriptionSettings();
1804
1805
                // Check if the subscription users/group to a LP is ON
1806
                if (1 == $lp->getSubscribeUsers() &&
1807
                    true === $subscriptionSettings['allow_add_users_to_lp']
1808
                ) {
1809
                    // Try group
1810
                    $is_visible = false;
1811
                    // Checking only the user visibility
1812
                    $userVisibility = self::isUserSubscribedToLp($lp, $student_id, $course, $session);
1813
1814
                    if (true === $userVisibility) {
1815
                        return true;
1816
                    }
1817
1818
                    // Try with groups
1819
                    $groupVisibility = self::isGroupSubscribedToLp($lp, $student_id, $course, $session);
1820
                    if (true === $groupVisibility) {
1821
                        return true;
1822
                    }
1823
                }
1824
            }
1825
1826
            return $is_visible;
1827
        } else {
1828
1829
            $is_visible = true;
1830
            $subscriptionSettings = self::getSubscriptionSettings();
1831
            // Check if the subscription users/group to a LP is ON
1832
            if (1 == $lp->getSubscribeUsers() &&
1833
                true === $subscriptionSettings['allow_add_users_to_lp']
1834
            ) {
1835
                $is_visible = false;
1836
                $userVisibility = self::isUserSubscribedToLp($lp, $student_id, $course, $session);
1837
1838
                if (true === $userVisibility) {
1839
                    return true;
1840
                }
1841
1842
                // Try with groups
1843
                $groupVisibility = self::isGroupSubscribedToLp($lp, $student_id, $course, $session);
1844
                if (true === $groupVisibility) {
1845
                    return true;
1846
                }
1847
            }
1848
1849
            return $is_visible;
1850
        }
1851
1852
        return true;
0 ignored issues
show
Unused Code introduced by
return true is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

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