Browse Source

Partial merge with 1.11.x see BT#15952

Julio Montoya 5 years ago
parent
commit
c2b5967e40
55 changed files with 3265 additions and 2913 deletions
  1. 7 29
      main/inc/global.inc.php
  2. 12 4
      main/inc/lib/AnnouncementEmail.php
  3. 55 24
      main/inc/lib/AnnouncementManager.php
  4. 1 1
      main/inc/lib/CourseChatUtils.php
  5. 2 2
      main/inc/lib/GamificationUtils.php
  6. 33 7
      main/inc/lib/ScheduledAnnouncement.php
  7. 17 16
      main/inc/lib/TicketManager.php
  8. 4 4
      main/inc/lib/VideoChat.php
  9. 8 4
      main/inc/lib/agenda.lib.php
  10. 94 82
      main/inc/lib/api.lib.php
  11. 1 1
      main/inc/lib/array.lib.php
  12. 125 66
      main/inc/lib/certificate.lib.php
  13. 2 18
      main/inc/lib/chat.lib.php
  14. 47 449
      main/inc/lib/course.lib.php
  15. 3 75
      main/inc/lib/course_category.lib.php
  16. 12 26
      main/inc/lib/course_description.lib.php
  17. 23 8
      main/inc/lib/course_home.lib.php
  18. 5 0
      main/inc/lib/dashboard.lib.php
  19. 1 0
      main/inc/lib/database.constants.inc.php
  20. 41 1
      main/inc/lib/diagnoser.lib.php
  21. 191 88
      main/inc/lib/display.lib.php
  22. 61 30
      main/inc/lib/document.lib.php
  23. 50 177
      main/inc/lib/events.lib.php
  24. 383 208
      main/inc/lib/exercise.lib.php
  25. 193 114
      main/inc/lib/exercise_show_functions.lib.php
  26. 12 11
      main/inc/lib/extra_field.lib.php
  27. 22 0
      main/inc/lib/extra_field_option.lib.php
  28. 56 13
      main/inc/lib/fileUpload.lib.php
  29. 1 1
      main/inc/lib/fixlinks.js
  30. 10 3
      main/inc/lib/geometry.lib.php
  31. 16 14
      main/inc/lib/glossary.lib.php
  32. 21 76
      main/inc/lib/groupmanager.lib.php
  33. 1 1
      main/inc/lib/image.lib.php
  34. 92 36
      main/inc/lib/internationalization.lib.php
  35. 7 80
      main/inc/lib/link.lib.php
  36. 1 1
      main/inc/lib/login.lib.php
  37. 246 294
      main/inc/lib/message.lib.php
  38. 11 6
      main/inc/lib/notebook.lib.php
  39. 8 5
      main/inc/lib/online.inc.php
  40. 40 14
      main/inc/lib/pdf.lib.php
  41. 196 245
      main/inc/lib/sessionmanager.lib.php
  42. 91 20
      main/inc/lib/skill.lib.php
  43. 51 49
      main/inc/lib/social.lib.php
  44. 88 43
      main/inc/lib/sortable_table.class.php
  45. 131 0
      main/inc/lib/statistics.lib.php
  46. 18 14
      main/inc/lib/system_announcements.lib.php
  47. 8 13
      main/inc/lib/table_sort.class.php
  48. 45 29
      main/inc/lib/template.lib.php
  49. 28 50
      main/inc/lib/text.lib.php
  50. 3 2
      main/inc/lib/timeline.lib.php
  51. 219 192
      main/inc/lib/tracking.lib.php
  52. 47 31
      main/inc/lib/urlmanager.lib.php
  53. 269 147
      main/inc/lib/usergroup.lib.php
  54. 149 87
      main/inc/lib/usermanager.lib.php
  55. 7 2
      main/link/link.php

+ 7 - 29
main/inc/global.inc.php

@@ -1,6 +1,8 @@
 <?php
 /* For licensing terms, see /license.txt */
 
+use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
+
 /**
  * It is recommended that ALL Chamilo scripts include this important file.
  * This script manages
@@ -14,8 +16,6 @@
  * @todo remove the code that displays the button that links to the install page
  * but use a redirect immediately. By doing so the $alreadyInstalled variable can be removed.
  */
-
-// Showing/hiding error codes in global error messages.
 define('SHOW_ERROR_CODES', false);
 
 // Include the libraries that are necessary everywhere
@@ -528,33 +528,10 @@ $language_interface_initial_value = $language_interface;
 /**
  * Include the trad4all language file.
  */
-// if the sub-language feature is on
-$parent_path = SubLanguageManager::get_parent_language_path($language_interface);
-if (!empty($parent_path)) {
-    // include English
-    include $langpath.'english/trad4all.inc.php';
-    // prepare string for current language and its parent
-    $lang_file = $langpath.$language_interface.'/trad4all.inc.php';
-    $parent_lang_file = $langpath.$parent_path.'/trad4all.inc.php';
-    // load the parent language file first
-    if (file_exists($parent_lang_file)) {
-        include $parent_lang_file;
-    }
-    // overwrite the parent language translations if there is a child
-    if (file_exists($lang_file)) {
-        include $lang_file;
-    }
-} else {
-    // if the sub-languages feature is not on, then just load the
-    // set language interface
-    // include English
-    include $langpath.'english/trad4all.inc.php';
-    // prepare string for current language
-    $langfile = $langpath.$language_interface.'/trad4all.inc.php';
-
-    if (file_exists($langfile)) {
-        include $langfile;
-    }
+$languageFilesToLoad = api_get_language_files_to_load($language_interface);
+
+foreach ($languageFilesToLoad as $languageFile) {
+    include $languageFile;
 }
 
 // include the local (contextual) parameters of this course or section
@@ -659,6 +636,7 @@ if (!empty($language_interface)) {
 // if portal is in test mode always generate the file
 if (!file_exists($file) || api_get_setting('server_type') === 'test') {
     $template = new Template();
+    $template->assign('quiz_markers_rolls_js', ChamiloApi::getQuizMarkersRollsJS());
     // Force use of default to avoid problems
     $tpl = 'default/layout/main.js.tpl';
     $contents = $template->fetch($tpl);

+ 12 - 4
main/inc/lib/AnnouncementEmail.php

@@ -191,11 +191,18 @@ class AnnouncementEmail
     /**
      * Email subject.
      *
+     * @param bool $directMessage
+     *
      * @return string
      */
-    public function subject()
+    public function subject($directMessage = false)
     {
-        $result = $this->course('title').' - '.$this->announcement('title');
+        if ($directMessage) {
+            $result = $this->announcement('title');
+        } else {
+            $result = $this->course('title').' - '.$this->announcement('title');
+        }
+
         $result = stripslashes($result);
 
         return $result;
@@ -298,13 +305,14 @@ class AnnouncementEmail
      * @param bool $sendToUsersInSession
      * @param bool $sendToDrhUsers       send a copy of the message to the DRH users
      * @param int  $senderId             related to the main user
+     * @param bool $directMessage
      *
      * @return array
      */
-    public function send($sendToUsersInSession = false, $sendToDrhUsers = false, $senderId = 0)
+    public function send($sendToUsersInSession = false, $sendToDrhUsers = false, $senderId = 0, $directMessage = false)
     {
         $senderId = empty($senderId) ? api_get_user_id() : (int) $senderId;
-        $subject = $this->subject();
+        $subject = $this->subject($directMessage);
 
         // Send email one by one to avoid antispam
         $users = $this->sent_to();

+ 55 - 24
main/inc/lib/AnnouncementManager.php

@@ -1,6 +1,7 @@
 <?php
 /* For licensing terms, see /license.txt */
 
+use Chamilo\CoreBundle\Entity\ExtraField as ExtraFieldEntity;
 use Chamilo\CoreBundle\Entity\ExtraFieldValues;
 use Chamilo\CourseBundle\Entity\CAnnouncement;
 use Chamilo\CourseBundle\Entity\CItemProperty;
@@ -125,7 +126,7 @@ class AnnouncementManager
                         $value = $extra['value'];
                         if ($value instanceof ExtraFieldValues) {
                             $field = $value->getField();
-                            if ($field) {
+                            if ($field instanceof ExtraFieldEntity) {
                                 $data['extra_'.$field->getVariable()] = $value->getValue();
                             }
                         }
@@ -134,7 +135,7 @@ class AnnouncementManager
             }
         }
 
-        if (!empty(api_get_session_id())) {
+        if (!empty($sessionId)) {
             $data['coaches'] = $coaches;
             $data['general_coach'] = $generalCoachName;
             $data['general_coach_email'] = $generalCoachEmail;
@@ -160,7 +161,7 @@ class AnnouncementManager
      */
     public static function get_all_annoucement_by_course($course_info, $session_id = 0)
     {
-        $session_id = intval($session_id);
+        $session_id = (int) $session_id;
         $courseId = $course_info['real_id'];
 
         $tbl_announcement = Database::get_course_table(TABLE_ANNOUNCEMENT);
@@ -316,14 +317,21 @@ class AnnouncementManager
      * @param int $announcementId
      * @param int $courseId
      * @param int $userId
+     * @param int $groupId
      *
      * @return array
      */
     public static function getAnnouncementInfoById(
         $announcementId,
         $courseId,
-        $userId
+        $userId,
+        $groupId = 0
     ) {
+        $announcementId = (int) $announcementId;
+        $courseId = (int) $courseId;
+        $userId = (int) $userId;
+        $groupId = (int) $groupId;
+
         if (api_is_allowed_to_edit(false, true) ||
             (api_get_course_setting('allow_user_edit_announcement') && !api_is_anonymous())
         ) {
@@ -337,13 +345,21 @@ class AnnouncementManager
                         a.cId = :course
                     ORDER BY a.displayOrder DESC";
         } else {
-            $group_list = GroupManager::get_group_ids($courseId, api_get_user_id());
-
-            if (empty($group_list)) {
-                $group_list[] = 0;
-            }
+            $groupList[] = $groupId;
 
             if (api_get_user_id() != 0) {
+                $extraGroupCondition = '';
+                if (!empty($groupId)) {
+                    $groupProperties = GroupManager::get_group_properties($groupId);
+                    if ($groupProperties['announcements_state'] == GroupManager::TOOL_PRIVATE_BETWEEN_USERS) {
+                        $extraGroupCondition = " AND (
+                            ip.toUser = $userId AND ip.group = $groupId OR
+                            (ip.group IN ('0') OR ip.group IS NULL) OR
+                            (ip.group = $groupId AND (ip.toUser IS NULL OR ip.toUser = 0))
+                        )";
+                    }
+                }
+
                 $dql = "SELECT a, ip
                         FROM ChamiloCourseBundle:CAnnouncement a 
                         JOIN ChamiloCourseBundle:CItemProperty ip
@@ -353,11 +369,12 @@ class AnnouncementManager
                             ip.tool='announcement' AND
                             (
                                 ip.toUser = $userId OR
-                                ip.group IN ('0', '".implode("', '", $group_list)."') OR
+                                ip.group IN ('0', '".$groupId."') OR
                                 ip.group IS NULL
                             ) AND
                             ip.visibility = '1' AND                       
                             ip.course = :course
+                            $extraGroupCondition
                         ORDER BY a.displayOrder DESC";
             } else {
                 $dql = "SELECT a, ip
@@ -381,10 +398,14 @@ class AnnouncementManager
             ]
         );
 
-        return [
-            'announcement' => $result[0],
-            'item_property' => $result[1],
-        ];
+        if (!empty($result)) {
+            return [
+                'announcement' => $result[0],
+                'item_property' => $result[1],
+            ];
+        }
+
+        return [];
     }
 
     /**
@@ -396,8 +417,10 @@ class AnnouncementManager
      */
     public static function displayAnnouncement($id)
     {
-        if ($id != strval(intval($id))) {
-            return null;
+        $id = (int) $id;
+
+        if (empty($id)) {
+            return '';
         }
 
         global $charset;
@@ -406,8 +429,14 @@ class AnnouncementManager
         $result = self::getAnnouncementInfoById(
             $id,
             api_get_course_int_id(),
-            api_get_user_id()
+            api_get_user_id(),
+            api_get_group_id()
         );
+
+        if (empty($result)) {
+            return '';
+        }
+
         /** @var CAnnouncement $announcement */
         $announcement = $result['announcement'];
         /** @var CItemProperty $itemProperty */
@@ -464,7 +493,7 @@ class AnnouncementManager
         $html .= "</td></tr>";
 
         $allow = !api_get_configuration_value('hide_announcement_sent_to_users_info');
-        if (api_is_allowed_to_edit(false, true) && $allow) {
+        if ($allow && api_is_allowed_to_edit(false, true)) {
             $sent_to = self::sent_to('announcement', $id);
             $sentToForm = self::sent_to_form($sent_to);
             $html .= Display::tag(
@@ -499,7 +528,7 @@ class AnnouncementManager
             }
             $html .= '</td></tr>';
         }
-        $html .= "</table>";
+        $html .= '</table>';
 
         return $html;
     }
@@ -1057,8 +1086,8 @@ class AnnouncementManager
      */
     public static function get_by_id($courseId, $id)
     {
-        $id = intval($id);
-        $courseId = $courseId ? intval($courseId) : api_get_course_int_id();
+        $id = (int) $id;
+        $courseId = $courseId ? (int) $courseId : api_get_course_int_id();
 
         $tbl_announcement = Database::get_course_table(TABLE_ANNOUNCEMENT);
         $tbl_item_property = Database::get_course_table(TABLE_ITEM_PROPERTY);
@@ -1513,7 +1542,8 @@ class AnnouncementManager
      * @param bool  $sendToUsersInSession
      * @param bool  $sendToDrhUsers
      * @param Monolog\Handler\HandlerInterface logger
-     * @param int $senderId
+     * @param int  $senderId
+     * @param bool $directMessage
      *
      * @return array
      */
@@ -1524,11 +1554,12 @@ class AnnouncementManager
         $sendToUsersInSession = false,
         $sendToDrhUsers = false,
         $logger = null,
-        $senderId = 0
+        $senderId = 0,
+        $directMessage = false
     ) {
         $email = new AnnouncementEmail($courseInfo, $sessionId, $announcementId, $logger);
 
-        return $email->send($sendToUsersInSession, $sendToDrhUsers, $senderId);
+        return $email->send($sendToUsersInSession, $sendToDrhUsers, $senderId, $directMessage);
     }
 
     /**

+ 1 - 1
main/inc/lib/CourseChatUtils.php

@@ -759,7 +759,7 @@ class CourseChatUtils
             'status' => $status,
             'image_url' => UserManager::getUserPicture($user->getId(), USER_IMAGE_SIZE_MEDIUM),
             'profile_url' => api_get_path(WEB_CODE_PATH).'social/profile.php?u='.$user->getId(),
-            'complete_name' => $user->getCompleteName(),
+            'complete_name' => UserManager::formatUserFullName($user),
             'username' => $user->getUsername(),
             'email' => $user->getEmail(),
             'isConnected' => $this->userIsConnected($user->getId()),

+ 2 - 2
main/inc/lib/GamificationUtils.php

@@ -59,7 +59,7 @@ class GamificationUtils
         foreach ($courses as $course) {
             $learnPathListObject = new LearnpathList(
                 $userId,
-                $course['code'],
+                api_get_course_info($course['code']),
                 $sessionId
             );
             $learnPaths = $learnPathListObject->get_flat_list();
@@ -145,7 +145,7 @@ class GamificationUtils
         foreach ($courses as $course) {
             $learnPathListObject = new LearnpathList(
                 $userId,
-                $course['code'],
+                api_get_course_info($course['code']),
                 $sessionId
             );
             $learnPaths = $learnPathListObject->get_flat_list();

+ 33 - 7
main/inc/lib/ScheduledAnnouncement.php

@@ -300,6 +300,7 @@ class ScheduledAnnouncement extends Model
         $messagesSent = 0;
         $now = api_get_utc_datetime();
         $result = $this->get_all();
+        $extraFieldValue = new ExtraFieldValue('scheduled_announcement');
 
         foreach ($result as $result) {
             if (empty($result['sent'])) {
@@ -323,21 +324,34 @@ class ScheduledAnnouncement extends Model
                     }
 
                     if ($users) {
+                        $sendToCoaches = $extraFieldValue->get_values_by_handler_and_field_variable($result['id'], 'send_to_coaches');
+                        $courseList = SessionManager::getCoursesInSession($sessionId);
+                        $coachList = [];
+                        if (!empty($sendToCoaches) && !empty($sendToCoaches['value']) && $sendToCoaches['value'] == 1) {
+                            foreach ($courseList as $courseItemId) {
+                                $coaches = SessionManager::getCoachesByCourseSession(
+                                    $sessionId,
+                                    $courseItemId
+                                );
+                                $coachList = array_merge($coachList, $coaches);
+                            }
+                            $coachList = array_unique($coachList);
+                        }
+
                         $this->update(['id' => $result['id'], 'sent' => 1]);
                         $attachments = $this->getAttachmentToString($result['id']);
                         $subject = $result['subject'];
 
+                        $courseInfo = [];
+                        if (!empty($courseList)) {
+                            $courseId = current($courseList);
+                            $courseInfo = api_get_course_info_by_id($courseId);
+                        }
+
                         foreach ($users as $user) {
                             // Take original message
                             $message = $result['message'];
-
                             $userInfo = api_get_user_info($user['user_id']);
-                            $courseList = SessionManager::getCoursesInSession($sessionId);
-                            $courseInfo = [];
-                            if (!empty($courseList)) {
-                                $courseId = current($courseList);
-                                $courseInfo = api_get_course_info_by_id($courseId);
-                            }
 
                             $progress = '';
                             if (!empty($sessionInfo) && !empty($courseInfo)) {
@@ -377,6 +391,7 @@ class ScheduledAnnouncement extends Model
                                     $generalCoachEmail = $coachInfo['email'];
                                 }
                             }
+
                             $tags = [
                                 '((session_name))' => $sessionInfo['name'],
                                 '((session_start_date))' => $startTime,
@@ -399,6 +414,17 @@ class ScheduledAnnouncement extends Model
                                 $coachId
                             );
                         }
+
+                        $message = get_lang('YouAreReceivingACopyBecauseYouAreACourseCoach').'<br /><br />'.$message;
+
+                        foreach ($coachList as $courseCoachId) {
+                            MessageManager::send_message_simple(
+                                $courseCoachId,
+                                get_lang('YouAreReceivingACopyBecauseYouAreACourseCoach').'&nbsp;'.$subject,
+                                $message,
+                                $coachId
+                            );
+                        }
                     }
 
                     $messagesSent++;

+ 17 - 16
main/inc/lib/TicketManager.php

@@ -375,7 +375,7 @@ class TicketManager
         $ticketId = Database::insert($table_support_tickets, $params);
 
         if ($ticketId) {
-            $ticket_code = "A".str_pad($ticketId, 11, '0', STR_PAD_LEFT);
+            $ticket_code = 'A'.str_pad($ticketId, 11, '0', STR_PAD_LEFT);
             $titleCreated = sprintf(
                 get_lang('TicketXCreated'),
                 $ticket_code
@@ -567,9 +567,9 @@ class TicketManager
             );
 
             return true;
-        } else {
-            return false;
         }
+
+        return false;
     }
 
     /**
@@ -718,6 +718,7 @@ class TicketManager
         $now = api_get_utc_datetime();
         $userId = api_get_user_id();
         $ticketId = (int) $ticketId;
+
         $new_file_name = add_ext_on_mime(
             stripslashes($file_attach['name']),
             $file_attach['type']
@@ -989,7 +990,7 @@ class TicketManager
                 ];
             }
             if ($isAdmin) {
-                $ticket['0'] .= '&nbsp;&nbsp;<a  href="javascript:void(0)" onclick="load_history_ticket(\'div_'.$row['ticket_id'].'\','.$row['ticket_id'].')">
+                $ticket['0'] .= '&nbsp;&nbsp;<a href="javascript:void(0)" onclick="load_history_ticket(\'div_'.$row['ticket_id'].'\','.$row['ticket_id'].')">
 					<img onclick="load_course_list(\'div_'.$row['ticket_id'].'\','.$row['ticket_id'].')" onmouseover="clear_course_list (\'div_'.$row['ticket_id'].'\')" src="'.Display::returnIconPath('history.gif').'" title="'.get_lang('Historial').'" alt="'.get_lang('Historial').'"/>
 					<div class="blackboard_hide" id="div_'.$row['ticket_id'].'">&nbsp;&nbsp;</div>
 					</a>&nbsp;&nbsp;';
@@ -1281,9 +1282,9 @@ class TicketManager
             );
 
             return true;
-        } else {
-            return false;
         }
+
+        return false;
     }
 
     /**
@@ -1457,9 +1458,9 @@ class TicketManager
             );
 
             return true;
-        } else {
-            return false;
         }
+
+        return false;
     }
 
     /**
@@ -1619,17 +1620,13 @@ class TicketManager
             TABLE_TICKET_CATEGORY
         );
         $table_support_tickets = Database::get_main_table(TABLE_TICKET_TICKET);
-        $table_support_priority = Database::get_main_table(
-            TABLE_TICKET_PRIORITY
-        );
+        $table_support_priority = Database::get_main_table(TABLE_TICKET_PRIORITY);
         $table_support_status = Database::get_main_table(TABLE_TICKET_STATUS);
-        $table_support_messages = Database::get_main_table(
-            TABLE_TICKET_MESSAGE
-        );
+        $table_support_messages = Database::get_main_table(TABLE_TICKET_MESSAGE);
         $table_main_user = Database::get_main_table(TABLE_MAIN_USER);
 
         if (is_null($direction)) {
-            $direction = "DESC";
+            $direction = 'DESC';
         }
         if (is_null($userId) || $userId == 0) {
             $userId = api_get_user_id();
@@ -1843,6 +1840,8 @@ class TicketManager
     }
 
     /**
+     * @param array $criteria
+     *
      * @return array
      */
     public static function getTicketsFromCriteria($criteria)
@@ -1869,6 +1868,7 @@ class TicketManager
             ->getRepository('ChamiloTicketBundle:Status')
             ->findOneBy(['code' => $code])
         ;
+
         if ($item) {
             return $item->getId();
         }
@@ -1960,12 +1960,13 @@ class TicketManager
         $project->setName($params['name']);
         $project->setDescription($params['description']);
         $project->setInsertUserId(api_get_user_id());
+
         Database::getManager()->persist($project);
         Database::getManager()->flush();
     }
 
     /**
-     * @param $id
+     * @param int $id
      *
      * @return Project
      */

+ 4 - 4
main/inc/lib/VideoChat.php

@@ -20,8 +20,8 @@ class VideoChat
      */
     public static function getChatRoomByUsers($user1, $user2)
     {
-        $user1 = intval($user1);
-        $user2 = intval($user2);
+        $user1 = (int) $user1;
+        $user2 = (int) $user2;
 
         if (empty($user1) || empty($user2)) {
             return false;
@@ -61,8 +61,8 @@ class VideoChat
         return Database::insert(
             Database::get_main_table(TABLE_MAIN_CHAT_VIDEO),
             [
-                'from_user' => intval($fromUser),
-                'to_user' => intval($toUser),
+                'from_user' => $fromUser,
+                'to_user' => $toUser,
                 'room_name' => $chatName,
                 'datetime' => api_get_utc_datetime(),
             ]

+ 8 - 4
main/inc/lib/agenda.lib.php

@@ -2490,12 +2490,16 @@ class Agenda
             $form->addElement('textarea', 'comment', get_lang('Comment'));
             $form->addLabel(
                 get_lang('FilesAttachment'),
-                '<span id="filepaths">
+                '<div id="filepaths" class="file-upload-event">
+                        
                         <div id="filepath_1">
-                            <input type="file" name="attach_1"/><br />
-                            '.get_lang('Description').'&nbsp;&nbsp;<input type="text" name="legend[]" /><br /><br />
+                            <input type="file" name="attach_1"/>
+                            
+                            <label>'.get_lang('Description').'</label>
+                            <input class="form-control" type="text" name="legend[]" />
                         </div>
-                    </span>'
+                        
+                    </div>'
             );
 
             $form->addLabel(

+ 94 - 82
main/inc/lib/api.lib.php

@@ -150,6 +150,7 @@ define('TOOL_NOTEBOOK', 'notebook');
 define('TOOL_ATTENDANCE', 'attendance');
 define('TOOL_COURSE_PROGRESS', 'course_progress');
 define('TOOL_PORTFOLIO', 'portfolio');
+define('TOOL_PLAGIARISM', 'compilatio');
 
 // CONSTANTS defining Chamilo interface sections
 define('SECTION_CAMPUS', 'mycampus');
@@ -468,7 +469,8 @@ define('ONE_PER_PAGE', 2);
 
 define('EXERCISE_FEEDBACK_TYPE_END', 0); //Feedback 		 - show score and expected answers
 define('EXERCISE_FEEDBACK_TYPE_DIRECT', 1); //DirectFeedback - Do not show score nor answers
-define('EXERCISE_FEEDBACK_TYPE_EXAM', 2); //NoFeedback 	 - Show score only
+define('EXERCISE_FEEDBACK_TYPE_EXAM', 2); // NoFeedback 	 - Show score only
+define('EXERCISE_FEEDBACK_TYPE_POPUP', 3); // Popup BT#15827
 
 define('RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS', 0); //show score and expected answers
 define('RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS', 1); //Do not show score nor answers
@@ -478,8 +480,7 @@ define('RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT', 4);
 define('RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK', 5);
 define('RESULT_DISABLE_RANKING', 6);
 define('RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER', 7);
-
-// 4: Show final score only with  and show expected answers only on the last attempt
+define('RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING', 8);
 
 define('EXERCISE_MAX_NAME_SIZE', 80);
 
@@ -587,6 +588,7 @@ define('MESSAGE_STATUS_WALL_DELETE', '9');
 define('MESSAGE_STATUS_WALL_POST', '10');
 define('MESSAGE_STATUS_CONVERSATION', '11');
 define('MESSAGE_STATUS_FORUM', '12');
+define('MESSAGE_STATUS_PROMOTED', '13');
 
 // Images
 define('IMAGE_WALL_SMALL_SIZE', 200);
@@ -1058,8 +1060,6 @@ function api_is_facebook_auth_activated()
 function api_add_trailing_slash($path)
 {
     return substr($path, -1) == '/' ? $path : $path.'/';
-    // This code is about 20% faster than the preg_replace equivalent
-    //return preg_replace('/([^\/])$/', '$1/', $path);
 }
 
 /**
@@ -1141,6 +1141,7 @@ function api_valid_email($address)
  *
  * @param bool Option to print headers when displaying error message. Default: false
  * @param bool whether session admins should be allowed or not
+ * @param bool $checkTool check if tool is available for users (user, group)
  *
  * @return bool True if the user has access to the current course or is out of a course context, false otherwise
  *
@@ -1148,7 +1149,7 @@ function api_valid_email($address)
  *
  * @author Roan Embrechts
  */
-function api_protect_course_script($print_headers = false, $allow_session_admins = false, $allow_drh = false)
+function api_protect_course_script($print_headers = false, $allow_session_admins = false, $checkTool = '')
 {
     $course_info = api_get_course_info();
     if (empty($course_info)) {
@@ -1157,9 +1158,6 @@ function api_protect_course_script($print_headers = false, $allow_session_admins
         return false;
     }
 
-    $is_allowed_in_course = api_is_allowed_in_course();
-    $is_visible = false;
-
     if (api_is_drh()) {
         return true;
     }
@@ -1174,24 +1172,26 @@ function api_protect_course_script($print_headers = false, $allow_session_admins
         return true;
     }
 
+    $isAllowedInCourse = api_is_allowed_in_course();
+    $is_visible = false;
     if (isset($course_info) && isset($course_info['visibility'])) {
         switch ($course_info['visibility']) {
             default:
             case COURSE_VISIBILITY_CLOSED:
                 // Completely closed: the course is only accessible to the teachers. - 0
-                if (api_get_user_id() && !api_is_anonymous() && $is_allowed_in_course) {
+                if (api_get_user_id() && !api_is_anonymous() && $isAllowedInCourse) {
                     $is_visible = true;
                 }
                 break;
             case COURSE_VISIBILITY_REGISTERED:
                 // Private - access authorized to course members only - 1
-                if (api_get_user_id() && !api_is_anonymous() && $is_allowed_in_course) {
+                if (api_get_user_id() && !api_is_anonymous() && $isAllowedInCourse) {
                     $is_visible = true;
                 }
                 break;
             case COURSE_VISIBILITY_OPEN_PLATFORM:
                 // Open - access allowed for users registered on the platform - 2
-                if (api_get_user_id() && !api_is_anonymous() && $is_allowed_in_course) {
+                if (api_get_user_id() && !api_is_anonymous() && $isAllowedInCourse) {
                     $is_visible = true;
                 }
                 break;
@@ -1208,7 +1208,7 @@ function api_protect_course_script($print_headers = false, $allow_session_admins
         }
 
         //If password is set and user is not registered to the course then the course is not visible
-        if ($is_allowed_in_course == false &&
+        if ($isAllowedInCourse == false &&
             isset($course_info['registration_code']) &&
             !empty($course_info['registration_code'])
         ) {
@@ -1216,12 +1216,23 @@ function api_protect_course_script($print_headers = false, $allow_session_admins
         }
     }
 
+    if (!empty($checkTool)) {
+        if (!api_is_allowed_to_edit(true, true, true)) {
+            $toolInfo = api_get_tool_information_by_name($checkTool);
+            if (!empty($toolInfo) && isset($toolInfo['visibility']) && $toolInfo['visibility'] == 0) {
+                api_not_allowed(true);
+
+                return false;
+            }
+        }
+    }
+
     // Check session visibility
     $session_id = api_get_session_id();
 
     if (!empty($session_id)) {
-        //$is_allowed_in_course was set in local.inc.php
-        if (!$is_allowed_in_course) {
+        // $isAllowedInCourse was set in local.inc.php
+        if (!$isAllowedInCourse) {
             $is_visible = false;
         }
     }
@@ -1685,7 +1696,12 @@ function api_get_user_info(
         $userFromSession = Session::read('_user');
 
         if (isset($userFromSession)) {
-            if ($cacheAvailable === true) {
+            if ($cacheAvailable === true &&
+                (
+                    empty($userFromSession['is_anonymous']) &&
+                    (isset($userFromSession['status']) && $userFromSession['status'] != ANONYMOUS)
+                )
+            ) {
                 $apcVar = api_get_configuration_value('apc_prefix').'userinfo_'.$userFromSession['user_id'];
                 if (apcu_exists($apcVar)) {
                     if ($updateCache) {
@@ -2245,6 +2261,7 @@ function api_format_course_array($course_data)
     $_course['registration_code'] = !empty($course_data['registration_code']) ? sha1($course_data['registration_code']) : null;
     $_course['disk_quota'] = $course_data['disk_quota'];
     $_course['course_public_url'] = $webCourseHome.'/index.php';
+    $_course['course_sys_path'] = $courseSys.'/';
 
     if (array_key_exists('add_teachers_to_sessions_courses', $course_data)) {
         $_course['add_teachers_to_sessions_courses'] = $course_data['add_teachers_to_sessions_courses'];
@@ -2281,7 +2298,7 @@ function api_format_course_array($course_data)
             null,
             null,
             true,
-            false
+            true
         );
     }
 
@@ -2685,18 +2702,19 @@ function api_get_session_visibility(
  * This function returns a (star) session icon if the session is not null and
  * the user is not a student.
  *
- * @param int $session_id
- * @param int $status_id  User status id - if 5 (student), will return empty
+ * @param int $sessionId
+ * @param int $statusId  User status id - if 5 (student), will return empty
  *
  * @return string Session icon
  */
-function api_get_session_image($session_id, $status_id)
+function api_get_session_image($sessionId, $statusId)
 {
-    $session_id = (int) $session_id;
-    $session_img = '';
-    if ((int) $status_id != 5) { //check whether is not a student
-        if ($session_id > 0) {
-            $session_img = "&nbsp;&nbsp;".Display::return_icon(
+    $sessionId = (int) $sessionId;
+    $image = '';
+    if ($statusId != STUDENT) {
+        // Check whether is not a student
+        if ($sessionId > 0) {
+            $image = '&nbsp;&nbsp;'.Display::return_icon(
                 'star.png',
                 get_lang('SessionSpecificResource'),
                 ['align' => 'absmiddle'],
@@ -2705,7 +2723,7 @@ function api_get_session_image($session_id, $status_id)
         }
     }
 
-    return $session_img;
+    return $image;
 }
 
 /**
@@ -2728,10 +2746,10 @@ function api_get_session_condition(
     $session_id = (int) $session_id;
 
     if (empty($session_field)) {
-        $session_field = "session_id";
+        $session_field = 'session_id';
     }
     // Condition to show resources by session
-    $condition_add = $and ? " AND " : " WHERE ";
+    $condition_add = $and ? ' AND ' : ' WHERE ';
 
     if ($with_base_content) {
         $condition_session = $condition_add." ( $session_field = $session_id OR $session_field = 0 OR $session_field IS NULL) ";
@@ -4745,6 +4763,8 @@ function api_display_language_form($hide_if_no_choice = false, $showAsButton = f
  */
 function languageToCountryIsoCode($languageIsoCode)
 {
+    $allow = api_get_configuration_value('language_flags_by_country');
+
     // @todo save in DB
     switch ($languageIsoCode) {
         case 'ko':
@@ -4755,9 +4775,15 @@ function languageToCountryIsoCode($languageIsoCode)
             break;
         case 'ca':
             $country = 'es';
+            if ($allow) {
+                $country = 'catalan';
+            }
             break;
-        case 'gl':
+        case 'gl': // galego
             $country = 'es';
+            if ($allow) {
+                $country = 'galician';
+            }
             break;
         case 'ka':
             $country = 'ge';
@@ -4765,8 +4791,11 @@ function languageToCountryIsoCode($languageIsoCode)
         case 'sl':
             $country = 'si';
             break;
-        case 'eu':
+        case 'eu': // Euskera
             $country = 'es';
+            if ($allow) {
+                $country = 'basque';
+            }
             break;
         case 'cs':
             $country = 'cz';
@@ -4783,8 +4812,8 @@ function languageToCountryIsoCode($languageIsoCode)
         case 'he':
             $country = 'il';
             break;
-        case 'uk':
-            $country = 'ua'; //Ukraine
+        case 'uk': // Ukraine
+            $country = 'ua';
             break;
         case 'da':
             $country = 'dk';
@@ -8768,10 +8797,11 @@ function api_create_protected_dir($name, $parentDirectory)
  * @param string    email body
  * @param string    sender name
  * @param string    sender e-mail
- * @param array     extra headers in form $headers = array($name => $value) to allow parsing
- * @param array     data file (path and filename)
- * @param bool      True for attaching a embedded file inside content html (optional)
- * @param array     Additional parameters
+ * @param array  $extra_headers  in form $headers = array($name => $value) to allow parsing
+ * @param array  $data_file      (path and filename)
+ * @param bool   $embedded_image True for attaching a embedded file inside content html (optional)
+ * @param array  $additionalParameters
+ * @param string $sendErrorTo    If there's an error while sending the email, $sendErrorTo will receive a notification
  *
  * @return int true if mail was sent
  *
@@ -8787,7 +8817,8 @@ function api_mail_html(
     $extra_headers = [],
     $data_file = [],
     $embedded_image = false,
-    $additionalParameters = []
+    $additionalParameters = [],
+    $sendErrorTo = ''
 ) {
     global $platform_email;
 
@@ -8813,43 +8844,18 @@ function api_mail_html(
     $mail->Priority = 3;
     $mail->SMTPKeepAlive = true;
 
-    // Default values
-    $notification = new Notification();
-    $defaultEmail = $notification->getDefaultPlatformSenderEmail();
-    $defaultName = $notification->getDefaultPlatformSenderName();
-
-    // If the parameter is set don't use the admin.
-    $senderName = !empty($senderName) ? $senderName : $defaultName;
-    $senderEmail = !empty($senderEmail) ? $senderEmail : $defaultEmail;
+    api_set_noreply_and_from_address_to_mailer(
+        $mail,
+        ['name' => $senderName, 'email' => $senderEmail],
+        !empty($extra_headers['reply_to']) ? $extra_headers['reply_to'] : []
+    );
 
-    // Reply to first
-    if (isset($extra_headers['reply_to']) && empty($platform_email['SMTP_UNIQUE_REPLY_TO'])) {
-        if (PHPMailer::validateAddress($extra_headers['reply_to']['mail'])) {
-            $mail->AddReplyTo(
-                $extra_headers['reply_to']['mail'],
-                $extra_headers['reply_to']['name']
-            );
-            // Errors to sender
-            $mail->AddCustomHeader('Errors-To: '.$extra_headers['reply_to']['mail']);
-            $mail->Sender = $extra_headers['reply_to']['mail'];
-            unset($extra_headers['reply_to']);
-        }
-    } else {
-        $mail->AddCustomHeader('Errors-To: '.$defaultEmail);
+    if (!empty($sendErrorTo) && PHPMailer::ValidateAddress($sendErrorTo)) {
+        $mail->AddCustomHeader('Errors-To: '.$sendErrorTo);
     }
 
-    //If the SMTP configuration only accept one sender
-    if (isset($platform_email['SMTP_UNIQUE_SENDER']) && $platform_email['SMTP_UNIQUE_SENDER']) {
-        $senderName = $platform_email['SMTP_FROM_NAME'];
-        $senderEmail = $platform_email['SMTP_FROM_EMAIL'];
-        $valid = PHPMailer::validateAddress($senderEmail);
-        if ($valid) {
-            //force-set Sender to $senderEmail, otherwise SetFrom only does it if it is currently empty
-            $mail->Sender = $senderEmail;
-        }
-    }
+    unset($extra_headers['reply_to']);
 
-    $mail->SetFrom($senderEmail, $senderName);
     $mail->Subject = $subject;
     $mail->AltBody = strip_tags(
         str_replace('<br />', "\n", api_html_entity_decode($message))
@@ -8858,7 +8864,6 @@ function api_mail_html(
     $list = api_get_configuration_value('send_all_emails_to');
     if (!empty($list) && isset($list['emails'])) {
         foreach ($list['emails'] as $email) {
-            //$mail->AddBCC($email);
             $mail->AddAddress($email);
         }
     }
@@ -8903,7 +8908,7 @@ function api_mail_html(
 
     $noReply = api_get_setting('noreply_email_address');
     if (!empty($noReply)) {
-        $message .= "<br />".get_lang('ThisIsAutomaticEmailNoReply');
+        $message .= '<br />'.get_lang('ThisIsAutomaticEmailNoReply');
     }
     $mailView->assign('content', $message);
 
@@ -8915,7 +8920,7 @@ function api_mail_html(
     $layout = $mailView->get_template('mail/mail.tpl');
     $mail->Body = $mailView->fetch($layout);
 
-    // Attachment ...
+    // Attachment.
     if (!empty($data_file)) {
         foreach ($data_file as $file_attach) {
             if (!empty($file_attach['path']) && !empty($file_attach['filename'])) {
@@ -9007,11 +9012,17 @@ function api_mail_html(
  */
 function api_protect_course_group($tool, $showHeader = true)
 {
-    $userId = api_get_user_id();
     $groupId = api_get_group_id();
-    $groupInfo = GroupManager::get_group_properties($groupId);
+    if (!empty($groupId)) {
+        $userId = api_get_user_id();
+        $groupInfo = GroupManager::get_group_properties($groupId);
+
+        // Group doesn't exists
+        if (empty($groupInfo)) {
+            api_not_allowed($showHeader);
+        }
 
-    if (!empty($groupInfo)) {
+        // Check group access
         $allow = GroupManager::user_has_access(
             $userId,
             $groupInfo['iid'],
@@ -9450,23 +9461,24 @@ function api_set_noreply_and_from_address_to_mailer(PHPMailer $mailer, array $se
     }
 
     $notification = new Notification();
-
     // If the parameter is set don't use the admin.
     $senderName = !empty($sender['name']) ? $sender['name'] : $notification->getDefaultPlatformSenderName();
     $senderEmail = !empty($sender['email']) ? $sender['email'] : $notification->getDefaultPlatformSenderEmail();
 
+    // Send errors to the platform admin
+    $adminEmail = api_get_setting('emailAdministrator');
+    if (PHPMailer::ValidateAddress($adminEmail)) {
+        $mailer->AddCustomHeader('Errors-To: '.$adminEmail);
+    }
+
     // Reply to first
     if (!$avoidReplyToAddress) {
-        $mailer->AddCustomHeader('Errors-To: '.$notification->getDefaultPlatformSenderEmail());
-
         if (
             !empty($replyToAddress) &&
-            $platformEmail['SMTP_UNIQUE_REPLY_TO'] &&
+            isset($platformEmail['SMTP_UNIQUE_REPLY_TO']) && $platformEmail['SMTP_UNIQUE_REPLY_TO'] &&
             PHPMailer::ValidateAddress($replyToAddress['mail'])
         ) {
             $mailer->AddReplyTo($replyToAddress['email'], $replyToAddress['name']);
-            // Errors to sender
-            $mailer->AddCustomHeader('Errors-To: '.$replyToAddress['mail']);
             $mailer->Sender = $replyToAddress['mail'];
         }
     }

+ 1 - 1
main/inc/lib/array.lib.php

@@ -81,7 +81,7 @@ function msort($array, $id = 'id', $order = 'desc')
  */
 function utf8_sort($array)
 {
-    $old_locale = setlocale(LC_ALL, null);
+    $old_locale = setlocale(LC_ALL, 0);
     $code = api_get_language_isocode();
     $locale_list = [$code.'.utf8', 'en.utf8', 'en_US.utf8', 'en_GB.utf8'];
     $try_sort = false;

+ 125 - 66
main/inc/lib/certificate.lib.php

@@ -71,7 +71,6 @@ class Certificate extends Model
             if ($this->force_certificate_generation) {
                 $this->generate([], $sendNotification);
             }
-
             if (isset($this->certificate_data) && $this->certificate_data) {
                 if (empty($this->certificate_data['path_certificate'])) {
                     $this->generate([], $sendNotification);
@@ -88,52 +87,46 @@ class Certificate extends Model
             $this->html_file = $this->certification_user_path.basename($this->certificate_data['path_certificate']);
             $this->qr_file = $this->certification_user_path.$pathinfo['filename'].'_qr.png';
         } else {
-            // General certificate
-            $name = md5($this->user_id).'.html';
-            $my_path_certificate = $this->certification_user_path.$name;
-            $path_certificate = '/'.$name;
-
-            // Getting QR filename
-            $file_info = pathinfo($path_certificate);
-            $content = $this->generateCustomCertificate();
-
-            $my_new_content_html = str_replace(
-                '((certificate_barcode))',
-                Display::img(
-                    $this->certification_web_user_path.$file_info['filename'].'_qr.png',
-                    'QR'
-                ),
-                $content
-            );
-
-            $my_new_content_html = mb_convert_encoding(
-                $my_new_content_html,
-                'UTF-8',
-                api_get_system_encoding()
-            );
-
-            $this->html_file = $my_path_certificate;
-            $result = @file_put_contents($my_path_certificate, $my_new_content_html);
+            $this->check_certificate_path();
+            if (api_get_configuration_value('allow_general_certificate')) {
+                // General certificate
+                $name = md5($this->user_id).'.html';
+                $my_path_certificate = $this->certification_user_path.$name;
+                $path_certificate = '/'.$name;
+
+                // Getting QR filename
+                $file_info = pathinfo($path_certificate);
+                $content = $this->generateCustomCertificate();
+
+                $my_new_content_html = str_replace(
+                    '((certificate_barcode))',
+                    Display::img(
+                        $this->certification_web_user_path.$file_info['filename'].'_qr.png',
+                        'QR'
+                    ),
+                    $content
+                );
 
-            if ($result) {
-                // Updating the path
-                self::updateUserCertificateInfo(
-                    0,
-                    $this->user_id,
-                    $path_certificate,
-                    $updateCertificateData
+                $my_new_content_html = mb_convert_encoding(
+                    $my_new_content_html,
+                    'UTF-8',
+                    api_get_system_encoding()
                 );
-                $this->certificate_data['path_certificate'] = $path_certificate;
 
-                if ($this->isHtmlFileGenerated()) {
-                    if (!empty($file_info)) {
-                        //$text = $this->parse_certificate_variables($new_content_html['variables']);
-                        //$this->generate_qr($text, $qr_code_filename);
-                    }
+                $this->html_file = $my_path_certificate;
+                $result = @file_put_contents($my_path_certificate, $my_new_content_html);
+
+                if ($result) {
+                    // Updating the path
+                    self::updateUserCertificateInfo(
+                        0,
+                        $this->user_id,
+                        $path_certificate,
+                        $updateCertificateData
+                    );
+                    $this->certificate_data['path_certificate'] = $path_certificate;
                 }
             }
-
-            return $result;
         }
     }
 
@@ -211,13 +204,14 @@ class Certificate extends Model
     {
         // The user directory should be set
         if (empty($this->certification_user_path) &&
-            $this->force_certificate_generation == false
+            $this->force_certificate_generation === false
         ) {
             return false;
         }
 
         $params['hide_print_button'] = isset($params['hide_print_button']) ? true : false;
         $categoryId = 0;
+        $my_category = [];
         if (isset($this->certificate_data) && isset($this->certificate_data['cat_id'])) {
             $categoryId = $this->certificate_data['cat_id'];
             $my_category = Category::load($categoryId);
@@ -226,14 +220,17 @@ class Certificate extends Model
         if (isset($my_category[0]) && !empty($categoryId) &&
             $my_category[0]->is_certificate_available($this->user_id)
         ) {
-            $courseInfo = api_get_course_info($my_category[0]->get_course_code());
+            /** @var Category $category */
+            $category = $my_category[0];
+
+            $courseInfo = api_get_course_info($category->get_course_code());
             $courseId = $courseInfo['real_id'];
-            $sessionId = $my_category[0]->get_session_id();
+            $sessionId = $category->get_session_id();
 
             $skill = new Skill();
             $skill->addSkillToUser(
                 $this->user_id,
-                $this->certificate_data['cat_id'],
+                $category,
                 $courseId,
                 $sessionId
             );
@@ -242,13 +239,13 @@ class Certificate extends Model
                 if (!empty($this->certificate_data)) {
                     $new_content_html = GradebookUtils::get_user_certificate_content(
                         $this->user_id,
-                        $my_category[0]->get_course_code(),
-                        $my_category[0]->get_session_id(),
+                        $category->get_course_code(),
+                        $category->get_session_id(),
                         false,
                         $params['hide_print_button']
                     );
 
-                    if ($my_category[0]->get_id() == strval(intval($this->certificate_data['cat_id']))) {
+                    if ($category->get_id() == $categoryId) {
                         $name = $this->certificate_data['path_certificate'];
                         $myPathCertificate = $this->certification_user_path.basename($name);
 
@@ -257,7 +254,7 @@ class Certificate extends Model
                             !is_dir($myPathCertificate) &&
                             $this->force_certificate_generation == false
                         ) {
-                            //Seems that the file was already generated
+                            // Seems that the file was already generated
                             return true;
                         } else {
                             // Creating new name
@@ -287,7 +284,7 @@ class Certificate extends Model
                             $result = @file_put_contents($myPathCertificate, $newContent);
                             if ($result) {
                                 // Updating the path
-                                self::updateUserCertificateInfo(
+                                $this->updateUserCertificateInfo(
                                     $this->certificate_data['cat_id'],
                                     $this->user_id,
                                     $path_certificate
@@ -308,7 +305,7 @@ class Certificate extends Model
                                             $subject = get_lang('NotificationCertificateSubject');
                                             $message = nl2br(get_lang('NotificationCertificateTemplate'));
                                             $score = $this->certificate_data['score_certificate'];
-                                            Certificate::sendNotification(
+                                            self::sendNotification(
                                                 $subject,
                                                 $message,
                                                 api_get_user_info($this->user_id),
@@ -328,6 +325,8 @@ class Certificate extends Model
                 }
             }
         } else {
+            $this->check_certificate_path();
+
             // General certificate
             $name = md5($this->user_id).'.html';
             $my_path_certificate = $this->certification_user_path.$name;
@@ -362,13 +361,6 @@ class Certificate extends Model
                     $path_certificate
                 );
                 $this->certificate_data['path_certificate'] = $path_certificate;
-
-                if ($this->isHtmlFileGenerated()) {
-                    if (!empty($file_info)) {
-                        //$text = $this->parse_certificate_variables($new_content_html['variables']);
-                        //$this->generate_qr($text, $qr_code_filename);
-                    }
-                }
             }
 
             return $result;
@@ -472,13 +464,18 @@ class Certificate extends Model
         $path_certificate,
         $updateCertificateData = true
     ) {
-        if (!UserManager::is_user_certified($categoryId, $user_id) && $updateCertificateData) {
+        $categoryId = (int) $categoryId;
+        $user_id = (int) $user_id;
+
+        if ($updateCertificateData &&
+            !UserManager::is_user_certified($categoryId, $user_id)
+        ) {
             $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
             $now = api_get_utc_datetime();
             $sql = 'UPDATE '.$table.' SET 
                         path_certificate="'.Database::escape_string($path_certificate).'",
                         created_at = "'.$now.'"
-                    WHERE cat_id="'.intval($categoryId).'" AND user_id="'.intval($user_id).'" ';
+                    WHERE cat_id = "'.$categoryId.'" AND user_id="'.$user_id.'" ';
             Database::query($sql);
         }
     }
@@ -626,9 +623,9 @@ class Certificate extends Model
             return false;
         }
 
-        $user_certificate = $this->certification_user_path.basename($this->certificate_data['path_certificate']);
+        $userCertificate = $this->certification_user_path.basename($this->certificate_data['path_certificate']);
 
-        if (!file_exists($user_certificate)) {
+        if (!file_exists($userCertificate)) {
             return false;
         }
 
@@ -640,8 +637,6 @@ class Certificate extends Model
      */
     public function show()
     {
-        header('Content-Type: text/html; charset='.api_get_system_encoding());
-
         $user_certificate = $this->certification_user_path.basename($this->certificate_data['path_certificate']);
         if (file_exists($user_certificate)) {
             // Needed in order to browsers don't add custom CSS
@@ -655,7 +650,10 @@ class Certificate extends Model
                 $certificateContent
             );
 
-            if ($this->user_id == api_get_user_id() && !empty($this->certificate_data)) {
+            if ($this->user_id == api_get_user_id() &&
+                !empty($this->certificate_data) &&
+                isset($this->certificate_data['id'])
+            ) {
                 $certificateId = $this->certificate_data['id'];
                 $extraFieldValue = new ExtraFieldValue('user_certificate');
                 $value = $extraFieldValue->get_values_by_handler_and_field_variable(
@@ -671,6 +669,7 @@ class Certificate extends Model
                 }
             }
 
+            header('Content-Type: text/html; charset='.api_get_system_encoding());
             echo $certificateContent;
 
             return;
@@ -854,4 +853,64 @@ class Certificate extends Model
 
         return Database::store_result($rs, 'ASSOC');
     }
+
+    /**
+     * @param int $userId
+     */
+    public static function generateUserSkills($userId)
+    {
+        $controller = new IndexManager(get_lang('MyCourses'));
+        $courseAndSessions = $controller->returnCoursesAndSessions($userId, true, null, true, false);
+
+        if (isset($courseAndSessions['courses']) && !empty($courseAndSessions['courses'])) {
+            foreach ($courseAndSessions['courses'] as $course) {
+                $cats = Category::load(
+                    null,
+                    null,
+                    $course['code'],
+                    null,
+                    null,
+                    null,
+                    false
+                );
+
+                if (isset($cats[0]) && !empty($cats[0])) {
+                    Category::generateUserCertificate(
+                        $cats[0]->get_id(),
+                        $userId
+                    );
+                }
+            }
+        }
+
+        if (isset($courseAndSessions['sessions']) && !empty($courseAndSessions['sessions'])) {
+            foreach ($courseAndSessions['sessions'] as $sessionCategory) {
+                if (isset($sessionCategory['sessions'])) {
+                    foreach ($sessionCategory['sessions'] as $sessionData) {
+                        if (!empty($sessionData['courses'])) {
+                            $sessionId = $sessionData['session_id'];
+                            foreach ($sessionData['courses'] as $courseData) {
+                                $cats = Category:: load(
+                                    null,
+                                    null,
+                                    $courseData['course_code'],
+                                    null,
+                                    null,
+                                    $sessionId,
+                                    false
+                                );
+
+                                if (isset($cats[0]) && !empty($cats[0])) {
+                                    Category::generateUserCertificate(
+                                        $cats[0]->get_id(),
+                                        $userId
+                                    );
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
 }

+ 2 - 18
main/inc/lib/chat.lib.php

@@ -332,27 +332,11 @@ class Chat extends Model
             ];
         }
 
-        /*if (!empty($_SESSION['openChatBoxes'])) {
-            foreach ($_SESSION['openChatBoxes'] as $userId => $time) {
-                if (!isset($_SESSION['tsChatBoxes'][$userId])) {
-                    $now = time() - $time;
-                    $time = api_convert_and_format_date($time, DATE_TIME_FORMAT_SHORT_TIME_FIRST);
-                    $message = sprintf(get_lang('SentAtX'), $time);
-
-                    if ($now > 180) {
-                        if (isset($chatHistory[$userId])) {
-                            $chatHistory[$userId]['items'][] = $item;
-                        }
-                        $_SESSION['tsChatBoxes'][$userId] = 1;
-                    }
-                }
-            }
-        }*/
         Session::write('chatHistory', $chatHistory);
 
         $sql = "UPDATE ".$this->table."
                 SET recd = 1
-                WHERE to_user = '".$currentUserId."' AND recd = 0";
+                WHERE to_user = $currentUserId AND recd = 0";
         Database::query($sql);
 
         echo json_encode(['items' => $chatHistory]);
@@ -441,7 +425,7 @@ class Chat extends Model
     /**
      * Close a specific chat box (user ID taken from $_POST['chatbox']).
      *
-     * @param $userId
+     * @param int $userId
      */
     public function closeWindow($userId)
     {

+ 47 - 449
main/inc/lib/course.lib.php

@@ -731,6 +731,16 @@ class CourseManager
                     );
                 }
 
+                $subscribe = (int) api_get_course_setting('subscribe_users_to_forum_notifications', $courseCode);
+                if ($subscribe === 1) {
+                    require_once api_get_path(SYS_CODE_PATH).'forum/forumfunction.inc.php';
+                    $forums = get_forums(0, $courseCode, true, $sessionId);
+                    foreach ($forums as $forum) {
+                        $forumId = $forum['iid'];
+                        set_notification('forum', $forumId, false, $userInfo, $courseInfo);
+                    }
+                }
+
                 // Add event to the system log
                 Event::addEvent(
                     LOG_SUBSCRIBE_USER_TO_COURSE,
@@ -1612,6 +1622,7 @@ class CourseManager
                                 $sessionId
                             )
                         );
+
                         $report_info['progress_100'] = $progress == 100 ? Display::label(get_lang('Yes'), 'success') : Display::label(get_lang('No'));
                         $report_info['progress'] = $progress."%";
 
@@ -1809,9 +1820,11 @@ class CourseManager
         $groupId = 0
     ) {
         $userTable = Database::get_main_table(TABLE_MAIN_USER);
-        $session_id = intval($session_id);
-        $course_code = Database::escape_string($course_code);
+        $session_id = (int) $session_id;
         $courseInfo = api_get_course_info($course_code);
+        if (empty($courseInfo)) {
+            return [];
+        }
         $courseId = $courseInfo['real_id'];
         $students = [];
 
@@ -2946,88 +2959,6 @@ class CourseManager
         return $result;
     }
 
-    /**
-     * Get emails of tutors to course.
-     *
-     * @param int $courseId
-     *
-     * @return array List of emails of tutors to course
-     *
-     * @author Carlos Vargas <carlos.vargas@dokeos.com>, Dokeos Latino
-     * */
-    public static function get_emails_of_tutors_to_course($courseId)
-    {
-        $list = [];
-        $courseId = intval($courseId);
-        $res = Database::query(
-            "SELECT user_id 
-            FROM ".Database::get_main_table(TABLE_MAIN_COURSE_USER)."
-            WHERE c_id = $courseId AND status = 1"
-        );
-        while ($list_users = Database::fetch_array($res)) {
-            $result = Database::query(
-                "SELECT * FROM ".Database::get_main_table(TABLE_MAIN_USER)."
-                WHERE user_id = ".$list_users['user_id']
-            );
-            while ($row_user = Database::fetch_array($result)) {
-                $name_teacher = api_get_person_name($row_user['firstname'], $row_user['lastname']);
-                $list[] = [$row_user['email'] => $name_teacher];
-            }
-        }
-
-        return $list;
-    }
-
-    /**
-     * Get coaches emails by session.
-     *
-     * @param int $session_id
-     * @param int $courseId
-     *
-     * @return array array(email => name_tutor)  by coach
-     *
-     * @author Carlos Vargas <carlos.vargas@dokeos.com>
-     */
-    public static function get_email_of_tutor_to_session($session_id, $courseId)
-    {
-        $tbl_session_course_user = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
-        $tbl_user = Database::get_main_table(TABLE_MAIN_USER);
-        $coachs_emails = [];
-
-        $courseId = intval($courseId);
-        $session_id = intval($session_id);
-
-        $sql = "SELECT user_id
-                FROM $tbl_session_course_user
-                WHERE
-                    session_id = $session_id AND
-                    c_id = $courseId AND
-                    status = 2
-                ";
-        $rs = Database::query($sql);
-
-        if (Database::num_rows($rs) > 0) {
-            $user_ids = [];
-            while ($row = Database::fetch_array($rs)) {
-                $user_ids[] = $row['user_id'];
-            }
-
-            $sql = "SELECT firstname, lastname, email FROM $tbl_user
-                    WHERE user_id IN (".implode(",", $user_ids).")";
-            $rs_user = Database::query($sql);
-
-            while ($row_emails = Database::fetch_array($rs_user)) {
-                $mail_tutor = [
-                    'email' => $row_emails['email'],
-                    'complete_name' => api_get_person_name($row_emails['firstname'], $row_emails['lastname']),
-                ];
-                $coachs_emails[] = $mail_tutor;
-            }
-        }
-
-        return $coachs_emails;
-    }
-
     /**
      * Creates a new extra field for a given course.
      *
@@ -3537,89 +3468,6 @@ class CourseManager
         }
     }
 
-    /**
-     * Builds the course block in user_portal.php.
-     *
-     * @todo use Twig
-     *
-     * @param array $params
-     *
-     * @return string
-     */
-    public static function course_item_html_no_icon($params)
-    {
-        $html = '<div class="course_item">';
-        $html .= '<div class="row">';
-        $html .= '<div class="col-md-7">';
-
-        $notifications = isset($params['notifications']) ? $params['notifications'] : null;
-
-        $html .= '<h3>'.$params['title'].$notifications.'</h3> ';
-
-        if (isset($params['description'])) {
-            $html .= '<p>'.$params['description'].'</p>';
-        }
-        if (!empty($params['subtitle'])) {
-            $html .= '<small>'.$params['subtitle'].'</small>';
-        }
-        if (!empty($params['teachers'])) {
-            $html .= '<h5 class="teacher">'.
-                Display::return_icon(
-                    'teacher.png',
-                    get_lang('Teacher'),
-                    [],
-                    ICON_SIZE_TINY
-                ).
-                $params['teachers'].'</h5>';
-        }
-        if (!empty($params['coaches'])) {
-            $html .= '<h5 class="teacher">'.
-                Display::return_icon(
-                    'teacher.png',
-                    get_lang('Coach'),
-                    [],
-                    ICON_SIZE_TINY
-                ).
-                $params['coaches'].'</h5>';
-        }
-
-        $html .= '</div>';
-        $params['right_actions'] = isset($params['right_actions']) ? $params['right_actions'] : null;
-        $html .= '<div class="pull-right course-box-actions">'.$params['right_actions'].'</div>';
-        $html .= '</div>';
-        $html .= '</div>';
-
-        return $html;
-    }
-
-    /**
-     * @param $params
-     * @param bool|false $is_sub_content
-     *
-     * @return string
-     */
-    public static function session_items_html($params, $is_sub_content = false)
-    {
-        $html = '';
-        $html .= '<div class="row">';
-        $html .= '<div class="col-md-2">';
-        if (!empty($params['link'])) {
-            $html .= '<a class="thumbnail" href="'.$params['link'].'">';
-            $html .= $params['icon'];
-            $html .= '</a>';
-        } else {
-            $html .= $params['icon'];
-        }
-        $html .= '</div>';
-        $html .= '<div class="col-md-10">';
-        $html .= $params['title'];
-        $html .= $params['coaches'];
-        $html .= '</div>';
-        $html .= '</div>';
-
-        return $html;
-    }
-
     /**
      * Display special courses (and only these) as several HTML divs of class userportal-course-item.
      *
@@ -3637,7 +3485,7 @@ class CourseManager
         $load_dirs = false,
         $useUserLanguageFilterIfAvailable = true
     ) {
-        $user_id = intval($user_id);
+        $user_id = (int) $user_id;
         $table = Database::get_main_table(TABLE_MAIN_COURSE);
         $specialCourseList = self::get_special_course_list();
 
@@ -3779,7 +3627,7 @@ class CourseManager
         // Step 1: We get all the categories of the user
         $table = Database::get_main_table(TABLE_USER_COURSE_CATEGORY);
         $sql = "SELECT * FROM $table
-                WHERE user_id = '".$user_id."'
+                WHERE user_id = $user_id
                 ORDER BY sort ASC";
 
         $result = Database::query($sql);
@@ -3859,7 +3707,7 @@ class CourseManager
         $user_id = 0,
         $useUserLanguageFilterIfAvailable = true
     ) {
-        $user_id = $user_id ?: api_get_user_id();
+        $user_id = $user_id ? (int) $user_id : api_get_user_id();
         $user_category_id = (int) $user_category_id;
 
         // Table definitions
@@ -3901,7 +3749,7 @@ class CourseManager
                 INNER JOIN $TABLE_ACCESS_URL_REL_COURSE url
                 ON (url.c_id = course.id)
                 WHERE
-                    course_rel_user.user_id = '".$user_id."' AND
+                    course_rel_user.user_id = $user_id AND
                     $userCategoryCondition
                     $without_special_courses
                     $languageCondition
@@ -3909,10 +3757,10 @@ class CourseManager
         // If multiple URL access mode is enabled, only fetch courses
         // corresponding to the current URL.
         if (api_get_multiple_access_url() && $current_url_id != -1) {
-            $sql .= " AND access_url_id='".$current_url_id."'";
+            $sql .= " AND access_url_id = $current_url_id";
         }
         // Use user's classification for courses (if any).
-        $sql .= " ORDER BY course_rel_user.user_course_cat, course_rel_user.sort ASC";
+        $sql .= ' ORDER BY course_rel_user.user_course_cat, course_rel_user.sort ASC';
         $result = Database::query($sql);
 
         $showCustomIcon = api_get_setting('course_images_in_courses_list');
@@ -3921,6 +3769,9 @@ class CourseManager
         $courseList = [];
         while ($row = Database::fetch_array($result)) {
             $course_info = api_get_course_info_by_id($row['id']);
+            if (empty($course_info)) {
+                continue;
+            }
 
             if (isset($course_info['visibility']) &&
                 $course_info['visibility'] == COURSE_VISIBILITY_HIDDEN
@@ -4006,7 +3857,7 @@ class CourseManager
             }
 
             $params['status'] = $row['status'];
-            if (api_get_setting('display_coursecode_in_courselist') == 'true') {
+            if (api_get_setting('display_coursecode_in_courselist') === 'true') {
                 $params['code_course'] = '('.$course_info['visual_code'].') ';
             }
 
@@ -4028,6 +3879,7 @@ class CourseManager
             $params['category'] = $course_info['categoryName'];
             $params['category_code'] = $course_info['categoryCode'];
             $params['teachers'] = $teachers;
+            $params['real_id'] = $course_info['real_id'];
 
             if ($course_info['visibility'] != COURSE_VISIBILITY_CLOSED) {
                 $params['notifications'] = $showNotification;
@@ -4039,150 +3891,6 @@ class CourseManager
         return $courseList;
     }
 
-    /**
-     *  Display courses inside a category (without special courses) as HTML dics of
-     *  class userportal-course-item.
-     *
-     * @deprecated use self::returnCoursesCategories(0);
-     *
-     * @param int      User category id
-     * @param bool      Whether to show the document quick-loader or not
-     *
-     * @return array
-     */
-    public static function returnCoursesWithoutCategories($user_category_id, $load_dirs = false)
-    {
-        $user_id = api_get_user_id();
-        // Table definitions
-        $TABLECOURS = Database::get_main_table(TABLE_MAIN_COURSE);
-        $TABLECOURSUSER = Database::get_main_table(TABLE_MAIN_COURSE_USER);
-        $TABLE_ACCESS_URL_REL_COURSE = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
-        $current_url_id = api_get_current_access_url_id();
-        $courseList = [];
-
-        // Get course list auto-register
-        $special_course_list = self::get_special_course_list();
-
-        $without_special_courses = '';
-        if (!empty($special_course_list)) {
-            $without_special_courses = ' AND course.id NOT IN ("'.implode('","', $special_course_list).'")';
-        }
-
-        $sql = "SELECT
-                    course.id,
-                    course.title,
-                    course.code,
-                    course.subscribe subscr,
-                    course.unsubscribe unsubscr,
-                    course_rel_user.status status,
-                    course_rel_user.sort sort,
-                    course_rel_user.user_course_cat user_course_cat
-                FROM $TABLECOURS course 
-                INNER JOIN $TABLECOURSUSER course_rel_user
-                ON (course.id = course_rel_user.c_id)
-                INNER JOIN $TABLE_ACCESS_URL_REL_COURSE url
-                ON (url.c_id = course.id)
-                WHERE
-                    course_rel_user.user_id = $user_id AND
-                    course_rel_user.user_course_cat = $user_category_id
-                    $without_special_courses ";
-
-        // If multiple URL access mode is enabled, only fetch courses
-        // corresponding to the current URL.
-        if (api_get_multiple_access_url() && $current_url_id != -1) {
-            $sql .= " AND url.c_id = course.id AND access_url_id = $current_url_id ";
-        }
-        // Use user's classification for courses (if any).
-        $sql .= " ORDER BY course_rel_user.user_course_cat, course_rel_user.sort ASC";
-        $result = Database::query($sql);
-
-        $showCustomIcon = api_get_setting('course_images_in_courses_list');
-        // Browse through all courses.
-        while ($row = Database::fetch_array($result)) {
-            $course_info = api_get_course_info($row['code']);
-            if (isset($course_info['visibility']) &&
-                $course_info['visibility'] == COURSE_VISIBILITY_HIDDEN
-            ) {
-                continue;
-            }
-            $course_info['id_session'] = null;
-            $course_info['status'] = $row['status'];
-
-            // For each course, get if there is any notification icon to show
-            // (something that would have changed since the user's last visit).
-            $showNotification = !api_get_configuration_value('hide_course_notification')
-                ? Display::show_notification($course_info)
-                : '';
-
-            $thumbnails = null;
-            $image = null;
-            $iconName = basename($course_info['course_image']);
-            if ($showCustomIcon === 'true' && $iconName != 'course.png') {
-                $thumbnails = $course_info['course_image'];
-                $image = $course_info['course_image_large'];
-            } else {
-                $image = Display::return_icon(
-                    'session_default.png',
-                    null,
-                    null,
-                    null,
-                    null,
-                    true
-                );
-            }
-
-            $params = [];
-            //Param (course_code) needed to get the student process
-            $params['course_code'] = $row['code'];
-            $params['edit_actions'] = '';
-            $params['document'] = '';
-            if (api_is_platform_admin()) {
-                $params['edit_actions'] .= api_get_path(WEB_CODE_PATH).'course_info/infocours.php?cidReq='.$course_info['code'];
-                if ($load_dirs) {
-                    $params['document'] = '<a id="document_preview_'.$course_info['real_id'].'_0" class="document_preview btn btn-default btn-sm" href="javascript:void(0);">'
-                               .Display::returnFontAwesomeIcon('folder-open').'</a>';
-                    $params['document'] .= Display::div('', ['id' => 'document_result_'.$course_info['real_id'].'_0', 'class' => 'document_preview_container']);
-                }
-            }
-            if ($load_dirs) {
-                $params['document'] = '<a id="document_preview_'.$course_info['real_id'].'_0" class="document_preview btn btn-default btn-sm" href="javascript:void(0);">'
-                    .Display::returnFontAwesomeIcon('folder-open').'</a>';
-                $params['document'] .= Display::div('', ['id' => 'document_result_'.$course_info['real_id'].'_0', 'class' => 'document_preview_container']);
-            }
-
-            $course_title_url = api_get_path(WEB_COURSE_PATH).$course_info['path'].'/index.php?id_session=0';
-
-            $teachers = [];
-            if (api_get_setting('display_teacher_in_courselist') === 'true') {
-                $teachers = self::getTeachersFromCourse(
-                    $course_info['real_id'],
-                    false
-                );
-            }
-            $params['status'] = $row['status'];
-
-            if (api_get_setting('display_coursecode_in_courselist') == 'true') {
-                $params['code_course'] = '('.$course_info['visual_code'].') ';
-            }
-
-            $params['visibility'] = $course_info['visibility'];
-            $params['link'] = $course_title_url;
-            $params['thumbnails'] = $thumbnails;
-            $params['image'] = $image;
-            $params['title'] = $course_info['title'];
-            $params['category'] = $course_info['categoryName'];
-            $params['teachers'] = $teachers;
-
-            if ($course_info['visibility'] != COURSE_VISIBILITY_CLOSED) {
-                $params['notifications'] = $showNotification;
-            }
-
-            $courseList[] = $params;
-        }
-
-        return $courseList;
-    }
-
     /**
      * Retrieves the user defined course categories.
      *
@@ -4497,6 +4205,7 @@ class CourseManager
             if (Skill::isAllowed($user_id, false)) {
                 $em = Database::getManager();
                 $objUser = api_get_user_entity($user_id);
+                /** @var Course $objCourse */
                 $objCourse = $em->find('ChamiloCoreBundle:Course', $course['real_id']);
                 $objSession = $em->find('ChamiloCoreBundle:Session', $session_id);
 
@@ -5026,20 +4735,21 @@ class CourseManager
             return [];
         }
 
-        $limit = intval($limit);
+        $limit = (int) $limit;
+        $userId = api_get_user_id();
 
         // Getting my courses
-        $my_course_list = self::get_courses_list_by_user_id(api_get_user_id());
+        $my_course_list = self::get_courses_list_by_user_id($userId);
 
-        $my_course_code_list = [];
+        $codeList = [];
         foreach ($my_course_list as $course) {
-            $my_course_code_list[$course['real_id']] = $course['real_id'];
+            $codeList[$course['real_id']] = $course['real_id'];
         }
 
         if (api_is_drh()) {
-            $courses = self::get_courses_followed_by_drh(api_get_user_id());
+            $courses = self::get_courses_followed_by_drh($userId);
             foreach ($courses as $course) {
-                $my_course_code_list[$course['real_id']] = $course['real_id'];
+                $codeList[$course['real_id']] = $course['real_id'];
             }
         }
 
@@ -5068,10 +4778,9 @@ class CourseManager
 
         $result = Database::query($sql);
         $courses = [];
-
         if (Database::num_rows($result)) {
             $courses = Database::store_result($result, 'ASSOC');
-            $courses = self::processHotCourseItem($courses, $my_course_code_list);
+            $courses = self::processHotCourseItem($courses, $codeList);
         }
 
         return $courses;
@@ -5079,11 +4788,11 @@ class CourseManager
 
     /**
      * @param array $courses
-     * @param array $my_course_code_list
+     * @param array $codeList
      *
      * @return mixed
      */
-    public static function processHotCourseItem($courses, $my_course_code_list = [])
+    public static function processHotCourseItem($courses, $codeList = [])
     {
         $hotCourses = [];
         $ajax_url = api_get_path(WEB_AJAX_PATH).'course.ajax.php?a=add_course_vote';
@@ -5101,12 +4810,14 @@ class CourseManager
             $access_link = self::get_access_link_by_user(
                 api_get_user_id(),
                 $course_info,
-                $my_course_code_list
+                $codeList
             );
 
             $userRegisteredInCourse = self::is_user_subscribed_in_course($user_id, $course_info['code']);
             $userRegisteredInCourseAsTeacher = self::is_course_teacher($user_id, $course_info['code']);
             $userRegistered = $userRegisteredInCourse && $userRegisteredInCourseAsTeacher;
+            $my_course['is_course_student'] = $userRegisteredInCourse;
+            $my_course['is_course_teacher'] = $userRegisteredInCourseAsTeacher;
             $my_course['is_registered'] = $userRegistered;
             $my_course['title_cut'] = cut($course_info['title'], 45);
 
@@ -5192,27 +4903,6 @@ class CourseManager
         return $hotCourses;
     }
 
-    /**
-     * @param int $limit
-     *
-     * @return array
-     */
-    public static function return_most_accessed_courses($limit = 5)
-    {
-        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_COURSE_RANKING);
-        $params['url_id'] = api_get_current_access_url_id();
-
-        $result = Database::select(
-            'c_id, accesses, total_score, users',
-            $table,
-            ['where' => ['url_id = ?' => $params], 'order' => 'accesses DESC', 'limit' => $limit],
-            'all',
-            true
-        );
-
-        return $result;
-    }
-
     /**
      * Get courses count.
      *
@@ -5289,10 +4979,10 @@ class CourseManager
         $visibilityCondition = '';
         $hidePrivate = api_get_setting('course_catalog_hide_private');
         if ($hidePrivate === 'true') {
-            $visibilityCondition .= ' AND '.$courseTableAlias.'.visibility <> '.COURSE_VISIBILITY_REGISTERED;
+            $visibilityCondition .= " AND $courseTableAlias.visibility <> ".COURSE_VISIBILITY_REGISTERED;
         }
         if ($hideClosed) {
-            $visibilityCondition .= ' AND '.$courseTableAlias.'.visibility NOT IN ('.COURSE_VISIBILITY_CLOSED.','.COURSE_VISIBILITY_HIDDEN.')';
+            $visibilityCondition .= " AND $courseTableAlias.visibility NOT IN (".COURSE_VISIBILITY_CLOSED.','.COURSE_VISIBILITY_HIDDEN.')';
         }
 
         // Check if course have users allowed to see it in the catalogue, then show only if current user is allowed to see it
@@ -5597,6 +5287,7 @@ class CourseManager
             'student_delete_own_publication',
             'hide_forum_notifications',
             'quiz_question_limit_per_day',
+            'subscribe_users_to_forum_notifications',
         ];
 
         $courseModels = ExerciseLib::getScoreModels();
@@ -5675,7 +5366,7 @@ class CourseManager
     public static function hasCourseSetting($variable, $courseId)
     {
         $courseSetting = Database::get_course_table(TABLE_COURSE_SETTING);
-        $courseId = intval($courseId);
+        $courseId = (int) $courseId;
         $variable = Database::escape_string($variable);
         $sql = "SELECT variable FROM $courseSetting
                 WHERE c_id = $courseId AND variable = '$variable'";
@@ -5684,34 +5375,6 @@ class CourseManager
         return Database::num_rows($result) > 0;
     }
 
-    /**
-     * Get information from the track_e_course_access table.
-     *
-     * @param int $sessionId
-     * @param int $userId
-     * @param int $limit
-     *
-     * @return array
-     */
-    public static function getCourseAccessPerSessionAndUser($sessionId, $userId, $limit = 0)
-    {
-        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
-
-        $sessionId = intval($sessionId);
-        $userId = intval($userId);
-
-        $sql = "SELECT * FROM $table
-                WHERE session_id = $sessionId AND user_id = $userId";
-
-        if (!empty($limit)) {
-            $limit = intval($limit);
-            $sql .= " LIMIT $limit";
-        }
-        $result = Database::query($sql);
-
-        return Database::store_result($result);
-    }
-
     /**
      * Get information from the track_e_course_access table.
      *
@@ -5729,8 +5392,8 @@ class CourseManager
         $endDate
     ) {
         $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
-        $courseId = intval($courseId);
-        $sessionId = intval($sessionId);
+        $courseId = (int) $courseId;
+        $sessionId = (int) $sessionId;
         $startDate = Database::escape_string($startDate);
         $endDate = Database::escape_string($endDate);
 
@@ -5917,27 +5580,6 @@ class CourseManager
         );
     }
 
-    /**
-     * @param $courseId
-     *
-     * @return array
-     */
-    public static function getCourseSettings($courseId)
-    {
-        $settingTable = Database::get_course_table(TABLE_COURSE_SETTING);
-        $courseId = intval($courseId);
-        $sql = "SELECT * FROM $settingTable WHERE c_id = $courseId";
-        $result = Database::query($sql);
-        $settings = [];
-        if (Database::num_rows($result)) {
-            while ($row = Database::fetch_array($result, 'ASSOC')) {
-                $settings[$row['variable']] = $row;
-            }
-        }
-
-        return $settings;
-    }
-
     /**
      * this function gets all the users of the course,
      * including users from linked courses.
@@ -6169,7 +5811,7 @@ class CourseManager
      */
     public static function get_course_by_category($category_id)
     {
-        $category_id = intval($category_id);
+        $category_id = (int) $category_id;
         $info = Database::fetch_array(
             Database::query(
                 'SELECT course_code 
@@ -6700,50 +6342,6 @@ class CourseManager
         return api_get_path(WEB_COURSE_PATH).$course->getDirectory().'/course-pic85x85.png';
     }
 
-    /**
-     * @return int
-     */
-    public static function getCountOpenCourses()
-    {
-        $visibility = [
-            COURSE_VISIBILITY_REGISTERED,
-            COURSE_VISIBILITY_OPEN_PLATFORM,
-            COURSE_VISIBILITY_OPEN_WORLD,
-        ];
-
-        $table = Database::get_main_table(TABLE_MAIN_COURSE);
-        $sql = "SELECT count(id) count 
-                FROM $table 
-                WHERE visibility IN (".implode(',', $visibility).")";
-        $result = Database::query($sql);
-        $row = Database::fetch_array($result);
-
-        return (int) $row['count'];
-    }
-
-    /**
-     * @return int
-     */
-    public static function getCountExercisesFromOpenCourse()
-    {
-        $visibility = [
-            COURSE_VISIBILITY_REGISTERED,
-            COURSE_VISIBILITY_OPEN_PLATFORM,
-            COURSE_VISIBILITY_OPEN_WORLD,
-        ];
-
-        $table = Database::get_main_table(TABLE_MAIN_COURSE);
-        $tableExercise = Database::get_course_table(TABLE_QUIZ_TEST);
-        $sql = "SELECT count(e.iid) count 
-                FROM $table c INNER JOIN $tableExercise e
-                ON (c.id = e.c_id)
-                WHERE e.active <> -1 AND visibility IN (".implode(',', $visibility).")";
-        $result = Database::query($sql);
-        $row = Database::fetch_array($result);
-
-        return (int) $row['count'];
-    }
-
     /**
      * Check if a specific access-url-related setting is a problem or not.
      *

+ 3 - 75
main/inc/lib/course_category.lib.php

@@ -33,11 +33,11 @@ class CourseCategory
      *
      * @return array
      */
-    public static function getCategory($category)
+    public static function getCategory($categoryCode)
     {
         $table = Database::get_main_table(TABLE_MAIN_CATEGORY);
-        $category = Database::escape_string($category);
-        $sql = "SELECT * FROM $table WHERE code ='$category'";
+        $categoryCode = Database::escape_string($categoryCode);
+        $sql = "SELECT * FROM $table WHERE code ='$categoryCode'";
         $result = Database::query($sql);
         if (Database::num_rows($result)) {
             $category = Database::fetch_array($result, 'ASSOC');
@@ -676,51 +676,6 @@ class CourseCategory
         return (int) $row['count'];
     }
 
-    /**
-     * create recursively all categories as option of the select passed in parameter.
-     *
-     * @param HTML_QuickForm_Element $element
-     * @param string                 $defaultCode the option value to select by default (used mainly for edition of courses)
-     * @param string                 $parentCode  the parent category of the categories added (default=null for root category)
-     * @param string                 $padding     the indent param (you shouldn't indicate something here)
-     */
-    public static function setCategoriesInForm(
-        $element,
-        $defaultCode = null,
-        $parentCode = null,
-        $padding = null
-    ) {
-        $tbl_category = Database::get_main_table(TABLE_MAIN_CATEGORY);
-
-        $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE_CATEGORY);
-        $conditions = " INNER JOIN $table a ON (c.id = a.course_category_id)";
-        $whereCondition = " AND a.access_url_id = ".api_get_current_access_url_id();
-
-        $sql = "SELECT code, name, auth_course_child, auth_cat_child
-                FROM $tbl_category c
-                $conditions
-                WHERE parent_id ".(empty($parentCode) ? "IS NULL" : "='".Database::escape_string($parentCode)."'")."
-                $whereCondition
-                ORDER BY name,  code";
-        $res = Database::query($sql);
-
-        while ($cat = Database::fetch_array($res, 'ASSOC')) {
-            $params = $cat['auth_course_child'] == 'TRUE' ? '' : 'disabled';
-            $params .= ($cat['code'] == $defaultCode) ? ' selected' : '';
-            $option = $padding.' '.$cat['name'].' ('.$cat['code'].')';
-
-            $element->addOption($option, $cat['code'], $params);
-            if ($cat['auth_cat_child'] == 'TRUE') {
-                self::setCategoriesInForm(
-                    $element,
-                    $defaultCode,
-                    $cat['code'],
-                    $padding.' - '
-                );
-            }
-        }
-    }
-
     /**
      * @param array $list
      *
@@ -778,33 +733,6 @@ class CourseCategory
         return Database::store_result($result, 'ASSOC');
     }
 
-    /**
-     * @param array $list
-     *
-     * @return array
-     */
-    public static function searchCategoryById($list)
-    {
-        if (empty($list)) {
-            return [];
-        } else {
-            $list = array_map('intval', $list);
-            $list = implode("','", $list);
-        }
-
-        $tableCategory = Database::get_main_table(TABLE_MAIN_CATEGORY);
-
-        $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE_CATEGORY);
-        $conditions = " INNER JOIN $table a ON (c.id = a.course_category_id)";
-        $whereCondition = " AND a.access_url_id = ".api_get_current_access_url_id();
-
-        $sql = "SELECT c.*, c.name as text FROM $tableCategory c $conditions
-                WHERE c.id IN $list $whereCondition";
-        $result = Database::query($sql);
-
-        return Database::store_result($result, 'ASSOC');
-    }
-
     /**
      * Get Pagination HTML div.
      *

+ 12 - 26
main/inc/lib/course_description.lib.php

@@ -43,6 +43,7 @@ class CourseDescription
      */
     public static function get_descriptions($course_id)
     {
+        $course_id = (int) $course_id;
         // Get course code
         $course_info = api_get_course_info_by_id($course_id);
         if (!empty($course_info)) {
@@ -59,7 +60,6 @@ class CourseDescription
             $desc_tmp = new CourseDescription();
             $desc_tmp->set_id($row['id']);
             $desc_tmp->set_title($row['title']);
-
             $desc_tmp->set_content($row['content']);
             $desc_tmp->set_session_id($row['session_id']);
             $desc_tmp->set_description_type($row['description_type']);
@@ -85,6 +85,11 @@ class CourseDescription
             true
         );
         $course_id = $this->course_id ?: api_get_course_int_id();
+
+        if (empty($course_id)) {
+            return [];
+        }
+
         $sql = "SELECT * FROM $table
 		        WHERE c_id = $course_id $condition_session
 		        ORDER BY id ";
@@ -113,6 +118,8 @@ class CourseDescription
         $session_id = null
     ) {
         $table = Database::get_course_table(TABLE_COURSE_DESCRIPTION);
+        $courseId = (int) $courseId;
+
         if (empty($courseId)) {
             $courseId = api_get_course_int_id();
         }
@@ -121,7 +128,8 @@ class CourseDescription
             $session_id = $this->session_id;
         }
         $condition_session = api_get_session_condition($session_id);
-        $description_type = intval($description_type);
+        $description_type = (int) $description_type;
+
         $sql = "SELECT * FROM $table
 		        WHERE 
 		            c_id = $courseId AND 
@@ -150,6 +158,7 @@ class CourseDescription
     {
         $table = Database::get_course_table(TABLE_COURSE_DESCRIPTION);
         $course_id = api_get_course_int_id();
+        $id = (int) $id;
 
         if (!isset($session_id)) {
             $session_id = $this->session_id;
@@ -159,7 +168,7 @@ class CourseDescription
             $course_info = api_get_course_info($course_code);
             $course_id = $course_info['real_id'];
         }
-        $id = intval($id);
+
         $sql = "SELECT * FROM $table
 		        WHERE c_id = $course_id AND id='$id' $condition_session ";
         $rs = Database::query($sql);
@@ -320,29 +329,6 @@ class CourseDescription
         return $affected_rows;
     }
 
-    /**
-     * Get description id by description type.
-     *
-     * @param int $description_type
-     *
-     * @return int description id
-     */
-    public function get_id_by_description_type($description_type)
-    {
-        $table = Database::get_course_table(TABLE_COURSE_DESCRIPTION);
-        $course_id = api_get_course_int_id();
-
-        $sql = "SELECT id FROM $table
-		        WHERE 
-		            c_id = $course_id AND 
-		            description_type = '".intval($description_type)."'";
-        $rs = Database::query($sql);
-        $row = Database::fetch_array($rs);
-        $description_id = $row['id'];
-
-        return $description_id;
-    }
-
     /**
      * Get description titles by default.
      *

+ 23 - 8
main/inc/lib/course_home.lib.php

@@ -140,7 +140,7 @@ class CourseHome
                     !learnpath::is_lp_visible_for_student(
                         $lpId,
                         api_get_user_id(),
-                        api_get_course_id(),
+                        api_get_course_info(),
                         api_get_session_id()
                     )
                 ) {
@@ -352,7 +352,7 @@ class CourseHome
                         !learnpath::is_lp_visible_for_student(
                             $lpId,
                             api_get_user_id(),
-                            api_get_course_id(),
+                            api_get_course_info(),
                             api_get_session_id()
                         )
                     ) {
@@ -520,6 +520,7 @@ class CourseHome
         );
 
         $lpTable = Database::get_course_table(TABLE_LP_MAIN);
+        $tblLpCategory = Database::get_course_table(TABLE_LP_CATEGORY);
         $orderBy = ' ORDER BY id ';
         switch ($course_tool_category) {
             case TOOL_STUDENT_VIEW:
@@ -542,18 +543,32 @@ class CourseHome
                 $sql = "SELECT t.* FROM $course_tool_table t
                         LEFT JOIN $lpTable l 
                         ON (t.c_id = l.c_id AND link LIKE concat('%/lp_controller.php?action=view&lp_id=', l.id, '&%'))
+                        LEFT JOIN $tblLpCategory lc
+                        ON (t.c_id = lc.c_id AND l.category_id = lc.iid)
                         $conditions AND
                         t.c_id = $course_id $condition_session 
-                        ORDER BY CASE WHEN l.display_order IS NULL THEN 0 ELSE 1 END, l.display_order, t.id";
+                        ORDER BY
+                            CASE WHEN l.category_id IS NULL THEN 0 ELSE 1 END,
+                            CASE WHEN l.display_order IS NULL THEN 0 ELSE 1 END,
+                            lc.position,
+                            l.display_order,
+                            t.id";
                 $orderBy = '';
                 break;
             case TOOL_AUTHORING:
                 $sql = "SELECT t.* FROM $course_tool_table t
-                        LEFT JOIN $lpTable l 
+                        LEFT JOIN $lpTable l
                         ON (t.c_id = l.c_id AND link LIKE concat('%/lp_controller.php?action=view&lp_id=', l.id, '&%'))
-                        WHERE 
+                        LEFT JOIN $tblLpCategory lc
+                        ON (t.c_id = lc.c_id AND l.category_id = lc.iid)
+                        WHERE
                             category = 'authoring' AND t.c_id = $course_id $condition_session
-                        ORDER BY CASE WHEN l.display_order IS NULL THEN 0 ELSE 1 END, l.display_order, t.id";
+                        ORDER BY
+                            CASE WHEN l.category_id IS NULL THEN 0 ELSE 1 END,
+                            CASE WHEN l.display_order IS NULL THEN 0 ELSE 1 END,
+                            lc.position,
+                            l.display_order,
+                            t.id";
                 $orderBy = '';
                 break;
             case TOOL_INTERACTION:
@@ -664,7 +679,7 @@ class CourseHome
                         $add = learnpath::is_lp_visible_for_student(
                             $lpId,
                             $userId,
-                            $courseInfo['code'],
+                            $courseInfo,
                             $sessionId
                         );
                     }
@@ -853,7 +868,7 @@ class CourseHome
                         !learnpath::is_lp_visible_for_student(
                             $lpId,
                             api_get_user_id(),
-                            api_get_course_id(),
+                            api_get_course_info(),
                             api_get_session_id()
                         )
                     ) {

+ 5 - 0
main/inc/lib/dashboard.lib.php

@@ -490,6 +490,11 @@ class DashboardManager
         $user_id = intval($user_id);
         $field_variable = 'dashboard';
         $extra_user_data = UserManager::get_extra_user_data_by_field($user_id, $field_variable);
+
+        if (!isset($extra_user_data[$field_variable])) {
+            return [];
+        }
+
         $extra_user_data = explode(';', $extra_user_data[$field_variable]);
         $data = [];
         foreach ($extra_user_data as $extra) {

+ 1 - 0
main/inc/lib/database.constants.inc.php

@@ -161,6 +161,7 @@ define('TABLE_STUDENT_PUBLICATION_ASSIGNMENT', 'student_publication_assignment')
 define('TABLE_STUDENT_PUBLICATION_REL_DOCUMENT', 'student_publication_rel_document');
 define('TABLE_STUDENT_PUBLICATION_REL_USER', 'student_publication_rel_user');
 define('TABLE_STUDENT_PUBLICATION_ASSIGNMENT_COMMENT', 'student_publication_comment');
+define('TABLE_PLAGIARISM', 'plagiarism_compilatio_docs');
 
 // Course forum tables
 define('TABLE_FORUM_CATEGORY', 'forum_category');

+ 41 - 1
main/inc/lib/diagnoser.lib.php

@@ -115,7 +115,7 @@ class Diagnoser
                 $row++;
             }
 
-            echo $table->display();
+            $table->display();
         }
         echo '</div></div>';
     }
@@ -194,6 +194,7 @@ class Diagnoser
         $access_url_id = api_get_current_access_url_id();
 
         if ($access_url_id === 1) {
+            $size = '-';
             global $_configuration;
             $message2 = '';
             if ($access_url_id === 1) {
@@ -203,6 +204,7 @@ class Diagnoser
                     $dir = api_get_path(SYS_PATH);
                     $du = exec('du -sh '.$dir, $err);
                     list($size, $none) = explode("\t", $du);
+                    unset($none);
                     $limit = 0;
                     if (isset($_configuration[$access_url_id])) {
                         if (isset($_configuration[$access_url_id]['hosting_limit_disk_space'])) {
@@ -224,6 +226,32 @@ class Diagnoser
                 $message2
             );
         }
+        $new_version = '-';
+        $new_version_status = '';
+        $file = api_get_path(SYS_CODE_PATH).'install/version.php';
+        if (is_file($file)) {
+            @include $file;
+        }
+        $array[] = $this->build_setting(
+            self::STATUS_INFORMATION,
+            '[CONFIG]',
+            get_lang('VersionFromVersionFile'),
+            '#',
+            $new_version.' '.$new_version_status,
+            '-',
+            null,
+            get_lang('TheVersionFromTheVersionFileIsUpdatedWithEachVersionIfMainInstallDirectoryIsPresent')
+        );
+        $array[] = $this->build_setting(
+            self::STATUS_INFORMATION,
+            '[CONFIG]',
+            get_lang('VersionFromConfigFile'),
+            '#',
+            api_get_configuration_value('system_version'),
+            $new_version,
+            null,
+            get_lang('TheVersionFromTheConfigurationFileShowsOnTheAdminPageButHasToBeChangedManuallyOnUpgrade')
+        );
 
         return $array;
     }
@@ -762,6 +790,17 @@ class Diagnoser
 
     /**
      * Additional functions needed for fast integration.
+     *
+     * @param int    $status         Status constant defining which icon to use to illustrate the info
+     * @param string $section        The name of the section this setting is included in
+     * @param string $title          The name of the setting (usually a translated string)
+     * @param string $url            A URL to point the user to regarding this setting, or '#' otherwise
+     * @param mixed  $current_value  The current value for this setting
+     * @param mixed  $expected_value The expected value for this setting
+     * @param string $formatter      If this setting is expressed in some kind of format, which format to use
+     * @param string $comment        A translated string explaining what this setting represents
+     *
+     * @return array A list of elements to show in an array's row
      */
     public function build_setting(
         $status,
@@ -784,6 +823,7 @@ class Diagnoser
                 $img = 'bullet_red.png';
                 break;
             case self::STATUS_INFORMATION:
+            default:
                 $img = 'bullet_blue.png';
                 break;
         }

+ 191 - 88
main/inc/lib/display.lib.php

@@ -1,6 +1,7 @@
 <?php
 /* For licensing terms, see /license.txt */
 
+use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
 use Chamilo\CoreBundle\Entity\ExtraField;
 use ChamiloSession as Session;
 
@@ -205,24 +206,27 @@ class Display
     /**
      * Displays a table.
      *
-     * @param array $header          Titles for the table header
-     *                               each item in this array can contain 3 values
-     *                               - 1st element: the column title
-     *                               - 2nd element: true or false (column sortable?)
-     *                               - 3th element: additional attributes for
-     *                               th-tag (eg for column-width)
-     *                               - 4the element: additional attributes for the td-tags
-     * @param array $content         2D-array with the tables content
-     * @param array $sorting_options Keys are:
-     *                               'column' = The column to use as sort-key
-     *                               'direction' = SORT_ASC or SORT_DESC
-     * @param array $paging_options  Keys are:
-     *                               'per_page_default' = items per page when switching from
-     *                               full-    list to per-page-view
-     *                               'per_page' = number of items to show per page
-     *                               'page_nr' = The page to display
-     * @param array $query_vars      Additional variables to add in the query-string
-     * @param string The style that the table will show. You can set 'table' or 'grid'
+     * @param array  $header          Titles for the table header
+     *                                each item in this array can contain 3 values
+     *                                - 1st element: the column title
+     *                                - 2nd element: true or false (column sortable?)
+     *                                - 3th element: additional attributes for
+     *                                th-tag (eg for column-width)
+     *                                - 4the element: additional attributes for the td-tags
+     * @param array  $content         2D-array with the tables content
+     * @param array  $sorting_options Keys are:
+     *                                'column' = The column to use as sort-key
+     *                                'direction' = SORT_ASC or SORT_DESC
+     * @param array  $paging_options  Keys are:
+     *                                'per_page_default' = items per page when switching from
+     *                                full-    list to per-page-view
+     *                                'per_page' = number of items to show per page
+     *                                'page_nr' = The page to display
+     * @param array  $query_vars      Additional variables to add in the query-string
+     * @param array  $form_actions
+     * @param string $style           The style that the table will show. You can set 'table' or 'grid'
+     * @param string $tableName
+     * @param string $tableId
      *
      * @author bart.mollet@hogent.be
      */
@@ -233,11 +237,13 @@ class Display
         $paging_options = [],
         $query_vars = null,
         $form_actions = [],
-        $style = 'table'
+        $style = 'table',
+        $tableName = 'tablename',
+        $tableId = ''
     ) {
         $column = isset($sorting_options['column']) ? $sorting_options['column'] : 0;
         $default_items_per_page = isset($paging_options['per_page']) ? $paging_options['per_page'] : 20;
-        $table = new SortableTableFromArray($content, $column, $default_items_per_page);
+        $table = new SortableTableFromArray($content, $column, $default_items_per_page, $tableName, null, $tableId);
         if (is_array($query_vars)) {
             $table->set_additional_parameters($query_vars);
         }
@@ -610,37 +616,68 @@ class Display
      * @param string  e-mail
      * @param string  clickable text
      * @param string  optional, class from stylesheet
+     * @param bool $addExtraContent
      *
      * @return string encrypted mailto hyperlink
      */
     public static function encrypted_mailto_link(
         $email,
         $clickable_text = null,
-        $style_class = ''
+        $style_class = '',
+        $addExtraContent = false
     ) {
         if (is_null($clickable_text)) {
             $clickable_text = $email;
         }
+
         // "mailto:" already present?
         if (substr($email, 0, 7) != 'mailto:') {
             $email = 'mailto:'.$email;
         }
+
         // Class (stylesheet) defined?
         if ($style_class != '') {
             $style_class = ' class="'.$style_class.'"';
         }
+
         // Encrypt email
         $hmail = '';
         for ($i = 0; $i < strlen($email); $i++) {
-            $hmail .= '&#'.ord($email[
-            $i]).';';
+            $hmail .= '&#'.ord($email[$i]).';';
         }
-        $hclickable_text = null;
+
+        $value = api_get_configuration_value('add_user_course_information_in_mailto');
+
+        if ($value) {
+            $hmail .= '?';
+            if (!api_is_anonymous()) {
+                $hmail .= '&subject='.Security::remove_XSS(api_get_setting('siteName'));
+            }
+            if ($addExtraContent) {
+                $content = '';
+                if (!api_is_anonymous()) {
+                    $userInfo = api_get_user_info();
+                    $content .= get_lang('User').': '.$userInfo['complete_name']."\n";
+
+                    $courseInfo = api_get_course_info();
+                    if (!empty($courseInfo)) {
+                        $content .= get_lang('Course').': ';
+                        $content .= $courseInfo['name'];
+                        $sessionInfo = api_get_session_info(api_get_session_id());
+                        if (!empty($sessionInfo)) {
+                            $content .= ' '.$sessionInfo['name'].' <br />';
+                        }
+                    }
+                }
+                $hmail .= '&body='.rawurlencode($content);
+            }
+        }
+
+        $hclickable_text = '';
         // Encrypt clickable text if @ is present
         if (strpos($clickable_text, '@')) {
             for ($i = 0; $i < strlen($clickable_text); $i++) {
-                $hclickable_text .= '&#'.ord($clickable_text[
-                $i]).';';
+                $hclickable_text .= '&#'.ord($clickable_text[$i]).';';
             }
         } else {
             $hclickable_text = @htmlspecialchars(
@@ -1130,6 +1167,7 @@ class Display
      * @param string $id            id of the container of the tab in the example "tabs"
      * @param array  $attributes    for the ul
      * @param array  $ul_attributes
+     * @param int    $selected
      *
      * @return string
      */
@@ -1138,7 +1176,8 @@ class Display
         $items,
         $id = 'tabs',
         $attributes = [],
-        $ul_attributes = []
+        $ul_attributes = [],
+        $selected = ''
     ) {
         if (empty($headers) || count($headers) == 0) {
             return '';
@@ -1151,6 +1190,14 @@ class Display
             if ($i == 1) {
                 $active = ' active';
             }
+
+            if (!empty($selected)) {
+                $active = '';
+                if ($selected == $i) {
+                    $active = ' active';
+                }
+            }
+
             $item = self::tag(
                 'a',
                 $item,
@@ -1166,6 +1213,7 @@ class Display
             $lis .= self::tag('li', $item, $ul_attributes);
             $i++;
         }
+
         $ul = self::tag(
             'ul',
             $lis,
@@ -1183,6 +1231,14 @@ class Display
             if ($i == 1) {
                 $active = ' active';
             }
+
+            if (!empty($selected)) {
+                $active = '';
+                if ($selected == $i) {
+                    $active = ' active';
+                }
+            }
+
             $divs .= self::tag(
                 'div',
                 $content,
@@ -1269,10 +1325,10 @@ class Display
      */
     public static function form_row($label, $form_item)
     {
-        $label = self::span($label, ['class' => 'control-label']);
-        $form_item = self::div($form_item, ['class' => 'controls']);
+        $label = self::tag('label', $label, ['class' => 'col-sm-2 control-label']);
+        $form_item = self::div($form_item, ['class' => 'col-sm-10']);
 
-        return self::div($label.$form_item, ['class' => 'control-group']);
+        return self::div($label.$form_item, ['class' => 'form-group']);
     }
 
     /**
@@ -1391,10 +1447,28 @@ class Display
 
         $beforeSelectRow = null;
         if (isset($extra_params['beforeSelectRow'])) {
-            $beforeSelectRow = "beforeSelectRow: ".$extra_params['beforeSelectRow'].", ";
+            $beforeSelectRow = 'beforeSelectRow: '.$extra_params['beforeSelectRow'].', ';
             unset($extra_params['beforeSelectRow']);
         }
 
+        $beforeProcessing = '';
+        if (isset($extra_params['beforeProcessing'])) {
+            $beforeProcessing = 'beforeProcessing : function() { '.$extra_params['beforeProcessing'].' },';
+            unset($extra_params['beforeProcessing']);
+        }
+
+        $beforeRequest = '';
+        if (isset($extra_params['beforeRequest'])) {
+            $beforeRequest = 'beforeRequest : function() { '.$extra_params['beforeRequest'].' },';
+            unset($extra_params['beforeRequest']);
+        }
+
+        $gridComplete = '';
+        if (isset($extra_params['gridComplete'])) {
+            $gridComplete = 'gridComplete : function() { '.$extra_params['gridComplete'].' },';
+            unset($extra_params['gridComplete']);
+        }
+
         // Adding extra params
         if (!empty($extra_params)) {
             foreach ($extra_params as $key => $element) {
@@ -1433,9 +1507,22 @@ class Display
         $json_encode = str_replace('"formatter":"extra_formatter"', 'formatter:extra_formatter', $json_encode);
         $json_encode = str_replace(['{"first":"first",', '"end":"end"}'], '', $json_encode);
 
+        if (api_get_configuration_value('allow_compilatio_tool') &&
+            (strpos($_SERVER['REQUEST_URI'], 'work/work.php') !== false ||
+             strpos($_SERVER['REQUEST_URI'], 'work/work_list_all.php') != false
+            )
+        ) {
+            $json_encode = str_replace('"function () { compilatioInit() }"',
+                'function () { compilatioInit() }',
+                $json_encode
+            );
+        }
         // Creating the jqgrid element.
         $json .= '$("#'.$div_id.'").jqGrid({';
         //$json .= $beforeSelectRow;
+        $json .= $gridComplete;
+        $json .= $beforeProcessing;
+        $json .= $beforeRequest;
         $json .= $json_encode;
         $json .= '});';
 
@@ -1648,7 +1735,7 @@ class Display
             }
 
             if ($notification['tool'] == TOOL_LEARNPATH) {
-                if (!learnpath::is_lp_visible_for_student($notification['ref'], $user_id, $course_code)) {
+                if (!learnpath::is_lp_visible_for_student($notification['ref'], $user_id, $courseInfo)) {
                     continue;
                 }
             }
@@ -1867,14 +1954,15 @@ class Display
         return self::page_subheader($title, $second_title);
     }
 
-    public static function page_subheader($title, $second_title = null, $size = 'h3')
+    public static function page_subheader($title, $second_title = null, $size = 'h3', $attributes = [])
     {
         if (!empty($second_title)) {
             $second_title = Security::remove_XSS($second_title);
             $title .= "<small> $second_title<small>";
         }
+        $subTitle = self::tag($size, Security::remove_XSS($title), $attributes);
 
-        return '<'.$size.'>'.Security::remove_XSS($title).'</'.$size.'>';
+        return $subTitle;
     }
 
     public static function page_subheader2($title, $second_title = null)
@@ -1908,18 +1996,21 @@ class Display
     }
 
     /**
-     * @param int    $percentage
+     * @param int    $percentage      int value between 0 and 100
      * @param bool   $show_percentage
      * @param string $extra_info
+     * @param string $class           danger/success/infowarning
      *
      * @return string
      */
-    public static function bar_progress($percentage, $show_percentage = true, $extra_info = '')
+    public static function bar_progress($percentage, $show_percentage = true, $extra_info = '', $class = '')
     {
         $percentage = (int) $percentage;
+        $class = empty($class) ? '' : "progress-bar-$class";
+
         $div = '<div class="progress">
                 <div
-                    class="progress-bar progress-bar-striped"
+                    class="progress-bar progress-bar-striped '.$class.'"
                     role="progressbar"
                     aria-valuenow="'.$percentage.'"
                     aria-valuemin="0"
@@ -2173,17 +2264,14 @@ class Display
     {
         $fileInfo = pathinfo($file);
 
+        $autoplay = isset($params['autoplay']) && 'true' === $params['autoplay'] ? 'autoplay' : '';
+        $id = isset($params['id']) ? $params['id'] : $fileInfo['basename'];
+        $width = isset($params['width']) ? 'width="'.$params['width'].'"' : null;
+        $class = isset($params['class']) ? ' class="'.$params['class'].'"' : null;
+
         switch ($fileInfo['extension']) {
             case 'mp3':
             case 'webm':
-                $autoplay = null;
-                if (isset($params['autoplay']) && $params['autoplay'] == 'true') {
-                    $autoplay = 'autoplay';
-                }
-                $width = isset($params['width']) ? 'width="'.$params['width'].'"' : null;
-                $id = isset($params['id']) ? $params['id'] : $fileInfo['basename'];
-                $class = isset($params['class']) ? ' class="'.$params['class'].'"' : null;
-
                 $html = '<audio id="'.$id.'" '.$class.' controls '.$autoplay.' '.$width.' src="'.$params['url'].'" >';
                 $html .= '<object width="'.$width.'" height="50" type="application/x-shockwave-flash" data="'.api_get_path(WEB_LIBRARY_PATH).'javascript/mediaelement/flashmediaelement.swf">
                             <param name="movie" value="'.api_get_path(WEB_LIBRARY_PATH).'javascript/mediaelement/flashmediaelement.swf" />
@@ -2195,7 +2283,7 @@ class Display
                 break;
             case 'wav':
             case 'ogg':
-                $html = '<audio width="300px" controls src="'.$params['url'].'" >';
+                $html = '<audio width="300px" controls id="'.$id.'" '.$autoplay.' src="'.$params['url'].'" >';
 
                 return $html;
                 break;
@@ -2804,70 +2892,85 @@ HTML;
      */
     public static function getFrameReadyBlock($frameName)
     {
-        $defaultFeatures = ['playpause', 'current', 'progress', 'duration', 'tracks', 'volume', 'fullscreen', 'vrview'];
+        $webPublicPath = api_get_path(WEB_PUBLIC_PATH);
+
+        $videoFeatures = [
+            'playpause',
+            'current',
+            'progress',
+            'duration',
+            'tracks',
+            'volume',
+            'fullscreen',
+            'vrview',
+            'markersrolls',
+        ];
         $features = api_get_configuration_value('video_features');
-        $bowerJsFiles = [];
-        $bowerCSSFiles = [];
+        $videoPluginsJS = [];
+        $videoPluginCSS = [];
         if (!empty($features) && isset($features['features'])) {
             foreach ($features['features'] as $feature) {
                 if ($feature === 'vrview') {
                     continue;
                 }
                 $defaultFeatures[] = $feature;
-                $bowerJsFiles[] = "mediaelement/plugins/$feature/$feature.js";
-                $bowerCSSFiles[] = "mediaelement/plugins/$feature/$feature.css";
+                $videoPluginsJS[] = "mediaelement/plugins/$feature/$feature.js";
+                $videoPluginCSS[] = "mediaelement/plugins/$feature/$feature.css";
             }
         }
 
-        $translateHtml = '';
-        $translate = api_get_configuration_value('translate_html');
-        if ($translate) {
-            $translateHtml = '{type:"script", id:"_fr4", src:"'.api_get_path(WEB_AJAX_PATH).'lang.ajax.php?a=translate_html&'.api_get_cidreq().'"},';
+        $videoPluginFiles = '';
+        foreach ($videoPluginsJS as $file) {
+            $videoPluginFiles .= '{type: "script", src: "'.$webPublicPath.'assets/'.$file.'"},';
         }
 
-        $counter = 10;
-        $extraMediaFiles = '';
-        foreach ($bowerJsFiles as $file) {
-            $extraMediaFiles .= '{type: "script", id: "media_'.$counter.'", src: "'.api_get_path(WEB_PUBLIC_PATH).'assets/'.$file.'"},';
-            $counter++;
+        $videoPluginCssFiles = '';
+        foreach ($videoPluginCSS as $file) {
+            $videoPluginCssFiles .= '{type: "stylesheet", src: "'.$webPublicPath.'assets/'.$file.'"},';
         }
 
-        foreach ($bowerCSSFiles as $file) {
-            $extraMediaFiles .= '{type: "stylesheet", id: "media_'.$counter.'", src: "'.api_get_path(WEB_PUBLIC_PATH).'assets/'.$file.'"},';
-            $counter++;
+        $translateHtml = '';
+        $translate = api_get_configuration_value('translate_html');
+        if ($translate) {
+            $translateHtml = '{type:"script", src:"'.api_get_path(WEB_AJAX_PATH).'lang.ajax.php?a=translate_html&'.api_get_cidreq().'"},';
         }
 
-        $defaultFeatures = implode("','", $defaultFeatures);
+        $videoFeatures = implode("','", $videoFeatures);
         $frameReady = '
-          $.frameReady(function() {
-            $(function() {
-                $("video:not(.skip), audio:not(.skip)").mediaelementplayer({                    
-                    pluginPath: "'.api_get_path(WEB_PUBLIC_PATH).'assets/mediaelement/build/",            
-                    features: ["'.$defaultFeatures.'"],
+        $.frameReady(function() {
+             $(function () {
+                $("video:not(.skip), audio:not(.skip)").mediaelementplayer({
+                    pluginPath: "'.$webPublicPath.'assets/mediaelement/plugins/",            
+                    features: [\''.$videoFeatures.'\'],
                     success: function(mediaElement, originalNode, instance) {
+                        '.ChamiloApi::getQuizMarkersRollsJS().'
                     },
-                    vrPath: "'.api_get_path(WEB_PUBLIC_PATH).'assets/vrview/build/vrview.js"
+                    vrPath: "'.$webPublicPath.'assets/vrview/build/vrview.js"
                 });
             });
-          }, "'.$frameName.'",
-          {
-            load: [
-                { type:"script", id:"_fr1", src:"'.api_get_jquery_web_path().'"},
-                { type:"script", id:"_fr7", src:"'.api_get_path(WEB_PUBLIC_PATH).'assets/MathJax/MathJax.js?config=AM_HTMLorMML"},
-                { type:"script", id:"_fr4", src:"'.api_get_path(WEB_PUBLIC_PATH).'assets/jquery-ui/jquery-ui.min.js"},
-                { type:"stylesheet", id:"_fr5", src:"'.api_get_path(WEB_PUBLIC_PATH).'assets/jquery-ui/themes/smoothness/jquery-ui.min.css"},
-                { type:"stylesheet", id:"_fr6", src:"'.api_get_path(WEB_PUBLIC_PATH).'assets/jquery-ui/themes/smoothness/theme.css"},
-                { type:"script", id:"_fr2", src:"'.api_get_path(WEB_LIBRARY_PATH).'javascript/jquery.highlight.js"},
-                { type:"stylesheet", id:"_fr7", src:"'.api_get_path(WEB_PUBLIC_PATH).'css/dialog.css"},
-                { type:"script", id:"_fr3", src:"'.api_get_path(WEB_CODE_PATH).'glossary/glossary.js.php?'.api_get_cidreq().'"},
-                {type: "script", id: "_media1", src: "'.api_get_path(WEB_PUBLIC_PATH).'assets/mediaelement/build/mediaelement-and-player.min.js"},
-                {type: "stylesheet", id: "_media2", src: "'.api_get_path(WEB_PUBLIC_PATH).'assets/mediaelement/build/mediaelementplayer.min.css"},                
-                {type: "stylesheet", id: "_media4", src: "'.api_get_path(WEB_PUBLIC_PATH).'assets/mediaelement/plugins/vrview/vrview.css"},
-                {type: "script", id: "_media4", src: "'.api_get_path(WEB_PUBLIC_PATH).'assets/mediaelement/plugins/vrview/vrview.js"},
-                '.$extraMediaFiles.'
+        }, 
+        "'.$frameName.'",
+        [
+            {type:"script", src:"'.api_get_jquery_web_path().'", deps: [
+                {type:"script", src:"'.api_get_path(WEB_LIBRARY_PATH).'javascript/jquery.highlight.js"},
+                {type:"script", src:"'.api_get_path(WEB_CODE_PATH).'glossary/glossary.js.php?'.api_get_cidreq().'"},
+                {type:"script", src:"'.$webPublicPath.'assets/jquery-ui/jquery-ui.min.js"},
+                {type:"script", src: "'.$webPublicPath.'assets/mediaelement/build/mediaelement-and-player.min.js", 
+                    deps: [
+                    {type:"script", src: "'.$webPublicPath.'assets/mediaelement/plugins/vrview/vrview.js"},
+                    {type:"script", src: "'.$webPublicPath.'assets/mediaelement/plugins/markersrolls/markersrolls.js"},
+                    '.$videoPluginFiles.'
+                ]},                
                 '.$translateHtml.'
-            ]
-          });';
+            ]},
+            '.$videoPluginCssFiles.'
+            {type:"script", src:"'.$webPublicPath.'assets/MathJax/MathJax.js?config=AM_HTMLorMML"},
+            {type:"stylesheet", src:"'.$webPublicPath.'assets/jquery-ui/themes/smoothness/jquery-ui.min.css"},
+            {type:"stylesheet", src:"'.$webPublicPath.'assets/jquery-ui/themes/smoothness/theme.css"},                
+            {type:"stylesheet", src:"'.$webPublicPath.'css/dialog.css"},
+            {type:"stylesheet", src: "'.$webPublicPath.'assets/mediaelement/build/mediaelementplayer.min.css"},                
+            {type:"stylesheet", src: "'.$webPublicPath.'assets/mediaelement/plugins/vrview/vrview.css"},
+        ]);';
 
         return $frameReady;
     }

+ 61 - 30
main/inc/lib/document.lib.php

@@ -535,6 +535,10 @@ class DocumentManager
         $search = false,
         $sessionId = 0
     ) {
+        if (empty($courseInfo)) {
+            return [];
+        }
+
         $tblItemProperty = Database::get_course_table(TABLE_ITEM_PROPERTY);
         $tblDocument = Database::get_course_table(TABLE_DOCUMENT);
 
@@ -635,6 +639,17 @@ class DocumentManager
                     }
                 }
 
+                if (self::isBasicCourseFolder($row['path'], $sessionId)) {
+                    $basicCourseDocumentsContent = self::getAllDocumentData(
+                        $courseInfo,
+                        $row['path']
+                    );
+
+                    if (empty($basicCourseDocumentsContent)) {
+                        continue;
+                    }
+                }
+
                 $rows[$row['id']] = $row;
             }
 
@@ -715,6 +730,10 @@ class DocumentManager
         $getInvisibleList = false,
         $path = ''
     ) {
+        if (empty($_course)) {
+            return [];
+        }
+
         $TABLE_ITEMPROPERTY = Database::get_course_table(TABLE_ITEM_PROPERTY);
         $TABLE_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
         $groupIid = (int) $groupIid;
@@ -1672,11 +1691,10 @@ class DocumentManager
             }
         }
 
-        $doc_id = intval($doc_id);
-        $session_id = intval($session_id);
-
-        //2. Course and Session visibility are handle in local.inc.php/global.inc.php
-        //3. Checking if user exist in course/session
+        $doc_id = (int) $doc_id;
+        $session_id = (int) $session_id;
+        // 2. Course and Session visibility are handle in local.inc.php/global.inc.php
+        // 3. Checking if user exist in course/session
         if ($session_id == 0) {
             if (is_null($userIsSubscribed)) {
                 $userIsSubscribed = CourseManager::is_user_subscribed_in_course(
@@ -1817,7 +1835,7 @@ class DocumentManager
     public static function get_default_certificate_id($course_id, $session_id = 0)
     {
         $tbl_category = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
-        $session_id = intval($session_id);
+        $session_id = (int) $session_id;
         if (empty($session_id)) {
             $session_id = api_get_session_id();
         }
@@ -1825,7 +1843,7 @@ class DocumentManager
         if (empty($session_id)) {
             $sql_session = 'AND (session_id = 0 OR isnull(session_id)) ';
         } elseif ($session_id > 0) {
-            $sql_session = 'AND session_id='.intval($session_id);
+            $sql_session = 'AND session_id='.$session_id;
         } else {
             $sql_session = '';
         }
@@ -2918,7 +2936,7 @@ class DocumentManager
                     $treat_spaces_as_hyphens
                 );
 
-                // Showing message when sending zip files
+                // When sending zip files
                 if ($new_path === true && $unzip == 1) {
                     if ($show_output) {
                         echo Display::return_message(
@@ -3444,7 +3462,7 @@ class DocumentManager
         if ($lp_id === false) {
             $url = $lpAjaxUrl.'?a=get_documents&lp_id=&cidReq='.$course_info['code'];
             $return .= "<script>
-            $(document).ready(function () {
+            $(function() {
                 $('.close_div').click(function() {
                     var course_id = this.id.split('_')[2];
                     var session_id = this.id.split('_')[3];
@@ -3460,7 +3478,7 @@ class DocumentManager
         }
 
         if (!empty($overwrite_url)) {
-            $url .= '&url='.Security::remove_XSS($overwrite_url);
+            $url .= '&url='.urlencode(Security::remove_XSS($overwrite_url));
         }
 
         if ($add_move_button) {
@@ -4466,7 +4484,7 @@ class DocumentManager
         $path = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document/';
         if (!is_dir($path.'audio')) {
             mkdir($path.'audio', api_get_permissions_for_new_directories());
-            $audioId = add_document($_course, '/audio', 'folder', 0, 'Audio');
+            $audioId = add_document($_course, '/audio', 'folder', 0, get_lang('Audio'));
             api_item_property_update(
                 $_course,
                 TOOL_DOCUMENT,
@@ -4720,7 +4738,7 @@ class DocumentManager
             return false;
         }
 
-        $sessionId = intval($sessionId);
+        $sessionId = (int) $sessionId;
         $folderWithSuffix = self::fixDocumentName(
             $folder,
             'folder',
@@ -4772,7 +4790,7 @@ class DocumentManager
             return false;
         }
 
-        $sessionId = intval($sessionId);
+        $sessionId = (int) $sessionId;
         $fileNameEscape = Database::escape_string($fileName);
 
         $fileNameWithSuffix = self::fixDocumentName(
@@ -5014,6 +5032,9 @@ class DocumentManager
         $path = $document_data['path'];
         $url_path = urlencode($document_data['path']);
 
+        $basePageUrl = api_get_path(WEB_CODE_PATH).'document/';
+        $pageUrl = $basePageUrl.'document.php';
+
         // Add class="invisible" on invisible files
         $visibility_class = $visibility == false ? ' class="muted"' : '';
         $forcedownload_link = '';
@@ -5023,7 +5044,9 @@ class DocumentManager
 
         if (!$show_as_icon) {
             // Build download link (icon)
-            $forcedownload_link = $filetype == 'folder' ? api_get_self().'?'.$courseParams.'&action=downloadfolder&id='.$document_data['id'] : api_get_self().'?'.$courseParams.'&amp;action=download&amp;id='.$document_data['id'];
+            $forcedownload_link = $filetype == 'folder'
+                ? $pageUrl.'?'.$courseParams.'&action=downloadfolder&id='.$document_data['id']
+                : $pageUrl.'?'.$courseParams.'&amp;action=download&amp;id='.$document_data['id'];
             // Folder download or file download?
             $forcedownload_icon = $filetype == 'folder' ? 'save_pack.png' : 'save.png';
             // Prevent multiple clicks on zipped folder download
@@ -5036,15 +5059,15 @@ class DocumentManager
         if ($filetype == 'file') {
             // Check the extension
             $ext = explode('.', $path);
-            $ext = strtolower($ext[sizeof($ext) - 1]);
+            $ext = strtolower($ext[count($ext) - 1]);
 
             // HTML-files an some other types are shown in a frameset by default.
             $is_browser_viewable_file = self::isBrowserViewable($ext);
             if ($is_browser_viewable_file) {
                 if ($ext == 'pdf' || in_array($ext, $webODFList)) {
-                    $url = api_get_self().'?'.$courseParams.'&amp;action=download&amp;id='.$document_data['id'];
+                    $url = $pageUrl.'?'.$courseParams.'&amp;action=download&amp;id='.$document_data['id'];
                 } else {
-                    $url = 'showinframes.php?'.$courseParams.'&id='.$document_data['id'];
+                    $url = $basePageUrl.'showinframes.php?'.$courseParams.'&id='.$document_data['id'];
                 }
             } else {
                 // url-encode for problematic characters (we may not call them dangerous characters...)
@@ -5052,7 +5075,7 @@ class DocumentManager
                 $url = $www.str_replace('%2F', '/', $url_path).'?'.$courseParams;
             }
         } else {
-            $url = api_get_self().'?'.$courseParams.'&id='.$document_data['id'];
+            $url = $pageUrl.'?'.$courseParams.'&id='.$document_data['id'];
         }
 
         if ($isCertificateMode) {
@@ -5110,19 +5133,19 @@ class DocumentManager
                             preg_match('/shared_folder_session_'.$sessionId.'\/sf_user_'.api_get_user_id().'$/', urldecode($forcedownload_link)) ||
                             $isAllowedToEdit || api_is_platform_admin()
                         ) {
-                            $force_download_html = ($size == 0) ? '' : '<a href="'.$forcedownload_link.'" style="float:right"'.$prevent_multiple_click.'>'.
+                            $force_download_html = $size == 0 ? '' : '<a href="'.$forcedownload_link.'" style="float:right"'.$prevent_multiple_click.'>'.
                                 Display::return_icon($forcedownload_icon, get_lang('Download'), [], ICON_SIZE_SMALL).'</a>';
                         }
                     } elseif (!preg_match('/shared_folder/', urldecode($forcedownload_link)) ||
                         $isAllowedToEdit ||
                         api_is_platform_admin()
                     ) {
-                        $force_download_html = ($size == 0) ? '' : '<a href="'.$forcedownload_link.'" style="float:right"'.$prevent_multiple_click.'>'.
+                        $force_download_html = $size == 0 ? '' : '<a href="'.$forcedownload_link.'" style="float:right"'.$prevent_multiple_click.'>'.
                             Display::return_icon($forcedownload_icon, get_lang('Download'), [], ICON_SIZE_SMALL).'</a>';
                     }
                 }
             } else {
-                $force_download_html = ($size == 0) ? '' : '<a href="'.$forcedownload_link.'" style="float:right"'.$prevent_multiple_click.' download="'.$document_data['basename'].'">'.
+                $force_download_html = $size == 0 ? '' : '<a href="'.$forcedownload_link.'" style="float:right"'.$prevent_multiple_click.' download="'.$document_data['basename'].'">'.
                     Display::return_icon($forcedownload_icon, get_lang('Download'), [], ICON_SIZE_SMALL).'</a>';
             }
 
@@ -5130,8 +5153,8 @@ class DocumentManager
             if (api_get_setting('allow_my_files') === 'true' &&
                 api_get_setting('users_copy_files') === 'true' && api_is_anonymous() === false
             ) {
-                $copy_myfiles_link = $filetype == 'file' ? api_get_self().'?'.$courseParams.'&action=copytomyfiles&id='.$document_data['id'] : api_get_self().'?'.$courseParams;
-                if ($filetype == 'file') {
+                $copy_myfiles_link = $filetype === 'file' ? $pageUrl.'?'.$courseParams.'&action=copytomyfiles&id='.$document_data['id'] : api_get_self().'?'.$courseParams;
+                if ($filetype === 'file') {
                     $copyToMyFiles = '<a href="'.$copy_myfiles_link.'" style="float:right"'.$prevent_multiple_click.'>'.
                         Display::return_icon('briefcase.png', get_lang('CopyToMyFiles'), [], ICON_SIZE_SMALL).'&nbsp;&nbsp;</a>';
 
@@ -5143,11 +5166,11 @@ class DocumentManager
 
             $pdf_icon = '';
             if (!$isAllowedToEdit &&
+                $filetype === 'file' &&
                 api_get_setting('students_export2pdf') == 'true' &&
-                $filetype == 'file' &&
                 in_array($extension, ['html', 'htm'])
             ) {
-                $pdf_icon = ' <a style="float:right".'.$prevent_multiple_click.' href="'.api_get_self().'?'.$courseParams.'&action=export_to_pdf&id='.$document_data['id'].'&curdirpath='.$curdirpath.'">'.
+                $pdf_icon = ' <a style="float:right".'.$prevent_multiple_click.' href="'.$pageUrl.'?'.$courseParams.'&action=export_to_pdf&id='.$document_data['id'].'&curdirpath='.$curdirpath.'">'.
                     Display::return_icon('pdf.png', get_lang('Export2PDF'), [], ICON_SIZE_SMALL).'</a> ';
             }
 
@@ -5156,7 +5179,7 @@ class DocumentManager
                     Display::return_icon('open_in_new_window.png', get_lang('OpenInANewWindow'), [], ICON_SIZE_SMALL).'&nbsp;&nbsp;</a>';
             }
 
-            if ($filetype == 'file') {
+            if ($filetype === 'file') {
                 // Sound preview
                 if (preg_match('/mp3$/i', urldecode($checkExtension)) ||
                     (preg_match('/wav$/i', urldecode($checkExtension))) ||
@@ -5198,7 +5221,7 @@ class DocumentManager
                     // For a "PDF Download" of the file.
                     $pdfPreview = null;
                     if ($ext != 'pdf' && !in_array($ext, $webODFList)) {
-                        $url = 'showinframes.php?'.$courseParams.'&id='.$document_data['id'];
+                        $url = $basePageUrl.'showinframes.php?'.$courseParams.'&id='.$document_data['id'];
                     } else {
                         $pdfPreview = Display::url(
                             Display::return_icon('preview.png', get_lang('Preview'), null, ICON_SIZE_SMALL),
@@ -5239,7 +5262,7 @@ class DocumentManager
                         preg_match('/bmp$/i', urldecode($checkExtension)) ||
                         preg_match('/svg$/i', urldecode($checkExtension))
                     ) {
-                        $url = 'showinframes.php?'.$courseParams.'&id='.$document_data['id'];
+                        $url = $basePageUrl.'showinframes.php?'.$courseParams.'&id='.$document_data['id'];
 
                         return '<a href="'.$url.'" title="'.$tooltip_title_alt.'" '.$visibility_class.' style="float:left">'.
                             self::build_document_icon_tag($filetype, $path, $isAllowedToEdit).
@@ -5258,7 +5281,7 @@ class DocumentManager
                     '</a>';
                 }
             } else {
-                if ($filetype == 'file') {
+                if ($filetype === 'file') {
                     // Sound preview with jplayer
                     if (preg_match('/mp3$/i', urldecode($checkExtension)) ||
                         (preg_match('/wav$/i', urldecode($checkExtension))) ||
@@ -5278,7 +5301,7 @@ class DocumentManager
                         preg_match('/bmp$/i', urldecode($checkExtension)) ||
                         preg_match('/svg$/i', urldecode($checkExtension))
                     ) {
-                        $url = 'showinframes.php?'.$courseParams.'&id='.$document_data['id']; //without preview
+                        $url = $basePageUrl.'showinframes.php?'.$courseParams.'&id='.$document_data['id']; //without preview
                         return '<a href="'.$url.'" title="'.$tooltip_title_alt.'" '.$visibility_class.' style="float:left">'.
                             self::build_document_icon_tag($filetype, $path, $isAllowedToEdit).
                         '</a>';
@@ -5840,6 +5863,14 @@ class DocumentManager
         }
     }
 
+    public static function isBasicCourseFolder($path, $sessionId)
+    {
+        $cleanPath = Security::remove_XSS($path);
+        $basicCourseFolder = '/basic-course-documents__'.$sessionId.'__0';
+
+        return $cleanPath == $basicCourseFolder;
+    }
+
     /**
      * Check if the file name or folder searched exist.
      *

+ 50 - 177
main/inc/lib/events.lib.php

@@ -504,6 +504,7 @@ class Event
         $exe_id = Database::escape_string($exe_id);
         $position = Database::escape_string($position);
         $now = api_get_utc_datetime();
+        $course_id = (int) $course_id;
 
         // check user_id or get from context
         if (empty($user_id)) {
@@ -514,10 +515,11 @@ class Event
             }
         }
         // check course_id or get from context
-        if (empty($course_id) or intval($course_id) != $course_id) {
+        if (empty($course_id)) {
             $course_id = api_get_course_int_id();
         }
         // check session_id or get from context
+        $session_id = (int) $session_id;
         if (empty($session_id)) {
             $session_id = api_get_session_id();
         }
@@ -549,8 +551,6 @@ class Event
             $answer = 0;
         }
 
-        $session_id = api_get_session_id();
-
         if (!empty($question_id) && !empty($exe_id) && !empty($user_id)) {
             if (is_null($answer)) {
                 $answer = '';
@@ -1089,10 +1089,10 @@ class Event
         if (Database::num_rows($query) > 0) {
             $attempt = Database::fetch_array($query, 'ASSOC');
 
-            return $attempt['count'];
-        } else {
-            return 0;
+            return (int) $attempt['count'];
         }
+
+        return 0;
     }
 
     /**
@@ -1171,17 +1171,11 @@ class Event
             $course_id = api_get_course_int_id();
         }
 
-        $track_e_exercises = Database::get_main_table(
-            TABLE_STATISTIC_TRACK_E_EXERCISES
-        );
-        $track_attempts = Database::get_main_table(
-            TABLE_STATISTIC_TRACK_E_ATTEMPT
-        );
-        $recording_table = Database::get_main_table(
-            TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING
-        );
+        $track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
+        $track_attempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
+        $recording_table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING);
 
-        //Make sure we have the exact lp_view_id
+        // Make sure we have the exact lp_view_id
         $sql = "SELECT id FROM $lp_view_table
                 WHERE
                     c_id = $course_id AND
@@ -1243,15 +1237,15 @@ class Event
         if (!empty($exe_list) && is_array($exe_list) && count($exe_list) > 0) {
             $exeListString = implode(',', $exe_list);
             $sql = "DELETE FROM $track_e_exercises
-                WHERE exe_id IN ($exeListString)";
+                    WHERE exe_id IN ($exeListString)";
             Database::query($sql);
 
             $sql = "DELETE FROM $track_attempts
-                WHERE exe_id IN ($exeListString)";
+                    WHERE exe_id IN ($exeListString)";
             Database::query($sql);
 
             $sql = "DELETE FROM $recording_table
-                WHERE exe_id IN ($exeListString)";
+                    WHERE exe_id IN ($exeListString)";
             Database::query($sql);
         }
 
@@ -1282,13 +1276,14 @@ class Event
         $course_id,
         $session_id = 0
     ) {
-        $track_e_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
         $user_id = (int) $user_id;
         $exercise_id = (int) $exercise_id;
         $course_id = (int) $course_id;
         $session_id = (int) $session_id;
-        if (!empty($user_id) && !empty($exercise_id) && !empty($course_code)) {
-            $sql = "DELETE FROM $track_e_exercises
+
+        if (!empty($user_id) && !empty($exercise_id) && !empty($course_id)) {
+            $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
+            $sql = "DELETE FROM $table
                     WHERE
                         exe_user_id = $user_id AND
                         exe_exo_id = $exercise_id AND
@@ -1311,9 +1306,9 @@ class Event
     /**
      * Gets all exercise results (NO Exercises in LPs ) from a given exercise id, course, session.
      *
-     * @param   int     exercise id
+     * @param int $exercise_id
      * @param int $courseId
-     * @param   int     session id
+     * @param int $session_id
      *
      * @return array with the results
      */
@@ -1365,8 +1360,8 @@ class Event
     /**
      * Gets all exercise results (NO Exercises in LPs ) from a given exercise id, course, session.
      *
-     * @param int $courseId
-     * @param   int     session id
+     * @param int  $courseId
+     * @param int  $session_id
      * @param bool $get_count
      *
      * @return array with the results
@@ -1409,9 +1404,9 @@ class Event
     /**
      * Gets all exercise results (NO Exercises in LPs) from a given exercise id, course, session.
      *
-     * @param   int     exercise id
+     * @param int $user_id
      * @param int $courseId
-     * @param   int     session id
+     * @param int $session_id
      *
      * @return array with the results
      */
@@ -1454,7 +1449,7 @@ class Event
     /**
      * Gets exercise results (NO Exercises in LPs) from a given exercise id, course, session.
      *
-     * @param int    $exe_id exercise id
+     * @param int    $exe_id attempt id
      * @param string $status
      *
      * @return array with the results
@@ -1574,9 +1569,9 @@ class Event
      * Count exercise attempts (NO Exercises in LPs ) from a given exercise id, course, session.
      *
      * @param int $user_id
-     * @param   int     exercise id
+     * @param int $exercise_id
      * @param int $courseId
-     * @param   int     session id
+     * @param int $session_id
      *
      * @return array with the results
      */
@@ -1650,7 +1645,7 @@ class Event
             $userId = (int) $userId;
             $sql .= " AND exe_user_id = $userId ";
         }
-        $sql .= " ORDER BY exe_id";
+        $sql .= ' ORDER BY exe_id';
 
         $res = Database::query($sql);
         $list = [];
@@ -1670,8 +1665,6 @@ class Event
         foreach ($list as $student_result) {
             $user_id = $student_result['exe_user_id'];
             $current_best_score[$user_id] = $student_result['exe_result'];
-
-            //echo $current_best_score[$user_id].' - '.$best_score_return[$user_id]['exe_result'].'<br />';
             if (!isset($best_score_return[$user_id]['exe_result'])) {
                 $best_score_return[$user_id] = $student_result;
             }
@@ -1774,42 +1767,6 @@ class Event
         return $row['count'];
     }
 
-    /**
-     * Gets all exercise BEST results attempts (NO Exercises in LPs)
-     * from a given exercise id, course, session per user.
-     *
-     * @param   int     exercise id
-     * @param   int   course id
-     * @param   int     session id
-     *
-     * @return array with the results
-     */
-    public static function get_count_exercises_attempted_by_course(
-        $courseId,
-        $session_id = 0
-    ) {
-        $table_track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
-        $courseId = (int) $courseId;
-        $session_id = (int) $session_id;
-
-        $sql = "SELECT DISTINCT exe_exo_id, exe_user_id
-                FROM $table_track_exercises
-                WHERE
-                    status = '' AND
-                    c_id = $courseId AND
-                    session_id = $session_id AND
-                    orig_lp_id = 0 AND
-                    orig_lp_item_id = 0
-                ORDER BY exe_id";
-        $res = Database::query($sql);
-        $count = 0;
-        if (Database::num_rows($res) > 0) {
-            $count = Database::num_rows($res);
-        }
-
-        return $count;
-    }
-
     /**
      * Gets all exercise events from a Learning Path within a Course    nd Session.
      *
@@ -1876,12 +1833,12 @@ class Event
                 ORDER BY parent_item_id, display_order";
         $res = Database::query($sql);
 
-        $my_exercise_list = [];
+        $list = [];
         while ($row = Database::fetch_array($res, 'ASSOC')) {
-            $my_exercise_list[] = $row;
+            $list[] = $row;
         }
 
-        return $my_exercise_list;
+        return $list;
     }
 
     /**
@@ -2218,7 +2175,6 @@ class Event
      * @param int    $sessionId   The session in which to add the time (if any)
      * @param string $virtualTime The amount of time to be added,
      *                            in a hh:mm:ss format. If int, we consider it is expressed in hours.
-     * @param string $ip          IP address to go on record for this time record
      *
      * @return true on successful insertion, false otherwise
      */
@@ -2226,80 +2182,32 @@ class Event
         $courseId,
         $userId,
         $sessionId,
-        $virtualTime = '',
-        $ip = ''
+        $virtualTime = ''
     ) {
-        $courseTrackingTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
-        $time = $loginDate = $logoutDate = api_get_utc_datetime();
-
         $courseId = (int) $courseId;
         $userId = (int) $userId;
         $sessionId = (int) $sessionId;
-        $ip = Database::escape_string($ip);
-
-        // Get the current latest course connection register. We need that
-        // record to re-use the data and create a new record.
-        $sql = "SELECT *
-                FROM $courseTrackingTable
-                WHERE
-                    user_id = $userId AND
-                    c_id = $courseId  AND
-                    session_id  = $sessionId AND
-                    login_course_date > '$time' - INTERVAL 3600 SECOND
-                ORDER BY login_course_date DESC 
-                LIMIT 0,1";
-        $result = Database::query($sql);
-
-        // Ignore if we didn't find any course connection record in the last
-        // hour. In this case it wouldn't be right to add a "fake" time record.
-        if (Database::num_rows($result) > 0) {
-            // Found the latest connection
-            $row = Database::fetch_array($result);
-            $courseAccessId = $row['course_access_id'];
-            $courseAccessLoginDate = $row['login_course_date'];
-            $counter = $row['counter'];
-            $counter = $counter ? $counter : 0;
-            // Insert a new record, copy of the current one (except the logout
-            // date that we update to the current time)
-            $sql = "INSERT INTO $courseTrackingTable(
-                    c_id,
-                    user_ip, 
-                    user_id, 
-                    login_course_date, 
-                    logout_course_date, 
-                    counter, 
-                    session_id
-                ) VALUES(
-                    $courseId, 
-                    '$ip', 
-                    $userId, 
-                    '$courseAccessLoginDate', 
-                    '$logoutDate', 
-                    $counter, 
-                    $sessionId
-                )";
-            Database::query($sql);
 
-            $loginDate = ChamiloApi::addOrSubTimeToDateTime(
-                $virtualTime,
-                $courseAccessLoginDate,
-                false
-            );
-            // We update the course tracking table
-            $sql = "UPDATE $courseTrackingTable  
-                    SET 
-                        login_course_date = '$loginDate',
-                        logout_course_date = '$courseAccessLoginDate',
-                        counter = 0
-                    WHERE 
-                        course_access_id = ".intval($courseAccessId)." AND 
-                        session_id = ".$sessionId;
-            Database::query($sql);
+        $logoutDate = api_get_utc_datetime();
+        $loginDate = ChamiloApi::addOrSubTimeToDateTime(
+            $virtualTime,
+            $logoutDate,
+            false
+        );
 
-            return true;
-        }
+        $params = [
+            'login_course_date' => $loginDate,
+            'logout_course_date' => $logoutDate,
+            'session_id' => $sessionId,
+            'user_id' => $userId,
+            'counter' => 0,
+            'c_id' => $courseId,
+            'user_ip' => api_get_real_ip(),
+        ];
+        $courseTrackingTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
+        Database::insert($courseTrackingTable, $params);
 
-        return false;
+        return true;
     }
 
     /**
@@ -2331,8 +2239,6 @@ class Event
             return false;
         }
         $courseTrackingTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
-        $time = $loginDate = $logoutDate = api_get_utc_datetime();
-
         $courseId = (int) $courseId;
         $userId = (int) $userId;
         $sessionId = (int) $sessionId;
@@ -2390,39 +2296,6 @@ class Event
         EventsMail::send_mail($event_name, $params);
     }
 
-    /**
-     * Internal function checking if the mail was already sent from that user to that user.
-     *
-     * @param string $event_name
-     * @param int    $user_from
-     * @param int    $user_to
-     *
-     * @return bool
-     */
-    public static function check_if_mail_already_sent(
-        $event_name,
-        $user_from,
-        $user_to = null
-    ) {
-        if ($user_to == null) {
-            $sql = 'SELECT COUNT(*) as total 
-                    FROM '.Database::get_main_table(TABLE_EVENT_SENT).'
-                    WHERE 
-                        user_from = '.$user_from.' AND 
-                        event_type_name = "'.$event_name.'"';
-        } else {
-            $sql = 'SELECT COUNT(*) as total 
-                    FROM '.Database::get_main_table(TABLE_EVENT_SENT).'
-                    WHERE 
-                        user_from = '.$user_from.' AND 
-                        user_to = '.$user_to.' AND 
-                        event_type_name = "'.$event_name.'"';
-        }
-        $result = Database::store_result(Database::query($sql), 'ASSOC');
-
-        return $result[0]["total"];
-    }
-
     /**
      * Filter EventEmailTemplate Filters see the main/inc/conf/events.conf.dist.php.
      */
@@ -2483,7 +2356,7 @@ class Event
      */
     public static function registerLog($logInfo)
     {
-        if (!Tracking::minimunTimeAvailable(api_get_session_id(), api_get_course_int_id())) {
+        if (!Tracking::minimumTimeAvailable(api_get_session_id(), api_get_course_int_id())) {
             return false;
         }
 

+ 383 - 208
main/inc/lib/exercise.lib.php

@@ -12,6 +12,7 @@ use ChamiloSession as Session;
  * @author Olivier Brouckaert <oli.brouckaert@skynet.be> 2003-2004
  * @author Hubert Borderiou 2011-10-21
  * @author ivantcholakov2009-07-20
+ * @author Julio Montoya
  */
 class ExerciseLib
 {
@@ -46,17 +47,22 @@ class ExerciseLib
         $show_answers = false,
         $show_icon = false
     ) {
-        $course_id = empty($exercise->course_id) ? api_get_course_int_id() : $exercise->course_id;
-        $course = api_get_course_info_by_id($course_id);
+        $course_id = $exercise->course_id;
+
+        if (empty($course_id)) {
+            return '';
+        }
+        $course = $exercise->course;
+
         // Change false to true in the following line to enable answer hinting
         $debug_mark_answer = $show_answers;
         // Reads question information
-        if (!$objQuestionTmp = Question::read($questionId, $course_id)) {
+        if (!$objQuestionTmp = Question::read($questionId, $course)) {
             // Question not found
             return false;
         }
 
-        if ($exercise->feedback_type != EXERCISE_FEEDBACK_TYPE_END) {
+        if ($exercise->getFeedbackType() != EXERCISE_FEEDBACK_TYPE_END) {
             $show_comment = false;
         }
 
@@ -113,111 +119,119 @@ class ExerciseLib
             // suggestions here, for the sake of comprehensions, while the ones
             // on the right side are called answers
             $num_suggestions = 0;
-            if (in_array($answerType, [MATCHING, DRAGGABLE, MATCHING_DRAGGABLE])) {
-                if ($answerType == DRAGGABLE) {
-                    $isVertical = $objQuestionTmp->extra == 'v';
-                    $s .= '
-                        <div class="col-md-12 ui-widget ui-helper-clearfix">
-                            <div class="clearfix">
-                            <ul class="exercise-draggable-answer '.($isVertical ? '' : 'list-inline').'"
-                                id="question-'.$questionId.'" data-question="'.$questionId.'">
-                    ';
-                } else {
-                    $s .= '<div id="drag'.$questionId.'_question" class="drag_question">
-                           <table class="data_table">';
-                }
 
-                // Iterate through answers
-                $x = 1;
-                //mark letters for each answer
-                $letter = 'A';
-                $answer_matching = [];
-                $cpt1 = [];
-                for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
-                    $answerCorrect = $objAnswerTmp->isCorrect($answerId);
-                    $numAnswer = $objAnswerTmp->selectAutoId($answerId);
-                    if ($answerCorrect == 0) {
-                        // options (A, B, C, ...) that will be put into the list-box
-                        // have the "correct" field set to 0 because they are answer
-                        $cpt1[$x] = $letter;
-                        $answer_matching[$x] = $objAnswerTmp->selectAnswerByAutoId(
-                            $numAnswer
-                        );
-                        $x++;
-                        $letter++;
+            switch ($answerType) {
+                case MATCHING:
+                case DRAGGABLE:
+                case MATCHING_DRAGGABLE:
+                    if ($answerType == DRAGGABLE) {
+                        $isVertical = $objQuestionTmp->extra == 'v';
+                        $s .= '
+                            <div class="col-md-12 ui-widget ui-helper-clearfix">
+                                <div class="clearfix">
+                                <ul class="exercise-draggable-answer '.($isVertical ? '' : 'list-inline').'"
+                                    id="question-'.$questionId.'" data-question="'.$questionId.'">
+                        ';
+                    } else {
+                        $s .= '<div id="drag'.$questionId.'_question" class="drag_question">
+                               <table class="data_table">';
                     }
-                }
 
-                $i = 1;
-                $select_items[0]['id'] = 0;
-                $select_items[0]['letter'] = '--';
-                $select_items[0]['answer'] = '';
-                foreach ($answer_matching as $id => $value) {
-                    $select_items[$i]['id'] = $value['id_auto'];
-                    $select_items[$i]['letter'] = $cpt1[$id];
-                    $select_items[$i]['answer'] = $value['answer'];
-                    $i++;
-                }
+                    // Iterate through answers
+                    $x = 1;
+                    //mark letters for each answer
+                    $letter = 'A';
+                    $answer_matching = [];
+                    $cpt1 = [];
+                    for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
+                        $answerCorrect = $objAnswerTmp->isCorrect($answerId);
+                        $numAnswer = $objAnswerTmp->selectAutoId($answerId);
+                        if ($answerCorrect == 0) {
+                            // options (A, B, C, ...) that will be put into the list-box
+                            // have the "correct" field set to 0 because they are answer
+                            $cpt1[$x] = $letter;
+                            $answer_matching[$x] = $objAnswerTmp->selectAnswerByAutoId(
+                                $numAnswer
+                            );
+                            $x++;
+                            $letter++;
+                        }
+                    }
 
-                $user_choice_array_position = [];
-                if (!empty($user_choice)) {
-                    foreach ($user_choice as $item) {
-                        $user_choice_array_position[$item['position']] = $item['answer'];
+                    $i = 1;
+                    $select_items[0]['id'] = 0;
+                    $select_items[0]['letter'] = '--';
+                    $select_items[0]['answer'] = '';
+                    foreach ($answer_matching as $id => $value) {
+                        $select_items[$i]['id'] = $value['id_auto'];
+                        $select_items[$i]['letter'] = $cpt1[$id];
+                        $select_items[$i]['answer'] = $value['answer'];
+                        $i++;
                     }
-                }
-                $num_suggestions = ($nbrAnswers - $x) + 1;
-            } elseif ($answerType == FREE_ANSWER) {
-                $fck_content = isset($user_choice[0]) && !empty($user_choice[0]['answer']) ? $user_choice[0]['answer'] : null;
-                $form = new FormValidator('free_choice_'.$questionId);
-                $config = [
-                    'ToolbarSet' => 'TestFreeAnswer',
-                ];
-                $form->addHtmlEditor(
-                    "choice[".$questionId."]",
-                    null,
-                    false,
-                    false,
-                    $config
-                );
-                $form->setDefaults(["choice[".$questionId."]" => $fck_content]);
-                $s .= $form->returnForm();
-            } elseif ($answerType == ORAL_EXPRESSION) {
-                // Add nanog
-                if (api_get_setting('enable_record_audio') === 'true') {
-                    //@todo pass this as a parameter
-                    global $exercise_stat_info, $exerciseId;
-                    if (!empty($exercise_stat_info)) {
-                        $objQuestionTmp->initFile(
-                            api_get_session_id(),
-                            api_get_user_id(),
-                            $exercise_stat_info['exe_exo_id'],
-                            $exercise_stat_info['exe_id']
-                        );
-                    } else {
-                        $objQuestionTmp->initFile(
-                            api_get_session_id(),
-                            api_get_user_id(),
-                            $exerciseId,
-                            'temp_exe'
-                        );
+
+                    $user_choice_array_position = [];
+                    if (!empty($user_choice)) {
+                        foreach ($user_choice as $item) {
+                            $user_choice_array_position[$item['position']] = $item['answer'];
+                        }
                     }
 
-                    echo $objQuestionTmp->returnRecorder();
-                }
+                    $num_suggestions = ($nbrAnswers - $x) + 1;
+                    break;
+                case FREE_ANSWER:
+                    $fck_content = isset($user_choice[0]) && !empty($user_choice[0]['answer']) ? $user_choice[0]['answer'] : null;
+                    $form = new FormValidator('free_choice_'.$questionId);
+                    $config = [
+                        'ToolbarSet' => 'TestFreeAnswer',
+                    ];
+                    $form->addHtmlEditor(
+                        "choice[".$questionId."]",
+                        null,
+                        false,
+                        false,
+                        $config
+                    );
+                    $form->setDefaults(["choice[".$questionId."]" => $fck_content]);
+                    $s .= $form->returnForm();
+                    break;
+                case ORAL_EXPRESSION:
+                    // Add nanog
+                    if (api_get_setting('enable_record_audio') === 'true') {
+                        //@todo pass this as a parameter
+                        global $exercise_stat_info, $exerciseId;
+                        if (!empty($exercise_stat_info)) {
+                            $objQuestionTmp->initFile(
+                                api_get_session_id(),
+                                api_get_user_id(),
+                                $exercise_stat_info['exe_exo_id'],
+                                $exercise_stat_info['exe_id']
+                            );
+                        } else {
+                            $objQuestionTmp->initFile(
+                                api_get_session_id(),
+                                api_get_user_id(),
+                                $exerciseId,
+                                'temp_exe'
+                            );
+                        }
+
+                        echo $objQuestionTmp->returnRecorder();
+                    }
 
-                $form = new FormValidator('free_choice_'.$questionId);
-                $config = ['ToolbarSet' => 'TestFreeAnswer'];
+                    $form = new FormValidator('free_choice_'.$questionId);
+                    $config = ['ToolbarSet' => 'TestFreeAnswer'];
 
-                $form->addHtml('<div id="'.'hide_description_'.$questionId.'_options" style="display: none;">');
-                $form->addHtmlEditor(
-                    "choice[$questionId]",
-                    null,
-                    false,
-                    false,
-                    $config
-                );
-                $form->addHtml('</div>');
-                $s .= $form->returnForm();
+                    $form->addHtml('<div id="'.'hide_description_'.$questionId.'_options" style="display: none;">');
+                    $form->addHtmlEditor(
+                        "choice[$questionId]",
+                        null,
+                        false,
+                        false,
+                        $config
+                    );
+                    $form->addHtml('</div>');
+                    $s .= $form->returnForm();
+                    break;
             }
 
             // Now navigate through the possible answers, using the max number of
@@ -295,7 +309,7 @@ class ExerciseLib
                         RadioValidator(question_id, answer_id);
                     }
                     
-                    $(document).ready(function() {
+                    $(function() {
                         var ShowAlert = '';
                         var typeRadioB = '';
                         var question_id = $('input[name=question_id]').val();
@@ -423,7 +437,7 @@ class ExerciseLib
                     ]
                 )) {
                     $header = Display::tag('th', get_lang('Options'));
-                    if ($exercise->feedback_type == EXERCISE_FEEDBACK_TYPE_END) {
+                    if ($exercise->getFeedbackType() == EXERCISE_FEEDBACK_TYPE_END) {
                         $header .= Display::tag('th', get_lang('Feedback'));
                     }
                     $s .= '<table class="table table-hover table-striped">';
@@ -906,9 +920,10 @@ class ExerciseLib
                          */
                         if ($origin !== null) {
                             global $exe_id;
+                            $exe_id = (int) $exe_id;
                             $trackAttempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
-                            $sql = 'SELECT answer FROM '.$trackAttempts.'
-                                    WHERE exe_id='.$exe_id.' AND question_id='.$questionId;
+                            $sql = "SELECT answer FROM $trackAttempts
+                                    WHERE exe_id = $exe_id AND question_id= $questionId";
                             $rsLastAttempt = Database::query($sql);
                             $rowLastAttempt = Database::fetch_array($rsLastAttempt);
                             $answer = $rowLastAttempt['answer'];
@@ -993,7 +1008,7 @@ class ExerciseLib
                             $answer = '';
                             $i = 0;
                             foreach ($studentAnswerList as $studentItem) {
-                                // remove surronding brackets
+                                // Remove surronding brackets
                                 $studentResponse = api_substr(
                                     $studentItem,
                                     1,
@@ -1076,7 +1091,7 @@ class ExerciseLib
                                     $selected = 'selected="selected"';
                                 }
                                 $s .= '<option value="'.$val['id'].'" '.$selected.'>'.$val['letter'].'</option>';
-                            }  // end foreach()
+                            }
 
                             $s .= '</select></div></td><td width="5%" class="separate">&nbsp;</td>';
                             $s .= '<td width="40%" valign="top" >';
@@ -1093,7 +1108,7 @@ class ExerciseLib
                             $s .= '</td>';
                             $s .= '</tr>';
                             $lines_count++;
-                            //if the left side of the "matching" has been completely
+                            // If the left side of the "matching" has been completely
                             // shown but the right side still has values to show...
                             if (($lines_count - 1) == $num_suggestions) {
                                 // if it remains answers to shown at the right side
@@ -1106,8 +1121,8 @@ class ExerciseLib
                                     $s .= "</td>
                                 </tr>";
                                     $lines_count++;
-                                }    // end while()
-                            }  // end if()
+                                }
+                            }
                             $matching_correct_answer++;
                         }
                         break;
@@ -1257,7 +1272,7 @@ HTML;
                                 if (!$freeze) {
                                     $s .= "
                                         <script>
-                                            $(document).on('ready', function() {
+                                            $(function() {
                                                 jsPlumb.ready(function() {
                                                     jsPlumb.connect({
                                                         source: 'window_$windowId',
@@ -1309,7 +1324,7 @@ HTML;
                         }
                         break;
                 }
-            } // end for()
+            }
 
             if ($show_comment) {
                 $s .= '</table>';
@@ -1329,9 +1344,8 @@ HTML;
 
             if ($answerType == DRAGGABLE) {
                 $isVertical = $objQuestionTmp->extra == 'v';
-
                 $s .= "</ul>";
-                $s .= "</div>"; //clearfix
+                $s .= "</div>";
                 $counterAnswer = 1;
                 $s .= $isVertical ? '' : '<div class="row">';
                 for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
@@ -1373,7 +1387,7 @@ HTML;
         } elseif ($answerType == HOT_SPOT || $answerType == HOT_SPOT_DELINEATION) {
             global $exerciseId, $exe_id;
             // Question is a HOT_SPOT
-            //checking document/images visibility
+            // Checking document/images visibility
             if (api_is_platform_admin() || api_is_course_admin()) {
                 $doc_id = $objQuestionTmp->getPictureId();
                 if (is_numeric($doc_id)) {
@@ -1483,7 +1497,7 @@ HOTSPOT;
             $s .= "<div class=\"col-sm-8 col-md-9\">
                    <div class=\"hotspot-image\"></div>
                     <script>
-                        $(document).on('ready', function() {
+                        $(function() {
                             new ".($answerType == HOT_SPOT_DELINEATION ? 'DelineationQuestion' : 'HotspotQuestion')."({
                                 questionId: $questionId,
                                 exerciseId: $exe_id,
@@ -1667,12 +1681,12 @@ HOTSPOT;
                 }
 
                 return true;
-            } else {
-                return false;
             }
-        } else {
-            return true;
+
+            return false;
         }
+
+        return true;
     }
 
     /**
@@ -1777,18 +1791,18 @@ HOTSPOT;
     }
 
     /**
-     * @param string $in_hotpot_path
+     * @param string $path
      *
      * @return int
      */
-    public static function get_count_exam_hotpotatoes_results($in_hotpot_path)
+    public static function get_count_exam_hotpotatoes_results($path)
     {
         return self::get_exam_results_hotpotatoes_data(
             0,
             0,
             '',
             '',
-            $in_hotpot_path,
+            $path,
             true,
             ''
         );
@@ -1823,16 +1837,20 @@ HOTSPOT;
         $in_direction = Database::escape_string($in_direction);
         $in_column = Database::escape_string($in_column);
         $in_number_of_items = intval($in_number_of_items);
-        $in_from = intval($in_from);
+        $in_from = (int) $in_from;
 
         $TBL_TRACK_HOTPOTATOES = Database::get_main_table(
             TABLE_STATISTIC_TRACK_E_HOTPOTATOES
         );
         $TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
 
-        $sql = "SELECT *, thp.id AS thp_id FROM $TBL_TRACK_HOTPOTATOES thp
-            JOIN $TBL_USER u ON thp.exe_user_id = u.user_id
-            WHERE thp.c_id = $courseId AND exe_name LIKE '$in_hotpot_path%'";
+        $sql = "SELECT *, thp.id AS thp_id 
+                FROM $TBL_TRACK_HOTPOTATOES thp
+                JOIN $TBL_USER u 
+                ON thp.exe_user_id = u.user_id
+                WHERE 
+                    thp.c_id = $courseId AND 
+                    exe_name LIKE '$in_hotpot_path%'";
 
         // just count how many answers
         if ($in_get_count) {
@@ -1867,7 +1885,7 @@ HOTSPOT;
                 'lastname' => $data['lastname'],
                 'username' => $data['username'],
                 'group_name' => implode(
-                    "<br/>",
+                    '<br/>',
                     GroupManager::get_user_group_name($data['user_id'])
                 ),
                 'exe_date' => $data['exe_date'],
@@ -1893,11 +1911,10 @@ HOTSPOT;
         $courseId,
         $sessionId
     ) {
-        $table = Database::get_main_table(
-            TABLE_STATISTIC_TRACK_E_HOTPOTATOES
-        );
+        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
         $exercisePath = Database::escape_string($exercisePath);
         $userId = (int) $userId;
+        $courseId = (int) $courseId;
 
         $sql = "SELECT * FROM $table
                 WHERE
@@ -1962,6 +1979,9 @@ HOTSPOT;
         }
 
         $course_id = $courseInfo['real_id'];
+        $sessionId = api_get_session_id();
+        $exercise_id = (int) $exercise_id;
+
         $is_allowedToEdit =
             api_is_allowed_to_edit(null, true) ||
             api_is_allowed_to_edit(true) ||
@@ -1982,7 +2002,7 @@ HOTSPOT;
             $session_id_and = " AND te.session_id = $sessionId ";
             $sessionCondition = " AND ttte.session_id = $sessionId";
         }
-        $exercise_id = (int) $exercise_id;
+
         $exercise_where = '';
         if (!empty($exercise_id)) {
             $exercise_where .= ' AND te.exe_exo_id = '.$exercise_id.'  ';
@@ -2155,9 +2175,9 @@ HOTSPOT;
 
             // sql for hotpotatoes tests for teacher / tutor view
             if ($get_count) {
-                $hpsql_select = "SELECT count(username)";
+                $hpsql_select = ' SELECT count(username) ';
             } else {
-                $hpsql_select = "SELECT
+                $hpsql_select = " SELECT
                     $first_and_last_name ,
                     username,
                     official_code,
@@ -2540,7 +2560,7 @@ HOTSPOT;
                                     $question_list = explode(',', $exercise_stat_info['data_tracking']);
                                     if (!empty($question_list)) {
                                         foreach ($question_list as $questionId) {
-                                            $objQuestionTmp = Question::read($questionId, $course_id);
+                                            $objQuestionTmp = Question::read($questionId, $objExercise->course);
                                             // We're inside *one* question. Go through each possible answer for this question
                                             $result = $objExercise->manage_answer(
                                                 $exeId,
@@ -2756,8 +2776,6 @@ HOTSPOT;
      */
     public static function convertScoreToPlatformSetting($score, $weight)
     {
-        $result = ['score' => $score, 'weight' => $weight];
-
         $maxNote = api_get_setting('exercise_max_score');
         $minNote = api_get_setting('exercise_min_score');
 
@@ -2976,7 +2994,7 @@ HOTSPOT;
      * @return bool
      */
     public static function addScoreModelInput(
-        FormValidator &$form,
+        FormValidator $form,
         $name,
         $weight,
         $selected
@@ -3036,7 +3054,7 @@ HOTSPOT;
             spanTag.addClass(optionClass);
         }
         
-        $(document).ready( function() {
+        $(function() {
             // Loading values
             $('.exercise_mark_select').on('loaded.bs.select', function() {
                 updateSelect($(this));
@@ -3077,7 +3095,6 @@ EOT;
                     ICON_SIZE_MEDIUM
                 );
             } else {
-                //$html .= Display::return_message(get_lang('YouDidNotReachTheMinimumScore'), 'warning');
                 $html = get_lang('YouDidNotReachTheMinimumScore');
                 $icon = Display::return_icon(
                     'warning.png',
@@ -3130,35 +3147,6 @@ EOT;
         return $return;
     }
 
-    /**
-     * Converts a score/weight values to the platform scale.
-     *
-     * @param float $score
-     * @param float $weight
-     *
-     * @deprecated seem not to be used
-     *
-     * @return float the score rounded converted to the new range
-     */
-    public static function convert_score($score, $weight)
-    {
-        $maxNote = api_get_setting('exercise_max_score');
-        $minNote = api_get_setting('exercise_min_score');
-
-        if ($score != '' && $weight != '') {
-            if ($maxNote != '' && $minNote != '') {
-                if (!empty($weight)) {
-                    $score = $minNote + ($maxNote - $minNote) * $score / $weight;
-                } else {
-                    $score = $minNote;
-                }
-            }
-        }
-        $score_rounded = float_format($score, 1);
-
-        return $score_rounded;
-    }
-
     /**
      * Getting all active exercises from a course from a session
      * (if a session_id is provided we will show all the exercises in the course +
@@ -3689,9 +3677,10 @@ EOT;
     /**
      * Get average score by score (NO Exercises in LPs ).
      *
-     * @param    int        exercise id
+     * @param int $exercise_id
      * @param int $courseId
-     * @param    int        session id
+     * @param int $session_id
+     * @param int $user_count
      *
      * @return float Best average score
      */
@@ -3714,9 +3703,7 @@ EOT;
                     $avg_score += $score;
                 }
             }
-            //We asumme that all exe_weighting
-            //$avg_score = show_score( $avg_score / count($user_results) , $result['exe_weighting']);
-            //$avg_score = ($avg_score / count($user_results));
+            // We asumme that all exe_weighting
             if (!empty($user_count)) {
                 $avg_score = float_format($avg_score / $user_count, 1) * 100;
             } else {
@@ -3730,9 +3717,9 @@ EOT;
     /**
      * Get average score by score (NO Exercises in LPs ).
      *
-     * @param    int        exercise id
+     * @param int $exercise_id
      * @param int $courseId
-     * @param    int        session id
+     * @param int $session_id
      *
      * @return float Best average score
      */
@@ -3989,12 +3976,8 @@ EOT;
         $course_code,
         $session_id
     ) {
-        $track_exercises = Database::get_main_table(
-            TABLE_STATISTIC_TRACK_E_EXERCISES
-        );
-        $track_hotspot = Database::get_main_table(
-            TABLE_STATISTIC_TRACK_E_HOTSPOT
-        );
+        $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
+        $track_hotspot = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
         $courseUser = Database::get_main_table(TABLE_MAIN_COURSE_USER);
         $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
 
@@ -4015,7 +3998,7 @@ EOT;
             $courseCondition = "
             INNER JOIN $courseUserSession cu
             ON cu.c_id = c.id AND cu.user_id = exe_user_id";
-            $courseConditionWhere = " AND cu.status = 0 ";
+            $courseConditionWhere = ' AND cu.status = 0 ';
         }
 
         $sql = "SELECT DISTINCT exe_user_id
@@ -4082,7 +4065,7 @@ EOT;
         switch ($question_type) {
             case FILL_IN_BLANKS:
                 $answer_condition = '';
-                $select_condition = " e.exe_id, answer ";
+                $select_condition = ' e.exe_id, answer ';
                 break;
             case MATCHING:
             case MATCHING_DRAGGABLE:
@@ -4401,24 +4384,50 @@ EOT;
         $remainingMessage = ''
     ) {
         $origin = api_get_origin();
+        $courseCode = api_get_course_id();
+        $sessionId = api_get_session_id();
 
         // Getting attempt info
         $exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
 
         // Getting question list
         $question_list = [];
+        $studentInfo = [];
         if (!empty($exercise_stat_info['data_tracking'])) {
+            $studentInfo = api_get_user_info($exercise_stat_info['exe_user_id']);
             $question_list = explode(',', $exercise_stat_info['data_tracking']);
         } else {
             // Try getting the question list only if save result is off
             if ($save_user_result == false) {
                 $question_list = $objExercise->get_validated_question_list();
             }
-            if ($objExercise->selectFeedbackType() == EXERCISE_FEEDBACK_TYPE_DIRECT) {
+            if (in_array(
+                $objExercise->getFeedbackType(),
+                [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
+            )) {
                 $question_list = $objExercise->get_validated_question_list();
             }
         }
 
+        if ($objExercise->getResultAccess()) {
+            if ($objExercise->hasResultsAccess($exercise_stat_info) === false) {
+                echo Display::return_message(
+                    sprintf(get_lang('YouPassedTheLimitOfXMinutesToSeeTheResults'), $objExercise->getResultsAccess())
+                );
+
+                return false;
+            }
+
+            if (!empty($objExercise->getResultAccess())) {
+                $url = api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id;
+                echo $objExercise->returnTimeLeftDiv();
+                echo $objExercise->showSimpleTimeControl(
+                    $objExercise->getResultAccessTimeDiff($exercise_stat_info),
+                    $url
+                );
+            }
+        }
+
         $counter = 1;
         $total_score = $total_weight = 0;
         $exercise_content = null;
@@ -4426,11 +4435,14 @@ EOT;
         // Hide results
         $show_results = false;
         $show_only_score = false;
-        if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS) {
-            $show_results = true;
-        }
 
-        if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER) {
+        if (in_array($objExercise->results_disabled,
+            [
+                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
+                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
+                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
+            ]
+        )) {
             $show_results = true;
         }
 
@@ -4449,7 +4461,7 @@ EOT;
         // Not display expected answer, but score, and feedback
         $show_all_but_expected_answer = false;
         if ($objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ONLY &&
-            $objExercise->feedback_type == EXERCISE_FEEDBACK_TYPE_END
+            $objExercise->getFeedbackType() == EXERCISE_FEEDBACK_TYPE_END
         ) {
             $show_all_but_expected_answer = true;
             $show_results = true;
@@ -4497,7 +4509,9 @@ EOT;
                 }
             }
 
-            if ($objExercise->results_disabled == RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK) {
+            if ($objExercise->results_disabled ==
+                RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK
+            ) {
                 $show_only_score = false;
                 $show_results = true;
                 $show_all_but_expected_answer = false;
@@ -4510,13 +4524,12 @@ EOT;
             }
         }
 
-        if ($show_results || $show_only_score) {
+        if (($show_results || $show_only_score) && $origin !== 'embeddable') {
             if (isset($exercise_stat_info['exe_user_id'])) {
-                $user_info = api_get_user_info($exercise_stat_info['exe_user_id']);
-                if ($user_info) {
+                if (!empty($studentInfo)) {
                     // Shows exercise header
                     echo $objExercise->showExerciseResultHeader(
-                        $user_info,
+                        $studentInfo,
                         $exercise_stat_info
                     );
                 }
@@ -4524,9 +4537,9 @@ EOT;
         }
 
         // Display text when test is finished #4074 and for LP #4227
-        $end_of_message = $objExercise->selectTextWhenFinished();
-        if (!empty($end_of_message)) {
-            echo Display::return_message($end_of_message, 'normal', false);
+        $endOfMessage = $objExercise->getTextWhenFinished();
+        if (!empty($endOfMessage)) {
+            echo Display::return_message($endOfMessage, 'normal', false);
             echo "<div class='clear'>&nbsp;</div>";
         }
 
@@ -4539,7 +4552,10 @@ EOT;
         $exerciseResultCoordinates = null;
         $delineationResults = null;
 
-        if ($objExercise->selectFeedbackType() == EXERCISE_FEEDBACK_TYPE_DIRECT) {
+        if (in_array(
+            $objExercise->getFeedbackType(),
+            [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP]
+        )) {
             $loadChoiceFromSession = true;
             $fromDatabase = false;
             $exerciseResult = Session::read('exerciseResult');
@@ -4554,7 +4570,7 @@ EOT;
         if (!empty($question_list)) {
             foreach ($question_list as $questionId) {
                 // Creates a temporary Question object
-                $objQuestionTmp = Question::read($questionId);
+                $objQuestionTmp = Question::read($questionId, $objExercise->course);
                 // This variable came from exercise_submit_modal.php
                 ob_start();
                 $choice = null;
@@ -4674,6 +4690,7 @@ EOT;
                         'score' => $my_total_score,
                         'weight' => $my_total_weight,
                         'comments' => $comnt,
+                        'user_answered' => $result['user_answered'],
                     ];
                 }
 
@@ -4727,6 +4744,7 @@ EOT;
         }
 
         $totalScoreText = null;
+        $certificateBlock = '';
         if (($show_results || $show_only_score) && $showTotalScore) {
             if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
                 echo '<h1 style="text-align : center; margin : 20px 0;">'.get_lang('YourResults').'</h1><br />';
@@ -4740,6 +4758,17 @@ EOT;
                     true
                 );
             } else {
+                $pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
+
+                if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
+                    $formula = $pluginEvaluation->getFormulaForExercise($objExercise->selectId());
+
+                    if (!empty($formula)) {
+                        $total_score = $pluginEvaluation->getResultWithFormula($exeId, $formula);
+                        $total_weight = $pluginEvaluation->getMaxScore();
+                    }
+                }
+
                 $totalScoreText .= self::getTotalScoreRibbon(
                     $objExercise,
                     $total_score,
@@ -4749,6 +4778,17 @@ EOT;
                 );
             }
             $totalScoreText .= '</div>';
+
+            if (!empty($studentInfo)) {
+                $certificateBlock = self::generateAndShowCertificateBlock(
+                    $total_score,
+                    $total_weight,
+                    $objExercise,
+                    $studentInfo['id'],
+                    $courseCode,
+                    $sessionId
+                );
+            }
         }
 
         if ($result['answer_type'] == MULTIPLE_ANSWER_TRUE_FALSE_DEGREE_CERTAINTY) {
@@ -4783,6 +4823,7 @@ EOT;
         );
 
         echo $totalScoreText;
+        echo $certificateBlock;
 
         // Ofaj change BT#11784
         if (api_get_configuration_value('quiz_show_description_on_results_page') &&
@@ -4817,6 +4858,15 @@ EOT;
                         $exercise_stat_info['exe_duration'],
                         $question_list
                     );
+
+                    $allowStats = api_get_configuration_value('allow_gradebook_stats');
+                    if ($allowStats) {
+                        $objExercise->generateStats(
+                            $objExercise->selectId(),
+                            api_get_course_info(),
+                            api_get_session_id()
+                        );
+                    }
                 }
             }
 
@@ -4835,7 +4885,10 @@ EOT;
             }
         }
 
-        if (RESULT_DISABLE_RANKING == $objExercise->selectResultsDisabled()) {
+        if (in_array(
+            $objExercise->selectResultsDisabled(),
+            [RESULT_DISABLE_RANKING, RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING]
+        )) {
             echo Display::page_header(get_lang('Ranking'), null, 'h4');
             echo self::displayResultsInRanking(
                 $objExercise->iId,
@@ -4871,6 +4924,9 @@ EOT;
         $table->setHeaderContents(0, 3, get_lang('Date'), ['class' => 'text-center']);
 
         foreach ($data as $r => $item) {
+            if (!isset($item[1])) {
+                continue;
+            }
             $selected = $item[1]->getId() == $currentUserId;
 
             foreach ($item as $c => $value) {
@@ -4987,7 +5043,7 @@ EOT;
                 $score, $weight, $objExercise->selectPassPercentage()
             );
             // Color the final test score if pass_percentage activated
-            $ribbonTotalSuccessOrError = "";
+            $ribbonTotalSuccessOrError = '';
             if (self::isPassPercentageEnabled($objExercise->selectPassPercentage())) {
                 if ($isSuccess) {
                     $ribbonTotalSuccessOrError = ' ribbon-total-success';
@@ -5001,9 +5057,10 @@ EOT;
         }
 
         if ($displayChartDegree) {
-            $ribbon .= '<h3>'.get_lang('YourTotalScore').":&nbsp;";
+            $ribbon .= '<h3>'.get_lang('YourTotalScore').':&nbsp;';
             $ribbon .= self::show_score($score, $weight, false, true);
             $ribbon .= '</h3>';
+
             $ribbon .= '</div>';
         }
 
@@ -5030,12 +5087,17 @@ EOT;
      * @return string
      */
     public static function getTotalScoreRibbon(
-        $objExercise,
+        Exercise $objExercise,
         $score,
         $weight,
         $checkPassPercentage = false,
         $countPendingQuestions = 0
     ) {
+        $hide = (int) $objExercise->getPageConfigurationAttribute('hide_total_score');
+        if ($hide === 1) {
+            return '';
+        }
+
         $passPercentage = $objExercise->selectPassPercentage();
         $ribbon = '<div class="title-score">';
         if ($checkPassPercentage) {
@@ -5057,7 +5119,7 @@ EOT;
         } else {
             $ribbon .= '<div class="total">';
         }
-        $ribbon .= '<h3>'.get_lang('YourTotalScore').":&nbsp;";
+        $ribbon .= '<h3>'.get_lang('YourTotalScore').':&nbsp;';
         $ribbon .= self::show_score($score, $weight, false, true);
         $ribbon .= '</h3>';
         $ribbon .= '</div>';
@@ -5278,12 +5340,11 @@ EOT;
                 SELECT COUNT(ea) FROM ChamiloCoreBundle:TrackEAttempt ea
                 WHERE ea.userId = :user AND ea.cId = :course AND ea.sessionId = :session
                     AND ea.tms > :time
-                GROUP BY ea.questionId
             ')
             ->setParameters(['user' => $userId, 'course' => $courseId, 'session' => $sessionId, 'time' => $time])
-            ->getResult();
+            ->getSingleScalarResult();
 
-        return count($result);
+        return $result;
     }
 
     /**
@@ -5306,7 +5367,7 @@ EOT;
 
         $midnightTime = ChamiloApi::getServerMidnightTime();
 
-        $answeredQuestionsCount = ExerciseLib::countAnsweredQuestionsByUserAfterTime(
+        $answeredQuestionsCount = self::countAnsweredQuestionsByUserAfterTime(
             $midnightTime,
             $userId,
             $courseId,
@@ -5315,4 +5376,118 @@ EOT;
 
         return ($answeredQuestionsCount + $numberOfQuestions) > $questionsLimitPerDay;
     }
+
+    /**
+     * Check if an exercise complies with the requirements to be embedded in the mobile app or a video.
+     * By making sure it is set on one question per page and it only contains unique-answer or multiple-answer questions
+     * or unique-answer image. And that the exam does not have immediate feedback.
+     *
+     * @param array $exercise Exercise info
+     *
+     * @throws \Doctrine\ORM\Query\QueryException
+     *
+     * @return bool
+     */
+    public static function isQuizEmbeddable(array $exercise)
+    {
+        $em = Database::getManager();
+
+        if (ONE_PER_PAGE != $exercise['type'] ||
+            in_array($exercise['feedback_type'], [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])
+        ) {
+            return false;
+        }
+
+        $countAll = $em
+            ->createQuery('SELECT COUNT(qq)
+                FROM ChamiloCourseBundle:CQuizQuestion qq
+                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
+                   WITH qq.iid = qrq.questionId
+                WHERE qrq.exerciceId = :id'
+            )
+            ->setParameter('id', $exercise['iid'])
+            ->getSingleScalarResult();
+
+        $countOfAllowed = $em
+            ->createQuery('SELECT COUNT(qq)
+                FROM ChamiloCourseBundle:CQuizQuestion qq
+                INNER JOIN ChamiloCourseBundle:CQuizRelQuestion qrq
+                   WITH qq.iid = qrq.questionId
+                WHERE qrq.exerciceId = :id AND qq.type IN (:types)'
+            )
+            ->setParameters(
+                [
+                    'id' => $exercise['iid'],
+                    'types' => [UNIQUE_ANSWER, MULTIPLE_ANSWER, UNIQUE_ANSWER_IMAGE],
+                ]
+            )
+            ->getSingleScalarResult();
+
+        return $countAll === $countOfAllowed;
+    }
+
+    /**
+     * Generate a certificate linked to current quiz and.
+     * Return the HTML block with links to download and view the certificate.
+     *
+     * @param float    $totalScore
+     * @param float    $totalWeight
+     * @param Exercise $objExercise
+     * @param int      $studentId
+     * @param string   $courseCode
+     * @param int      $sessionId
+     *
+     * @return string
+     */
+    public static function generateAndShowCertificateBlock(
+        $totalScore,
+        $totalWeight,
+        Exercise $objExercise,
+        $studentId,
+        $courseCode,
+        $sessionId = 0
+    ) {
+        if (!api_get_configuration_value('quiz_generate_certificate_ending') ||
+            !self::isSuccessExerciseResult($totalScore, $totalWeight, $objExercise->selectPassPercentage())
+        ) {
+            return '';
+        }
+
+        /** @var Category $category */
+        $category = Category::load(null, null, $courseCode, null, null, $sessionId, 'ORDER By id');
+
+        if (empty($category)) {
+            return '';
+        }
+
+        /** @var Category $category */
+        $category = $category[0];
+        $categoryId = $category->get_id();
+        $link = LinkFactory::load(
+            null,
+            null,
+            $objExercise->selectId(),
+            null,
+            $courseCode,
+            $categoryId
+        );
+
+        if (empty($link)) {
+            return '';
+        }
+
+        $resourceDeletedMessage = $category->show_message_resource_delete($courseCode);
+
+        if (false !== $resourceDeletedMessage || api_is_allowed_to_edit() || api_is_excluded_user_type()) {
+            return '';
+        }
+
+        $certificate = Category::generateUserCertificate($categoryId, $studentId);
+
+        if (!is_array($certificate)) {
+            return '';
+        }
+
+        return Category::getDownloadCertificateBlock($certificate);
+    }
 }

+ 193 - 114
main/inc/lib/exercise_show_functions.lib.php

@@ -22,15 +22,17 @@ class ExerciseShowFunctions
     /**
      * Shows the answer to a fill-in-the-blanks question, as HTML.
      *
-     * @param int    $feedbackType
-     * @param string $answer
-     * @param int    $id                           Exercise ID
-     * @param int    $questionId                   Question ID
-     * @param int    $resultsDisabled
-     * @param string $originalStudentAnswer
-     * @param bool   $showTotalScoreAndUserChoices
+     * @param Exercise $exercise
+     * @param int      $feedbackType
+     * @param string   $answer
+     * @param int      $id                           Exercise ID
+     * @param int      $questionId                   Question ID
+     * @param int      $resultsDisabled
+     * @param string   $originalStudentAnswer
+     * @param bool     $showTotalScoreAndUserChoices
      */
     public static function display_fill_in_blanks_answer(
+        $exercise,
         $feedbackType,
         $answer,
         $id,
@@ -45,10 +47,7 @@ class ExerciseShowFunctions
             $resultsDisabled,
             $showTotalScoreAndUserChoices
         );
-        // ofaj
-        /*if (strpos($originalStudentAnswer, 'font color') !== false) {
-            $answerHTML = $originalStudentAnswer;
-        }*/
+
         if (empty($id)) {
             echo '<tr><td>';
             echo Security::remove_XSS($answerHTML, COURSEMANAGERLOWSECURITY);
@@ -85,7 +84,10 @@ class ExerciseShowFunctions
             if (empty($id)) {
                 echo '<tr><td>'.Security::remove_XSS($answer).'</td>';
                 echo '<td>'.Security::remove_XSS($choice).'</td>';
-                echo '<td>'.Security::remove_XSS($expectedChoice).'</td>';
+                if ($exercise->showExpectedChoiceColumn()) {
+                    echo '<td>'.Security::remove_XSS($expectedChoice).'</td>';
+                }
+
                 echo '<td>'.Security::remove_XSS($status).'</td>';
                 echo '</tr>';
             } else {
@@ -93,9 +95,13 @@ class ExerciseShowFunctions
                 echo Security::remove_XSS($answer);
                 echo '</td><td>';
                 echo Security::remove_XSS($choice);
-                echo '</td><td>';
-                echo Security::remove_XSS($expectedChoice);
-                echo '</td><td>';
+                echo '</td>';
+                if ($exercise->showExpectedChoiceColumn()) {
+                    echo '<td>';
+                    echo Security::remove_XSS($expectedChoice);
+                    echo '</td>';
+                }
+                echo '<td>';
                 echo Security::remove_XSS($status);
                 echo '</td>';
                 echo '</tr>';
@@ -138,7 +144,7 @@ class ExerciseShowFunctions
             if ($questionScore > 0 || !empty($comments)) {
             } else {
                 echo '<tr>';
-                echo Display::tag('td', ExerciseLib::getNotCorrectedYetText(), []);
+                echo Display::tag('td', ExerciseLib::getNotCorrectedYetText());
                 echo '</tr>';
             }
         }
@@ -263,7 +269,10 @@ class ExerciseShowFunctions
             if ($studentChoice) {
                 $status = Display::label(get_lang('Correct'), 'success');
             } else {
-                if ($resultsDisabled == RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER) {
+                if (in_array($resultsDisabled, [
+                    RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
+                ])
+                ) {
                     return '';
                 }
             }
@@ -330,15 +339,25 @@ class ExerciseShowFunctions
 
         $studentChoiceInt = (int) $studentChoice;
         $answerCorrectChoice = (int) $answerCorrect;
-
+        $hideStudentChoice = false;
         $hide_expected_answer = false;
+
+        $status = '';
+        if ($exercise->showExpectedChoice()) {
+            $status = Display::label(get_lang('Incorrect'), 'danger');
+            if ($answerCorrect || ($answerCorrect && $studentChoiceInt === $answerCorrectChoice)) {
+                $status = Display::label(get_lang('Correct'), 'success');
+            }
+        }
+
+        $showComment = false;
         switch ($resultsDisabled) {
             case RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER:
-                if ($studentChoiceInt !== $answerCorrectChoice) {
-                    return '';
-                }
-
-                if (!$answerCorrect) {
+                $hideStudentChoice = false;
+                $hide_expected_answer = true;
+                $status = Display::label(get_lang('Correct'), 'success');
+                $showComment = true;
+                if (!$answerCorrect && empty($studentChoice)) {
                     return '';
                 }
                 break;
@@ -363,16 +382,36 @@ class ExerciseShowFunctions
         $iconAnswer .= $answerCorrect ? '_on' : '_off';
         $iconAnswer .= '.png';
 
-        echo '<tr>';
-        echo '<td width="5%">';
-        echo Display::return_icon($icon, null, null, ICON_SIZE_TINY);
-        echo '</td><td width="5%">';
+        $studentChoiceClass = '';
+        if (in_array(
+            $resultsDisabled,
+            [
+                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
+                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
+            ]
+        )
+        ) {
+            if ($answerCorrect) {
+                $studentChoiceClass = 'success';
+            }
+        }
+
+        echo '<tr class="'.$studentChoiceClass.'">';
+        if ($hideStudentChoice === false) {
+            echo '<td width="5%">';
+            echo Display::return_icon($icon, null, null, ICON_SIZE_TINY);
+            echo '</td>';
+        }
+
         if (!$hide_expected_answer) {
-            echo Display::return_icon($iconAnswer, null, null, ICON_SIZE_TINY);
-        } else {
-            echo '-';
+            if ($exercise->showExpectedChoiceColumn()) {
+                echo '<td width="5%">';
+                echo Display::return_icon($iconAnswer, null, null, ICON_SIZE_TINY);
+                echo '</td>';
+            }
         }
-        echo '</td><td width="40%">';
+
+        echo '<td width="40%">';
         echo $answer;
         echo '</td>';
 
@@ -387,29 +426,27 @@ class ExerciseShowFunctions
         }
 
         if ($feedbackType != EXERCISE_FEEDBACK_TYPE_EXAM) {
-            echo '<td width="20%">';
-            if ($studentChoice) {
-                $color = 'black';
-                if ($answerCorrect) {
-                    $color = 'green';
-                }
-                if ($hide_expected_answer) {
-                    $color = '';
-                }
-
-                $comment = '<span style="font-weight: bold; color: '.$color.';">'.
-                    Security::remove_XSS($answerComment).
-                    '</span>';
+            $showComment = true;
+        }
 
-                if (!$answerCorrect && $resultsDisabled == RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER) {
-                    $comment = '';
-                }
-                echo $comment;
+        if ($showComment) {
+            echo '<td width="20%">';
+            $color = 'black';
+            if ($answerCorrect) {
+                $color = 'green';
+            }
+            if ($hide_expected_answer) {
+                $color = '';
             }
+            $comment = '<span style="font-weight: bold; color: '.$color.';">'.
+                Security::remove_XSS($answerComment).
+                '</span>';
+            echo $comment;
             echo '</td>';
         } else {
             echo '<td>&nbsp;</td>';
         }
+
         echo '</tr>';
     }
 
@@ -441,7 +478,13 @@ class ExerciseShowFunctions
         $showTotalScoreAndUserChoices
     ) {
         $hide_expected_answer = false;
+        $hideStudentChoice = false;
         switch ($resultsDisabled) {
+            //case RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING:
+            case RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER:
+                $hideStudentChoice = false;
+                $hide_expected_answer = true;
+                break;
             case RESULT_DISABLE_SHOW_SCORE_ONLY:
                 if ($feedbackType == 0) {
                     $hide_expected_answer = true;
@@ -456,27 +499,34 @@ class ExerciseShowFunctions
                 break;
         }
 
-        $content = '<tr><td width="5%">';
-        $course_id = api_get_course_int_id();
-        $new_options = Question::readQuestionOption($questionId, $course_id);
-        // Your choice
-        if (isset($new_options[$studentChoice])) {
-            $content .= get_lang($new_options[$studentChoice]['name']);
-        } else {
-            $content .= '-';
+        $content = '<tr>';
+        if ($hideStudentChoice === false) {
+            $content .= '<td width="5%">';
+            $course_id = api_get_course_int_id();
+            $new_options = Question::readQuestionOption($questionId, $course_id);
+            // Your choice
+            if (isset($new_options[$studentChoice])) {
+                $content .= get_lang($new_options[$studentChoice]['name']);
+            } else {
+                $content .= '-';
+            }
+            $content .= '</td>';
         }
-        $content .= '</td><td width="5%">';
+
         // Expected choice
         if (!$hide_expected_answer) {
-            if (isset($new_options[$answerCorrect])) {
-                $content .= get_lang($new_options[$answerCorrect]['name']);
-            } else {
-                $content .= '-';
+            if ($exercise->showExpectedChoiceColumn()) {
+                $content .= '<td width="5%">';
+                if (isset($new_options[$answerCorrect])) {
+                    $content .= get_lang($new_options[$answerCorrect]['name']);
+                } else {
+                    $content .= '-';
+                }
+                $content .= '</td>';
             }
-        } else {
-            $content .= '-';
         }
-        $content .= '</td><td width="40%">';
+
+        $content .= '<td width="40%">';
         $content .= $answer;
         $content .= '</td>';
 
@@ -491,15 +541,18 @@ class ExerciseShowFunctions
             $content .= $status;
             $content .= '</td>';
         }
+
         if ($feedbackType != EXERCISE_FEEDBACK_TYPE_EXAM) {
-            if ($resultsDisabled == RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER) {
-                if ($studentChoice != $answerCorrect) {
-                    return '';
-                }
-            }
             $content .= '<td width="20%">';
             $color = 'black';
-            if (isset($new_options[$studentChoice])) {
+            if (isset($new_options[$studentChoice]) || in_array(
+                    $exercise->results_disabled,
+                    [
+                        RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
+                        RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
+                    ]
+                )
+            ) {
                 if ($studentChoice == $answerCorrect) {
                     $color = 'green';
                 }
@@ -510,8 +563,6 @@ class ExerciseShowFunctions
                 $content .= '<span style="font-weight: bold; color: '.$color.';">'.nl2br($answerComment).'</span>';
             }
             $content .= '</td>';
-        } else {
-            $content .= '<td>&nbsp;</td>';
         }
         $content .= '</tr>';
 
@@ -521,16 +572,18 @@ class ExerciseShowFunctions
     /**
      * Display the answers to a multiple choice question.
      *
-     * @param int    $feedbackType
-     * @param int    $studentChoice
-     * @param int    $studentChoiceDegree
-     * @param string $answer
-     * @param string $answerComment
-     * @param int    $answerCorrect
-     * @param int    $questionId
-     * @param bool   $inResultsDisabled
+     * @param Exercise $exercise
+     * @param int      $feedbackType
+     * @param int      $studentChoice
+     * @param int      $studentChoiceDegree
+     * @param string   $answer
+     * @param string   $answerComment
+     * @param int      $answerCorrect
+     * @param int      $questionId
+     * @param bool     $inResultsDisabled
      */
     public static function displayMultipleAnswerTrueFalseDegreeCertainty(
+        $exercise,
         $feedbackType,
         $studentChoice,
         $studentChoiceDegree,
@@ -550,26 +603,30 @@ class ExerciseShowFunctions
         $courseId = api_get_course_int_id();
         $newOptions = Question::readQuestionOption($questionId, $courseId);
 
-        //Your choice
+        // Your choice
         if (isset($newOptions[$studentChoice])) {
             echo get_lang($newOptions[$studentChoice]['name']);
         } else {
             echo '-';
         }
-        echo '</td><td width="5%">';
+        echo '</td>';
 
         // Expected choice
-        if (!$hideExpectedAnswer) {
-            if (isset($newOptions[$answerCorrect])) {
-                echo get_lang($newOptions[$answerCorrect]['name']);
+        if ($exercise->showExpectedChoiceColumn()) {
+            echo '<td width="5%">';
+            if (!$hideExpectedAnswer) {
+                if (isset($newOptions[$answerCorrect])) {
+                    echo get_lang($newOptions[$answerCorrect]['name']);
+                } else {
+                    echo '-';
+                }
             } else {
                 echo '-';
             }
-        } else {
-            echo '-';
+            echo '</td>';
         }
 
-        echo '</td><td width="20%">';
+        echo '<td width="20%">';
         echo $answer;
         echo '</td><td width="5%" style="text-align:center;">';
         if (isset($newOptions[$studentChoiceDegree])) {
@@ -577,12 +634,18 @@ class ExerciseShowFunctions
         }
         echo '</td>';
 
+        $position = isset($newOptions[$studentChoiceDegree]) ? $newOptions[$studentChoiceDegree]['position'] : '';
         $degreeInfo = $question->getResponseDegreeInfo(
             $studentChoice,
             $answerCorrect,
-            $newOptions[$studentChoiceDegree]['position']
+            $position
         );
 
+        $degreeInfo['color'] = isset($degreeInfo['color']) ? $degreeInfo['color'] : '';
+        $degreeInfo['background-color'] = isset($degreeInfo['background-color']) ? $degreeInfo['background-color'] : '';
+        $degreeInfo['description'] = isset($degreeInfo['description']) ? $degreeInfo['description'] : '';
+        $degreeInfo['label'] = isset($degreeInfo['label']) ? $degreeInfo['label'] : '';
+
         echo '
             <td width="15%">
                 <div style="text-align:center;color: '.$degreeInfo['color'].';
@@ -633,7 +696,13 @@ class ExerciseShowFunctions
         $showTotalScoreAndUserChoices
     ) {
         $hide_expected_answer = false;
+        $hideStudentChoice = false;
         switch ($resultsDisabled) {
+            case RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING:
+            case RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER:
+                $hideStudentChoice = true;
+                $hide_expected_answer = true;
+                break;
             case RESULT_DISABLE_SHOW_SCORE_ONLY:
                 if ($feedbackType == 0) {
                     $hide_expected_answer = true;
@@ -648,28 +717,34 @@ class ExerciseShowFunctions
                 break;
         }
 
-        echo '<tr><td width="5%">';
-        // Your choice
-        $question = new MultipleAnswerCombinationTrueFalse();
-        if (isset($question->options[$studentChoice])) {
-            echo $question->options[$studentChoice];
-        } else {
-            echo $question->options[2];
-        }
-        echo '</td><td width="5%">';
-        // Expected choice
-        if (!$hide_expected_answer) {
-            if (isset($question->options[$answerCorrect])) {
-                echo $question->options[$answerCorrect];
+        echo '<tr>';
+
+        if ($hideStudentChoice === false) {
+            echo '<td width="5%">';
+            // Your choice
+            $question = new MultipleAnswerCombinationTrueFalse();
+            if (isset($question->options[$studentChoice])) {
+                echo $question->options[$studentChoice];
             } else {
                 echo $question->options[2];
             }
-        } else {
-            echo '-';
+            echo '</td>';
         }
-        echo '</td>';
+
+        // Expected choice
+        if ($exercise->showExpectedChoiceColumn()) {
+            if (!$hide_expected_answer) {
+                echo '<td width="5%">';
+                if (isset($question->options[$answerCorrect])) {
+                    echo $question->options[$answerCorrect];
+                } else {
+                    echo $question->options[2];
+                }
+                echo '</td>';
+            }
+        }
+
         echo '<td width="40%">';
-        // my answer
         echo $answer;
         echo '</td>';
 
@@ -689,10 +764,14 @@ class ExerciseShowFunctions
         if ($feedbackType != EXERCISE_FEEDBACK_TYPE_EXAM) {
             echo '<td width="20%">';
             //@todo replace this harcoded value
-            if ($studentChoice) {
-                $color = "black";
+            if ($studentChoice || in_array($resultsDisabled, [
+                RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
+                RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
+            ])
+            ) {
+                $color = 'black';
                 if ($studentChoice == $answerCorrect) {
-                    $color = "green";
+                    $color = 'green';
                 }
                 if ($hide_expected_answer) {
                     $color = '';
@@ -707,20 +786,20 @@ class ExerciseShowFunctions
     }
 
     /**
-     * @param $feedbackType
-     * @param $exe_id
-     * @param $questionId
+     * @param int  $feedbackType
+     * @param int  $exeId
+     * @param int  $questionId
      * @param null $questionScore
      * @param int  $resultsDisabled
      */
     public static function displayAnnotationAnswer(
         $feedbackType,
-        $exe_id,
+        $exeId,
         $questionId,
         $questionScore = null,
         $resultsDisabled = 0
     ) {
-        $comments = Event::get_comments($exe_id, $questionId);
+        $comments = Event::get_comments($exeId, $questionId);
         if ($feedbackType != EXERCISE_FEEDBACK_TYPE_EXAM) {
             if ($questionScore <= 0 && empty($comments)) {
                 echo '<br />'.ExerciseLib::getNotCorrectedYetText();

+ 12 - 11
main/inc/lib/extra_field.lib.php

@@ -116,6 +116,9 @@ class ExtraField extends Model
                 $this->extraFieldType = EntityExtraField::SESSION_FIELD_TYPE;
                 $this->primaryKey = 'id';
                 break;
+            case 'exercise':
+                $this->extraFieldType = EntityExtraField::EXERCISE_FIELD_TYPE;
+                break;
             case 'question':
                 $this->extraFieldType = EntityExtraField::QUESTION_FIELD_TYPE;
                 break;
@@ -188,6 +191,7 @@ class ExtraField extends Model
             'terms_and_condition',
             'forum_category',
             'forum_post',
+            'exercise',
         ];
 
         if (api_get_configuration_value('allow_scheduled_announcements')) {
@@ -2203,14 +2207,14 @@ class ExtraField extends Model
         $form = new FormValidator($this->type.'_field', 'post', $url);
 
         $form->addElement('hidden', 'type', $this->type);
-        $id = isset($_GET['id']) ? intval($_GET['id']) : null;
+        $id = isset($_GET['id']) ? (int) $_GET['id'] : null;
         $form->addElement('hidden', 'id', $id);
 
         // Setting the form elements
         $header = get_lang('Add');
         $defaults = [];
 
-        if ($action == 'edit') {
+        if ($action === 'edit') {
             $header = get_lang('Modify');
             // Setting the defaults
             $defaults = $this->get($id, false);
@@ -2218,7 +2222,7 @@ class ExtraField extends Model
 
         $form->addElement('header', $header);
 
-        if ($action == 'edit') {
+        if ($action === 'edit') {
             $translateUrl = api_get_path(WEB_CODE_PATH).'extrafield/translate.php?'
                 .http_build_query(['extra_field' => $id]);
             $translateButton = Display::toolbarButton(get_lang('TranslateThisTerm'), $translateUrl, 'language', 'link');
@@ -2410,11 +2414,11 @@ JAVASCRIPT;
                     : null;
 
                 if ($field['field_type'] == self::FIELD_TYPE_DOUBLE_SELECT) {
-                    //Add 2 selects
+                    // Add 2 selects
                     $options = $extraFieldOption->get_field_options_by_field($field['id']);
                     $options = self::extra_field_double_select_convert_array_to_ordered_array($options);
-                    $first_options = [];
 
+                    $first_options = [];
                     if (!empty($options)) {
                         foreach ($options as $option) {
                             foreach ($option as $sub_option) {
@@ -2658,16 +2662,13 @@ JAVASCRIPT;
         if (empty($col)) {
             return '';
         }
-
-        if ($oper === 'bw' || $oper === 'bn') {
+        if ($oper == 'bw' || $oper == 'bn') {
             $val .= '%';
         }
-
-        if ($oper === 'ew' || $oper === 'en') {
+        if ($oper == 'ew' || $oper == 'en') {
             $val = '%'.$val;
         }
-
-        if ($oper === 'cn' || $oper === 'nc' || $oper === 'in' || $oper === 'ni') {
+        if ($oper == 'cn' || $oper == 'nc' || $oper == 'in' || $oper == 'ni') {
             if (is_array($val)) {
                 $result = '"%'.implode(';', $val).'%"';
                 foreach ($val as $item) {

+ 22 - 0
main/inc/lib/extra_field_option.lib.php

@@ -924,4 +924,26 @@ class ExtraFieldOption extends Model
 
         return $result;
     }
+
+    /**
+     * @param string $variable
+     *
+     * @return array|\Chamilo\CoreBundle\Entity\ExtraFieldOptions[]
+     */
+    public function getOptionsByFieldVariable($variable)
+    {
+        $extraFieldType = $this->getExtraField()->getExtraFieldType();
+
+        $dql = "SELECT o FROM ChamiloCoreBundle:ExtraFieldOptions o
+            INNER JOIN ChamiloCoreBundle:ExtraField f WITH o.field = f.id
+            WHERE f.variable = :variable AND f.extraFieldType = :extra_field_type
+            ORDER BY o.value ASC";
+
+        $result = Database::getManager()
+            ->createQuery($dql)
+            ->setParameters(['variable' => $variable, 'extra_field_type' => $extraFieldType])
+            ->getResult();
+
+        return $result;
+    }
 }

+ 56 - 13
main/inc/lib/fileUpload.lib.php

@@ -68,6 +68,10 @@ function get_document_title($name)
     $name = disable_dangerous_file($name);
     $ext = substr(strrchr($name, '.'), 0);
 
+    if (empty($ext)) {
+        return substr($name, 0, strlen($name));
+    }
+
     return substr($name, 0, strlen($name) - strlen(strstr($name, $ext)));
 }
 
@@ -99,7 +103,7 @@ function process_uploaded_file($uploaded_file, $show_output = true)
                 // The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.
                 // Not used at the moment, but could be handy if we want to limit the size of an upload
                 // (e.g. image upload in html editor).
-                $max_file_size = intval($_POST['MAX_FILE_SIZE']);
+                $max_file_size = (int) $_POST['MAX_FILE_SIZE'];
                 if ($show_output) {
                     Display::addFlash(
                         Display::return_message(
@@ -736,7 +740,11 @@ function moveUploadedFile($file, $storePath)
     $handleFromFile = isset($file['from_file']) && $file['from_file'] ? true : false;
     $moveFile = isset($file['move_file']) && $file['move_file'] ? true : false;
     if ($moveFile) {
-        copy($file['tmp_name'], $storePath);
+        $copied = copy($file['tmp_name'], $storePath);
+
+        if (!$copied) {
+            return false;
+        }
     }
     if ($handleFromFile) {
         return file_exists($file['tmp_name']);
@@ -1203,6 +1211,41 @@ function clean_up_files_in_zip($p_event, &$p_header)
         '.Thumbs.db',
         'Thumbs.db',
     ];
+
+    if (in_array($baseName, $skipFiles)) {
+        return 0;
+    }
+    $modifiedStoredFileName = clean_up_path($originalStoredFileName);
+    $p_header['filename'] = str_replace($originalStoredFileName, $modifiedStoredFileName, $p_header['filename']);
+
+    return 1;
+}
+
+/**
+ * Allow .htaccess file.
+ *
+ * @param $p_event
+ * @param $p_header
+ *
+ * @return int
+ */
+function cleanZipFilesAllowHtaccess($p_event, &$p_header)
+{
+    $originalStoredFileName = $p_header['stored_filename'];
+    $baseName = basename($originalStoredFileName);
+
+    $allowFiles = ['.htaccess'];
+    if (in_array($baseName, $allowFiles)) {
+        return 1;
+    }
+
+    // Skip files
+    $skipFiles = [
+        '__MACOSX',
+        '.Thumbs.db',
+        'Thumbs.db',
+    ];
+
     if (in_array($baseName, $skipFiles)) {
         return 0;
     }
@@ -1617,7 +1660,7 @@ function create_unexisting_directory(
     $sendNotification = true
 ) {
     $course_id = $_course['real_id'];
-    $session_id = intval($session_id);
+    $session_id = (int) $session_id;
 
     $folderExists = DocumentManager::folderExists(
         $desired_dir_name,
@@ -1704,18 +1747,18 @@ function create_unexisting_directory(
                 );
 
                 if ($document_id) {
+                    $lastEditType = [
+                        0 => 'invisible',
+                        1 => 'visible',
+                        2 => 'delete',
+                    ];
                     // Update document item_property
-                    if (!empty($visibility)) {
-                        $visibilities = [
-                            0 => 'invisible',
-                            1 => 'visible',
-                            2 => 'delete',
-                        ];
+                    if (isset($lastEditType[$visibility])) {
                         api_item_property_update(
                             $_course,
                             TOOL_DOCUMENT,
                             $document_id,
-                            $visibilities[$visibility],
+                            $lastEditType[$visibility],
                             $user_id,
                             $groupInfo,
                             $to_user_id,
@@ -1791,7 +1834,7 @@ function move_uploaded_file_collection_into_directory(
     $max_filled_space
 ) {
     $number_of_uploaded_images = count($uploaded_file_collection['name']);
-    $new_file_list = [];
+    $list = [];
     for ($i = 0; $i < $number_of_uploaded_images; $i++) {
         $missing_file['name'] = $uploaded_file_collection['name'][$i];
         $missing_file['type'] = $uploaded_file_collection['type'][$i];
@@ -1801,7 +1844,7 @@ function move_uploaded_file_collection_into_directory(
 
         $upload_ok = process_uploaded_file($missing_file);
         if ($upload_ok) {
-            $new_file_list[] = handle_uploaded_document(
+            $list[] = handle_uploaded_document(
                 $_course,
                 $missing_file,
                 $base_work_dir,
@@ -1817,7 +1860,7 @@ function move_uploaded_file_collection_into_directory(
         unset($missing_file);
     }
 
-    return $new_file_list;
+    return $list;
 }
 
 /**

+ 1 - 1
main/inc/lib/fixlinks.js

@@ -1,4 +1,4 @@
-$(document).ready(function() {
+$(function() {
     var objects = $(document).find('object');
     var pathname = location.pathname;
     var coursePath = pathname.substr(0, pathname.indexOf('/courses/'));

+ 10 - 3
main/inc/lib/geometry.lib.php

@@ -327,9 +327,16 @@ function convert_coordinates($coords, $sep = '|')
 {
     $points = [];
     $pairs = explode($sep, $coords);
-    foreach ($pairs as $idx => $pcoord) {
-        list($x, $y) = explode(';', $pcoord);
-        $points[] = ['x' => $x, 'y' => $y];
+    if (!empty($pairs)) {
+        foreach ($pairs as $idx => $pcoord) {
+            if (empty($pcoord)) {
+                continue;
+            }
+            $parts = explode(';', $pcoord);
+            if (!empty($parts)) {
+                $points[] = ['x' => $parts[0], 'y' => $parts[1]];
+            }
+        }
     }
 
     return $points;

+ 16 - 14
main/inc/lib/glossary.lib.php

@@ -43,7 +43,7 @@ class GlossaryManager
     }
 
     /**
-     * Get glossary term by glossary id.
+     * Get glossary description by glossary id.
      *
      * @author Isaac Flores <florespaz@bidsoftperu.com>
      *
@@ -55,17 +55,19 @@ class GlossaryManager
     {
         $table = Database::get_course_table(TABLE_GLOSSARY);
         $course_id = api_get_course_int_id();
+        $glossary_id = (int) $glossary_id;
+
         $sql = "SELECT description 
                 FROM $table
-                WHERE c_id = $course_id  AND glossary_id =".intval($glossary_id);
+                WHERE c_id = $course_id  AND glossary_id =".$glossary_id;
         $rs = Database::query($sql);
         if (Database::num_rows($rs) > 0) {
             $row = Database::fetch_array($rs);
 
             return $row['description'];
-        } else {
-            return '';
         }
+
+        return '';
     }
 
     /**
@@ -411,7 +413,7 @@ class GlossaryManager
                 Display::return_icon('save.png', get_lang('Export'), '', ICON_SIZE_MEDIUM).'</a>';
         }
 
-        if (($view == 'table') || (!isset($view))) {
+        if ($view === 'table' || !isset($view)) {
             $actionsLeft .= '<a href="index.php?'.api_get_cidreq().'&action=changeview&view=list">'.
                 Display::return_icon('view_detailed.png', get_lang('ListView'), '', ICON_SIZE_MEDIUM).'</a>';
         } else {
@@ -455,7 +457,6 @@ class GlossaryManager
                 ['GlossaryManager', 'get_glossary_data'],
                 0
             );
-            //$table->set_header(0, '', false);
             $table->set_header(0, get_lang('TermName'), true);
             $table->set_header(1, get_lang('TermDefinition'), true);
             if (api_is_allowed_to_edit(null, true)) {
@@ -504,7 +505,7 @@ class GlossaryManager
         // Database table definition
         $t_glossary = Database::get_course_table(TABLE_GLOSSARY);
         $course_id = api_get_course_int_id();
-        $session_id = intval($session_id);
+        $session_id = (int) $session_id;
         $sql_filter = api_get_session_condition($session_id, true, true);
 
         $keyword = isset($_GET['keyword']) ? Database::escape_string($_GET['keyword']) : '';
@@ -550,12 +551,12 @@ class GlossaryManager
         $t_item_propery = Database::get_course_table(TABLE_ITEM_PROPERTY);
 
         if (api_is_allowed_to_edit(null, true)) {
-            $col2 = " glossary.glossary_id	as col2, ";
+            $col2 = ' glossary.glossary_id	as col2, ';
         } else {
             $col2 = ' ';
         }
 
-        //condition for the session
+        // Condition for the session
         $session_id = api_get_session_id();
         $condition_session = api_get_session_condition(
             $session_id,
@@ -564,12 +565,13 @@ class GlossaryManager
             'glossary.session_id'
         );
 
-        $column = intval($column);
+        $column = (int) $column;
+        $from = (int) $from;
+        $number_of_items = (int) $number_of_items;
+
         if (!in_array($direction, ['DESC', 'ASC'])) {
             $direction = 'ASC';
         }
-        $from = intval($from);
-        $number_of_items = intval($number_of_items);
 
         $keyword = isset($_GET['keyword']) ? Database::escape_string($_GET['keyword']) : '';
         $keywordCondition = '';
@@ -591,7 +593,7 @@ class GlossaryManager
 					ip.c_id = ".api_get_course_int_id()."
 					$keywordCondition
 		        ORDER BY col$column $direction
-		        LIMIT $from,$number_of_items";
+		        LIMIT $from, $number_of_items";
         $res = Database::query($sql);
 
         $return = [];
@@ -607,7 +609,7 @@ class GlossaryManager
                 $array[1] = $data[1];
             }
 
-            if (isset($_GET['action']) && $_GET['action'] == 'export') {
+            if (isset($_GET['action']) && $_GET['action'] === 'export') {
                 $array[1] = api_html_entity_decode($data[1]);
             }
 

+ 21 - 76
main/inc/lib/groupmanager.lib.php

@@ -45,6 +45,8 @@ class GroupManager
     const TOOL_NOT_AVAILABLE = 0;
     const TOOL_PUBLIC = 1;
     const TOOL_PRIVATE = 2;
+    const TOOL_PRIVATE_BETWEEN_USERS = 3;
+
     /**
      * Constants for the available group tools.
      */
@@ -1812,11 +1814,11 @@ class GroupManager
         $course_id = empty($course_id) ? api_get_course_int_id() : (int) $course_id;
         $group_id = $groupInfo['id'];
 
-        $table = Database::get_course_table(TABLE_GROUP_USER);
         if (!empty($user_ids)) {
+            $table = Database::get_course_table(TABLE_GROUP_USER);
             foreach ($user_ids as $user_id) {
                 if (self::canUserSubscribe($user_id, $groupInfo)) {
-                    $user_id = intval($user_id);
+                    $user_id = (int) $user_id;
                     $sql = "INSERT INTO ".$table." (c_id, user_id, group_id)
                             VALUES ('$course_id', '".$user_id."', '".$group_id."')";
                     Database::query($sql);
@@ -2052,32 +2054,6 @@ class GroupManager
         return $groups;
     }
 
-    /**
-     * Remove all users that are not students and all users who have tutor status
-     * from  the list.
-     *
-     * @param array $user_array_in
-     *
-     * @return array
-     */
-    public static function filter_only_students($user_array_in)
-    {
-        $user_array_out = [];
-        foreach ($user_array_in as $this_user) {
-            if (api_get_session_id()) {
-                if ($this_user['status_session'] == 0) {
-                    $user_array_out[] = $this_user;
-                }
-            } else {
-                if ($this_user['status_rel'] == STUDENT) {
-                    $user_array_out[] = $this_user;
-                }
-            }
-        }
-
-        return $user_array_out;
-    }
-
     /**
      * Check if a user has access to a certain group tool.
      *
@@ -2161,6 +2137,13 @@ class GroupManager
                     return true;
                 }
                 break;
+            case self::TOOL_PRIVATE_BETWEEN_USERS:
+                // Only works for announcements for now
+                $userIsInGroup = self::is_user_in_group($user_id, $groupInfo);
+                if ($userIsInGroup && $tool == self::GROUP_TOOL_ANNOUNCEMENT) {
+                    return true;
+                }
+                break;
         }
 
         return false;
@@ -2250,16 +2233,18 @@ class GroupManager
      * Get all groups where a specific user is subscribed.
      *
      * @param int $user_id
+     * @param int $courseId
      *
      * @return array
      */
-    public static function getAllGroupPerUserSubscription($user_id)
+    public static function getAllGroupPerUserSubscription($user_id, $courseId = 0)
     {
         $table_group_user = Database::get_course_table(TABLE_GROUP_USER);
         $table_tutor_user = Database::get_course_table(TABLE_GROUP_TUTOR);
         $table_group = Database::get_course_table(TABLE_GROUP);
-        $user_id = intval($user_id);
-        $course_id = api_get_course_int_id();
+        $user_id = (int) $user_id;
+        $courseId = empty($courseId) ? api_get_course_int_id() : (int) $courseId;
+
         $sql = "SELECT DISTINCT g.*
                FROM $table_group g
                LEFT JOIN $table_group_user gu
@@ -2267,7 +2252,7 @@ class GroupManager
                LEFT JOIN $table_tutor_user tu
                ON (tu.group_id = g.iid AND g.c_id = tu.c_id)
                WHERE
-                  g.c_id = $course_id AND
+                  g.c_id = $courseId AND
                   (gu.user_id = $user_id OR tu.user_id = $user_id) ";
         $res = Database::query($sql);
         $groups = [];
@@ -2278,45 +2263,6 @@ class GroupManager
         return $groups;
     }
 
-    /**
-     * @param array $userList
-     * @param array $groupInfo
-     *
-     * @return mixed
-     */
-    public static function getNumberLeftFromGroupFromUserList($userList, $groupInfo)
-    {
-        $groupIid = (int) $groupInfo['iid'];
-        $category = self::get_category_from_group($groupIid);
-        $number_groups_per_user = $groupInfo['maximum_number_of_students'];
-        $categoryId = 0;
-        if ($category) {
-            $groups_per_user = $category['groups_per_user'];
-            $number_groups_per_user = $groups_per_user == self::GROUP_PER_MEMBER_NO_LIMIT ? self::INFINITE : $groups_per_user;
-            $categoryId = $category['id'];
-        }
-
-        $usersAdded = [];
-        foreach ($userList as &$userInfo) {
-            // find # of groups the user is enrolled in
-            $numberOfGroups = self::user_in_number_of_groups(
-                $userInfo['user_id'],
-                $categoryId
-            );
-
-            if (in_array($userInfo['user_id'], $usersAdded)) {
-                continue;
-            }
-
-            $usersAdded[] = $userInfo['user_id'];
-
-            // add # of groups to user list
-            $userInfo['number_groups_left'] = $number_groups_per_user - $numberOfGroups;
-        }
-
-        return $userList;
-    }
-
     /**
      * @param array $group_list
      * @param int   $category_id
@@ -2918,17 +2864,16 @@ class GroupManager
         echo '
             <ul class="toolbar-groups nav nav-tabs">
                 <li class="'.$activeSettings.'">
-                    <a href="'.sprintf($url, 'settings.php').'">
+                    <a id="group_settings_tab" href="'.sprintf($url, 'settings.php').'">
                     '.Display::return_icon('settings.png').' '.get_lang('Settings').'
                     </a>
                 </li>
                 <li class="'.$activeMember.'">
-                    <a href="'.sprintf($url, 'member_settings.php').'">
-                    '.Display::return_icon('user.png').' '.get_lang('GroupMembers').'
-                    </a>
+                    <a id="group_members_tab" href="'.sprintf($url, 'member_settings.php').'">
+                    '.Display::return_icon('user.png').' '.get_lang('GroupMembers').'</a>
                 </li>
                 <li class="'.$activeTutor.'">
-                    <a href="'.sprintf($url, 'tutor_settings.php').'">
+                    <a id="group_tutors_tab" href="'.sprintf($url, 'tutor_settings.php').'">
                     '.Display::return_icon('teacher.png').' '.get_lang('GroupTutors').'
                     </a>
                 </li>

+ 1 - 1
main/inc/lib/image.lib.php

@@ -66,7 +66,7 @@ class Image
     }
 
     /**
-     * @param $cropParameters
+     * @param string $cropParameters
      *
      * @return bool
      */

+ 92 - 36
main/inc/lib/internationalization.lib.php

@@ -1,6 +1,7 @@
 <?php
 /* For licensing terms, see /license.txt */
 
+use ChamiloSession as Session;
 use Patchwork\Utf8;
 
 /**
@@ -15,10 +16,6 @@ use Patchwork\Utf8;
  * @package chamilo.library
  */
 
-/**
- * Constants.
- */
-
 // Special tags for marking untranslated variables.
 define('SPECIAL_OPENING_TAG', '[=');
 define('SPECIAL_CLOSING_TAG', '=]');
@@ -386,43 +383,45 @@ function api_get_timezones()
  */
 function api_get_timezone()
 {
-    // First, get the default timezone of the server
-    $to_timezone = date_default_timezone_get();
-    // Second, see if a timezone has been chosen for the platform
-    $timezone_value = api_get_setting('timezone_value', 'timezones');
+    $timezone = Session::read('system_timezone');
+    if (empty($timezone)) {
+        // First, get the default timezone of the server
+        $timezone = date_default_timezone_get();
+        // Second, see if a timezone has been chosen for the platform
+        $timezoneFromSettings = api_get_setting('timezone_value', 'timezones');
 
-    if ($timezone_value != null) {
-        $to_timezone = $timezone_value;
-    }
-    // If allowed by the administrator
-    $use_users_timezone = api_get_setting('use_users_timezone', 'timezones');
+        if ($timezoneFromSettings != null) {
+            $timezone = $timezoneFromSettings;
+        }
 
-    if ($use_users_timezone === 'true') {
-        $userId = api_get_user_id();
-        // Get the timezone based on user preference, if it exists
-        $timezone_user = UserManager::get_extra_user_data_by_field(
-            $userId,
-            'timezone'
-        );
-        if (isset($timezone_user['timezone']) && $timezone_user['timezone'] != null) {
-            $to_timezone = $timezone_user['timezone'];
+        // If allowed by the administrator
+        $allowUserTimezones = api_get_setting('use_users_timezone', 'timezones');
+
+        if ($allowUserTimezones === 'true') {
+            $userId = api_get_user_id();
+            // Get the timezone based on user preference, if it exists
+            $newExtraField = new ExtraFieldValue('user');
+            $data = $newExtraField->get_values_by_handler_and_field_variable($userId, 'timezone');
+            if (!empty($data) && isset($data['timezone']) && !empty($data['timezone'])) {
+                $timezone = $data['timezone'];
+            }
         }
+        Session::write('system_timezone', $timezone);
     }
 
-    return $to_timezone;
+    return $timezone;
 }
 
 /**
  * Returns the given date as a DATETIME in UTC timezone.
  * This function should be used before entering any date in the DB.
  *
- * @param mixed $time                    date to be converted (can be a string supported by date() or a timestamp)
- * @param bool  $returnNullIfInvalidDate if the date is not correct return null instead of the current date
- * @param bool  $returnObj
+ * @param mixed $time                    Date to be converted (can be a string supported by date() or a timestamp)
+ * @param bool  $returnNullIfInvalidDate If the date is not correct return null instead of the current date
+ * @param bool  $returnObj               Returns a DateTime object
  *
  * @return string|DateTime The DATETIME in UTC to be inserted in the DB,
  *                         or null if the format of the argument is not supported
- *                         or datetime
  *
  * @author Julio Montoya - Adding the 2nd parameter
  * @author Guillaume Viguier <guillaume.viguier@beeznest.com>
@@ -466,11 +465,15 @@ function api_get_utc_datetime(
 /**
  * Returns a DATETIME string converted to the right timezone.
  *
- * @param mixed The time to be converted
- * @param string The timezone to be converted to.
- * If null, the timezone will be determined based on user preference,
- * or timezone chosen by the admin for the platform.
- * @param string The timezone to be converted from. If null, UTC will be assumed.
+ * @param mixed  $time                    The time to be converted
+ * @param string $to_timezone             The timezone to be converted to.
+ *                                        If null, the timezone will be determined based on user preference,
+ *                                        or timezone chosen by the admin for the platform.
+ * @param string $from_timezone           The timezone to be converted from. If null, UTC will be assumed.
+ * @param bool   $returnNullIfInvalidDate
+ * @param bool   $showTime
+ * @param bool   $humanForm
+ * @param string $format
  *
  * @return string The converted time formatted as Y-m-d H:i:s
  *
@@ -480,9 +483,10 @@ function api_get_local_time(
     $time = null,
     $to_timezone = null,
     $from_timezone = null,
-    $return_null_if_invalid_date = false,
+    $returnNullIfInvalidDate = false,
     $showTime = true,
-    $humanForm = false
+    $humanForm = false,
+    $format = ''
 ) {
     // Determining the timezone to be converted from
     if (is_null($from_timezone)) {
@@ -491,7 +495,7 @@ function api_get_local_time(
 
     // If time is a timestamp, convert it to a string
     if (is_null($time) || empty($time) || $time == '0000-00-00 00:00:00') {
-        if ($return_null_if_invalid_date) {
+        if ($returnNullIfInvalidDate) {
             return null;
         }
         $from_timezone = 'UTC';
@@ -500,7 +504,7 @@ function api_get_local_time(
 
     if (is_numeric($time)) {
         $time = (int) $time;
-        if ($return_null_if_invalid_date) {
+        if ($returnNullIfInvalidDate) {
             if (strtotime(date('d-m-Y H:i:s', $time)) !== (int) $time) {
                 return null;
             }
@@ -509,6 +513,7 @@ function api_get_local_time(
         $from_timezone = 'UTC';
         $time = gmdate('Y-m-d H:i:s', $time);
     }
+
     if ($time instanceof DateTime) {
         $time = $time->format('Y-m-d H:i:s');
         $from_timezone = 'UTC';
@@ -523,6 +528,10 @@ function api_get_local_time(
         $date = new DateTime($time, new DateTimezone($from_timezone));
         $date->setTimezone(new DateTimeZone($to_timezone));
 
+        if (!empty($format)) {
+            return $date->format($format);
+        }
+
         return api_get_human_date_time($date, $showTime, $humanForm);
     } catch (Exception $e) {
         return '';
@@ -2374,3 +2383,50 @@ function api_get_human_date_time($date, $showTime = true, $humanForm = false)
         }
     }
 }
+
+/**
+ * @param string $language
+ *
+ * @return array
+ */
+function api_get_language_files_to_load($language)
+{
+    $parent_path = SubLanguageManager::get_parent_language_path($language);
+    $langPath = api_get_path(SYS_LANG_PATH);
+
+    $languagesToLoad = [
+        $langPath.'english/trad4all.inc.php', // include English always
+    ];
+
+    if (!empty($parent_path)) { // if the sub-language feature is on
+        // prepare string for current language and its parent
+        $lang_file = $langPath.$language.'/trad4all.inc.php';
+        $parent_lang_file = $langPath.$parent_path.'/trad4all.inc.php';
+        // load the parent language file first
+        if (file_exists($parent_lang_file)) {
+            $languagesToLoad[] = $parent_lang_file;
+        }
+        // overwrite the parent language translations if there is a child
+        if (file_exists($lang_file)) {
+            $languagesToLoad[] = $lang_file;
+        }
+    } else {
+        // if the sub-languages feature is not on, then just load the
+        // set language interface
+        // prepare string for current language
+        $langFile = $langPath.$language.'/trad4all.inc.php';
+
+        if (file_exists($langFile)) {
+            $languagesToLoad[] = $langFile;
+        }
+
+        // Check if language/custom.php exists
+        $customLanguage = $langPath.$language.'/custom.php';
+
+        if (file_exists($customLanguage)) {
+            $languagesToLoad[] = $customLanguage;
+        }
+    }
+
+    return $languagesToLoad;
+}

+ 7 - 80
main/inc/lib/link.lib.php

@@ -1435,81 +1435,6 @@ class Link extends Model
         return $rv;
     }
 
-    /**
-     * CSV file import functions.
-     *
-     * @author René Haentjens , Ghent University
-     */
-    public static function import_link($linkdata)
-    {
-        // url, category_id, title, description, ...
-        // Field names used in the uploaded file
-        $known_fields = [
-            'url',
-            'category',
-            'title',
-            'description',
-            'on_homepage',
-            'hidden',
-        ];
-
-        $hide_fields = [
-            'kw',
-            'kwd',
-            'kwds',
-            'keyword',
-            'keywords',
-        ];
-
-        // All other fields are added to description, as "name:value".
-        // Only one hide_field is assumed to be present, <> is removed from value.
-        if (!($url = trim($linkdata['url'])) || !($title = trim($linkdata['title']))) {
-            return 0; // 0 = fail
-        }
-
-        $cat = ($catname = trim($linkdata['category'])) ? self::get_cat($catname) : 0;
-
-        $regs = []; // Will be passed to ereg()
-        $d = '';
-        foreach ($linkdata as $key => $value) {
-            if (!in_array($key, $known_fields)) {
-                if (in_array($key, $hide_fields) && ereg(
-                        '^<?([^>]*)>?$',
-                        $value,
-                        $regs
-                    )
-                ) { // possibly in <...>
-                    if (($kwlist = trim($regs[1])) != '') {
-                        $kw = '<i kw="'.htmlspecialchars($kwlist).'">';
-                    } else {
-                        $kw = '';
-                    }
-                    // i.e. assume only one of the $hide_fields will be present
-                    // and if found, hide the value as expando property of an <i> tag
-                } elseif (trim($value)) {
-                    $d .= ', '.$key.':'.$value;
-                }
-            }
-        }
-        if (!empty($d)) {
-            $d = substr($d, 2).' - ';
-        }
-
-        return self::put_link(
-            $url,
-            $cat,
-            $title,
-            $kw.ereg_replace(
-                '\[((/?(b|big|i|small|sub|sup|u))|br/)\]',
-                '<\\1>',
-                htmlspecialchars($d.$linkdata['description'])
-            ).($kw ? '</i>' : ''),
-            $linkdata['on_homepage'] ? '1' : '0',
-            $linkdata['hidden'] ? '1' : '0'
-        );
-        // i.e. allow some BBcode tags, e.g. [b]...[/b]
-    }
-
     /**
      * This function checks if the url is a vimeo link.
      *
@@ -1638,6 +1563,7 @@ class Link extends Model
      * @param string $show
      * @param null   $token
      * @param bool   $showActionLinks
+     * @param bool   $forceOpenCategories
      *
      * @return string
      */
@@ -1647,12 +1573,11 @@ class Link extends Model
         $categoryId,
         $show = 'none',
         $token = null,
-        $showActionLinks = true
+        $showActionLinks = true,
+        $forceOpenCategories = false
     ) {
         $categoryId = (int) $categoryId;
-
         $content = '';
-
         $categories = self::getLinkCategories($course_id, $session_id);
         $countCategories = count($categories);
         $linksPerCategory = self::showLinksPerCategory(0, $course_id, $session_id, $showActionLinks);
@@ -1712,8 +1637,10 @@ class Link extends Model
             }
 
             // Validation when belongs to a session
-            $showChildren = $categoryId == $myrow['id'] || $show == 'all';
-            $myrow['description'] = $myrow['description'];
+            $showChildren = $categoryId == $myrow['id'] || $show === 'all';
+            if ($forceOpenCategories) {
+                $showChildren = true;
+            }
 
             $strVisibility = '';
             $visibilityClass = null;

+ 1 - 1
main/inc/lib/login.lib.php

@@ -230,7 +230,7 @@ class Login
         );
 
         api_mail_html(
-            $user->getCompleteName(),
+            UserManager::formatUserFullName($user),
             $user->getEmail(),
             $mailSubject,
             $mailBody

File diff suppressed because it is too large
+ 246 - 294
main/inc/lib/message.lib.php


+ 11 - 6
main/inc/lib/notebook.lib.php

@@ -116,6 +116,7 @@ class NotebookManager
         // Database table definition
         $table = Database::get_course_table(TABLE_NOTEBOOK);
         $course_id = api_get_course_int_id();
+        $notebook_id = (int) $notebook_id;
 
         $sql = "SELECT
                 notebook_id 		AS notebook_id,
@@ -123,7 +124,7 @@ class NotebookManager
                 description 		AS note_comment,
                 session_id			AS session_id
                 FROM $table
-                WHERE c_id = $course_id AND notebook_id = '".intval($notebook_id)."' ";
+                WHERE c_id = $course_id AND notebook_id = '".$notebook_id."' ";
         $result = Database::query($sql);
         if (Database::num_rows($result) != 1) {
             return [];
@@ -149,6 +150,7 @@ class NotebookManager
         if (!is_array($values) or empty($values['note_title'])) {
             return false;
         }
+
         // Database table definition
         $table = Database::get_course_table(TABLE_NOTEBOOK);
 
@@ -194,7 +196,9 @@ class NotebookManager
      */
     public static function delete_note($notebook_id)
     {
-        if (empty($notebook_id) || $notebook_id != strval(intval($notebook_id))) {
+        $notebook_id = (int) $notebook_id;
+
+        if (empty($notebook_id)) {
             return false;
         }
 
@@ -205,10 +209,11 @@ class NotebookManager
         $sql = "DELETE FROM $table
                 WHERE
                     c_id = $course_id AND
-                    notebook_id='".intval($notebook_id)."' AND
+                    notebook_id='".$notebook_id."' AND
                     user_id = '".api_get_user_id()."'";
         $result = Database::query($sql);
         $affected_rows = Database::affected_rows($result);
+
         if ($affected_rows != 1) {
             return false;
         }
@@ -217,7 +222,7 @@ class NotebookManager
         api_item_property_update(
             api_get_course_info(),
             TOOL_NOTEBOOK,
-            intval($notebook_id),
+            $notebook_id,
             'delete',
             api_get_user_id()
         );
@@ -271,12 +276,12 @@ class NotebookManager
 
         // Database table definition
         $table = Database::get_course_table(TABLE_NOTEBOOK);
-        $order_by = " ORDER BY ".$notebookView." $sort_direction ";
+        $order_by = ' ORDER BY '.$notebookView." $sort_direction ";
 
         // Condition for the session
         $condition_session = api_get_session_condition($sessionId);
 
-        $cond_extra = $notebookView == 'update_date' ? " AND update_date <> ''" : " ";
+        $cond_extra = $notebookView == 'update_date' ? " AND update_date <> ''" : ' ';
         $course_id = api_get_course_int_id();
 
         $sql = "SELECT * FROM $table

+ 8 - 5
main/inc/lib/online.inc.php

@@ -166,13 +166,16 @@ function online_logout($user_id = null, $logout_redirect = false)
     $url = api_get_path(WEB_PATH).'index.php';
 
     if ($logout_redirect && api_get_plugin_setting('azure_active_directory', 'enable') == 'true') {
-        $activeDirectoryPlugin = AzureActiveDirectory::create();
-        $azureLogout = $activeDirectoryPlugin->getUrl(AzureActiveDirectory::URL_TYPE_SIGNOUT);
-        if (!empty($azureLogout)) {
-            $url = $azureLogout;
+        if (ChamiloSession::read('_user_auth_source') === 'azure_active_directory') {
+            $activeDirectoryPlugin = AzureActiveDirectory::create();
+            $azureLogout = $activeDirectoryPlugin->getUrl(AzureActiveDirectory::URL_TYPE_LOGOUT);
+            if (!empty($azureLogout)) {
+                $url = $azureLogout;
+            }
         }
     }
 
+    api_delete_firstpage_parameter();
     Session::erase('last_id');
     CourseChatUtils::exitChat($user_id);
     session_regenerate_id();
@@ -206,7 +209,7 @@ function user_is_online($user_id)
 
     $online_time = time() - $time_limit * 60;
     $limit_date = api_get_utc_datetime($online_time);
-    $user_id = intval($user_id);
+    $user_id = (int) $user_id;
 
     $query = " SELECT login_user_id, login_date
                FROM $track_online_table track

+ 40 - 14
main/inc/lib/pdf.lib.php

@@ -110,6 +110,7 @@ class PDF
         $tpl->assign('pdf_content', $content);
 
         // Showing only the current teacher/admin instead the all teacher list name see BT#4080
+        $teacher_list = null;
         if (isset($this->params['show_real_course_teachers']) &&
             $this->params['show_real_course_teachers']
         ) {
@@ -127,7 +128,6 @@ class PDF
             }
         } else {
             $user_info = api_get_user_info();
-
             if ($this->params['show_teacher_as_myself']) {
                 $teacher_list = $user_info['complete_name'];
             }
@@ -665,17 +665,17 @@ class PDF
     /**
      * Sets the PDF header.
      *
-     * @param array $course_data
+     * @param array $courseInfo
      */
-    public function set_header($course_data)
+    public function set_header($courseInfo)
     {
         $this->pdf->defaultheaderfontsize = 10; // in pts
         $this->pdf->defaultheaderfontstyle = 'BI'; // blank, B, I, or BI
         $this->pdf->defaultheaderline = 1; // 1 to include line below header/above footer
 
         $userId = api_get_user_id();
-        if (!empty($course_data['code'])) {
-            $teacher_list = CourseManager::get_teacher_list_from_course_code($course_data['code']);
+        if (!empty($courseInfo['code'])) {
+            $teacher_list = CourseManager::get_teacher_list_from_course_code($courseInfo['code']);
 
             $teachers = '';
             if (!empty($teacher_list)) {
@@ -732,14 +732,14 @@ class PDF
      * Pre-formats a PDF to the right size and, if not stated otherwise, with
      * header, footer and watermark (if any).
      *
-     * @param array $course_data General course information (to fill headers)
-     * @param bool  $complete    Whether we want headers, footers and watermark or not
+     * @param array $courseInfo General course information (to fill headers)
+     * @param bool  $complete   Whether we want headers, footers and watermark or not
      */
-    public function format_pdf($course_data, $complete = true)
+    public function format_pdf($courseInfo, $complete = true)
     {
-        $course_code = null;
-        if (!empty($course_data)) {
-            $course_code = $course_data['code'];
+        $courseCode = null;
+        if (!empty($courseInfo)) {
+            $courseCode = $courseInfo['code'];
         }
 
         /*$pdf->SetAuthor('Documents Chamilo');
@@ -757,7 +757,7 @@ class PDF
         if ($complete) {
             // Adding watermark
             if (api_get_setting('pdf_export_watermark_enable') == 'true') {
-                $watermark_file = self::get_watermark($course_code);
+                $watermark_file = self::get_watermark($courseCode);
                 if ($watermark_file) {
                     //http://mpdf1.com/manual/index.php?tid=269&searchstring=watermark
                     $this->pdf->SetWatermarkImage($watermark_file);
@@ -769,7 +769,7 @@ class PDF
                         $this->pdf->showWatermarkImage = true;
                     }
                 }
-                if ($course_code) {
+                if ($courseCode) {
                     $watermark_text = api_get_course_setting('pdf_export_watermark_text');
                     if (empty($watermark_text)) {
                         $watermark_text = api_get_setting('pdf_export_watermark_text');
@@ -787,7 +787,7 @@ class PDF
             }
 
             if (empty($this->custom_header)) {
-                self::set_header($course_data);
+                self::set_header($courseInfo);
             } else {
                 $this->pdf->SetHTMLHeader($this->custom_header, 'E');
                 $this->pdf->SetHTMLHeader($this->custom_header, 'O');
@@ -886,6 +886,32 @@ class PDF
         Display::addFlash(Display::return_message(get_lang('ItemAdded')));
     }
 
+    /**
+     * @param string $theme
+     *
+     * @throws MpdfException
+     */
+    public function setBackground($theme)
+    {
+        $themeName = empty($theme) ? api_get_visual_theme() : $theme;
+        $themeDir = \Template::getThemeDir($themeName);
+        $customLetterhead = $themeDir.'images/letterhead.png';
+        $urlPathLetterhead = api_get_path(SYS_CSS_PATH).$customLetterhead;
+
+        $urlWebLetterhead = '#FFFFFF';
+        $fullPage = false;
+        if (file_exists($urlPathLetterhead)) {
+            $fullPage = true;
+            $urlWebLetterhead = 'url('.api_get_path(WEB_CSS_PATH).$customLetterhead.')';
+        }
+
+        if ($fullPage) {
+            $this->pdf->SetDisplayMode('fullpage');
+            $this->pdf->SetDefaultBodyCSS('background', $urlWebLetterhead);
+            $this->pdf->SetDefaultBodyCSS('background-image-resize', '6');
+        }
+    }
+
     /**
      * Fix images source paths to allow export to pdf.
      *

File diff suppressed because it is too large
+ 196 - 245
main/inc/lib/sessionmanager.lib.php


+ 91 - 20
main/inc/lib/skill.lib.php

@@ -1152,44 +1152,68 @@ class Skill extends Model
     }
 
     /**
-     * @param int $user_id
-     * @param int $gradebookId
-     * @param int $courseId
-     * @param int $sessionId
+     * @param int      $userId
+     * @param Category $category
+     * @param int      $courseId
+     * @param int      $sessionId
+     *
+     * @return bool
      */
     public function addSkillToUser(
-        $user_id,
-        $gradebookId,
-        $courseId = 0,
-        $sessionId = 0
+        $userId,
+        $category,
+        $courseId,
+        $sessionId
     ) {
         $skill_gradebook = new SkillRelGradebook();
         $skill_rel_user = new SkillRelUser();
 
-        $skill_gradebooks = $skill_gradebook->get_all(
-            ['where' => ['gradebook_id = ?' => $gradebookId]]
-        );
+        if (empty($category)) {
+            return false;
+        }
+
+        // Load subcategories
+        if (empty($category->get_parent_id())) {
+            $subCategories = $category->get_subcategories(
+                $userId,
+                $category->get_course_code(),
+                $category->get_session_id()
+            );
+            if (!empty($subCategories)) {
+                /** @var Category $subCategory */
+                foreach ($subCategories as $subCategory) {
+                    $this->addSkillToUser($userId, $subCategory, $courseId, $sessionId);
+                }
+            }
+        }
+
+        $gradebookId = $category->get_id();
+        $skill_gradebooks = $skill_gradebook->get_all(['where' => ['gradebook_id = ?' => $gradebookId]]);
+
         if (!empty($skill_gradebooks)) {
             foreach ($skill_gradebooks as $skill_gradebook) {
-                $user_has_skill = $this->userHasSkill(
-                    $user_id,
+                $hasSkill = $this->userHasSkill(
+                    $userId,
                     $skill_gradebook['skill_id'],
                     $courseId,
                     $sessionId
                 );
-                if (!$user_has_skill) {
+
+                if (!$hasSkill) {
                     $params = [
-                        'user_id' => $user_id,
+                        'user_id' => $userId,
                         'skill_id' => $skill_gradebook['skill_id'],
                         'acquired_skill_at' => api_get_utc_datetime(),
-                        'course_id' => intval($courseId),
-                        'session_id' => $sessionId ? intval($sessionId) : null,
+                        'course_id' => (int) $courseId,
+                        'session_id' => $sessionId ? (int) $sessionId : null,
                     ];
 
                     $skill_rel_user->save($params);
                 }
             }
         }
+
+        return true;
     }
 
     /* Deletes a skill */
@@ -1426,7 +1450,7 @@ class Skill extends Model
             ];
 
             if (!empty($courseInfo)) {
-                $tableRow['course_image'] = $courseInfo['course_image_source'];
+                $tableRow['course_image'] = $courseInfo['course_image'];
                 $tableRow['course_name'] = $courseInfo['title'];
             }
             $tableRows[] = $tableRow;
@@ -1441,7 +1465,7 @@ class Skill extends Model
         }
 
         if ($addTitle) {
-            $tableResult .= '<div class="header-title">'.get_lang('AchievedSkills').'</div>
+            $tableResult .= '<h3 class="section-title">'.get_lang('AchievedSkills').'</h3>
                     <div class="skills-badges">
                    ';
         }
@@ -2438,8 +2462,8 @@ class Skill extends Model
             $form->addButtonCreate(get_lang('Add'));
         } else {
             $form->addButtonUpdate(get_lang('Update'));
+            $form->addHidden('id', $skillInfo['id']);
         }
-        $form->addHidden('id', null);
 
         return $returnParams;
     }
@@ -2956,4 +2980,51 @@ class Skill extends Model
 
         return api_get_path(WEB_UPLOAD_PATH)."badges/{$skill->getIcon()}";
     }
+
+    /**
+     * @param User                             $user
+     * @param \Chamilo\CoreBundle\Entity\Skill $skill
+     * @param int                              $levelId
+     * @param string                           $argumentation
+     * @param int                              $authorId
+     *
+     * @throws \Doctrine\ORM\OptimisticLockException
+     *
+     * @return SkillRelUserEntity
+     */
+    public function addSkillToUserBadge($user, $skill, $levelId, $argumentation, $authorId)
+    {
+        $showLevels = api_get_configuration_value('hide_skill_levels') === false;
+
+        $entityManager = Database::getManager();
+
+        $skillUserRepo = $entityManager->getRepository('ChamiloSkillBundle:SkillRelUser');
+
+        $criteria = ['user' => $user, 'skill' => $skill];
+        $result = $skillUserRepo->findOneBy($criteria);
+
+        if (!empty($result)) {
+            return false;
+        }
+        $skillLevelRepo = $entityManager->getRepository('ChamiloSkillBundle:Level');
+
+        $skillUser = new \Chamilo\CoreBundle\Entity\SkillRelUser();
+        $skillUser->setUser($user);
+        $skillUser->setSkill($skill);
+
+        if ($showLevels && !empty($levelId)) {
+            $level = $skillLevelRepo->find($levelId);
+            $skillUser->setAcquiredLevel($level);
+        }
+
+        $skillUser->setArgumentation($argumentation);
+        $skillUser->setArgumentationAuthorId($authorId);
+        $skillUser->setAcquiredSkillAt(new DateTime());
+        $skillUser->setAssignedBy(0);
+
+        $entityManager->persist($skillUser);
+        $entityManager->flush();
+
+        return $skillUser;
+    }
 }

+ 51 - 49
main/inc/lib/social.lib.php

@@ -1,6 +1,8 @@
 <?php
 /* For licensing terms, see /license.txt */
 
+use Chamilo\CourseBundle\Entity\CForumPost;
+use Chamilo\CourseBundle\Entity\CForumThread;
 use ChamiloSession as Session;
 use Zend\Feed\Reader\Entry\Rss;
 use Zend\Feed\Reader\Reader;
@@ -409,12 +411,14 @@ class SocialManager extends UserManager
      * @author isaac flores paz
      *
      * @param int $userId
+     * @param int $limit
      *
      * @return array
      */
-    public static function get_list_invitation_of_friends_by_user_id($userId)
+    public static function get_list_invitation_of_friends_by_user_id($userId, $limit = 0)
     {
         $userId = (int) $userId;
+        $limit = (int) $limit;
 
         if (empty($userId)) {
             return [];
@@ -426,6 +430,9 @@ class SocialManager extends UserManager
                 WHERE
                     user_receiver_id = '.$userId.' AND
                     msg_status = '.MESSAGE_STATUS_INVITATION_PENDING;
+        if ($limit != null && $limit > 0) {
+            $sql .= ' LIMIT '.$limit;
+        }
         $res = Database::query($sql);
         $list = [];
         while ($row = Database::fetch_array($res, 'ASSOC')) {
@@ -556,25 +563,6 @@ class SocialManager extends UserManager
         return true;
     }
 
-    /**
-     * Allow attaching to group.
-     *
-     * @author Isaac Flores Paz
-     *
-     * @param int $id_friend_qualify User to qualify
-     * @param int $type_qualify      Kind of rating
-     *
-     * @deprecated 2017-03
-     */
-    public static function qualify_friend($id_friend_qualify, $type_qualify)
-    {
-        $table = Database::get_main_table(TABLE_MAIN_USER_REL_USER);
-        $user_id = api_get_user_id();
-        $sql = 'UPDATE '.$table.' SET relation_type='.((int) $type_qualify).'
-                WHERE user_id = '.$user_id.' AND friend_user_id='.(int) $id_friend_qualify;
-        Database::query($sql);
-    }
-
     /**
      * Get user's feeds.
      *
@@ -929,7 +917,7 @@ class SocialManager extends UserManager
         ];
 
         // get count unread message and total invitations
-        $count_unread_message = MessageManager::getNumberOfMessages(true);
+        $count_unread_message = MessageManager::getNumberOfMessages(['message_status' => [MESSAGE_STATUS_UNREAD]]);
         $count_unread_message = !empty($count_unread_message) ? Display::badge($count_unread_message) : null;
 
         $number_of_new_messages_of_friend = self::get_message_number_invitation_by_user_id(api_get_user_id());
@@ -976,7 +964,7 @@ class SocialManager extends UserManager
                         '.$homeIcon.' '.get_lang('Home').'
                     </a>
                 </li>';
-            $active = $show == 'messages' ? 'active' : null;
+            $active = $show === 'messages' ? 'active' : null;
             $links .= '
                 <li class="messages-icon '.$active.'">
                     <a href="'.api_get_path(WEB_CODE_PATH).'messages/inbox.php">
@@ -985,7 +973,7 @@ class SocialManager extends UserManager
                 </li>';
 
             // Invitations
-            $active = $show == 'invitations' ? 'active' : null;
+            $active = $show === 'invitations' ? 'active' : null;
             $links .= '
                 <li class="invitations-icon '.$active.'">
                     <a href="'.api_get_path(WEB_CODE_PATH).'social/invitations.php">
@@ -994,14 +982,14 @@ class SocialManager extends UserManager
                 </li>';
 
             // Shared profile and groups
-            $active = $show == 'shared_profile' ? 'active' : null;
+            $active = $show === 'shared_profile' ? 'active' : null;
             $links .= '
                 <li class="shared-profile-icon'.$active.'">
                     <a href="'.api_get_path(WEB_CODE_PATH).'social/profile.php">
                         '.$sharedProfileIcon.' '.get_lang('ViewMySharedProfile').'
                     </a>
                 </li>';
-            $active = $show == 'friends' ? 'active' : null;
+            $active = $show === 'friends' ? 'active' : null;
             $links .= '
                 <li class="friends-icon '.$active.'">
                     <a href="'.api_get_path(WEB_CODE_PATH).'social/friends.php">
@@ -1017,7 +1005,7 @@ class SocialManager extends UserManager
                 </li>';
 
             // Search users
-            $active = $show == 'search' ? 'active' : null;
+            $active = $show === 'search' ? 'active' : null;
             $links .= '
                 <li class="search-icon '.$active.'">
                     <a href="'.api_get_path(WEB_CODE_PATH).'social/search.php">
@@ -1026,7 +1014,7 @@ class SocialManager extends UserManager
                 </li>';
 
             // My files
-            $active = $show == 'myfiles' ? 'active' : null;
+            $active = $show === 'myfiles' ? 'active' : null;
 
             $myFiles = '
                 <li class="myfiles-icon '.$active.'">
@@ -1041,7 +1029,7 @@ class SocialManager extends UserManager
             $links .= $myFiles;
             if (api_get_configuration_value('allow_portfolio_tool')) {
                 $links .= '
-                    <li class="portoflio-icon '.($show == 'portfolio' ? 'active' : '').'">
+                    <li class="portoflio-icon '.($show === 'portfolio' ? 'active' : '').'">
                         <a href="'.api_get_path(WEB_CODE_PATH).'portfolio/index.php">
                             '.$portfolioIcon.' '.get_lang('Portfolio').'
                         </a>
@@ -1050,7 +1038,7 @@ class SocialManager extends UserManager
             }
 
             if (!api_get_configuration_value('disable_gdpr')) {
-                $active = $show == 'personal-data' ? 'active' : null;
+                $active = $show === 'personal-data' ? 'active' : null;
                 $personalData = '
                     <li class="personal-data-icon '.$active.'">
                         <a href="'.api_get_path(WEB_CODE_PATH).'social/personal_data.php">
@@ -1058,10 +1046,19 @@ class SocialManager extends UserManager
                         </a>
                     </li>';
                 $links .= $personalData;
-
-                $links .= '</ul>';
             }
 
+            if (api_is_platform_admin()) {
+                $active = $show === 'promoted_messages' ? 'active' : null;
+                $personalData = '
+                    <li class="personal-data-icon '.$active.'">
+                        <a href="'.api_get_path(WEB_CODE_PATH).'social/promoted_messages.php">
+                            '.$personalDataIcon.' '.get_lang('PromotedMessages').'
+                        </a>
+                    </li>';
+                $links .= $personalData;
+            }
+            $links .= '</ul>';
             $html .= Display::panelCollapse(
                 get_lang('SocialNetwork'),
                 $links,
@@ -1072,7 +1069,7 @@ class SocialManager extends UserManager
             );
         }
 
-        if (in_array($show, $show_groups) && !empty($group_id)) {
+        if (!empty($group_id) && in_array($show, $show_groups)) {
             $html .= $usergroup->show_group_column_information(
                 $group_id,
                 api_get_user_id(),
@@ -1201,10 +1198,10 @@ class SocialManager extends UserManager
             }
 
             // Check if I already sent an invitation message
-            $invitation_sent_list = self::get_list_invitation_sent_by_user_id(api_get_user_id());
+            $invitationSentList = self::get_list_invitation_sent_by_user_id(api_get_user_id());
 
-            if (isset($invitation_sent_list[$user_id]) && is_array($invitation_sent_list[$user_id]) &&
-                count($invitation_sent_list[$user_id]) > 0
+            if (isset($invitationSentList[$user_id]) && is_array($invitationSentList[$user_id]) &&
+                count($invitationSentList[$user_id]) > 0
             ) {
                 $links .= '<li><a href="'.api_get_path(WEB_CODE_PATH).'social/invitations.php">'.
                     Display::return_icon('invitation.png', get_lang('YouAlreadySentAnInvitation'))
@@ -1642,7 +1639,6 @@ class SocialManager extends UserManager
         $userId = (int) $userId;
         $start = (int) $start;
         $length = (int) $length;
-        $startDate = Database::escape_string($startDate);
 
         $select = " SELECT
                     id,
@@ -1663,20 +1659,23 @@ class SocialManager extends UserManager
         }
 
         $sql = "$select                    
-                    FROM $tblMessage tm
+                    FROM $tblMessage m
                 WHERE
                     msg_status <> ".MESSAGE_STATUS_WALL_DELETE.' AND ';
 
-        // My own posts
+        // Get my own posts
         $userReceiverCondition = ' (
             user_receiver_id = '.$userId.' AND 
             msg_status IN ('.MESSAGE_STATUS_WALL_POST.', '.MESSAGE_STATUS_WALL.') AND
             parent_id = '.$parentId.'
         )';
 
-        // User condition
         $sql .= $userReceiverCondition;
 
+        $sql .= ' OR (
+            msg_status IN ('.MESSAGE_STATUS_PROMOTED.')             
+        ) ';
+
         // Get my group posts
         $groupCondition = '';
         if (!empty($groupId)) {
@@ -1691,8 +1690,8 @@ class SocialManager extends UserManager
             $groupCondition .= ' AND msg_status IN ('.MESSAGE_STATUS_NEW.', '.MESSAGE_STATUS_UNREAD.')) ';
         }
 
-        $friendCondition = '';
         // Get my friend posts
+        $friendCondition = '';
         if (!empty($friendId)) {
             if (is_array($friendId)) {
                 $friendId = array_map('intval', $friendId);
@@ -1725,7 +1724,7 @@ class SocialManager extends UserManager
                                 forum_id,
                                 thread_id,
                                 c_id                            
-        ";
+                            ";
             }
 
             $threadList = array_map('intval', $threadList);
@@ -1757,7 +1756,6 @@ class SocialManager extends UserManager
             $repo = $em->getRepository('ChamiloCourseBundle:CForumPost');
             $repoThread = $em->getRepository('ChamiloCourseBundle:CForumThread');
             $groups = [];
-            $forums = [];
             $userGroup = new UserGroup();
             $urlGroup = api_get_path(WEB_CODE_PATH).'social/group_view.php?id=';
             while ($row = Database::fetch_array($res, 'ASSOC')) {
@@ -1773,14 +1771,14 @@ class SocialManager extends UserManager
                     }
                 }
 
-                // forums
+                // Forums
                 $row['post_title'] = '';
                 $row['forum_title'] = '';
                 $row['thread_url'] = '';
                 if ($row['msg_status'] == MESSAGE_STATUS_FORUM) {
-                    /** @var \Chamilo\CourseBundle\Entity\CForumPost $post */
+                    /** @var CForumPost $post */
                     $post = $repo->find($row['id']);
-                    /** @var \Chamilo\CourseBundle\Entity\CForumThread $thread */
+                    /** @var CForumThread $thread */
                     $thread = $repoThread->find($row['thread_id']);
                     if ($post && $thread) {
                         $courseInfo = api_get_course_info_by_id($post->getCId());
@@ -2520,7 +2518,7 @@ class SocialManager extends UserManager
         foreach ($messages as $message) {
             $post = $message['html'];
             $comments = '';
-            if ($message['msg_status'] == MESSAGE_STATUS_WALL_POST) {
+            if (in_array($message['msg_status'], [MESSAGE_STATUS_WALL_POST, MESSAGE_STATUS_PROMOTED])) {
                 $comments = self::getWallPostComments($userId, $message);
             }
 
@@ -2550,7 +2548,10 @@ class SocialManager extends UserManager
     }
 
     /**
-     * @param int $userId
+     * @param int   $userId
+     * @param array $groupList
+     * @param array $friendList
+     * @param array $threadList
      *
      * @return int
      */
@@ -2621,7 +2622,8 @@ class SocialManager extends UserManager
     }
 
     /**
-     * @param int $user_id
+     * @param int  $user_id
+     * @param bool $isArray
      *
      * @return string|array
      */
@@ -3053,7 +3055,7 @@ class SocialManager extends UserManager
         $userId = (int) $userId;
         $socialAjaxUrl = api_get_path(WEB_AJAX_PATH).'social.ajax.php';
         $socialAutoExtendLink = '';
-        if ($countPost > self::DEFAULT_SCROLL_NEW_POST) {
+        if ($countPost > self::DEFAULT_WALL_POSTS) {
             $socialAutoExtendLink = Display::url(
                 get_lang('SeeMore'),
                 $socialAjaxUrl.'?u='.$userId.'&a=list_wall_message&start='.

+ 88 - 43
main/inc/lib/sortable_table.class.php

@@ -96,10 +96,10 @@ class SortableTable extends HTML_Table
     public $table_id = null;
     public $headers = [];
     /**
-     * @var array
-     *            Columns to hide
+     * @var array Columns to hide
      */
     private $columnsToHide = [];
+    private $dataFunctionParams;
 
     /**
      * Create a new SortableTable.
@@ -116,6 +116,7 @@ class SortableTable extends HTML_Table
      * @param string $default_order_direction   The default order direction;
      *                                          either the constant 'ASC' or 'DESC'
      * @param string $table_id
+     * @param array  $parameters                They are custom attributes of the table
      */
     public function __construct(
         $table_name = 'table',
@@ -124,21 +125,26 @@ class SortableTable extends HTML_Table
         $default_column = 1,
         $default_items_per_page = 20,
         $default_order_direction = 'ASC',
-        $table_id = null
+        $table_id = null,
+        $parameters = []
     ) {
         if (empty($table_id)) {
-            $table_id = $table_name.uniqid();
+            $table_id = $table_name.uniqid('table', true);
         }
+        if (isset($parameters) && empty($parameters)) {
+            $parameters = ['class' => 'table table-bordered data_table', 'id' => $table_id];
+        }
+
         $this->table_id = $table_id;
-        parent::__construct(['class' => 'table table-bordered data_table', 'id' => $table_id]);
+        parent::__construct($parameters);
         $this->table_name = $table_name;
         $this->additional_parameters = [];
         $this->param_prefix = $table_name.'_';
 
         $this->page_nr = Session::read($this->param_prefix.'page_nr', 1);
-        $this->page_nr = isset($_GET[$this->param_prefix.'page_nr']) ? intval($_GET[$this->param_prefix.'page_nr']) : $this->page_nr;
+        $this->page_nr = isset($_GET[$this->param_prefix.'page_nr']) ? (int) $_GET[$this->param_prefix.'page_nr'] : $this->page_nr;
         $this->column = Session::read($this->param_prefix.'column', $default_column);
-        $this->column = isset($_GET[$this->param_prefix.'column']) ? intval($_GET[$this->param_prefix.'column']) : $this->column;
+        $this->column = isset($_GET[$this->param_prefix.'column']) ? (int) $_GET[$this->param_prefix.'column'] : $this->column;
 
         $defaultRow = api_get_configuration_value('table_default_row');
         if (!empty($defaultRow)) {
@@ -155,9 +161,9 @@ class SortableTable extends HTML_Table
             if (!in_array($my_session_direction, ['ASC', 'DESC'])) {
                 $this->direction = 'ASC';
             } else {
-                if ($my_session_direction == 'ASC') {
+                if ($my_session_direction === 'ASC') {
                     $this->direction = 'ASC';
-                } elseif ($my_session_direction == 'DESC') {
+                } elseif ($my_session_direction === 'DESC') {
                     $this->direction = 'DESC';
                 }
             }
@@ -168,9 +174,9 @@ class SortableTable extends HTML_Table
             if (!in_array($my_get_direction, ['ASC', 'DESC'])) {
                 $this->direction = 'ASC';
             } else {
-                if ($my_get_direction == 'ASC') {
+                if ($my_get_direction === 'ASC') {
                     $this->direction = 'ASC';
-                } elseif ($my_get_direction == 'DESC') {
+                } elseif ($my_get_direction === 'DESC') {
                     $this->direction = 'DESC';
                 }
             }
@@ -180,7 +186,7 @@ class SortableTable extends HTML_Table
         Session::erase($this->param_prefix.'per_page');
 
         $this->per_page = Session::read($this->param_prefix.'per_page', $default_items_per_page);
-        $this->per_page = isset($_GET[$this->param_prefix.'per_page']) ? intval($_GET[$this->param_prefix.'per_page']) : $this->per_page;
+        $this->per_page = isset($_GET[$this->param_prefix.'per_page']) ? (int) $_GET[$this->param_prefix.'per_page'] : $this->per_page;
 
         Session::write($this->param_prefix.'per_page', $this->per_page);
         Session::write($this->param_prefix.'direction', $this->direction);
@@ -198,18 +204,40 @@ class SortableTable extends HTML_Table
         $this->td_attributes = [];
         $this->th_attributes = [];
         $this->other_tables = [];
+        $this->dataFunctionParams = [];
+    }
+
+    /**
+     * @return array
+     */
+    public function getDataFunctionParams()
+    {
+        return $this->dataFunctionParams;
+    }
+
+    /**
+     * @param array $dataFunctionParams
+     *
+     * @return $this
+     */
+    public function setDataFunctionParams($dataFunctionParams)
+    {
+        $this->dataFunctionParams = $dataFunctionParams;
+
+        return $this;
     }
 
     /**
      * Get the Pager object to split the showed data in several pages.
+     *
+     * @return Pager_Sliding
      */
     public function get_pager()
     {
-        if (is_null($this->pager)) {
-            $total_number_of_items = $this->get_total_number_of_items();
+        if ($this->pager === null) {
             $params['mode'] = 'Sliding';
             $params['perPage'] = $this->per_page;
-            $params['totalItems'] = $total_number_of_items;
+            $params['totalItems'] = $this->get_total_number_of_items();
             $params['urlVar'] = $this->param_prefix.'page_nr';
             $params['currentPage'] = $this->page_nr;
             $icon_attributes = ['style' => 'vertical-align: middle;'];
@@ -732,7 +760,6 @@ class SortableTable extends HTML_Table
     public function processHeaders()
     {
         $counter = 0;
-
         foreach ($this->headers as $column => $columnInfo) {
             $label = $columnInfo['label'];
             $sortable = $columnInfo['sortable'];
@@ -980,12 +1007,23 @@ class SortableTable extends HTML_Table
     public function get_total_number_of_items()
     {
         if ($this->total_number_of_items == -1 && !is_null($this->get_total_number_function)) {
-            $this->total_number_of_items = call_user_func($this->get_total_number_function);
+            $this->total_number_of_items = call_user_func(
+                $this->get_total_number_function,
+                $this->getDataFunctionParams()
+            );
         }
 
         return $this->total_number_of_items;
     }
 
+    /**
+     * @param int $value
+     */
+    public function setTotalNumberOfItems($value)
+    {
+        $this->total_number_of_items = (int) $value;
+    }
+
     /**
      * Get the data to display.  This function calls the function given as
      * 2nd argument in the constructor of a SortableTable. Make sure your
@@ -1009,13 +1047,14 @@ class SortableTable extends HTML_Table
         $sort = null
     ) {
         $data = [];
-        if (!is_null($this->get_data_function)) {
+        if ($this->get_data_function !== null) {
             $data = call_user_func(
                 $this->get_data_function,
                 $from,
                 $this->per_page,
                 $this->column,
-                $this->direction
+                $this->direction,
+                $this->dataFunctionParams
             );
         }
 
@@ -1038,23 +1077,29 @@ class SortableTableFromArray extends SortableTable
     /**
      * Constructor.
      *
-     * @param array $table_data
-     * @param int   $default_column
-     * @param int   $default_items_per_page
+     * @param array  $table_data
+     * @param int    $default_column
+     * @param int    $default_items_per_page
+     * @param string $tableName
+     * @param string $get_total_number_function
+     * @param string $tableId
      */
     public function __construct(
         $table_data,
         $default_column = 1,
         $default_items_per_page = 20,
-        $tablename = 'tablename',
-        $get_total_number_function = null
+        $tableName = 'tablename',
+        $get_total_number_function = null,
+        $tableId = ''
     ) {
         parent:: __construct(
-            $tablename,
+            $tableName,
             $get_total_number_function,
             null,
             $default_column,
-            $default_items_per_page
+            $default_items_per_page,
+            null,
+            $tableId
         );
         $this->table_data = $table_data;
     }
@@ -1075,7 +1120,7 @@ class SortableTableFromArray extends SortableTable
             $content = TableSort::sort_table(
                 $this->table_data,
                 $this->column,
-                $this->direction == 'ASC' ? SORT_ASC : SORT_DESC
+                $this->direction === 'ASC' ? SORT_ASC : SORT_DESC
             );
         } else {
             $content = $this->table_data;
@@ -1137,20 +1182,20 @@ class SortableTableFromArrayConfig extends SortableTable
     /**
      * Constructor.
      *
-     * @param array  $table_data             All the information of the table
-     * @param int    $default_column         Default column that will be use in the sorts functions
-     * @param int    $default_items_per_page quantity of pages that we are going to see
-     * @param int    $tablename              Name of the table
-     * @param array  $column_show            An array with binary values 1: we show the column 2: we don't show it
-     * @param array  $column_order           an array of integers that let us decide how the columns are going to be sort
+     * @param array  $data         All the information of the table
+     * @param int    $column       Default column that will be use in the sorts functions
+     * @param int    $itemsPerPage quantity of pages that we are going to see
+     * @param string $tableName    Name of the table
+     * @param array  $column_show  An array with binary values 1: we show the column 2: we don't show it
+     * @param array  $column_order an array of integers that let us decide how the columns are going to be sort
      * @param string $direction
-     * @param bool   $doc_filter             special modification to fix the document name order
+     * @param bool   $doc_filter   special modification to fix the document name order
      */
     public function __construct(
-        $table_data,
-        $default_column = 1,
-        $default_items_per_page = 20,
-        $tablename = 'tablename',
+        $data,
+        $column = 1,
+        $itemsPerPage = 20,
+        $tableName = 'tablename',
         $column_show = [],
         $column_order = [],
         $direction = 'ASC',
@@ -1161,14 +1206,14 @@ class SortableTableFromArrayConfig extends SortableTable
         $this->doc_filter = $doc_filter;
 
         parent::__construct(
-            $tablename,
+            $tableName,
             null,
             null,
-            $default_column,
-            $default_items_per_page,
+            $column,
+            $itemsPerPage,
             $direction
         );
-        $this->table_data = $table_data;
+        $this->table_data = $data;
     }
 
     /**
@@ -1186,7 +1231,7 @@ class SortableTableFromArrayConfig extends SortableTable
         $content = TableSort::sort_table_config(
             $this->table_data,
             $this->column,
-            $this->direction == 'ASC' ? SORT_ASC : SORT_DESC,
+            $this->direction === 'ASC' ? SORT_ASC : SORT_DESC,
             $this->column_show,
             $this->column_order,
             SORT_REGULAR,

+ 131 - 0
main/inc/lib/statistics.lib.php

@@ -1149,4 +1149,135 @@ class Statistics
 
         return $chartCode;
     }
+
+    /**
+     * Display the Logins By Date report and allow export its result to XLS.
+     */
+    public static function printLoginsByDate()
+    {
+        if (isset($_GET['export']) && 'xls' === $_GET['export']) {
+            $result = self::getLoginsByDate($_GET['start'], $_GET['end']);
+            $data = [[get_lang('Username'), get_lang('FirstName'), get_lang('LastName'), get_lang('TotalTime')]];
+
+            foreach ($result as $i => $item) {
+                $data[] = [
+                    $item['username'],
+                    $item['firstname'],
+                    $item['lastname'],
+                    api_time_to_hms($item['time_count']),
+                ];
+            }
+
+            Export::arrayToXls($data);
+
+            exit;
+        }
+
+        echo Display::page_header(get_lang('LoginsByDate'));
+
+        $actions = '';
+        $content = '';
+
+        $form = new FormValidator('frm_logins_by_date', 'get');
+        $form->addDateRangePicker(
+            'daterange',
+            get_lang('DateRange'),
+            true,
+            ['format' => 'YYYY-MM-DD', 'timePicker' => 'false', 'validate_format' => 'Y-m-d']
+        );
+        $form->addHidden('report', 'logins_by_date');
+        $form->addButtonFilter(get_lang('Search'));
+
+        if ($form->validate()) {
+            $values = $form->exportValues();
+
+            $result = self::getLoginsByDate($values['daterange_start'], $values['daterange_end']);
+
+            if (!empty($result)) {
+                $actions = Display::url(
+                    Display::return_icon('excel.png', get_lang('ExportToXls'), [], ICON_SIZE_MEDIUM),
+                    api_get_self().'?'.http_build_query(
+                        [
+                            'report' => 'logins_by_date',
+                            'export' => 'xls',
+                            'start' => Security::remove_XSS($values['daterange_start']),
+                            'end' => Security::remove_XSS($values['daterange_end']),
+                        ]
+                    )
+                );
+            }
+
+            $table = new HTML_Table(['class' => 'data_table']);
+            $table->setHeaderContents(0, 0, get_lang('Username'));
+            $table->setHeaderContents(0, 1, get_lang('FirstName'));
+            $table->setHeaderContents(0, 2, get_lang('LastName'));
+            $table->setHeaderContents(0, 3, get_lang('TotalTime'));
+
+            foreach ($result as $i => $item) {
+                $table->setCellContents($i + 1, 0, $item['username']);
+                $table->setCellContents($i + 1, 1, $item['firstname']);
+                $table->setCellContents($i + 1, 2, $item['lastname']);
+                $table->setCellContents($i + 1, 3, api_time_to_hms($item['time_count']));
+            }
+
+            $table->setColAttributes(0, ['class' => 'text-center']);
+            $table->setColAttributes(3, ['class' => 'text-center']);
+            $content = $table->toHtml();
+        }
+
+        $form->display();
+
+        if (!empty($actions)) {
+            echo  Display::toolbarAction('logins_by_date_toolbar', [$actions]);
+        }
+
+        echo $content;
+    }
+
+    /**
+     * @param string $startDate
+     * @param string $endDate
+     *
+     * @return array
+     */
+    private static function getLoginsByDate($startDate, $endDate)
+    {
+        /** @var DateTime $startDate */
+        $startDate = api_get_utc_datetime("$startDate 00:00:00");
+        /** @var DateTime $endDate */
+        $endDate = api_get_utc_datetime("$endDate 23:59:59");
+
+        if (empty($startDate) || empty($endDate)) {
+            return [];
+        }
+
+        $tblUser = Database::get_main_table(TABLE_MAIN_USER);
+        $tblLogin = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
+        $urlJoin = '';
+        $urlWhere = '';
+
+        if (api_is_multiple_url_enabled()) {
+            $tblUrlUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
+
+            $urlJoin = "INNER JOIN $tblUrlUser au ON u.id = au.user_id";
+            $urlWhere = 'AND au.access_url_id = '.api_get_current_access_url_id();
+        }
+
+        $sql = "SELECT u.id,
+                    u.firstname,
+                    u.lastname,
+                    u.username,
+                    SUM(TIMESTAMPDIFF(SECOND, l.login_date, l.logout_date)) AS time_count
+                FROM $tblUser u
+                INNER JOIN $tblLogin l ON u.id = l.login_user_id
+                $urlJoin
+                WHERE l.login_date BETWEEN '$startDate' AND '$endDate'
+                $urlWhere
+                GROUP BY u.id";
+
+        $stmt = Database::query($sql);
+        $result = Database::store_result($stmt, 'ASSOC');
+
+        return $result;
+    }
 }

+ 18 - 14
main/inc/lib/system_announcements.lib.php

@@ -757,26 +757,33 @@ class SystemAnnouncementManager
             return true;
         }
 
+        $urlJoin = '';
+        $urlCondition = '';
         $user_table = Database::get_main_table(TABLE_MAIN_USER);
         if (api_is_multiple_url_enabled()) {
             $current_access_url_id = api_get_current_access_url_id();
             $url_rel_user = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
-            $url_condition = " INNER JOIN $url_rel_user uu ON uu.user_id = u.user_id ";
+            $urlJoin = " INNER JOIN $url_rel_user uu ON uu.user_id = u.user_id ";
+            $urlCondition = " AND access_url_id = '".$current_access_url_id."' ";
         }
 
         if ($teacher != 0 && $student == 0) {
-            $sql = "SELECT DISTINCT u.user_id FROM $user_table u $url_condition 
-                    WHERE status = '1' ";
+            $sql = "SELECT DISTINCT u.user_id FROM $user_table u $urlJoin 
+                    WHERE status = '1' $urlCondition";
         }
 
         if ($teacher == 0 && $student != 0) {
-            $sql = "SELECT DISTINCT u.user_id FROM $user_table u $url_condition 
-                    WHERE status = '5' ";
+            $sql = "SELECT DISTINCT u.user_id FROM $user_table u $urlJoin 
+                    WHERE status = '5' $urlCondition";
         }
 
         if ($teacher != 0 && $student != 0) {
-            $sql = "SELECT DISTINCT u.user_id FROM $user_table u $url_condition 
-                    WHERE 1 = 1 ";
+            $sql = "SELECT DISTINCT u.user_id FROM $user_table u $urlJoin 
+                    WHERE 1 = 1 $urlCondition";
+        }
+
+        if (!isset($sql)) {
+            return false;
         }
 
         if (!empty($language)) {
@@ -784,10 +791,6 @@ class SystemAnnouncementManager
             $sql .= " AND language = '".Database::escape_string($language)."' ";
         }
 
-        if (api_is_multiple_url_enabled()) {
-            $sql .= " AND access_url_id = '".$current_access_url_id."' ";
-        }
-
         // Sent to active users.
         $sql .= " AND email <>'' AND active = 1 ";
 
@@ -843,7 +846,7 @@ class SystemAnnouncementManager
         $sql .= self::getVisibilityCondition($visible);
 
         if (isset($id) && !empty($id)) {
-            $id = intval($id);
+            $id = (int) $id;
             $sql .= " AND id = $id ";
         }
 
@@ -852,7 +855,7 @@ class SystemAnnouncementManager
             $sql .= " AND access_url_id IN ('1', '$current_url_id') ";
         }
 
-        $sql .= " ORDER BY date_start DESC";
+        $sql .= ' ORDER BY date_start DESC';
         $result = Database::query($sql);
         $announcements = [];
 
@@ -900,11 +903,12 @@ class SystemAnnouncementManager
         $selectedUserLanguage = Database::escape_string(api_get_interface_language());
         $announcementTable = Database::get_main_table(TABLE_MAIN_SYSTEM_ANNOUNCEMENTS);
         $now = api_get_utc_datetime();
+        $announcementId = (int) $announcementId;
 
         $whereConditions = [
             "(lang = ? OR lang IS NULL OR lang = '') " => $selectedUserLanguage,
             "AND (? >= date_start AND ? <= date_end) " => [$now, $now],
-            "AND id = ? " => intval($announcementId),
+            "AND id = ? " => $announcementId,
         ];
 
         $condition = self::getVisibilityCondition($visibility);

+ 8 - 13
main/inc/lib/table_sort.class.php

@@ -36,10 +36,8 @@ class TableSort
         if (!is_array($data) || empty($data)) {
             return [];
         }
-        if ($column != strval(intval($column))) {
-            // Probably an attack
-            return $data;
-        }
+        $column = (int) $column;
+
         if (!in_array($direction, [SORT_ASC, SORT_DESC])) {
             // Probably an attack
             return $data;
@@ -78,7 +76,7 @@ class TableSort
             case SORT_NUMERIC:
                 $function = function ($a, $b) use ($column, $compareOperator) {
                     $result = strip_tags($a[$column]) <= strip_tags($b[$column]);
-                    if ($compareOperator == '>') {
+                    if ($compareOperator === '>') {
                         $result = strip_tags($a[$column]) > strip_tags($b[$column]);
                     }
 
@@ -91,7 +89,7 @@ class TableSort
                         api_strtolower(strip_tags($a[$column], "<img>")),
                         api_strtolower(strip_tags($b[$column], "<img>"))
                     ) <= 0;
-                    if ($compareOperator == '>') {
+                    if ($compareOperator === '>') {
                         $result = api_strnatcmp(
                             api_strtolower(strip_tags($a[$column], "<img>")),
                             api_strtolower(strip_tags($b[$column], "<img>"))
@@ -105,7 +103,7 @@ class TableSort
             case SORT_DATE:
                 $function = function ($a, $b) use ($column, $compareOperator) {
                     $result = strtotime(strip_tags($a[$column])) <= strtotime(strip_tags($b[$column]));
-                    if ($compareOperator == '>') {
+                    if ($compareOperator === '>') {
                         $result = strtotime(strip_tags($a[$column])) > strtotime(strip_tags($b[$column]));
                     }
 
@@ -119,7 +117,7 @@ class TableSort
                         api_strtolower(strip_tags($a[$column])),
                         api_strtolower(strip_tags($b[$column]))
                     ) <= 0;
-                    if ($compareOperator == '>') {
+                    if ($compareOperator === '>') {
                         $result = api_strnatcmp(
                             api_strtolower(strip_tags($a[$column])),
                             api_strtolower(strip_tags($b[$column]))
@@ -164,10 +162,7 @@ class TableSort
             return [];
         }
 
-        if ($column != strval(intval($column))) {
-            // Probably an attack
-            return $data;
-        }
+        $column = (int) $column;
 
         if (!in_array($direction, [SORT_ASC, SORT_DESC])) {
             // Probably an attack
@@ -198,7 +193,7 @@ class TableSort
             $new_data = [];
             if (!empty($data)) {
                 foreach ($data as $document) {
-                    if ($document['type'] == 'folder') {
+                    if ($document['type'] === 'folder') {
                         $docs_to_sort[$document['id']] = api_strtolower($document['name']);
                     } else {
                         $folder_to_sort[$document['id']] = api_strtolower($document['name']);

+ 45 - 29
main/inc/lib/template.lib.php

@@ -149,6 +149,7 @@ class Template
             'api_get_user_info',
             'api_get_configuration_value',
             'api_get_setting',
+            'api_get_plugin_setting',
             [
                 'name' => 'return_message',
                 'callable' => 'Display::return_message_and_translate',
@@ -588,7 +589,17 @@ class Template
         }
 
         $features = api_get_configuration_value('video_features');
-        $defaultFeatures = ['playpause', 'current', 'progress', 'duration', 'tracks', 'volume', 'fullscreen', 'vrview'];
+        $defaultFeatures = [
+            'playpause',
+            'current',
+            'progress',
+            'duration',
+            'tracks',
+            'volume',
+            'fullscreen',
+            'vrview',
+            'markersrolls',
+        ];
 
         if (!empty($features) && isset($features['features'])) {
             foreach ($features['features'] as $feature) {
@@ -731,6 +742,7 @@ class Template
             "select2/dist/js/i18n/$isoCode.js",
             'mediaelement/plugins/vrview/vrview.js',
             'js-cookie/src/js.cookie.js',
+            'mediaelement/plugins/markersrolls/markersrolls.min.js',
         ];
 
         $viewBySession = api_get_setting('my_courses_view_by_session') === 'true';
@@ -1207,7 +1219,7 @@ class Template
 
         /** @var User $tutor */
         foreach ($tutors as $tutor) {
-            $names[] = $tutor->getCompleteName();
+            $names[] = UserManager::formatUserFullName($tutor);
         }
 
         return implode(CourseManager::USER_SEPARATOR, $names);
@@ -1237,7 +1249,7 @@ class Template
 
         /** @var User $teacher */
         foreach ($teachers as $teacher) {
-            $names[] = $teacher->getCompleteName();
+            $names[] = UserManager::formatUserFullName($teacher);
         }
 
         return implode(CourseManager::USER_SEPARATOR, $names);
@@ -1618,11 +1630,13 @@ class Template
             $adminName = '';
             // Administrator name
             if (!empty($name)) {
-                $adminName = get_lang('Manager').' : '.
-                    Display::encrypted_mailto_link(
-                        api_get_setting('emailAdministrator'),
-                        $name
-                    );
+                $adminName = get_lang('Manager').' : ';
+                $adminName .= Display::encrypted_mailto_link(
+                    api_get_setting('emailAdministrator'),
+                    $name,
+                    '',
+                    true
+                );
             }
             $this->assign('administrator_name', $adminName);
         }
@@ -1643,21 +1657,23 @@ class Template
             if (!empty($courseId)) {
                 $tutor_data = '';
                 if ($id_session != 0) {
-                    $coachs_email = CourseManager::get_email_of_tutor_to_session(
-                        $id_session,
-                        $courseId
-                    );
-                    $email_link = [];
-                    foreach ($coachs_email as $coach) {
-                        $email_link[] = Display::encrypted_mailto_link($coach['email'], $coach['complete_name']);
+                    $users = SessionManager::getCoachesByCourseSession($id_session, $courseId);
+                    $links = [];
+                    if (!empty($users)) {
+                        $coaches = [];
+                        foreach ($users as $userId) {
+                            $coaches[] = api_get_user_info($userId);
+                        }
+                        $links = array_column($coaches, 'complete_name_with_message_link');
                     }
-                    if (count($coachs_email) > 1) {
+                    $count = count($links);
+                    if ($count > 1) {
                         $tutor_data .= get_lang('Coachs').' : ';
-                        $tutor_data .= array_to_string($email_link, CourseManager::USER_SEPARATOR);
-                    } elseif (count($coachs_email) == 1) {
+                        $tutor_data .= array_to_string($links, CourseManager::USER_SEPARATOR);
+                    } elseif ($count === 1) {
                         $tutor_data .= get_lang('Coach').' : ';
-                        $tutor_data .= array_to_string($email_link, CourseManager::USER_SEPARATOR);
-                    } elseif (count($coachs_email) == 0) {
+                        $tutor_data .= array_to_string($links, CourseManager::USER_SEPARATOR);
+                    } elseif ($count === 0) {
                         $tutor_data .= '';
                     }
                 }
@@ -1670,19 +1686,19 @@ class Template
             $courseId = api_get_course_int_id();
             if (!empty($courseId)) {
                 $teacher_data = '';
-                $mail = CourseManager::get_emails_of_tutors_to_course($courseId);
-                if (!empty($mail)) {
-                    $teachers_parsed = [];
-                    foreach ($mail as $value) {
-                        foreach ($value as $email => $name) {
-                            $teachers_parsed[] = Display::encrypted_mailto_link($email, $name);
-                        }
+                $teachers = CourseManager::getTeachersFromCourse($courseId);
+                if (!empty($teachers)) {
+                    $teachersParsed = [];
+                    foreach ($teachers as $teacher) {
+                        $userId = $teacher['id'];
+                        $teachersParsed[] = api_get_user_info($userId);
                     }
+                    $links = array_column($teachersParsed, 'complete_name_with_message_link');
                     $label = get_lang('Teacher');
-                    if (count($mail) > 1) {
+                    if (count($links) > 1) {
                         $label = get_lang('Teachers');
                     }
-                    $teacher_data .= $label.' : '.array_to_string($teachers_parsed, CourseManager::USER_SEPARATOR);
+                    $teacher_data .= $label.' : '.array_to_string($links, CourseManager::USER_SEPARATOR);
                 }
                 $this->assign('teachers', $teacher_data);
             }

+ 28 - 50
main/inc/lib/text.lib.php

@@ -10,6 +10,13 @@
  */
 define('EXERCISE_NUMBER_OF_DECIMALS', 2);
 
+/* XML processing functions */
+
+// A regular expression for accessing declared encoding within xml-formatted text.
+// Published by Steve Minutillo,
+// http://minutillo.com/steve/weblog/2004/6/17/php-xml-and-character-encodings-a-tale-of-sadness-rage-and-data-loss/
+define('_PCRE_XML_ENCODING', '/<\?xml.*encoding=[\'"](.*?)[\'"].*\?>/m');
+
 /**
  * This function strips all html-tags found in the input string and outputs a pure text.
  * Mostly, the function is to be used before language or encoding detection of the input string.
@@ -73,12 +80,21 @@ function api_set_encoding_html(&$string, $encoding)
         }
     } else {
         $count = 1;
-        $string = str_ireplace(
-            '</head>',
-            '<meta http-equiv="Content-Type" content="text/html; charset='.$encoding.'"/></head>',
-            $string,
-            $count
-        );
+        if (strpos('</head>', strtolower($string)) !== false) {
+            $string = str_ireplace(
+                '</head>',
+                '<meta http-equiv="Content-Type" content="text/html; charset='.$encoding.'"/></head>',
+                $string,
+                $count
+            );
+        } else {
+            $string = str_ireplace(
+                '<body>',
+                '<head><meta http-equiv="Content-Type" content="text/html; charset='.$encoding.'"/></head><body>',
+                $string,
+                $count
+            );
+        }
     }
     $string = api_convert_encoding($string, $encoding, $old_encoding);
 }
@@ -119,19 +135,13 @@ function api_get_title_html(&$string, $output_encoding = null, $input_encoding =
     return '';
 }
 
-/* XML processing functions */
-
-// A regular expression for accessing declared encoding within xml-formatted text.
-// Published by Steve Minutillo,
-// http://minutillo.com/steve/weblog/2004/6/17/php-xml-and-character-encodings-a-tale-of-sadness-rage-and-data-loss/
-define('_PCRE_XML_ENCODING', '/<\?xml.*encoding=[\'"](.*?)[\'"].*\?>/m');
-
 /**
  * Detects encoding of xml-formatted text.
  *
  * @param string $string           the input xml-formatted text
  * @param string $default_encoding This is the default encoding to be returned
- *                                 if there is no way the xml-text's encoding to be detected. If it not spesified, the system encoding is assumed then.
+ *                                 if there is no way the xml-text's encoding to be detected.
+ *                                 If it not spesified, the system encoding is assumed then.
  *
  * @return string returns the detected encoding
  *
@@ -307,8 +317,6 @@ function api_contains_asciisvg($html)
     return false;
 }
 
-/* Miscellaneous text processing functions */
-
 /**
  * Convers a string from camel case into underscore.
  * Works correctly with ASCII strings only, implementation for human-language strings is not necessary.
@@ -405,9 +413,7 @@ function api_trunc_str($text, $length = 30, $suffix = '...', $middle = false, $e
  */
 function domesticate($input)
 {
-    $input = stripslashes($input);
-    $input = str_replace("'", "''", $input);
-    $input = str_replace('"', "''", $input);
+    $input = str_replace(["'", '"'], "''", stripslashes($input));
 
     return $input;
 }
@@ -518,34 +524,6 @@ function esc_url($url, $protocols = null, $_context = 'display')
     }
 
     return Security::remove_XSS($url);
-
-    /*// Replace ampersands and single quotes only when displaying.
-    if ( 'display' == $_context ) {
-        $url = wp_kses_normalize_entities( $url );
-        $url = str_replace( '&amp;', '&#038;', $url );
-        $url = str_replace( "'", '&#039;', $url );
-    }
-
-    if ( '/' === $url[0] ) {
-        $good_protocol_url = $url;
-    } else {
-        if ( ! is_array( $protocols ) )
-            $protocols = wp_allowed_protocols();
-        $good_protocol_url = wp_kses_bad_protocol( $url, $protocols );
-        if ( strtolower( $good_protocol_url ) != strtolower( $url ) )
-            return '';
-    }
-
-    /**
-     * Filter a string cleaned and escaped for output as a URL.
-     *
-     * @since 2.3.0
-     *
-     * @param string $good_protocol_url The cleaned URL to be returned.
-     * @param string $original_url      The URL prior to cleaning.
-     * @param string $_context          If 'display', replace ampersands and single quotes only.
-     */
-    //return apply_filters( 'clean_url', $good_protocol_url, $original_url, $_context );98
 }
 
 /**
@@ -865,9 +843,9 @@ function get_week_from_day($date)
         $time = api_strtotime($date, 'UTC');
 
         return date('W', $time);
-    } else {
-        return date('W');
     }
+
+    return date('W');
 }
 
 /**
@@ -931,7 +909,7 @@ function implode_with_key($glue, $array)
  */
 function format_file_size($file_size)
 {
-    $file_size = intval($file_size);
+    $file_size = (int) $file_size;
     if ($file_size >= 1073741824) {
         $file_size = (round($file_size / 1073741824 * 100) / 100).'G';
     } elseif ($file_size >= 1048576) {

+ 3 - 2
main/inc/lib/timeline.lib.php

@@ -219,15 +219,16 @@ class Timeline extends Model
 
     /**
      * @param array $params
+     * @param bool  $showQuery
      *
      * @return bool
      */
-    public function save($params)
+    public function save($params, $showQuery = false)
     {
         $params['c_id'] = api_get_course_int_id();
         $params['parent_id'] = '0';
         $params['type'] = 'default';
-        $id = parent::save($params);
+        $id = parent::save($params, $showQuery);
         if (!empty($id)) {
             //event_system(LOG_CAREER_CREATE, LOG_CAREER_ID, $id, api_get_utc_datetime(), api_get_user_id());
         }

File diff suppressed because it is too large
+ 219 - 192
main/inc/lib/tracking.lib.php


+ 47 - 31
main/inc/lib/urlmanager.lib.php

@@ -19,7 +19,7 @@ class UrlManager
      * @param string $description The description of the site
      * @param int    $active      is active or not
      *
-     * @return bool if success
+     * @return int
      */
     public static function add($url, $description, $active)
     {
@@ -31,9 +31,11 @@ class UrlManager
                 active 		= '".intval($active)."',
                 created_by 	= '".api_get_user_id()."',
                 tms = FROM_UNIXTIME(".$tms.")";
-        $result = Database::query($sql);
+        Database::query($sql);
 
-        return $result;
+        $id = Database::insert_id();
+
+        return $id;
     }
 
     /**
@@ -50,12 +52,14 @@ class UrlManager
      */
     public static function update($urlId, $url, $description, $active)
     {
-        $urlId = intval($urlId);
+        $urlId = (int) $urlId;
+        $active = (int) $active;
+
         $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL);
         $sql = "UPDATE $table
                 SET url 	= '".Database::escape_string($url)."',
                 description = '".Database::escape_string($description)."',
-                active 		= '".intval($active)."',
+                active 		= '".$active."',
                 created_by 	= '".api_get_user_id()."',
                 tms 		= '".api_get_utc_datetime()."'
                 WHERE id = '$urlId'";
@@ -76,7 +80,7 @@ class UrlManager
      * */
     public static function delete($id)
     {
-        $id = intval($id);
+        $id = (int) $id;
         $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL);
         $tableUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
         $tableCourse = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
@@ -119,17 +123,18 @@ class UrlManager
     }
 
     /**
-     * @param string $url
+     * @param string $urlId
      *
      * @return int
      */
-    public static function url_id_exist($url)
+    public static function url_id_exist($urlId)
     {
-        if (empty($url)) {
+        $urlId = (int) $urlId;
+        if (empty($urlId)) {
             return false;
         }
         $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL);
-        $sql = "SELECT id FROM $table WHERE id = ".intval($url)."";
+        $sql = "SELECT id FROM $table WHERE id = ".$urlId;
         $res = Database::query($sql);
         $num = Database::num_rows($res);
 
@@ -192,10 +197,11 @@ class UrlManager
      * */
     public static function get_url_data_from_id($urlId)
     {
+        $urlId = (int) $urlId;
         $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL);
         $sql = "SELECT id, url, description, active
                 FROM $table
-                WHERE id = ".intval($urlId);
+                WHERE id = ".$urlId;
         $res = Database::query($sql);
         $row = Database::fetch_array($res);
 
@@ -214,11 +220,12 @@ class UrlManager
      */
     public static function get_url_rel_user_data($urlId = 0, $order_by = null)
     {
+        $urlId = (int) $urlId;
         $where = '';
         $table_url_rel_user = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
         $tbl_user = Database::get_main_table(TABLE_MAIN_USER);
         if (!empty($urlId)) {
-            $where = "WHERE $table_url_rel_user.access_url_id = ".intval($urlId);
+            $where = "WHERE $table_url_rel_user.access_url_id = ".$urlId;
         }
         if (empty($order_by)) {
             $order_clause = api_sort_by_first_name(
@@ -249,11 +256,12 @@ class UrlManager
     public static function get_url_rel_course_data($urlId = 0)
     {
         $where = '';
+        $urlId = (int) $urlId;
         $table_url_rel_course = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
         $tbl_course = Database::get_main_table(TABLE_MAIN_COURSE);
 
         if (!empty($urlId)) {
-            $where = " WHERE uc.access_url_id = ".intval($urlId);
+            $where = " WHERE uc.access_url_id = $urlId ";
         }
 
         $sql = "SELECT u.id, c_id, title, uc.access_url_id
@@ -280,14 +288,14 @@ class UrlManager
      */
     public static function getCountUrlRelCourse($courseId)
     {
-        $courseId = intval($courseId);
+        $courseId = (int) $courseId;
         $tableUrlRelCourse = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
         $sql = "SELECT *
                 FROM $tableUrlRelCourse
                 WHERE c_id = '$courseId'";
         $res = Database::query($sql);
 
-        return Database::num_rows($res);
+        return (int) Database::num_rows($res);
     }
 
     /**
@@ -301,12 +309,13 @@ class UrlManager
      */
     public static function get_url_rel_session_data($urlId = 0)
     {
+        $urlId = (int) $urlId;
         $where = '';
         $table_url_rel_session = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_SESSION);
         $tbl_session = Database::get_main_table(TABLE_MAIN_SESSION);
 
         if (!empty($urlId)) {
-            $where = "WHERE $table_url_rel_session.access_url_id = ".intval($urlId);
+            $where = "WHERE $table_url_rel_session.access_url_id = ".$urlId;
         }
 
         $sql = "SELECT id, name, access_url_id
@@ -337,8 +346,9 @@ class UrlManager
         $table_url_rel_usergroup = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USERGROUP);
         $table_user_group = Database::get_main_table(TABLE_USERGROUP);
 
+        $urlId = (int) $urlId;
         if (!empty($urlId)) {
-            $where = " WHERE $table_url_rel_usergroup.access_url_id = ".intval($urlId);
+            $where = " WHERE $table_url_rel_usergroup.access_url_id = ".$urlId;
         }
 
         $sql = "SELECT u.id, u.name, access_url_id
@@ -367,11 +377,13 @@ class UrlManager
     {
         $table_url_rel = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE_CATEGORY);
         $table = Database::get_main_table(TABLE_MAIN_CATEGORY);
-        $where = " WHERE 1=1 ";
+        $where = ' WHERE 1=1 ';
+
+        $urlId = (int) $urlId;
         if (!empty($urlId)) {
-            $where .= " AND $table_url_rel.access_url_id = ".intval($urlId);
+            $where .= " AND $table_url_rel.access_url_id = ".$urlId;
         }
-        $where .= " AND (parent_id IS NULL) ";
+        $where .= ' AND (parent_id IS NULL) ';
 
         $sql = "SELECT u.id, name, access_url_id
                 FROM $table u
@@ -483,20 +495,20 @@ class UrlManager
      *
      * @author Julio Montoya
      *
-     * @param int user id
-     * @param int url id
+     * @param int $sessionId
+     * @param int $urlId
      *
      * @return bool true if success
      * */
-    public static function relation_url_session_exist($session_id, $urlId)
+    public static function relation_url_session_exist($sessionId, $urlId)
     {
         $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_SESSION);
-        $session_id = intval($session_id);
-        $urlId = intval($urlId);
+        $sessionId = (int) $sessionId;
+        $urlId = (int) $urlId;
         $sql = "SELECT session_id FROM $table
                 WHERE
-                    access_url_id = ".intval($urlId)." AND
-                    session_id = ".Database::escape_string($session_id);
+                    access_url_id = $urlId AND
+                    session_id = $sessionId ";
         $result = Database::query($sql);
         $num = Database::num_rows($result);
 
@@ -812,16 +824,17 @@ class UrlManager
     public static function add_session_to_url($session_id, $urlId = 1)
     {
         $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_SESSION);
+        $session_id = (int) $session_id;
+        $urlId = (int) $urlId;
+
         if (empty($urlId)) {
             $urlId = 1;
         }
         $result = false;
         $count = self::relation_url_session_exist($session_id, $urlId);
-        $session_id = intval($session_id);
         if (empty($count) && !empty($session_id)) {
-            $urlId = intval($urlId);
             $sql = "INSERT INTO $table
-                    SET session_id = ".intval($session_id).", access_url_id = ".$urlId;
+                    SET session_id = ".$session_id.", access_url_id = ".$urlId;
             try {
                 Database::query($sql);
             } catch (Exception $e) {
@@ -979,9 +992,11 @@ class UrlManager
     public static function update_urls_rel_user($user_list, $urlId)
     {
         $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
+        $urlId = (int) $urlId;
+
         $sql = "SELECT user_id 
                 FROM $table 
-                WHERE access_url_id = ".intval($urlId);
+                WHERE access_url_id = $urlId";
         $result = Database::query($sql);
         $existing_users = [];
 
@@ -1301,6 +1316,7 @@ class UrlManager
                 }
             }
         }
+
         $response->addAssign(
             'ajax_list_courses',
             'innerHTML',

File diff suppressed because it is too large
+ 269 - 147
main/inc/lib/usergroup.lib.php


+ 149 - 87
main/inc/lib/usermanager.lib.php

@@ -163,20 +163,20 @@ class UserManager
      *
      * @param string        $firstName
      * @param string        $lastName
-     * @param int           $status               (1 for course tutor, 5 for student, 6 for anonymous)
+     * @param int           $status                  (1 for course tutor, 5 for student, 6 for anonymous)
      * @param string        $email
      * @param string        $loginName
      * @param string        $password
-     * @param string        $official_code        Any official code (optional)
-     * @param string        $language             User language    (optional)
-     * @param string        $phone                Phone number    (optional)
-     * @param string        $picture_uri          Picture URI        (optional)
-     * @param string        $authSource           Authentication source (defaults to 'platform', dependind on constant)
-     * @param string        $expirationDate       Account expiration date (optional, defaults to null)
-     * @param int           $active               Whether the account is enabled or disabled by default
-     * @param int           $hr_dept_id           The department of HR in which the user is registered (defaults to 0)
-     * @param array         $extra                Extra fields
-     * @param string        $encrypt_method       Used if password is given encrypted. Set to an empty string by default
+     * @param string        $official_code           Any official code (optional)
+     * @param string        $language                User language    (optional)
+     * @param string        $phone                   Phone number    (optional)
+     * @param string        $picture_uri             Picture URI        (optional)
+     * @param string        $authSource              Authentication source (defaults to 'platform', dependind on constant)
+     * @param string        $expirationDate          Account expiration date (optional, defaults to null)
+     * @param int           $active                  Whether the account is enabled or disabled by default
+     * @param int           $hr_dept_id              The department of HR in which the user is registered (defaults to 0)
+     * @param array         $extra                   Extra fields
+     * @param string        $encrypt_method          Used if password is given encrypted. Set to an empty string by default
      * @param bool          $send_mail
      * @param bool          $isAdmin
      * @param string        $address
@@ -184,6 +184,7 @@ class UserManager
      * @param FormValidator $form
      * @param int           $creatorId
      * @param array         $emailTemplate
+     * @param string        $redirectToURLAfterLogin
      *
      * @return mixed new user id - if the new user creation succeeds, false otherwise
      * @desc The function tries to retrieve user id from the session.
@@ -214,9 +215,13 @@ class UserManager
         $sendEmailToAllAdmins = false,
         $form = null,
         $creatorId = 0,
-        $emailTemplate = []
+        $emailTemplate = [],
+        $redirectToURLAfterLogin = ''
     ) {
         $creatorId = empty($creatorId) ? api_get_user_id() : 0;
+        $creatorInfo = api_get_user_info($creatorId);
+        $creatorEmail = isset($creatorInfo['email']) ? $creatorInfo['email'] : '';
+
         $hook = HookCreateUser::create();
         if (!empty($hook)) {
             $hook->notifyCreateUser(HOOK_EVENT_TYPE_PRE);
@@ -419,6 +424,10 @@ class UserManager
                 'false'
             );
 
+            if (!empty($redirectToURLAfterLogin) && api_get_configuration_value('plugin_redirection_enabled')) {
+                RedirectionPlugin::insert($userId, $redirectToURLAfterLogin);
+            }
+
             if (!empty($email) && $send_mail) {
                 $recipient_name = api_get_person_name(
                     $firstName,
@@ -490,7 +499,6 @@ class UserManager
                     EventsDispatcher::events('user_registration', $values);
                 } else {
                     $phoneNumber = isset($extra['mobile_phone_number']) ? $extra['mobile_phone_number'] : null;
-
                     $additionalParameters = [
                         'smsType' => SmsPlugin::WELCOME_LOGIN_PASSWORD,
                         'userId' => $return,
@@ -535,7 +543,8 @@ class UserManager
                             null,
                             null,
                             null,
-                            $additionalParameters
+                            $additionalParameters,
+                            $creatorEmail
                         );
 
                         $layoutContent = $tplContent->get_template('mail/new_user_second_email_confirmation.tpl');
@@ -561,7 +570,8 @@ class UserManager
                             null,
                             null,
                             null,
-                            $additionalParameters
+                            $additionalParameters,
+                            $creatorEmail
                         );
                     } else {
                         if (!empty($emailBodyTemplate)) {
@@ -593,7 +603,8 @@ class UserManager
                                 null,
                                 null,
                                 null,
-                                $additionalParameters
+                                $additionalParameters,
+                                $creatorEmail
                             );
                         }
                     }
@@ -694,6 +705,7 @@ class UserManager
      * @param int $user_id The user id
      *
      * @return bool true if user can be deleted
+     *
      * @assert (null) === false
      * @assert (-1) === false
      * @assert ('abc') === false
@@ -707,12 +719,12 @@ class UserManager
         }
 
         $table_course_user = Database::get_main_table(TABLE_MAIN_COURSE_USER);
-        if ($user_id != strval(intval($user_id))) {
-            return false;
-        }
-        if ($user_id === false) {
+        $user_id = (int) $user_id;
+
+        if (empty($user_id)) {
             return false;
         }
+
         $sql = "SELECT * FROM $table_course_user
                 WHERE status = 1 AND user_id = ".$user_id;
         $res = Database::query($sql);
@@ -809,6 +821,10 @@ class UserManager
                 WHERE user_id = '".$user_id."'";
         Database::query($sql);
 
+        if (api_get_configuration_value('plugin_redirection_enabled')) {
+            RedirectionPlugin::deleteUserRedirection($user_id);
+        }
+
         // Delete user picture
         /* TODO: Logic about api_get_setting('split_users_upload_directory') == 'true'
         a user has 4 different sized photos to be deleted. */
@@ -1159,10 +1175,8 @@ class UserManager
             $hook->notifyUpdateUser(HOOK_EVENT_TYPE_PRE);
         }
         $original_password = $password;
-
-        if ($user_id != strval(intval($user_id))) {
-            return false;
-        }
+        $user_id = (int) $user_id;
+        $creator_id = (int) $creator_id;
 
         if (empty($user_id)) {
             return false;
@@ -1274,7 +1288,7 @@ class UserManager
                 PERSON_NAME_EMAIL_ADDRESS
             );
             $email_admin = api_get_setting('emailAdministrator');
-
+            $url = api_get_path(WEB_PATH);
             if (api_is_multiple_url_enabled()) {
                 $access_url_id = api_get_current_access_url_id();
                 if ($access_url_id != -1) {
@@ -1394,22 +1408,26 @@ class UserManager
         $t_uf = Database::get_main_table(TABLE_EXTRA_FIELD);
         $t_ufv = Database::get_main_table(TABLE_EXTRA_FIELD_VALUES);
         $extraFieldType = EntityExtraField::USER_FIELD_TYPE;
+
+        $original_user_id_name = Database::escape_string($original_user_id_name);
+        $original_user_id_value = Database::escape_string($original_user_id_value);
+
         $sql = "SELECT item_id as user_id
                 FROM $t_uf uf
                 INNER JOIN $t_ufv ufv
-                ON ufv.field_id=uf.id
+                ON ufv.field_id = uf.id
                 WHERE
-                    variable='$original_user_id_name' AND
-                    value='$original_user_id_value' AND
+                    variable = '$original_user_id_name' AND
+                    value = '$original_user_id_value' AND
                     extra_field_type = $extraFieldType
                 ";
         $res = Database::query($sql);
         $row = Database::fetch_object($res);
         if ($row) {
             return $row->user_id;
-        } else {
-            return 0;
         }
+
+        return 0;
     }
 
     /**
@@ -1675,7 +1693,8 @@ class UserManager
         $conditions = [],
         $order_by = [],
         $limit_from = false,
-        $limit_to = false
+        $limit_to = false,
+        $idCampus = null
     ) {
         $user_table = Database::get_main_table(TABLE_MAIN_USER);
         $userUrlTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
@@ -1683,7 +1702,11 @@ class UserManager
         $sql = "SELECT user.* FROM $user_table user ";
 
         if (api_is_multiple_url_enabled()) {
-            $urlId = api_get_current_access_url_id();
+            if ($idCampus) {
+                $urlId = $idCampus;
+            } else {
+                $urlId = api_get_current_access_url_id();
+            }
             $sql .= " INNER JOIN $userUrlTable url_user
                       ON (user.user_id = url_user.user_id)
                       WHERE url_user.access_url_id = $urlId";
@@ -1835,7 +1858,7 @@ class UserManager
         if (empty($userInfo)) {
             $user_table = Database::get_main_table(TABLE_MAIN_USER);
             $sql = "SELECT email, picture_uri FROM $user_table
-                    WHERE id=".$id;
+                    WHERE id = ".$id;
             $res = Database::query($sql);
 
             if (!Database::num_rows($res)) {
@@ -1898,7 +1921,6 @@ class UserManager
         }
 
         $id = (int) $id;
-
         if (empty($userInfo)) {
             $user_table = Database::get_main_table(TABLE_MAIN_USER);
             $sql = "SELECT email, picture_uri FROM $user_table WHERE id = $id";
@@ -2563,6 +2585,8 @@ class UserManager
         }
         $extra_data = self::get_extra_user_data_by_field($user_id, $extra_field);
         $extra_files = $extra_data[$extra_field];
+
+        $files = [];
         if (is_array($extra_files)) {
             foreach ($extra_files as $key => $value) {
                 if (!$full_path) {
@@ -2763,18 +2787,15 @@ class UserManager
         $all_visibility = true,
         $splitmultiple = false
     ) {
-        // A sanity check.
+        $user_id = (int) $user_id;
+
         if (empty($user_id)) {
-            $user_id = 0;
-        } else {
-            if ($user_id != strval(intval($user_id))) {
-                return [];
-            }
+            return [];
         }
+
         $extra_data = [];
         $t_uf = Database::get_main_table(TABLE_EXTRA_FIELD);
         $t_ufv = Database::get_main_table(TABLE_EXTRA_FIELD_VALUES);
-        $user_id = (int) $user_id;
 
         $sql = "SELECT f.id as id, f.variable as fvar, f.field_type as type
                 FROM $t_uf f
@@ -2785,7 +2806,7 @@ class UserManager
         }
 
         $sql .= " AND extra_field_type = ".EntityExtraField::USER_FIELD_TYPE;
-        $sql .= " ORDER BY f.field_order";
+        $sql .= " ORDER BY f.field_order ";
 
         $res = Database::query($sql);
         if (Database::num_rows($res) > 0) {
@@ -2997,6 +3018,7 @@ class UserManager
      *                                           (to give space for courses out of categories)
      * @param bool $ignore_visibility_for_admins optional true if limit time from session is over, false otherwise
      * @param bool $ignoreTimeLimit              ignore time start/end
+     * @param bool $getCount
      *
      * @return array list of statuses [session_category][session_id]
      *
@@ -3009,7 +3031,9 @@ class UserManager
         $ignoreTimeLimit = false,
         $getCount = false
     ) {
-        if ($user_id != strval(intval($user_id))) {
+        $user_id = (int) $user_id;
+
+        if (empty($user_id)) {
             return [];
         }
 
@@ -3025,7 +3049,7 @@ class UserManager
         // LEFT JOIN is used for session_rel_course_rel_user because an inner
         // join would not catch session-courses where the user is general
         // session coach but which do not have students nor coaches registered
-        $dqlSelect = " COUNT(DISTINCT s.id) ";
+        $dqlSelect = ' COUNT(DISTINCT s.id) ';
 
         if (!$getCount) {
             $dqlSelect = " DISTINCT
@@ -3244,7 +3268,7 @@ class UserManager
             $collapsed = '';
             $collapsedAction = '';
             if ($collapsable) {
-                $collapsableData = Sessionmanager::getCollapsableData(
+                $collapsableData = SessionManager::getCollapsableData(
                     $user_id,
                     $session_id,
                     $extraField,
@@ -3289,13 +3313,14 @@ class UserManager
         $tbl_course_user = Database::get_main_table(TABLE_MAIN_COURSE_USER);
         $tbl_session_course_user = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
 
-        if ($user_id != strval(intval($user_id))) {
+        $user_id = (int) $user_id;
+
+        if (empty($user_id)) {
             return [];
         }
 
         // We filter the courses from the URL
         $join_access_url = $where_access_url = '';
-
         if (api_get_multiple_access_url()) {
             $access_url_id = api_get_current_access_url_id();
             if ($access_url_id != -1) {
@@ -3508,9 +3533,8 @@ class UserManager
 
         $user_id = (int) $user_id;
         $session_id = (int) $session_id;
-        //we filter the courses from the URL
+        // We filter the courses from the URL
         $join_access_url = $where_access_url = '';
-
         if (api_get_multiple_access_url()) {
             $urlId = api_get_current_access_url_id();
             if ($urlId != -1) {
@@ -3687,8 +3711,9 @@ class UserManager
         $resourceType = 'all'
     ) {
         $return = '';
+        $user_id = (int) $user_id;
+
         if (!empty($user_id) && !empty($course)) {
-            $user_id = (int) $user_id;
             $path = api_get_path(SYS_COURSE_PATH).$course.'/document/shared_folder/sf_user_'.$user_id.'/';
             $web_path = api_get_path(WEB_COURSE_PATH).$course.'/document/shared_folder/sf_user_'.$user_id.'/';
             $file_list = [];
@@ -3918,7 +3943,8 @@ class UserManager
      */
     public static function is_admin($user_id)
     {
-        if (empty($user_id) || $user_id != strval(intval($user_id))) {
+        $user_id = (int) $user_id;
+        if (empty($user_id)) {
             return false;
         }
         $admin_table = Database::get_main_table(TABLE_MAIN_ADMIN);
@@ -4257,6 +4283,8 @@ class UserManager
         // database table definition
         $table_user_tag = Database::get_main_table(TABLE_MAIN_TAG);
         $table_user_tag_values = Database::get_main_table(TABLE_MAIN_USER_REL_TAG);
+        $user_id = (int) $user_id;
+
         $tags = self::get_user_tags($user_id, $field_id);
         if (is_array($tags) && count($tags) > 0) {
             foreach ($tags as $key => $tag) {
@@ -4524,7 +4552,6 @@ class UserManager
             FormValidator::LAYOUT_HORIZONTAL
         );
 
-
         $query = Security::remove_XSS($query);
 
         if (!empty($query)) {
@@ -4936,7 +4963,6 @@ class UserManager
             $userConditions .= " AND u.last_login <= '$lastConnectionDate' ";
         }
 
-        $courseConditions = null;
         $sessionConditionsCoach = null;
         $sessionConditionsTeacher = null;
         $drhConditions = null;
@@ -5148,8 +5174,8 @@ class UserManager
         $userRelUserTable = Database::get_main_table(TABLE_MAIN_USER_REL_USER);
         $userRelAccessUrlTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
 
-        $userId = intval($userId);
-        $relationType = intval($relationType);
+        $userId = (int) $userId;
+        $relationType = (int) $relationType;
         $affectedRows = 0;
 
         if ($deleteOtherAssignedUsers) {
@@ -5195,17 +5221,20 @@ class UserManager
         // Inserting new user list
         if (is_array($subscribedUsersId)) {
             foreach ($subscribedUsersId as $subscribedUserId) {
-                $subscribedUserId = intval($subscribedUserId);
-                $sql = "SELECT id FROM $userRelUserTable
-                        WHERE user_id = $subscribedUserId
-                        AND friend_user_id = $userId
-                        AND relation_type = $relationType";
+                $subscribedUserId = (int) $subscribedUserId;
+                $sql = "SELECT id 
+                        FROM $userRelUserTable
+                        WHERE 
+                            user_id = $subscribedUserId AND 
+                            friend_user_id = $userId AND 
+                            relation_type = $relationType";
+
                 $result = Database::query($sql);
                 $num = Database::num_rows($result);
                 if ($num === 0) {
                     $date = api_get_utc_datetime();
                     $sql = "INSERT INTO $userRelUserTable (user_id, friend_user_id, relation_type, last_edit)
-                        VALUES ($subscribedUserId, $userId, $relationType, '$date')";
+                            VALUES ($subscribedUserId, $userId, $relationType, '$date')";
                     $result = Database::query($sql);
                     $affectedRows += Database::affected_rows($result);
                 }
@@ -5225,10 +5254,9 @@ class UserManager
      */
     public static function is_user_followed_by_drh($user_id, $hr_dept_id)
     {
-        // Database table and variables Definitions
         $tbl_user_rel_user = Database::get_main_table(TABLE_MAIN_USER_REL_USER);
-        $user_id = intval($user_id);
-        $hr_dept_id = intval($hr_dept_id);
+        $user_id = (int) $user_id;
+        $hr_dept_id = (int) $hr_dept_id;
         $result = false;
 
         $sql = "SELECT user_id FROM $tbl_user_rel_user
@@ -5257,6 +5285,11 @@ class UserManager
         $table_user = Database::get_main_table(TABLE_MAIN_USER);
         $table_course_user = Database::get_main_table(TABLE_MAIN_COURSE_USER);
         $table_session_course_user = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
+
+        if (empty($courseInfo)) {
+            return false;
+        }
+
         $courseId = $courseInfo['real_id'];
 
         if ($session == 0 || is_null($session)) {
@@ -5304,18 +5337,23 @@ class UserManager
      */
     public static function is_user_certified($cat_id, $user_id)
     {
-        $table_certificate = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
-        $sql = 'SELECT path_certificate FROM '.$table_certificate.'
+        $cat_id = (int) $cat_id;
+        $user_id = (int) $user_id;
+
+        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
+        $sql = 'SELECT path_certificate 
+                FROM '.$table.'
                 WHERE
-                    cat_id="'.intval($cat_id).'" AND
-                    user_id="'.intval($user_id).'"';
+                    cat_id = "'.$cat_id.'" AND
+                    user_id = "'.$user_id.'"';
         $rs = Database::query($sql);
         $row = Database::fetch_array($rs);
+
         if ($row['path_certificate'] == '' || is_null($row['path_certificate'])) {
             return false;
-        } else {
-            return true;
         }
+
+        return true;
     }
 
     /**
@@ -5385,7 +5423,7 @@ class UserManager
         $table_gradebook_category = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
 
         $session_id = api_get_session_id();
-        $user_id = intval($user_id);
+        $user_id = (int) $user_id;
         if ($session_id == 0 || is_null($session_id)) {
             $sql_session = 'AND (session_id='.intval($session_id).' OR isnull(session_id)) ';
         } elseif ($session_id > 0) {
@@ -5519,7 +5557,7 @@ class UserManager
     public static function remove_user_admin($userId)
     {
         $table_admin = Database::get_main_table(TABLE_MAIN_ADMIN);
-        $userId = intval($userId);
+        $userId = (int) $userId;
         if (self::is_admin($userId)) {
             $sql = "DELETE FROM $table_admin WHERE user_id = $userId";
             Database::query($sql);
@@ -5781,8 +5819,9 @@ class UserManager
         $from = '',
         $until = ''
     ) {
-        $userId = intval($userId);
-        $sessionId = intval($sessionId);
+        $userId = (int) $userId;
+        $sessionId = (int) $sessionId;
+
         $trackCourseAccessTable = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
         $whereConditions = [
             'user_id = ? ' => $userId,
@@ -5820,7 +5859,7 @@ class UserManager
      */
     public static function getFirstStudentBoss($userId)
     {
-        $userId = intval($userId);
+        $userId = (int) $userId;
         if ($userId > 0) {
             $userRelTable = Database::get_main_table(TABLE_MAIN_USER_REL_USER);
             $row = Database::select(
@@ -5853,6 +5892,7 @@ class UserManager
     public static function getStudentBossList($userId)
     {
         $userId = (int) $userId;
+
         if ($userId > 0) {
             $userRelTable = Database::get_main_table(TABLE_MAIN_USER_REL_USER);
             $result = Database::select(
@@ -5908,9 +5948,9 @@ class UserManager
                 $userInfo['complete_name_with_username'],
                 $userInfo['profile_url']
             );
-        } else {
-            return get_lang('Anonymous');
         }
+
+        return get_lang('Anonymous');
     }
 
     /**
@@ -6067,12 +6107,10 @@ SQL;
             Session::write('is_allowedCreateCourse', $userInfo['status'] == 1);
             // will be useful later to know if the user is actually an admin or not (example reporting)
             Session::write('login_as', true);
-
             $logInfo = [
                 'tool' => 'login',
                 'tool_id' => 0,
                 'tool_id_detail' => 0,
-                'action' => '',
                 'info' => $userId,
             ];
             Event::registerLog($logInfo);
@@ -6220,9 +6258,13 @@ SQL;
     public static function anonymize($userId, $deleteIP = true)
     {
         global $debug;
+
+        $userId = (int) $userId;
+
         if (empty($userId)) {
             return false;
         }
+
         $em = Database::getManager();
         $user = api_get_user_entity($userId);
         $uniqueId = uniqid('anon', true);
@@ -6499,11 +6541,10 @@ SQL;
         $timezone = new DateTimeZone(api_get_timezone());
 
         $sessions = [];
-
         if (DRH == $userInfo['status']) {
             $sessions = SessionManager::get_sessions_followed_by_drh($userInfo['id']);
-        } elseif (SESSIONADMIN == $userInfo['status']) {
-            $sessions = SessionManager::getSessionsAdmin($userInfo['id']);
+        } elseif (api_is_platform_admin(true)) {
+            $sessions = SessionManager::getSessionsForAdmin($userInfo['id']);
         } else {
             $sessionsByCategory = self::get_sessions_by_category($userInfo['id'], false, true, true);
             $sessionsByCategory = array_column($sessionsByCategory, 'sessions');
@@ -6663,6 +6704,27 @@ SQL;
         return $calendar;
     }
 
+    /**
+     * Return the user's full name. Optionally with the username.
+     *
+     * @param User $user
+     * @param bool $includeUsername Optional. By default username is not included.
+     *
+     * @return string
+     */
+    public static function formatUserFullName(User $user, $includeUsername = false)
+    {
+        $fullName = api_get_person_name($user->getFirstname(), $user->getLastname());
+
+        if ($includeUsername && api_get_configuration_value('hide_username_with_complete_name') !== true) {
+            $username = $user->getUsername();
+
+            return "$fullName ($username)";
+        }
+
+        return $fullName;
+    }
+
     /**
      * @return EncoderFactory
      */
@@ -6702,13 +6764,13 @@ SQL;
      */
     private static function change_active_state($user_id, $active)
     {
-        if (strval(intval($user_id)) != $user_id) {
-            return false;
-        }
-        if ($user_id < 1) {
+        $user_id = (int) $user_id;
+        $active = (int) $active;
+
+        if (empty($user_id)) {
             return false;
         }
-        $user_id = intval($user_id);
+
         $table_user = Database::get_main_table(TABLE_MAIN_USER);
         $sql = "UPDATE $table_user SET active = '$active' WHERE id = $user_id";
         $r = Database::query($sql);

+ 7 - 2
main/link/link.php

@@ -212,12 +212,17 @@ switch ($action) {
         exit;
         break;
     case 'export':
-        $content = Link::listLinksAndCategories($course_id, $session_id, $categoryId, $show, null, false);
+        $content = Link::listLinksAndCategories($course_id, $session_id, $categoryId, $show, null, false, true);
+        $courseInfo = api_get_course_info_by_id($course_id);
+        if (!empty($session_id)) {
+            $sessionInfo = api_get_session_info($session_id);
+            $courseInfo['title'] = $courseInfo['title'].' '.$sessionInfo['name'];
+        }
         $pdf = new PDF();
         $pdf->content_to_pdf(
             $content,
             null,
-            $courseInfo['code'].get_lang('Link'),
+            $courseInfo['title'].'_'.get_lang('Link'),
             $courseInfo['code'],
             'D',
             false,

Some files were not shown because too many files changed in this diff