Browse Source

Merge branch 'ofaj_dev' of https://github.com/chamilo/chamilo-lms into ofaj_dev

Alex Aragón 6 years ago
parent
commit
0acb157898
100 changed files with 1845 additions and 1410 deletions
  1. 10 1
      app/Resources/public/css/base.css
  2. 1 1
      extrafield.php
  3. 2 5
      load_search.php
  4. 14 10
      main/admin/access_urls.php
  5. 5 5
      main/admin/course_user_import.php
  6. 1 1
      main/admin/course_user_import_by_email.php
  7. 0 1
      main/admin/skill_translate.php
  8. 2 2
      main/admin/subscribe_user2course.php
  9. 1 1
      main/admin/user_import.php
  10. 3 3
      main/admin/user_information.php
  11. 1 1
      main/admin/user_update_import.php
  12. 29 13
      main/announcements/announcements.php
  13. 1 1
      main/attendance/attendance_controller.php
  14. 1 1
      main/attendance/attendance_sheet.php
  15. 1 0
      main/attendance/index.php
  16. 1 1
      main/auth/external_login/functions.inc.php
  17. 1 1
      main/auth/external_login/newUser.php
  18. 3 3
      main/auth/inscription.php
  19. 0 1
      main/auth/ldap/syncro_users.php
  20. 1 1
      main/auth/sso/sso.Drupal.class.php
  21. 2 2
      main/auth/sso/sso.TCC.class.php
  22. 9 0
      main/chat/chat.php
  23. 11 9
      main/course_description/index.php
  24. 1 3
      main/course_home/activity.php
  25. 1 1
      main/course_home/course_home.php
  26. 1 2
      main/course_info/infocours.php
  27. 1 0
      main/course_progress/index.php
  28. 5 5
      main/coursecopy/import_moodle.php
  29. 1 1
      main/cron/import_csv.php
  30. 1 2
      main/document/showinframes.php
  31. 1 1
      main/dropbox/dropbox_functions.inc.php
  32. 0 1
      main/exercise/MatchingDraggable.php
  33. 24 8
      main/exercise/MultipleAnswerTrueFalseDegreeCertainty.php
  34. 1 1
      main/exercise/TestCategory.php
  35. 1 1
      main/exercise/hotspot_answers.as.php
  36. 2 4
      main/exercise/overview.php
  37. 5 15
      main/forum/forumfunction.inc.php
  38. 0 1
      main/forum/index.php
  39. 0 1
      main/forum/newthread.php
  40. 1 1
      main/forum/reply.php
  41. 7 7
      main/forum/viewforum.php
  42. 1 1
      main/forum/viewthread.php
  43. 3 3
      main/gradebook/lib/be/category.class.php
  44. 1 1
      main/gradebook/lib/fe/gradebooktable.class.php
  45. 2 2
      main/gradebook/lib/fe/linkform.class.php
  46. 1 1
      main/gradebook/lib/fe/resulttable.class.php
  47. 1 1
      main/gradebook/lib/gradebook_data_generator.class.php
  48. 1 0
      main/group/group_category.php
  49. 22 10
      main/group/group_space.php
  50. 2 1
      main/group/import.php
  51. 8 0
      main/inc/ajax/course.ajax.php
  52. 0 1
      main/inc/ajax/course_chat.ajax.php
  53. 5 4
      main/inc/ajax/document.ajax.php
  54. 5 1
      main/inc/ajax/exercise.ajax.php
  55. 1 1
      main/inc/ajax/model.ajax.php
  56. 2 1
      main/inc/ajax/statistics.ajax.php
  57. 46 0
      main/inc/ajax/survey.ajax.php
  58. 22 14
      main/inc/ajax/user_manager.ajax.php
  59. 10 1
      main/inc/ajax/work.ajax.php
  60. 6 1
      main/inc/email_editor.php
  61. 0 6
      main/inc/global.inc.php
  62. 1 0
      main/inc/introductionSection.inc.php
  63. 13 4
      main/inc/lib/CourseChatUtils.php
  64. 130 2
      main/inc/lib/CoursesAndSessionsCatalog.class.php
  65. 3 3
      main/inc/lib/ScheduledAnnouncement.php
  66. 51 51
      main/inc/lib/TicketManager.php
  67. 117 0
      main/inc/lib/UnserializeApi.php
  68. 45 9
      main/inc/lib/agenda.lib.php
  69. 76 19
      main/inc/lib/api.lib.php
  70. 46 38
      main/inc/lib/attendance.lib.php
  71. 1 1
      main/inc/lib/banner.lib.php
  72. 1 1
      main/inc/lib/blog.lib.php
  73. 6 1
      main/inc/lib/certificate.lib.php
  74. 2 4
      main/inc/lib/chat.lib.php
  75. 271 192
      main/inc/lib/course.lib.php
  76. 49 2
      main/inc/lib/course_category.lib.php
  77. 2 0
      main/inc/lib/database.constants.inc.php
  78. 0 1
      main/inc/lib/database.lib.php
  79. 32 16
      main/inc/lib/display.lib.php
  80. 152 86
      main/inc/lib/document.lib.php
  81. 26 7
      main/inc/lib/events.lib.php
  82. 34 9
      main/inc/lib/exercise.lib.php
  83. 0 5
      main/inc/lib/extra_field.lib.php
  84. 2 2
      main/inc/lib/extra_field_option.lib.php
  85. 2 0
      main/inc/lib/extra_field_value.lib.php
  86. 1 0
      main/inc/lib/fixlinks.js
  87. 67 67
      main/inc/lib/formvalidator/Element/DateTimeRangePicker.php
  88. 2 4
      main/inc/lib/formvalidator/Element/HtmlEditor.php
  89. 218 254
      main/inc/lib/formvalidator/FormValidator.class.php
  90. 1 1
      main/inc/lib/glossary.lib.php
  91. 28 16
      main/inc/lib/groupmanager.lib.php
  92. 2 4
      main/inc/lib/internationalization.lib.php
  93. 2 2
      main/inc/lib/legal.lib.php
  94. 15 0
      main/inc/lib/link.lib.php
  95. 112 112
      main/inc/lib/lp_item.lib.php
  96. 16 5
      main/inc/lib/message.lib.php
  97. 27 72
      main/inc/lib/myspace.lib.php
  98. 0 201
      main/inc/lib/nanogong/LICENSE
  99. 0 11
      main/inc/lib/nanogong/NOTICE
  100. 0 39
      main/inc/lib/nanogong/README

+ 10 - 1
app/Resources/public/css/base.css

@@ -9418,6 +9418,12 @@ ul.dropdown-menu.inner > li > a {
     margin: 16px 6px;
 }
 
+/* end document mp3 preview */
+
+#announcement_preview_result .label {
+    display: inline-table;
+}
+
 .forum_revision {
     background-color: #fff3cd;
 }
@@ -9426,7 +9432,10 @@ ul.dropdown-menu.inner > li > a {
     max-width: 30px;
 }
 
-/* end document mp3 preview */
+.chatboxcontent .status {
+    float:left;
+    margin-right: 10px;
+}
 
 /* Fix grid with one row and multiples columns. Works for .col-sm-6.col-md-4.col-lg-3 */
 .row.clear-rows-6-4-3 {

+ 1 - 1
extrafield.php

@@ -75,4 +75,4 @@ foreach ($variables as $variable => $data) {
     }
     $id = $extraField->save($params);
     //$extraField->delete($id);
-}
+}

+ 2 - 5
load_search.php

@@ -329,7 +329,7 @@ $extra = $extraField->addElements(
 $fieldsToShow = [
     'heures_disponibilite_par_semaine',
     'moment_de_disponibilite',
-    'langue_cible'
+    'langue_cible',
 ];
 
 $extra = $extraFieldUser->addElements(
@@ -387,9 +387,8 @@ $extra = $extraField->addElements(
     true
 );
 
-
 $fieldsToShow = [
-    'langue_cible'
+    'langue_cible',
 ];
 
 $extra = $extraFieldUser->addElements(
@@ -659,7 +658,6 @@ if ($form->validate()) {
             'item_id' => $userToLoad,
             'extra_heures_disponibilite_par_semaine' => isset($userData['extra_heures_disponibilite_par_semaine']) ? $userData['extra_heures_disponibilite_par_semaine'] : '',
             'extra_langue_cible' => isset($userData['extra_langue_cible']) ? $userData['extra_langue_cible'] : '',
-
         ];
         $extraFieldValue->saveFieldValues(
             $userDataToSave,
@@ -929,7 +927,6 @@ if (!empty($filterToSend)) {
     $url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_sessions&_search=true&load_extra_field='.$extraFieldListToString.'&_force_search=true&rows=20&page=1&sidx=&sord=asc';
 }
 
-
 // Autowidth
 $extra_params['autowidth'] = 'true';
 

+ 14 - 10
main/admin/access_urls.php

@@ -102,7 +102,8 @@ if ($current_access_url_id == -1) {
     );
     if ($quant == 0) {
         echo Display::return_message(
-            '<a href="'.api_get_self().'?action=register&sec_token='.$parameters['sec_token'].'">'.get_lang('ClickToRegisterAdmin').'</a>',
+            '<a href="'.api_get_self().'?action=register&sec_token='.$parameters['sec_token'].'">'.
+            get_lang('ClickToRegisterAdmin').'</a>',
             'warning',
             false
         );
@@ -139,23 +140,22 @@ echo Display::url(
 
 echo '</div>';
 
-$sortable_data = UrlManager::get_url_data();
+$data = UrlManager::get_url_data();
 $urls = [];
-foreach ($sortable_data as $row) {
+foreach ($data as $row) {
     //title
     $url = Display::url($row['url'], $row['url'], ['target' => '_blank']);
     $description = $row['description'];
+    $createdAt = api_get_local_time($row['tms']);
 
     //Status
     $active = $row['active'];
+    $action = 'unlock';
+    $image = 'wrong';
     if ($active == '1') {
         $action = 'lock';
         $image = 'right';
     }
-    if ($active == '0') {
-        $action = 'unlock';
-        $image = 'wrong';
-    }
     // you cannot lock the default
     if ($row['id'] == '1') {
         $status = Display::return_icon($image.'.gif', get_lang(ucfirst($action)));
@@ -165,12 +165,15 @@ foreach ($sortable_data as $row) {
     }
     // Actions
     $url_id = $row['id'];
-    $actions = Display::url(Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_SMALL), "access_url_edit.php?url_id=$url_id");
+    $actions = Display::url(
+        Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_SMALL),
+        "access_url_edit.php?url_id=$url_id"
+    );
     if ($url_id != '1') {
         $actions .= '<a href="access_urls.php?action=delete_url&amp;url_id='.$url_id.'" onclick="javascript:if(!confirm('."'".addslashes(api_htmlentities(get_lang("ConfirmYourChoice"), ENT_QUOTES, $charset))."'".')) return false;">'.
             Display::return_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_SMALL).'</a>';
     }
-    $urls[] = [$url, $description, $status, $actions];
+    $urls[] = [$url, $description, $status, $createdAt, $actions];
 }
 
 $table = new SortableTableFromArrayConfig($urls, 2, 50, 'urls');
@@ -178,7 +181,8 @@ $table->set_additional_parameters($parameters);
 $table->set_header(0, 'URL');
 $table->set_header(1, get_lang('Description'));
 $table->set_header(2, get_lang('Active'));
-$table->set_header(3, get_lang('Modify'), false);
+$table->set_header(3, get_lang('CreatedAt'));
+$table->set_header(4, get_lang('Modify'), false);
 $table->display();
 
 Display :: display_footer();

+ 5 - 5
main/admin/course_user_import.php

@@ -108,11 +108,11 @@ function save_data($users_courses)
                 $courseInfo = $courseListById[$courseId];
                 $courseCode = $courseInfo['code'];
 
-                CourseManager::subscribe_user(
-                        $user_id,
-                        $courseCode,
-                        $csv_subscriptions[$courseId]
-                    );
+                CourseManager::subscribeUser(
+                    $user_id,
+                    $courseCode,
+                    $csv_subscriptions[$courseId]
+                );
                 $inserted_in_course[$courseInfo['code']] = $courseInfo['title'];
             }
         }

+ 1 - 1
main/admin/course_user_import_by_email.php

@@ -104,7 +104,7 @@ function save_data($users_courses)
                     $course_info = api_get_course_info($course_code);
                     $inserted_in_course[$course_code] = $course_info['title'];
 
-                    CourseManager::subscribe_user(
+                    CourseManager::subscribeUser(
                         $user_id,
                         $course_code,
                         $csv_subscriptions[$course_code]

+ 0 - 1
main/admin/skill_translate.php

@@ -2,7 +2,6 @@
 /* For licensing terms, see /license.txt */
 
 use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
-use Chamilo\CoreBundle\Entity\ExtraField;
 use Chamilo\CoreBundle\Entity\Language;
 use Chamilo\CoreBundle\Entity\Skill;
 

+ 2 - 2
main/admin/subscribe_user2course.php

@@ -99,7 +99,7 @@ if (isset($_POST['form_sent']) && $_POST['form_sent']) {
                 foreach ($users as $user_id) {
                     $user = api_get_user_info($user_id);
                     if ($user['status'] != DRH) {
-                        CourseManager::subscribe_user($user_id, $course_code);
+                        CourseManager::subscribeUser($user_id, $course_code);
                     } else {
                         $errorDrh = 1;
                     }
@@ -306,7 +306,7 @@ if (is_array($extra_field_list)) {
      <b><?php echo get_lang('CourseList'); ?> :</b>
      <br/><br/>
         <?php echo get_lang('FirstLetterCourse'); ?> :
-     <select name="firstLetterCourse" 
+     <select name="firstLetterCourse"
         onchange="javascript:document.formulaire.form_sent.value='2'; document.formulaire.submit();"
         aria-label="<?php echo get_lang('FirstLetterCourse'); ?>">
       <option value="">--</option>

+ 1 - 1
main/admin/user_import.php

@@ -212,7 +212,7 @@ function save_data($users)
             if (isset($user['Courses']) && is_array($user['Courses'])) {
                 foreach ($user['Courses'] as $course) {
                     if (CourseManager::course_exists($course)) {
-                        CourseManager::subscribe_user($user_id, $course, $user['Status']);
+                        CourseManager::subscribeUser($user_id, $course, $user['Status']);
                         $course_info = CourseManager::get_course_information($course);
                         $inserted_in_course[$course] = $course_info['title'];
                     }

+ 3 - 3
main/admin/user_information.php

@@ -526,11 +526,11 @@ if (isset($_GET['action'])) {
                 api_get_path(WEB_PATH)
             );
             MessageManager::send_message_simple($userId, $subject, $content);
-	    Display::addFlash(Display::return_message(get_lang('Sent')));*/
+        Display::addFlash(Display::return_message(get_lang('Sent')));*/
             // ofaj
             LegalManager::sendLegal($userId);
             break;
-	case 'delete_legal':
+    case 'delete_legal':
             // ofaj
             LegalManager::deleteLegal($userId);
             /*$extraFieldValue = new ExtraFieldValue('user');
@@ -541,7 +541,7 @@ if (isset($_GET['action'])) {
             $result = $extraFieldValue->delete($value['id']);
             if ($result) {
                 Display::addFlash(Display::return_message(get_lang('Deleted')));
-	    }*/
+        }*/
             break;
         case 'unsubscribe':
             $courseId = !empty($_GET['course_id']) ? (int) $_GET['course_id'] : 0;

+ 1 - 1
main/admin/user_update_import.php

@@ -199,7 +199,7 @@ function updateUsers($users)
             if (is_array($user['Courses'])) {
                 foreach ($user['Courses'] as $course) {
                     if (CourseManager::course_exists($course)) {
-                        CourseManager::subscribe_user($user_id, $course, $user['Status']);
+                        CourseManager::subscribeUser($user_id, $course, $user['Status']);
                         $course_info = CourseManager::get_course_information($course);
                         $inserted_in_course[$course] = $course_info['title'];
                     }

+ 29 - 13
main/announcements/announcements.php

@@ -41,7 +41,6 @@ $allowToEdit = (
 
 $sessionId = api_get_session_id();
 $drhHasAccessToSessionContent = api_drh_can_access_all_session_content();
-
 if (!empty($sessionId) && $drhHasAccessToSessionContent) {
     $allowToEdit = $allowToEdit || api_is_drh();
 }
@@ -66,12 +65,12 @@ $isTutor = false;
 if (!empty($group_id)) {
     $groupProperties = GroupManager:: get_group_properties($group_id);
     $interbreadcrumb[] = [
-        "url" => api_get_path(WEB_CODE_PATH)."group/group.php?".api_get_cidreq(),
-        "name" => get_lang('Groups'),
+        'url' => api_get_path(WEB_CODE_PATH)."group/group.php?".api_get_cidreq(),
+        'name' => get_lang('Groups'),
     ];
     $interbreadcrumb[] = [
-        "url" => api_get_path(WEB_CODE_PATH)."group/group_space.php?".api_get_cidreq(),
-        "name" => get_lang('GroupSpace').' '.$groupProperties['name'],
+        'url' => api_get_path(WEB_CODE_PATH)."group/group_space.php?".api_get_cidreq(),
+        'name' => get_lang('GroupSpace').' '.$groupProperties['name'],
     ];
 
     if ($allowToEdit === false) {
@@ -86,7 +85,7 @@ if (!empty($group_id)) {
 /* Tracking */
 Event::event_access_tool(TOOL_ANNOUNCEMENT);
 
-$announcement_id = isset($_GET['id']) ? intval($_GET['id']) : null;
+$announcement_id = isset($_GET['id']) ? (int) $_GET['id'] : null;
 $action = isset($_GET['action']) ? Security::remove_XSS($_GET['action']) : 'list';
 $announcement_number = AnnouncementManager::getNumberAnnouncements();
 
@@ -94,6 +93,14 @@ $homeUrl = api_get_self().'?action=list&'.api_get_cidreq();
 $content = '';
 $searchFormToString = '';
 
+$logInfo = [
+    'tool' => TOOL_ANNOUNCEMENT,
+    'tool_id' => 0,
+    'tool_id_detail' => 0,
+    'action' => $action,
+];
+Event::registerLog($logInfo);
+
 switch ($action) {
     case 'move':
         if (!$allowToEdit) {
@@ -118,8 +125,9 @@ switch ($action) {
 
             $announcementInfo = AnnouncementManager::get_by_id($courseId, $thisAnnouncementId);
             $sql = "SELECT DISTINCT announcement.id, announcement.display_order
-                    FROM $tbl_announcement announcement,
-                    $tbl_item_property itemproperty
+                    FROM $tbl_announcement announcement 
+                    INNER JOIN $tbl_item_property itemproperty
+                    ON (announcement.c_id = itemproperty.c_id)
                     WHERE
                         announcement.c_id = $courseId AND
                         itemproperty.c_id = $courseId AND
@@ -156,8 +164,8 @@ switch ($action) {
         break;
     case 'view':
         $interbreadcrumb[] = [
-            "url" => api_get_path(WEB_CODE_PATH).'announcements/announcements.php?'.api_get_cidreq(),
-            "name" => $nameTools,
+            'url' => api_get_path(WEB_CODE_PATH).'announcements/announcements.php?'.api_get_cidreq(),
+            'name' => $nameTools,
         ];
 
         $nameTools = get_lang('View');
@@ -309,8 +317,16 @@ switch ($action) {
         if ($sessionId != 0 && api_is_allowed_to_session_edit(false, true) == false) {
             api_not_allowed();
         }
+        $delete = false;
+        if (api_is_platform_admin()) {
+            $delete = true;
+        }
 
         if (!api_is_session_general_coach() || api_is_element_in_the_session(TOOL_ANNOUNCEMENT, $id)) {
+            $delete = true;
+        }
+
+        if ($delete) {
             AnnouncementManager::delete_announcement($_course, $id);
             Display::addFlash(Display::return_message(get_lang('AnnouncementDeleted')));
         }
@@ -329,7 +345,7 @@ switch ($action) {
         }
         break;
     case 'delete_attachment':
-        $id = $_GET['id_attach'];
+        $id = (int) $_GET['id_attach'];
 
         if (api_is_allowed_to_edit()) {
             AnnouncementManager::delete_announcement_attachment_file($id);
@@ -382,7 +398,7 @@ switch ($action) {
         $url = api_get_self().'?action='.$action.'&id='.$id.'&'.api_get_cidreq();
 
         $form = new FormValidator(
-            'f1',
+            'announcement',
             'post',
             $url,
             null,
@@ -450,7 +466,7 @@ switch ($action) {
                 );
                 // when we want to remind the users who have never been active
                 // then we have a different subject and content for the announcement
-                if ($_GET['since'] == 'never') {
+                if ($_GET['since'] === 'never') {
                     $title_to_modify = sprintf(
                         get_lang('RemindInactiveLearnersMailSubject'),
                         api_get_setting('siteName')

+ 1 - 1
main/attendance/attendance_controller.php

@@ -303,7 +303,7 @@ class AttendanceController
         $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh(
             api_get_user_id(),
             api_get_course_info()
-        );
+        ) || api_is_drh();
 
         if ($edit == true) {
             if (api_is_allowed_to_edit(null, true) || $isDrhOfCourse) {

+ 1 - 1
main/attendance/attendance_sheet.php

@@ -16,7 +16,7 @@ api_protect_course_script(true);
 $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh(
     api_get_user_id(),
     api_get_course_info()
-);
+) || api_is_drh();
 
 if (api_is_allowed_to_edit(null, true) ||
     api_is_coach(api_get_session_id(), api_get_course_int_id()) ||

+ 1 - 0
main/attendance/index.php

@@ -187,6 +187,7 @@ $(function() {
 </script>';
 $student_param = '';
 $student_id = null;
+
 if (api_is_drh() && isset($_GET['student_id'])) {
     $student_id = intval($_GET['student_id']);
     $student_param = '&student_id='.$student_id;

+ 1 - 1
main/auth/external_login/functions.inc.php

@@ -216,7 +216,7 @@ function external_update_user($new_user)
         $autoSubscribe = explode('|', $u['courses']);
         foreach ($autoSubscribe as $code) {
             if (CourseManager::course_exists($code)) {
-                CourseManager::subscribe_user($u['user_id'], $code);
+                CourseManager::subscribeUser($u['user_id'], $code, STUDENT);
             }
         }
     }

+ 1 - 1
main/auth/external_login/newUser.php

@@ -34,7 +34,7 @@ if ($user !== false && ($chamilo_uid = external_add_user($user)) !== false) {
         $autoSubscribe = explode('|', $user['courses']);
         foreach ($autoSubscribe as $code) {
             if (CourseManager::course_exists($code)) {
-                CourseManager::subscribe_user($_user['user_id'], $code);
+                CourseManager::subscribeUser($_user['user_id'], $code);
             }
         }
     }

+ 3 - 3
main/auth/inscription.php

@@ -22,7 +22,7 @@ $allowedFields = [
     'status',
     'language',
     'extra_fields',
-    'address'
+    'address',
 ];
 
 $allowedFieldsConfiguration = api_get_configuration_value('allow_fields_inscription');
@@ -974,7 +974,7 @@ if ($form->validate()) {
                         ]
                     )
                     ) {
-                        CourseManager::subscribe_user(
+                        CourseManager::subscribeUser(
                             $user_id,
                             $course_info['code']
                         );
@@ -1274,7 +1274,7 @@ if ($form->validate()) {
                         ]
                     )
                     ) {
-                        CourseManager::subscribe_user(
+                        CourseManager::subscribeUser(
                             $user_id,
                             $course_info['code']
                         );

+ 0 - 1
main/auth/ldap/syncro_users.php

@@ -1,6 +1,5 @@
 <?php
 
-
 require_once '../../inc/global.inc.php';
 require_once 'authldap.php';
 

+ 1 - 1
main/auth/sso/sso.Drupal.class.php

@@ -91,7 +91,7 @@ class ssoDrupal
     /**
      * Validates the received active connection data with the database.
      *
-     * @return null|false Return the loginFailed variable value to local.inc.php
+     * @return false|null Return the loginFailed variable value to local.inc.php
      */
     public function check_user()
     {

+ 2 - 2
main/auth/sso/sso.TCC.class.php

@@ -89,7 +89,7 @@ class ssoTCC
     /**
      * Validates the received active connection data with the database.
      *
-     * @return null|false Return the loginFailed variable value to local.inc.php
+     * @return false|null Return the loginFailed variable value to local.inc.php
      */
     public function check_user()
     {
@@ -297,7 +297,7 @@ class ssoTCC
                 // In all other cases, generate a link to the Drupal profile edition
                 $drupalUserId = $drupalUserIdData['value'];
                 $url = "{$this->protocol}{$this->domain}/user/{$drupalUserId}/edit";
-        
+
                 return $url;
         */
         return api_get_path(WEB_CODE_PATH).'auth/profile.php';

+ 9 - 0
main/chat/chat.php

@@ -9,6 +9,15 @@ api_protect_course_script(true);
 
 Event::event_access_tool(TOOL_CHAT);
 
+$logInfo = [
+    'tool' => TOOL_CHAT,
+    'tool_id' => 0,
+    'tool_id_detail' => 0,
+    'action' => 'start',
+    'action_details' => 'start-chat',
+];
+Event::registerLog($logInfo);
+
 // View
 $externalCSS = [
     'jquery-emojiarea/jquery.emojiarea.css',

+ 11 - 9
main/course_description/index.php

@@ -18,15 +18,19 @@ define('ADD_BLOCK', 8);
 // current section
 $this_section = SECTION_COURSES;
 
+$action = !empty($_GET['action']) ? Security::remove_XSS($_GET['action']) : 'listing';
 // protect a course script
-api_protect_course_script(true);
+$logInfo = [
+    'tool' => TOOL_COURSE_DESCRIPTION,
+    'tool_id' => 0,
+    'tool_id_detail' => 0,
+    'action' => $action,
+    'info' => '',
+];
+Event::registerLog($logInfo);
 
 // get actions
-$actions = ['listing', 'add', 'edit', 'delete', 'history'];
-$action = 'listing';
-if (isset($_GET['action']) && in_array($_GET['action'], $actions)) {
-    $action = $_GET['action'];
-}
+api_protect_course_script(true);
 
 $description_type = '';
 if (isset($_GET['description_type'])) {
@@ -84,9 +88,6 @@ if (in_array($action, ['add', 'edit', 'delete']) &&
 
 // Actions to controller
 switch ($action) {
-    case 'listing':
-        $descriptionController->listing();
-        break;
     case 'history':
         $descriptionController->listing(true);
         break;
@@ -99,6 +100,7 @@ switch ($action) {
     case 'delete':
         $descriptionController->destroy($id);
         break;
+    case 'listing':
     default:
         $descriptionController->listing();
 }

+ 1 - 3
main/course_home/activity.php

@@ -12,7 +12,7 @@
  *
  * @package chamilo.course_home
  */
-$id = isset($_GET['id']) ? intval($_GET['id']) : null;
+$id = isset($_GET['id']) ? (int) $_GET['id'] : null;
 $course_id = api_get_course_int_id();
 $session_id = api_get_session_id();
 
@@ -29,8 +29,6 @@ if (api_is_platform_admin()) {
         /*
         * Process hiding a tools from available tools.
         */
-        //where $id is set?
-        $id = intval($id);
         Database::query("DELETE FROM $tool_table WHERE c_id = $course_id AND id='$id' AND added_tool=1");
     }
 }

+ 1 - 1
main/course_home/course_home.php

@@ -132,7 +132,7 @@ $isSpecialCourse = CourseManager::isSpecialCourse($courseId);
 
 if ($isSpecialCourse) {
     if (isset($_GET['autoreg']) && $_GET['autoreg'] == 1) {
-        if (CourseManager::subscribe_user($user_id, $course_code, STUDENT)) {
+        if (CourseManager::subscribeUser($user_id, $course_code, STUDENT)) {
             Session::write('is_allowed_in_course', true);
         }
     }

+ 1 - 2
main/course_info/infocours.php

@@ -127,9 +127,8 @@ $form->applyFilter('department_url', 'html_filter');
 
 // Extra fields
 $extra_field = new ExtraField('course');
-
 $extraFieldAdminPermissions = false;
-$showOnlyTheseFields = ['tags', 'video_url', 'course_hours_duration'];
+$showOnlyTheseFields = ['tags', 'video_url', 'course_hours_duration', 'max_subscribed_students'];
 $extra = $extra_field->addElements(
     $form,
     $courseId,

+ 1 - 0
main/course_progress/index.php

@@ -75,6 +75,7 @@ $thematic = new Thematic();
 
 // thematic controller object
 $thematic_controller = new ThematicController();
+
 $thematic_data = [];
 if (!empty($thematic_id)) {
     // thematic data by id

+ 5 - 5
main/coursecopy/import_moodle.php

@@ -35,21 +35,21 @@ $form->addButtonImport(get_lang('Import'));
 
 if ($form->validate()) {
     $file = $_FILES['moodle_file'];
+
     $moodleImport = new MoodleImport();
-    $responseImport = $moodleImport->import($file);
 
-    Display::cleanFlashMessages();
+    try {
+        $responseImport = $moodleImport->import($file);
 
-    if ($responseImport) {
         Display::addFlash(
             Display::return_message(
                 get_lang('MoodleFileImportedSuccessfully'),
                 'success'
             )
         );
-    } else {
+    } catch (Exception $exception) {
         Display::addFlash(
-            Display::return_message(get_lang('ErrorImportingFile'), 'error')
+            Display::return_message($exception->getMessage(), 'error')
         );
     }
 }

+ 1 - 1
main/cron/import_csv.php

@@ -2347,7 +2347,7 @@ class ImportCsv
                     $userCourseCategory = $courseUserData['user_course_cat'];
                 }
 
-                CourseManager::subscribe_user(
+                CourseManager::subscribeUser(
                     $userId,
                     $courseInfo['code'],
                     $status,

+ 1 - 2
main/document/showinframes.php

@@ -27,7 +27,7 @@ require_once __DIR__.'/../inc/global.inc.php';
 api_protect_course_script();
 
 $header_file = isset($_GET['file']) ? Security::remove_XSS($_GET['file']) : null;
-$document_id = intval($_GET['id']);
+$document_id = (int) $_GET['id'];
 $originIsLearnpath = isset($_GET['origin']) && $_GET['origin'] === 'learnpathitem';
 $courseInfo = api_get_course_info();
 $course_code = api_get_course_id();
@@ -244,7 +244,6 @@ if ($is_freemind_available) {
     $execute_iframe = false;
 }
 
-
 $execute_iframe = true;
 
 if (!$playerSupported && $execute_iframe) {

+ 1 - 1
main/dropbox/dropbox_functions.inc.php

@@ -862,7 +862,7 @@ function removeMoreIfMailing($file_id)
  * @param array            $file
  * @param Dropbox_SentWork $work
  *
- * @return array|null|string
+ * @return array|string|null
  */
 function store_add_dropbox($file = [], $work = null)
 {

+ 0 - 1
main/exercise/MatchingDraggable.php

@@ -272,7 +272,6 @@ class MatchingDraggable extends Question
             $header .= '<th>'.get_lang('ExpectedChoice').'</th>';
             $header .= '<th>'.get_lang('Status').'</th>';
         } else {
-            $header .= '<th>'.get_lang('ElementList').'</th>';
             $header .= '<th>'.get_lang('CorrespondsTo').'</th>';
         }
         $header .= '</tr>';

+ 24 - 8
main/exercise/MultipleAnswerTrueFalseDegreeCertainty.php

@@ -88,7 +88,7 @@ class MultipleAnswerTrueFalseDegreeCertainty extends Question
         if (!empty($this->id)) {
             $answer = new Answer($this->id);
             $answer->read();
-            if (count($answer->nbrAnswers) > 0 && !$form->isSubmitted()) {
+            if ($answer->nbrAnswers > 0 && !$form->isSubmitted()) {
                 $nbAnswers = $answer->nbrAnswers;
             }
         }
@@ -130,11 +130,23 @@ class MultipleAnswerTrueFalseDegreeCertainty extends Question
             $defaults['correct['.$i.']'] = '';
 
             if (is_object($answer)) {
-                $defaults['answer['.$i.']'] = $answer->answer[$i];
-                $defaults['comment['.$i.']'] = $answer->comment[$i];
-                $defaults['weighting['.$i.']'] = float_format($answer->weighting[$i], 1);
-                $correct = $answer->correct[$i];
+                $defaults['answer['.$i.']'] = isset($answer->answer[$i]) ? $answer->answer[$i] : '';
+                if (isset($_POST['answer']) && isset($_POST['answer'][$i])) {
+                    $defaults['answer['.$i.']'] = Security::remove_XSS($_POST['answer'][$i]);
+                }
+
+                $defaults['comment['.$i.']'] = isset($answer->comment[$i]) ? $answer->comment[$i] : '';
+                if (isset($_POST['comment']) && isset($_POST['comment'][$i])) {
+                    $defaults['comment['.$i.']'] = Security::remove_XSS($_POST['comment'][$i]);
+                }
+
+                $defaults['weighting['.$i.']'] = isset($answer->weighting[$i]) ? float_format($answer->weighting[$i], 1) : '';
+                $correct = isset($answer->correct[$i]) ? $answer->correct[$i] : '';
                 $defaults['correct['.$i.']'] = $correct;
+                if (isset($_POST['correct']) && isset($_POST['correct'][$i])) {
+                    $defaults['correct['.$i.']'] = Security::remove_XSS($_POST['correct'][$i]);
+                }
+
                 $j = 1;
                 if (!empty($optionData)) {
                     foreach ($optionData as $id => $data) {
@@ -293,7 +305,7 @@ class MultipleAnswerTrueFalseDegreeCertainty extends Question
      * @param int      $counter
      * @param float    $score
      *
-     * @return null|string
+     * @return string|null
      */
     public function return_header($exercise, $counter = null, $score = null)
     {
@@ -1086,10 +1098,14 @@ class MultipleAnswerTrueFalseDegreeCertainty extends Question
         }
 
         // get student answer option id
-        $studentAnswerOptionId = $splitAnswer[1];
+        $studentAnswerOptionId = isset($splitAnswer[1]) ? $splitAnswer[1] : null;
 
         // we got the correct answer option id, let's compare ti with the student answer
-        $percentage = self::getPercentagePosition($splitAnswer[2]);
+        $percentage = null;
+        if (isset($splitAnswer[2])) {
+            $percentage = self::getPercentagePosition($splitAnswer[2]);
+        }
+
         if ($studentAnswerOptionId == $correctAnswerOptionId) {
             // yeah, student got correct answer
             switch ($percentage) {

+ 1 - 1
main/exercise/TestCategory.php

@@ -652,7 +652,7 @@ class TestCategory
      * @param int $questionId
      * @param int $in_display_category_name
      *
-     * @return null|string
+     * @return string|null
      */
     public static function returnCategoryAndTitle($questionId, $in_display_category_name = 1)
     {

+ 1 - 1
main/exercise/hotspot_answers.as.php

@@ -96,7 +96,7 @@ if ($objExercise->selectResultsDisabled() == RESULT_DISABLE_SHOW_SCORE_ATTEMPT_S
     $showOnlyScore = true;
     $showResults = true;
     $lpId = isset($trackExerciseInfo['orig_lp_id']) ? $trackExerciseInfo['orig_lp_id'] : 0;
-    $lpItemId  = isset($trackExerciseInfo['orig_lp_item_id']) ? $trackExerciseInfo['orig_lp_item_id'] : 0;
+    $lpItemId = isset($trackExerciseInfo['orig_lp_item_id']) ? $trackExerciseInfo['orig_lp_item_id'] : 0;
     if ($objExercise->attempts > 0) {
         $attempts = Event::getExerciseResultsByUser(
             api_get_user_id(),

+ 2 - 4
main/exercise/overview.php

@@ -76,12 +76,11 @@ if ($origin != 'learnpath') {
 }
 
 $tpl = new Template('Overview');
-$list = array();
+$list = [];
 
 $html = '';
 $message = '';
 
-
 $isAllowedToEdit = api_is_allowed_to_edit(null, true);
 $list['id'] = $objExercise->id;
 $list['session_id'] = $objExercise->sessionId;
@@ -316,7 +315,7 @@ if ($objExercise->selectAttempts()) {
 }
 
 if ($time_control) {
-    $tpl->assign('time_control',$objExercise->return_time_left_div());
+    $tpl->assign('time_control', $objExercise->return_time_left_div());
 }
 
 $tpl->assign('fluid', $fluid);
@@ -329,4 +328,3 @@ $tpl->assign('table_result', $tableContent);
 $layout = $tpl->get_template('exercise/overview.tpl');
 $tpl->assign('content', $tpl->fetch($layout));
 $tpl->display_one_col_template();
-

+ 5 - 15
main/forum/forumfunction.inc.php

@@ -646,7 +646,6 @@ function store_forumcategory($values, $courseInfo = [], $showMessage = true)
             'tool_id_detail' => 0,
             'action' => 'update-forumcategory',
             'action_details' => 'forumcategory',
-            'current_id' => $values['forum_category_id'],
             'info' => $clean_cat_title,
         ];
         Event::registerLog($logInfo);
@@ -690,7 +689,6 @@ function store_forumcategory($values, $courseInfo = [], $showMessage = true)
             'tool_id_detail' => 0,
             'action' => 'new-forumcategory',
             'action_details' => 'forumcategory',
-            'current_id' => $last_id,
             'info' => $clean_cat_title,
         ];
         Event::registerLog($logInfo);
@@ -900,7 +898,6 @@ function store_forum($values, $courseInfo = [], $returnId = false)
             'tool_id_detail' => 0,
             'action' => 'update-forum',
             'action_details' => 'forum',
-            'current_id' => $values['forum_id'],
             'info' => $values['forum_title'],
         ];
         Event::registerLog($logInfo);
@@ -959,7 +956,6 @@ function store_forum($values, $courseInfo = [], $returnId = false)
                 'tool_id_detail' => 0,
                 'action' => 'new-forum',
                 'action_details' => 'forum',
-                'current_id' => $forumId,
                 'info' => $values['forum_title'],
             ];
             Event::registerLog($logInfo);
@@ -2711,7 +2707,6 @@ function updateThread($values)
         'tool_id_detail' => $values['thread_id'],
         'action' => 'edit-thread',
         'action_details' => 'thread',
-        'current_id' => $values['thread_id'],
         'info' => $values['thread_title'],
     ];
     Event::registerLog($logInfo);
@@ -2965,7 +2960,6 @@ function store_thread(
             'tool_id_detail' => $lastThread->getIid(),
             'action' => 'new-thread',
             'action_details' => '',
-            'current_id' => $lastThread->getIid(),
             'info' => $clean_post_title,
         ];
         Event::registerLog($logInfo);
@@ -2996,6 +2990,7 @@ function store_thread(
 
     $em->persist($lastPost);
     $em->flush();
+
     $lastPostId = $lastPost->getIid();
 
     $logInfo = [
@@ -3004,7 +2999,6 @@ function store_thread(
         'tool_id_detail' => $lastThread->getIid(),
         'action' => 'new-post',
         'action_details' => '',
-        'current_id' => $lastPostId,
         'info' => $clean_post_title,
     ];
     Event::registerLog($logInfo);
@@ -3320,7 +3314,6 @@ function show_add_post_form($current_forum, $forum_setting, $action, $id = '', $
 
     if (($action == 'quote' || $action == 'replymessage' || $giveRevision) && !empty($my_post)) {
         // We also need to put the parent_id of the post in a hidden form when
-        // we are quoting or replying to a message (<> reply to a thread !!!)
         $form->addHidden('post_parent_id', $my_post);
 
         // If we are replying or are quoting then we display a default title.
@@ -3876,7 +3869,6 @@ function store_reply($current_forum, $values, $courseId = 0, $userId = 0)
                 'tool_id_detail' => $values['thread_id'],
                 'action' => 'new-post',
                 'action_details' => $values['action'],
-                'current_id' => $new_post_id,
                 'info' => $values['post_title'],
             ];
             Event::registerLog($logInfo);
@@ -4081,7 +4073,6 @@ function store_edit_post($forumInfo, $values)
         'tool_id_detail' => $values['thread_id'],
         'action' => 'edit-post',
         'action_details' => 'post',
-        'current_id' => $values['post_id'],
         'info' => $values['post_title'],
     ];
     Event::registerLog($logInfo);
@@ -5708,7 +5699,6 @@ function send_notifications($forum_id = 0, $thread_id = 0, $post_id = 0)
     $forumCourseId = api_get_configuration_value('global_forums_course_id');
     if (!empty($forumCourseId)) {
         if ($_course['real_id'] == $forumCourseId) {
-
             return false;
         }
     }
@@ -6652,7 +6642,7 @@ function postIsEditableByStudent($forum, $post)
 }
 
 /**
- * @param int  $postId
+ * @param int $postId
  *
  * @return bool
  */
@@ -6701,7 +6691,7 @@ function getPostRevision($postId)
     );
     $revision = '';
     if ($value && isset($value['value'])) {
-        $revision =  $value['value'];
+        $revision = $value['value'];
     }
 
     return $revision;
@@ -6751,7 +6741,7 @@ function getAskRevisionButton($postId, $threadInfo)
 }
 
 /**
- * @param int $postId
+ * @param int   $postId
  * @param array $threadInfo
  *
  * @return string
@@ -6778,6 +6768,7 @@ function giveRevisionButton($postId, $threadInfo)
 /**
  * @param int   $postId
  * @param array $threadInfo
+ *
  * @return string
  */
 function getReportButton($postId, $threadInfo)
@@ -6841,7 +6832,6 @@ function getReportRecepients()
                 case 'community_managers':
                     $managers = api_get_configuration_value('community_managers_user_list');
                     if (!empty($managers) && isset($managers['users'])) {
-
                         $users = array_merge($users, $managers['users']);
                     }
                     break;

+ 0 - 1
main/forum/index.php

@@ -131,7 +131,6 @@ $logInfo = [
     'tool_id_detail' => 0,
     'action' => !empty($actions) ? $actions : 'list-category',
     'action_details' => isset($_GET['content']) ? $_GET['content'] : '',
-    'current_id' => isset($_GET['id']) ? $_GET['id'] : 0,
 ];
 Event::registerLog($logInfo);
 

+ 0 - 1
main/forum/newthread.php

@@ -51,7 +51,6 @@ $logInfo = [
     'tool_id_detail' => 0,
     'action' => 'add-thread',
     'action_details' => '',
-    'current_id' => 0,
 ];
 Event::registerLog($logInfo);
 

+ 1 - 1
main/forum/reply.php

@@ -26,6 +26,7 @@ api_protect_course_script(true);
 
 $nameTools = get_lang('ForumCategories');
 $origin = api_get_origin();
+$_user = api_get_user_info();
 
 /* Including necessary files */
 require_once 'forumconfig.inc.php';
@@ -161,7 +162,6 @@ $logInfo = [
     'tool_id_detail' => $threadId,
     'action' => !empty($my_action) ? $my_action : 'reply',
     'action_details' => '',
-    'current_id' => $my_post,
 ];
 Event::registerLog($logInfo);
 

+ 7 - 7
main/forum/viewforum.php

@@ -48,6 +48,7 @@ $groupId = api_get_group_id();
 $courseId = api_get_course_int_id();
 $groupInfo = GroupManager::get_group_properties($groupId);
 $isTutor = GroupManager::is_tutor_of_group($userId, $groupInfo, $courseId);
+
 $isAllowedToEdit = api_is_allowed_to_edit(false, true) && api_is_allowed_to_session_edit(false, true);
 
 /* MAIN DISPLAY SECTION */
@@ -108,7 +109,6 @@ $logInfo = [
     'tool_id_detail' => 0,
     'action' => !empty($my_action) ? $my_action : 'list-threads',
     'action_details' => isset($_GET['content']) ? $_GET['content'] : '',
-    'current_id' => isset($_GET['id']) ? $_GET['id'] : 0,
 ];
 Event::registerLog($logInfo);
 
@@ -500,16 +500,16 @@ if (is_array($threads)) {
                 );
             }
 
-            if($_user['status']==5) {
-                if($_user['has_certificates']){
+            if ($_user['status'] == 5) {
+                if ($_user['has_certificates']) {
                     $iconStatus = '<img src="'.$urlImg.'icons/svg/ofaj_graduated.svg" width="22px" height="22px">';
-                }else{
+                } else {
                     $iconStatus = '<img src="'.$urlImg.'icons/svg/ofaj_student.svg" width="22px" height="22px">';
                 }
-            }else if($_user['status'] == 1){
-                if($isAdmin){
+            } elseif ($_user['status'] == 1) {
+                if ($isAdmin) {
                     $iconStatus = '<img src="'.$urlImg.'icons/svg/ofaj_admin.svg" width="22px" height="22px">';
-                }else{
+                } else {
                     $iconStatus = '<img src="'.$urlImg.'icons/svg/ofaj_teacher.svg" width="22px" height="22px">';
                 }
             }

+ 1 - 1
main/forum/viewthread.php

@@ -24,6 +24,7 @@ $forumUrl = api_get_path(WEB_CODE_PATH).'forum/';
 
 // Are we in a lp ?
 $origin = api_get_origin();
+$_user = api_get_user_info();
 $my_search = null;
 
 /* MAIN DISPLAY SECTION */
@@ -69,7 +70,6 @@ $(function() {
     
 </script>';
 
-
 /* Actions */
 $my_action = isset($_GET['action']) ? $_GET['action'] : '';
 

+ 3 - 3
main/gradebook/lib/be/category.class.php

@@ -296,7 +296,7 @@ class Category implements GradebookItem
     }
 
     /**
-     * @return null|int
+     * @return int|null
      */
     public function get_grade_model_id()
     {
@@ -514,7 +514,7 @@ class Category implements GradebookItem
                 $sql .= ' '.$order_by;
             }
         }
-//var_dump($sql);
+        //var_dump($sql);
         $result = Database::query($sql);
         $categories = [];
         if (Database::num_rows($result) > 0) {
@@ -2644,7 +2644,7 @@ class Category implements GradebookItem
      * Internal function used by get_tree().
      *
      * @param int      $level
-     * @param null|int $visible
+     * @param int|null $visible
      *
      * @return array
      */

+ 1 - 1
main/gradebook/lib/fe/gradebooktable.class.php

@@ -1140,7 +1140,7 @@ class GradebookTable extends SortableTable
     /**
      * @param AbstractLink $item
      *
-     * @return null|string
+     * @return string|null
      */
     private function build_edit_column($item)
     {

+ 2 - 2
main/gradebook/lib/fe/linkform.class.php

@@ -140,9 +140,9 @@ class LinkForm extends FormValidator
 
     /**
      * @param int         $link
-     * @param null|string $courseCode
+     * @param string|null $courseCode
      *
-     * @return AttendanceLink|DropboxLink|ExerciseLink|ForumThreadLink|LearnpathLink|null|StudentPublicationLink|SurveyLink
+     * @return AttendanceLink|DropboxLink|ExerciseLink|ForumThreadLink|LearnpathLink|StudentPublicationLink|SurveyLink|null
      */
     private function createLink($link, $courseCode)
     {

+ 1 - 1
main/gradebook/lib/fe/resulttable.class.php

@@ -22,7 +22,7 @@ class ResultTable extends SortableTable
      *
      * @param string      $evaluation
      * @param array       $results
-     * @param null|string $iscourse
+     * @param string|null $iscourse
      * @param array       $addparams
      * @param bool        $forprint
      */

+ 1 - 1
main/gradebook/lib/gradebook_data_generator.class.php

@@ -431,7 +431,7 @@ class GradebookDataGenerator
      * @param GradebookItem $item
      * @param bool          $ignore_score_color
      *
-     * @return null|string
+     * @return string|null
      */
     private function build_result_column(
         $userId,

+ 1 - 0
main/group/group_category.php

@@ -79,6 +79,7 @@ if (isset($_GET['id'])) {
         'announcements_state' => GroupManager::TOOL_PRIVATE,
         'forum_state' => GroupManager::TOOL_PRIVATE,
         'max_student' => 0,
+        'document_access' => 0,
     ];
 }
 

+ 22 - 10
main/group/group_space.php

@@ -367,14 +367,14 @@ if (api_is_western_name_order()) {
     $table->set_header(2, get_lang('FirstName'));
 }
 
-if (api_get_setting('show_email_addresses') == 'true') {
+if (api_get_setting('show_email_addresses') == 'true' || api_is_allowed_to_edit() == 'true') {
     $table->set_header(3, get_lang('Email'));
     $table->set_column_filter(3, 'email_filter');
+    $table->set_header(4, get_lang('Active'));
+    $table->set_column_filter(4, 'activeFilter');
 } else {
-    if (api_is_allowed_to_edit() == 'true') {
-        $table->set_header(3, get_lang('Email'));
-        $table->set_column_filter(3, 'email_filter');
-    }
+    $table->set_header(3, get_lang('Active'));
+    $table->set_column_filter(3, 'activeFilter');
 }
 //the order of these calls is important
 //$table->set_column_filter(1, 'user_name_filter');
@@ -455,11 +455,12 @@ function get_group_user_data($from, $number_of_items, $column, $direction)
 				user.firstname 	AS col2,"
             )."
 				user.email		AS col3
-				FROM $table_user user INNER JOIN 
-				$table_group_user group_rel_user
+				, user.active AS col4
+				FROM $table_user user 
+				INNER JOIN $table_group_user group_rel_user
 				ON (group_rel_user.user_id = user.id)
 				INNER JOIN $tableGroup g
-				ON (group_rel_user.group_id = g.id)
+				ON (group_rel_user.group_id = g.iid)
 				WHERE 
 				    group_rel_user.c_id = $course_id AND 
 				    g.iid = '".$groupInfo['iid']."'
@@ -476,11 +477,12 @@ function get_group_user_data($from, $number_of_items, $column, $direction)
                         "u.lastname 	AS col1,
                         u.firstname 	AS col2,")."
                         u.email		AS col3
+                        , u.active AS col4
                     FROM $table_user u 
                     INNER JOIN $table_group_user gu 
                     ON (gu.user_id = u.id)
                     INNER JOIN $tableGroup g
-				    ON (gu.group_id = g.id)
+				    ON (gu.group_id = g.iid)
                     WHERE 
                         g.iid = '".$groupInfo['iid']."' AND 
                         gu.c_id = $course_id
@@ -497,11 +499,12 @@ function get_group_user_data($from, $number_of_items, $column, $direction)
                     "user.lastname 	AS col1,
 						user.firstname 	AS col2 "
                     )."
+                    , user.active AS col3
                     FROM $table_user user 
                     INNER JOIN $table_group_user group_rel_user
                     ON (group_rel_user.user_id = user.id)
                     INNER JOIN $tableGroup g
-                    ON (group_rel_user.group_id = g.id)
+                    ON (group_rel_user.group_id = g.iid)
                     WHERE 
                         g.iid = '".$groupInfo['iid']."' AND 
                         group_rel_user.c_id = $course_id AND  
@@ -533,6 +536,15 @@ function email_filter($email)
     return Display::encrypted_mailto_link($email, $email);
 }
 
+function activeFilter($isActive)
+{
+    if ($isActive) {
+        return Display::return_icon('accept.png', get_lang('Active'), [], ICON_SIZE_TINY);
+    }
+
+    return Display::return_icon('error.png', get_lang('Inactive'), [], ICON_SIZE_TINY);
+}
+
 /**
  * Display a user icon that links to the user page.
  *

+ 2 - 1
main/group/import.php

@@ -38,7 +38,8 @@ $form->addElement(
     null,
     Display::url(
         get_lang('ExampleCSVFile'),
-        api_get_path(WEB_CODE_PATH).'group/example.csv'
+        api_get_path(WEB_CODE_PATH).'group/example.csv',
+        ['download' => true]
     )
 );
 $form->addButtonImport(get_lang('Import'));

+ 8 - 0
main/inc/ajax/course.ajax.php

@@ -365,6 +365,14 @@ switch ($action) {
             'sid' => api_get_session_id(),
         ];
 
+        $logInfo = [
+            'tool' => 'close-window',
+            'tool_id' => 0,
+            'tool_id_detail' => 0,
+            'action' => 'exit',
+        ];
+        Event::registerLog($logInfo);
+
         $result = (int) Event::courseLogout($logoutInfo);
         echo $result;
         break;

+ 0 - 1
main/inc/ajax/course_chat.ajax.php

@@ -25,7 +25,6 @@ switch ($_REQUEST['action']) {
             'tool_id_detail' => 0,
             'action' => 'exit',
             'action_details' => 'exit-chat',
-            'current_id' => 0,
             'info' => '',
         ];
         Event::registerLog($logInfo);

+ 5 - 4
main/inc/ajax/document.ajax.php

@@ -108,7 +108,8 @@ switch ($action) {
 
                 $json = [];
                 if (!empty($result) && is_array($result)) {
-                    $json['name'] = Display::url(
+                    $json['name'] = api_htmlentities($result['title']);
+                    $json['link'] = Display::url(
                         api_htmlentities($result['title']),
                         api_htmlentities($result['url']),
                         ['target' => '_blank']
@@ -133,10 +134,10 @@ switch ($action) {
         exit;
         break;
     case 'document_preview':
-        $course_info = api_get_course_info_by_id($_REQUEST['course_id']);
-        if (!empty($course_info) && is_array($course_info)) {
+        $courseInfo = api_get_course_info_by_id($_REQUEST['course_id']);
+        if (!empty($courseInfo) && is_array($courseInfo)) {
             echo DocumentManager::get_document_preview(
-                $course_info,
+                $courseInfo,
                 false,
                 '_blank',
                 $_REQUEST['session_id']

+ 5 - 1
main/inc/ajax/exercise.ajax.php

@@ -730,7 +730,11 @@ switch ($action) {
         $objExercise = new Exercise();
         $objExercise->read($exerciseId);
         $objQuestion = Question::read($questionId);
-
+        $id = '';
+        if (api_get_configuration_value('show_question_id')) {
+            $id = '<h4>#'.$objQuestion->course['code'].'-'.$objQuestion->iid.'</h4>';
+        }
+        echo $id;
         echo '<p class="lead">'.$objQuestion->get_question_type_name().'</p>';
         if ($objQuestion->type === FILL_IN_BLANKS) {
             echo '<script>

+ 1 - 1
main/inc/ajax/model.ajax.php

@@ -676,7 +676,7 @@ switch ($action) {
         break;
     case 'get_sessions':
         $list_type = isset($_REQUEST['list_type']) ? $_REQUEST['list_type'] : 'simple';
-        $language =  isset($_REQUEST['lang']) ? $_REQUEST['lang'] : '';
+        $language = isset($_REQUEST['lang']) ? $_REQUEST['lang'] : '';
         $session_columns = SessionManager::getGridColumns($list_type);
         $columns = $session_columns['simple_column_name'];
 

+ 2 - 1
main/inc/ajax/statistics.ajax.php

@@ -82,7 +82,8 @@ switch ($action) {
         }
 
         if (!empty($operation)) {
-            $fileName = !empty($action) ? get_lang('PortalUserSessionStats').'_'.api_get_local_time() : 'report';
+            $fileName = !empty($action) ? api_get_setting('siteName').
+                '_'.get_lang('PortalUserSessionStats').'_'.api_get_local_time() : 'report';
             switch ($exportFormat) {
                 case 'xls':
                     Export::arrayToXls($list, $fileName);

+ 46 - 0
main/inc/ajax/survey.ajax.php

@@ -0,0 +1,46 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+require_once __DIR__.'/../global.inc.php';
+
+$action = isset($_GET['a']) ? $_GET['a'] : null;
+
+$current_user_id = api_get_user_id();
+$courseId = api_get_course_int_id();
+
+switch ($action) {
+    case 'save_question':
+        if (api_is_anonymous()) {
+            echo '';
+            break;
+        }
+        $surveyId = isset($_GET['survey_id']) ? $_GET['survey_id'] : null;
+        $questionId = isset($_GET['question_id']) ? $_GET['question_id'] : null;
+        $status = isset($_GET['status']) ? (int) $_GET['status'] : null;
+        $userId = api_get_user_id();
+
+        $surveyData = SurveyManager::get_survey($surveyId);
+
+        if (empty($surveyData)) {
+            exit;
+        }
+
+        SurveyUtil::remove_answer(
+            $userId,
+            $surveyId,
+            $questionId,
+            $courseId
+        );
+
+        SurveyUtil::store_answer(
+            $userId,
+            $surveyId,
+            $questionId,
+            1,
+            $status,
+            $surveyData
+        );
+
+        break;
+}
+exit;

+ 22 - 14
main/inc/ajax/user_manager.ajax.php

@@ -14,21 +14,23 @@ $action = $_GET['a'];
 
 switch ($action) {
     case 'get_user_like':
-        $query = $_REQUEST['q'];
-        $conditions = [
-            'username' => $query,
-            'firstname' => $query,
-            'lastname' => $query,
-        ];
-        $users = UserManager::getUserListLike($conditions, [], false, 'OR');
-        $result = [];
-        if (!empty($users)) {
-            foreach ($users as $user) {
-                $result[] = ['id' => $user['id'], 'text' => $user['complete_name'].' ('.$user['username'].')'];
+        if (api_is_platform_admin() || api_is_drh()) {
+            $query = $_REQUEST['q'];
+            $conditions = [
+                'username' => $query,
+                'firstname' => $query,
+                'lastname' => $query,
+            ];
+            $users = UserManager::getUserListLike($conditions, [], false, 'OR');
+            $result = [];
+            if (!empty($users)) {
+                foreach ($users as $user) {
+                    $result[] = ['id' => $user['id'], 'text' => $user['complete_name'].' ('.$user['username'].')'];
+                }
+                $result['items'] = $result;
             }
-            $result['items'] = $result;
+            echo json_encode($result);
         }
-        echo json_encode($result);
         break;
     case 'get_user_popup':
         $courseId = isset($_REQUEST['course_id']) ? (int) $_REQUEST['course_id'] : 0;
@@ -58,7 +60,13 @@ switch ($action) {
 
         $userData = '<h3>'.$user_info['complete_name'].'</h3>'.$user_info['mail'].$user_info['official_code'];
         if ($isAnonymous) {
-            echo $userData;
+            // Only allow anonymous users to see user popup if the popup user
+            // is a teacher (which might be necessary to illustrate a course)
+            if ($user_info['status'] === COURSEMANAGER) {
+                echo $userData;
+            } else {
+                echo '<h3>-</h3>';
+            }
         } else {
             echo Display::url(
                 $userData,

+ 10 - 1
main/inc/ajax/work.ajax.php

@@ -20,6 +20,14 @@ switch ($action) {
         $userId = api_get_user_id();
         $groupId = api_get_group_id();
 
+        $onlyOnePublication = api_get_configuration_value('allow_only_one_student_publication_per_user');
+        if ($onlyOnePublication) {
+            $count = get_work_count_by_student($userId, $workId);
+            if ($count >= 1) {
+                exit;
+            }
+        }
+
         if (!empty($_FILES)) {
             $files = $_FILES['files'];
             $fileList = [];
@@ -56,7 +64,8 @@ switch ($action) {
 
                 $json = [];
                 if (!empty($result) && is_array($result) && empty($result['error'])) {
-                    $json['name'] = Display::url(
+                    $json['name'] = api_htmlentities($result['title']);
+                    $json['link'] = Display::url(
                         api_htmlentities($result['title']),
                         api_htmlentities($result['view_url']),
                         ['target' => '_blank']

+ 6 - 1
main/inc/email_editor.php

@@ -78,7 +78,12 @@ if ($form->validate()) {
             $title,
             $text,
             api_get_person_name($_user['firstname'], $_user['lastname']),
-            $_user['mail']
+            '',
+            ['reply_to' => [
+                    'mail' => $_user['mail'],
+                    'name' => api_get_person_name($_user['firstname'], $_user['lastname']),
+                ],
+            ]
         );
     } else {
         api_mail_html(

+ 0 - 6
main/inc/global.inc.php

@@ -308,12 +308,6 @@ require_once $libraryPath.'formvalidator/Rule/allowed_tags.inc.php';
 $this_section = SECTION_GLOBAL;
 
 // Include Chamilo Mail conf this is added here because the api_get_setting works
-
-// Fixes bug in Chamilo 1.8.7.1 array was not set
-$administrator['email'] = isset($administrator['email']) ? $administrator['email'] : 'admin@example.com';
-$administrator['name'] = isset($administrator['name']) ? $administrator['name'] : 'Admin';
-
-// Including configuration files
 $configurationFiles = [
     'mail.conf.php',
     'profile.conf.php',

+ 1 - 0
main/inc/introductionSection.inc.php

@@ -32,6 +32,7 @@ use Chamilo\CourseBundle\Entity\CToolIntro;
 $em = Database::getManager();
 $intro_editAllowed = $is_allowed_to_edit = api_is_allowed_to_edit();
 $session_id = api_get_session_id();
+$blogParam = isset($_GET['blog_id']) ? ('&blog_id='.(int) $_GET['blog_id']) : '';
 
 $introduction_section = '';
 

+ 13 - 4
main/inc/lib/CourseChatUtils.php

@@ -783,7 +783,8 @@ class CourseChatUtils
                     'SELECT u FROM ChamiloUserBundle:User u
                     INNER JOIN ChamiloCourseBundle:CGroupRelUser gru
                         WITH u.id = gru.userId AND gru.cId = :course
-                    WHERE u.id != :user AND gru.groupId = :group'
+                    WHERE u.id != :user AND gru.groupId = :group
+                        AND u.active = true'
                 )
                 ->setParameters(['course' => $this->courseId, 'user' => $this->userId, 'group' => $this->groupId])
                 ->getResult();
@@ -792,7 +793,8 @@ class CourseChatUtils
                     'SELECT u FROM ChamiloUserBundle:User u
                     INNER JOIN ChamiloCourseBundle:CGroupRelTutor grt
                         WITH u.id = grt.userId AND grt.cId = :course
-                    WHERE u.id != :user AND grt.groupId = :group'
+                    WHERE u.id != :user AND grt.groupId = :group
+                        AND u.active = true'
                 )
                 ->setParameters(['course' => $this->courseId, 'user' => $this->userId, 'group' => $this->groupId])
                 ->getResult();
@@ -825,10 +827,17 @@ class CourseChatUtils
 
             return $session
                 ->getUserCourseSubscriptions()
-                ->matching($criteria);
+                ->matching($criteria)
+                ->filter(function (SessionRelCourseRelUser $sessionRelCourseRelUser) {
+                    return $sessionRelCourseRelUser->getUser()->isActive();
+                });
         }
 
-        return $course->getUsers();
+        return $course
+            ->getUsers()
+            ->filter(function (CourseRelUser $courseRelUser) {
+                return $courseRelUser->getUser()->isActive();
+            });
     }
 
     /**

+ 130 - 2
main/inc/lib/CoursesAndSessionsCatalog.class.php

@@ -156,6 +156,63 @@ class CoursesAndSessionsCatalog
         return $row[0];
     }
 
+    public static function getCourseCategoriesTree()
+    {
+        $urlId = 1;
+        if (api_is_multiple_url_enabled()) {
+            $urlId = api_get_current_access_url_id();
+        }
+
+        $countCourses = self::countAvailableCoursesToShowInCatalog($urlId);
+        $categories = [];
+        $list = [];
+
+        $categories['ALL'] = [
+            'id' => 0,
+            'name' => get_lang('DisplayAll'),
+            'code' => 'ALL',
+            'parent_id' => null,
+            'tree_pos' => 0,
+            'number_courses' => $countCourses,
+            'level' => 0,
+        ];
+
+        $allCategories = CourseCategory::getAllCategories();
+
+        foreach ($allCategories as $category) {
+            $subList = [];
+            if (empty($category['parent_id'])) {
+                $list[$category['code']] = $category;
+                $list[$category['code']]['level'] = 0;
+                list($subList, $childrenCount) = self::buildCourseCategoryTree($allCategories, $category['code'], 0);
+                //$list = array($list, $subList);
+                foreach ($subList as $item) {
+                    $list[$item['code']] = $item;
+                }
+                $list[$category['code']]['number_courses'] = $childrenCount + $category['number_courses'];
+            }
+        }
+
+        // count courses that are in no category
+        $count_courses = CourseCategory::countCoursesInCategory();
+        $categories['NONE'] = [
+            'id' => 0,
+            'name' => get_lang('WithoutCategory'),
+            'code' => 'NONE',
+            'parent_id' => null,
+            'tree_pos' => 0,
+            'children_count' => 0,
+            'auth_course_child' => true,
+            'auth_cat_child' => true,
+            'number_courses' => $count_courses,
+            'level' => 0,
+        ];
+
+        $result = array_merge($list, $categories);
+
+        return $result;
+    }
+
     /**
      * @return array
      */
@@ -215,8 +272,8 @@ class CoursesAndSessionsCatalog
     {
         $limitFilter = '';
         if (!empty($limit) && is_array($limit)) {
-            $limitStart = isset($limit['start']) ? $limit['start'] : 0;
-            $limitLength = isset($limit['length']) ? $limit['length'] : 12;
+            $limitStart = isset($limit['start']) ? (int) $limit['start'] : 0;
+            $limitLength = isset($limit['length']) ? (int) $limit['length'] : 12;
             $limitFilter = 'LIMIT '.$limitStart.', '.$limitLength;
         }
 
@@ -469,6 +526,8 @@ class CoursesAndSessionsCatalog
      * @param string $date  (optional) The date of sessions
      * @param array  $limit
      *
+     * @throws Exception
+     *
      * @return array The session list
      */
     public static function browseSessions($date = null, $limit = [])
@@ -487,6 +546,7 @@ class CoursesAndSessionsCatalog
         ";
 
         if (!is_null($date)) {
+            $date = Database::escape_string($date);
             $sql .= "
                 AND (
                     ('$date' BETWEEN DATE(s.access_start_date) AND DATE(s.access_end_date))
@@ -501,6 +561,8 @@ class CoursesAndSessionsCatalog
         }
 
         if (!empty($limit)) {
+            $limit['start'] = (int) $limit['start'];
+            $limit['length'] = (int) $limit['length'];
             $sql .= "LIMIT {$limit['start']}, {$limit['length']} ";
         }
 
@@ -610,4 +672,70 @@ class CoursesAndSessionsCatalog
 
         return $sessionsToBrowse;
     }
+
+    /**
+     * Build a recursive tree of course categories.
+     *
+     * @param $categories
+     * @param $parentId
+     *
+     * @return array
+     */
+    public static function buildCourseCategoryTree($categories, $parentId = 0, $level = 0)
+    {
+        $list = [];
+        $count = 0;
+        $level++;
+        foreach ($categories as $category) {
+            $childrenCount = 0;
+            $subList = [];
+            if (empty($category['parent_id'])) {
+                continue;
+            }
+            if ($category['parent_id'] == $parentId) {
+                $list[$category['code']] = $category;
+                $count += $category['number_courses'];
+                $list[$category['code']]['level'] = $level;
+                list($subList, $childrenCount) = self::buildCourseCategoryTree(
+                    $categories,
+                    $category['code'],
+                    $level
+                );
+                $list[$category['code']]['number_courses'] += $childrenCount;
+                foreach ($subList as $item) {
+                    $list[$item['code']] = $item;
+                }
+                $count += $childrenCount;
+            }
+        }
+
+        return [$list, $count];
+    }
+
+    /**
+     * List Code Search Category.
+     *
+     * @param $code
+     *
+     * @return array
+     */
+    public static function childrenCategories($code)
+    {
+        $allCategories = CourseCategory::getAllCategories();
+        $list = [];
+        $row = [];
+
+        if ($code != 'ALL' and $code != 'NONE') {
+            foreach ($allCategories as $category) {
+                if ($category['code'] === $code) {
+                    $list = self::buildCourseCategoryTree($allCategories, $category['code'], 0);
+                }
+            }
+            foreach ($list[0] as $item) {
+                $row[] = $item['code'];
+            }
+        }
+
+        return $row;
+    }
 }

+ 3 - 3
main/inc/lib/ScheduledAnnouncement.php

@@ -324,7 +324,7 @@ class ScheduledAnnouncement extends Model
                     }
 
                     if ($users) {
-                        self::update(['id' => $result['id'], 'sent' => 1]);
+                        $this->update(['id' => $result['id'], 'sent' => 1]);
                         $attachments = $this->getAttachmentToString($result['id']);
                         $subject = $result['subject'];
 
@@ -371,8 +371,8 @@ class ScheduledAnnouncement extends Model
 
                             $generalCoach = '';
                             $generalCoachEmail = '';
-                            if (!empty($sessionInfo['coach_id'])) {
-                                $coachInfo = api_get_user_info($sessionInfo['coach_id']);
+                            if (!empty($sessionInfo['id_coach'])) {
+                                $coachInfo = api_get_user_info($sessionInfo['id_coach']);
                                 if (!empty($coachInfo)) {
                                     $generalCoach = $coachInfo['complete_name'];
                                     $generalCoachEmail = $coachInfo['email'];

+ 51 - 51
main/inc/lib/TicketManager.php

@@ -50,6 +50,7 @@ class TicketManager
         $table_support_project = Database::get_main_table(TABLE_TICKET_PROJECT);
 
         $order = empty($order) ? 'category.total_tickets DESC' : $order;
+        $order = Database::escape_string($order);
         $projectId = (int) $projectId;
 
         $sql = "SELECT 
@@ -89,9 +90,9 @@ class TicketManager
         if (!in_array($direction, ['ASC', 'DESC'])) {
             $direction = 'ASC';
         }
-        $column = intval($column);
-        $from = intval($from);
-        $numberItems = intval($numberItems);
+        $column = (int) $column;
+        $from = (int) $from;
+        $numberItems = (int) $numberItems;
 
         //$sql .= " ORDER BY col$column $direction ";
         $sql .= " LIMIT $from,$numberItems";
@@ -113,7 +114,7 @@ class TicketManager
     public static function getCategory($id)
     {
         $table = Database::get_main_table(TABLE_TICKET_CATEGORY);
-        $id = intval($id);
+        $id = (int) $id;
         $sql = "SELECT id, name, description, total_tickets
                 FROM $table WHERE id = $id";
 
@@ -146,7 +147,7 @@ class TicketManager
     public static function updateCategory($id, $params)
     {
         $table = Database::get_main_table(TABLE_TICKET_CATEGORY);
-        $id = intval($id);
+        $id = (int) $id;
         Database::update($table, $params, ['id = ?' => $id]);
     }
 
@@ -166,7 +167,7 @@ class TicketManager
      */
     public static function deleteCategory($id)
     {
-        $id = intval($id);
+        $id = (int) $id;
         if (empty($id)) {
             return false;
         }
@@ -260,7 +261,7 @@ class TicketManager
     public static function get_all_tickets_status()
     {
         $table = Database::get_main_table(TABLE_TICKET_STATUS);
-        $sql = "SELECT * FROM ".$table;
+        $sql = "SELECT * FROM $table";
         $result = Database::query($sql);
         $types = [];
         while ($row = Database::fetch_assoc($result)) {
@@ -314,10 +315,10 @@ class TicketManager
         $currentUserId = api_get_user_id();
         $currentUserInfo = api_get_user_info();
         $now = api_get_utc_datetime();
-        $course_id = intval($course_id);
-        $category_id = intval($category_id);
-        $project_id = intval($project_id);
-        $priority = empty($priority) ? self::PRIORITY_NORMAL : $priority;
+        $course_id = (int) $course_id;
+        $category_id = (int) $category_id;
+        $project_id = (int) $project_id;
+        $priority = empty($priority) ? self::PRIORITY_NORMAL : (int) $priority;
 
         if ($status === '') {
             $status = self::STATUS_NEW;
@@ -457,11 +458,11 @@ class TicketManager
                         </tr>
                         <tr>
                             <td width="100px"><b>'.get_lang('Title').'</b></td>
-                            <td width="400px">'.$subject.'</td>
+                            <td width="400px">'.Security::remove_XSS($subject).'</td>
                         </tr>
                         <tr>
                             <td width="100px"><b>'.get_lang('Description').'</b></td>
-                            <td width="400px">'.$content.'</td>
+                            <td width="400px">'.Security::remove_XSS($content).'</td>
                         </tr>
                     </table>';
 
@@ -590,11 +591,11 @@ class TicketManager
             return false;
         }
 
-        $table_support_tickets = Database::get_main_table(TABLE_TICKET_TICKET);
         $ticket = self::get_ticket_detail_by_id($ticketId);
 
         if ($ticket) {
-            $sql = "UPDATE $table_support_tickets
+            $table = Database::get_main_table(TABLE_TICKET_TICKET);
+            $sql = "UPDATE $table
                     SET assigned_last_user = $userId
                     WHERE id = $ticketId";
             Database::query($sql);
@@ -641,10 +642,13 @@ class TicketManager
         $table_support_messages = Database::get_main_table(TABLE_TICKET_MESSAGE);
         $table_support_tickets = Database::get_main_table(TABLE_TICKET_TICKET);
         if ($sendConfirmation) {
-            $form = '<form action="ticket_details.php?ticket_id='.$ticketId.'" id="confirmticket" method="POST" >
+            $form =
+                '<form action="ticket_details.php?ticket_id='.$ticketId.'" id="confirmticket" method="POST" >
                          <p>'.get_lang('TicketWasThisAnswerSatisfying').'</p>
-                         <button class="btn btn-primary responseyes" name="response" id="responseyes" value="1">'.get_lang('Yes').'</button>
-                         <button class="btn btn-danger responseno" name="response" id="responseno" value="0">'.get_lang('No').'</button>
+                     <button class="btn btn-primary responseyes" name="response" id="responseyes" value="1">'.
+                get_lang('Yes').'</button>
+                     <button class="btn btn-danger responseno" name="response" id="responseno" value="0">'.
+                get_lang('No').'</button>
                      </form>';
             $content .= $form;
         }
@@ -655,7 +659,7 @@ class TicketManager
             'ticket_id' => $ticketId,
             'subject' => $subject,
             'message' => $content,
-            'ip_address' => $_SERVER['REMOTE_ADDR'],
+            'ip_address' => api_get_real_ip(),
             'sys_insert_user_id' => $userId,
             'sys_insert_datetime' => $now,
             'sys_lastedit_user_id' => $userId,
@@ -667,12 +671,12 @@ class TicketManager
             // update_total_message
             $sql = "UPDATE $table_support_tickets
                     SET 
-                        sys_lastedit_user_id ='$userId',
+                        sys_lastedit_user_id = $userId,
                         sys_lastedit_datetime ='$now',
                         total_messages = (
                             SELECT COUNT(*) as total_messages
                             FROM $table_support_messages
-                            WHERE ticket_id ='$ticketId'
+                            WHERE ticket_id = $ticketId
                         )
                     WHERE id = $ticketId ";
             Database::query($sql);
@@ -713,7 +717,7 @@ class TicketManager
     ) {
         $now = api_get_utc_datetime();
         $userId = api_get_user_id();
-        $ticketId = intval($ticketId);
+        $ticketId = (int) $ticketId;
         $new_file_name = add_ext_on_mime(
             stripslashes($file_attach['name']),
             $file_attach['type']
@@ -966,7 +970,7 @@ class TicketManager
 
             if ($isAdmin) {
                 $ticket = [
-                    $icon.' '.$row['subject'],
+                    $icon.' '.Security::remove_XSS($row['subject']),
                     $row['status_name'],
                     $row['start_date'],
                     $row['sys_lastedit_datetime'],
@@ -977,7 +981,7 @@ class TicketManager
                 ];
             } else {
                 $ticket = [
-                    $icon.' '.$row['subject'],
+                    $icon.' '.Security::remove_XSS($row['subject']),
                     $row['status_name'],
                     $row['start_date'],
                     $row['sys_lastedit_datetime'],
@@ -1144,7 +1148,7 @@ class TicketManager
      */
     public static function get_ticket_detail_by_id($ticketId)
     {
-        $ticketId = intval($ticketId);
+        $ticketId = (int) $ticketId;
         $table_support_category = Database::get_main_table(TABLE_TICKET_CATEGORY);
         $table_support_tickets = Database::get_main_table(TABLE_TICKET_TICKET);
         $table_support_priority = Database::get_main_table(TABLE_TICKET_PRIORITY);
@@ -1232,7 +1236,8 @@ class TicketManager
                 $result_attach = Database::query($sql);
                 while ($row2 = Database::fetch_assoc($result_attach)) {
                     $archiveURL = $webPath.'ticket/download.php?ticket_id='.$ticketId.'&id='.$row2['id'];
-                    $row2['attachment_link'] = $attach_icon.'&nbsp;<a href="'.$archiveURL.'">'.$row2['filename'].'</a>&nbsp;('.$row2['size'].')';
+                    $row2['attachment_link'] = $attach_icon.
+                        '&nbsp;<a href="'.$archiveURL.'">'.$row2['filename'].'</a>&nbsp;('.$row2['size'].')';
                     $message['attachments'][] = $row2;
                 }
                 $ticket['messages'][] = $message;
@@ -1250,11 +1255,9 @@ class TicketManager
      */
     public static function update_message_status($ticketId, $userId)
     {
-        $ticketId = intval($ticketId);
-        $userId = intval($userId);
-        $table_support_messages = Database::get_main_table(
-            TABLE_TICKET_MESSAGE
-        );
+        $ticketId = (int) $ticketId;
+        $userId = (int) $userId;
+        $table_support_messages = Database::get_main_table(TABLE_TICKET_MESSAGE);
         $table_support_tickets = Database::get_main_table(TABLE_TICKET_TICKET);
         $now = api_get_utc_datetime();
         $sql = "UPDATE $table_support_messages
@@ -1409,9 +1412,9 @@ class TicketManager
         $now = api_get_utc_datetime();
         $table = Database::get_main_table(TABLE_TICKET_TICKET);
         $newParams = [
-            'priority_id' => isset($params['priority_id']) ? $params['priority_id'] : '',
-            'status_id' => isset($params['status_id']) ? $params['status_id'] : '',
-            'sys_lastedit_user_id' => $userId,
+            'priority_id' => isset($params['priority_id']) ? (int) $params['priority_id'] : '',
+            'status_id' => isset($params['status_id']) ? (int) $params['status_id'] : '',
+            'sys_lastedit_user_id' => (int) $userId,
             'sys_lastedit_datetime' => $now,
         ];
         Database::update($table, $newParams, ['id = ? ' => $ticketId]);
@@ -1433,11 +1436,11 @@ class TicketManager
     ) {
         $table_support_tickets = Database::get_main_table(TABLE_TICKET_TICKET);
 
-        $ticketId = intval($ticketId);
-        $status_id = intval($status_id);
-        $userId = intval($userId);
-
+        $ticketId = (int) $ticketId;
+        $status_id = (int) $status_id;
+        $userId = (int) $userId;
         $now = api_get_utc_datetime();
+
         $sql = "UPDATE $table_support_tickets
                 SET
                     status_id = '$status_id',
@@ -1465,9 +1468,7 @@ class TicketManager
     public static function getNumberOfMessages()
     {
         $table_support_tickets = Database::get_main_table(TABLE_TICKET_TICKET);
-        $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);
         $table_main_admin = Database::get_main_table(TABLE_MAIN_ADMIN);
         $user_info = api_get_user_info();
@@ -1503,14 +1504,14 @@ class TicketManager
         $table_support_tickets = Database::get_main_table(TABLE_TICKET_TICKET);
         $now = api_get_utc_datetime();
 
-        $ticketId = intval($ticketId);
-        $userId = intval($userId);
+        $ticketId = (int) $ticketId;
+        $userId = (int) $userId;
 
         $sql = "UPDATE $table_support_tickets SET
                   priority_id = '".self::PRIORITY_HIGH."',
-                  sys_lastedit_user_id ='$userId',
+                  sys_lastedit_user_id = $userId,
                   sys_lastedit_datetime ='$now'
-                WHERE id = '$ticketId'";
+                WHERE id = $ticketId";
         Database::query($sql);
     }
 
@@ -1520,8 +1521,8 @@ class TicketManager
      */
     public static function close_ticket($ticketId, $userId)
     {
-        $ticketId = intval($ticketId);
-        $userId = intval($userId);
+        $ticketId = (int) $ticketId;
+        $userId = (int) $userId;
 
         $table_support_tickets = Database::get_main_table(TABLE_TICKET_TICKET);
         $now = api_get_utc_datetime();
@@ -1570,7 +1571,7 @@ class TicketManager
     public static function get_assign_log($ticketId)
     {
         $table = Database::get_main_table(TABLE_TICKET_ASSIGNED_LOG);
-        $ticketId = intval($ticketId);
+        $ticketId = (int) $ticketId;
 
         $sql = "SELECT * FROM $table
                 WHERE ticket_id = $ticketId
@@ -1612,8 +1613,8 @@ class TicketManager
         $direction,
         $userId = null
     ) {
-        $from = intval($from);
-        $number_of_items = intval($number_of_items);
+        $from = (int) $from;
+        $number_of_items = (int) $number_of_items;
         $table_support_category = Database::get_main_table(
             TABLE_TICKET_CATEGORY
         );
@@ -2274,7 +2275,6 @@ class TicketManager
      */
     public static function getSettingsMenuItems($exclude = null)
     {
-        $items = [];
         $project = [
             'icon' => 'project.png',
             'url' => 'projects.php',

+ 117 - 0
main/inc/lib/UnserializeApi.php

@@ -0,0 +1,117 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+use Brumann\Polyfill\Unserialize;
+
+/**
+ * Class UnserializeApi.
+ */
+class UnserializeApi
+{
+    /**
+     * Unserialize content using Brummann\Polyfill\Unserialize.
+     *
+     * @param string $type
+     * @param string $serialized
+     * @param bool   $ignoreErrors. Optional.
+     *
+     * @return mixed
+     */
+    public static function unserialize($type, $serialized, $ignoreErrors = false)
+    {
+        $allowedClasses = [];
+
+        switch ($type) {
+            case 'career':
+            case 'sequence_graph':
+                $allowedClasses = [
+                    \Fhaculty\Graph\Graph::class,
+                    \Fhaculty\Graph\Set\VerticesMap::class,
+                    \Fhaculty\Graph\Set\Vertices::class,
+                    \Fhaculty\Graph\Set\Edges::class,
+                ];
+                break;
+            case 'course':
+                $allowedClasses = [
+                    \Chamilo\CourseBundle\Component\CourseCopy\Course::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Asset::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Announcement::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Attendance::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\CalendarEvent::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseCopyLearnpath::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseCopyTestCategory::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseDescription::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\CourseSession::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Document::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Forum::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumCategory::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumPost::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\ForumTopic::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Glossary::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\GradeBookBackup::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Link::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\LinkCategory::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Quiz::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\QuizQuestion::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\QuizQuestionOption::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\ScormDocument::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Survey::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\SurveyInvitation::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\SurveyQuestion::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Thematic::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\ToolIntro::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Wiki::class,
+                    \Chamilo\CourseBundle\Component\CourseCopy\Resources\Work::class,
+                    stdClass::class,
+                    Category::class,
+                    AttendanceLink::class,
+                    DropboxLink::class,
+                    Evaluation::class,
+                    ExerciseLink::class,
+                    ForumThreadLink::class,
+                    LearnpathLink::class,
+                    LinkFactory::class,
+                    Result::class,
+                    StudentPublicationLink::class,
+                    SurveyLink::class,
+                ];
+            // no break
+            case 'lp':
+                $allowedClasses = array_merge(
+                    $allowedClasses,
+                    [
+                        learnpath::class,
+                        learnpathItem::class,
+                        aicc::class,
+                        aiccBlock::class,
+                        aiccItem::class,
+                        aiccObjective::class,
+                        aiccResource::class,
+                        scorm::class,
+                        scormItem::class,
+                        scormMetadata::class,
+                        scormOrganization::class,
+                        scormResource::class,
+                        Link::class,
+                        LpItem::class,
+                    ]
+                );
+                break;
+            case 'not_allowed_classes':
+            default:
+                $allowedClasses = false;
+        }
+
+        if ($ignoreErrors) {
+            return @Unserialize::unserialize(
+                $serialized,
+                ['allowed_classes' => $allowedClasses]
+            );
+        }
+
+        return Unserialize::unserialize(
+            $serialized,
+            ['allowed_classes' => $allowedClasses]
+        );
+    }
+}

+ 45 - 9
main/inc/lib/agenda.lib.php

@@ -160,6 +160,18 @@ class Agenda
         }
     }
 
+    /**
+     * Returns the type previously set (and filtered) through setType
+     * If setType() was not called, then type defaults to "personal" as
+     * set in the class definition.
+     */
+    public function getType()
+    {
+        if (isset($this->type)) {
+            return $this->type;
+        }
+    }
+
     /**
      * @param int $id
      */
@@ -1337,6 +1349,25 @@ class Agenda
         }
     }
 
+    /**
+     * Clean events.
+     *
+     * @return bool
+     */
+    public function cleanEvents()
+    {
+        if (empty($this->events)) {
+            return false;
+        }
+
+        foreach ($this->events as &$event) {
+            $event['description'] = Security::remove_XSS($event['description']);
+            $event['title'] = Security::remove_XSS($event['title']);
+        }
+
+        return true;
+    }
+
     /**
      * @param int $id
      * @param int $minute_delta
@@ -2304,10 +2335,9 @@ class Agenda
         $action = isset($params['action']) ? Security::remove_XSS($params['action']) : null;
         $id = isset($params['id']) ? (int) $params['id'] : 0;
 
+        $url = api_get_self().'?action='.$action.'&id='.$id.'&type='.$this->type;
         if ($this->type == 'course') {
             $url = api_get_self().'?'.api_get_cidreq().'&action='.$action.'&id='.$id.'&type='.$this->type;
-        } else {
-            $url = api_get_self().'?action='.$action.'&id='.$id.'&type='.$this->type;
         }
 
         $form = new FormValidator(
@@ -2673,10 +2703,14 @@ class Agenda
      */
     public function getAttachment($attachmentId, $eventId, $courseInfo)
     {
+        if (empty($courseInfo) || empty($attachmentId) || empty($eventId)) {
+            return [];
+        }
+
         $tableAttachment = Database::get_course_table(TABLE_AGENDA_ATTACHMENT);
-        $courseId = intval($courseInfo['real_id']);
-        $eventId = intval($eventId);
-        $attachmentId = intval($attachmentId);
+        $courseId = (int) $courseInfo['real_id'];
+        $eventId = (int) $eventId;
+        $attachmentId = (int) $attachmentId;
 
         $row = [];
         $sql = "SELECT id, path, filename, comment
@@ -2711,7 +2745,7 @@ class Agenda
         $courseInfo
     ) {
         $agenda_table_attachment = Database::get_course_table(TABLE_AGENDA_ATTACHMENT);
-        $eventId = intval($eventId);
+        $eventId = (int) $eventId;
 
         // Storing the attachments
         $upload_ok = false;
@@ -2848,6 +2882,8 @@ class Agenda
     public function getAllRepeatEvents($eventId)
     {
         $events = [];
+        $eventId = (int) $eventId;
+
         switch ($this->type) {
             case 'personal':
                 break;
@@ -3062,7 +3098,7 @@ class Agenda
             api_get_self().'?action=importical&type='.$this->type,
             ['enctype' => 'multipart/form-data']
         );
-        $form->addElement('header', get_lang('ICalFileImport'));
+        $form->addHeader(get_lang('ICalFileImport'));
         $form->addElement('file', 'ical_import', get_lang('ICalFileImport'));
         $form->addRule(
             'ical_import',
@@ -3350,12 +3386,12 @@ class Agenda
 
         // sorting by hour for every day
         $agendaitems = [];
-        while (list($agendaday, $tmpitems) = each($items)) {
+        foreach ($items as $agendaday => $tmpitems) {
             if (!isset($agendaitems[$agendaday])) {
                 $agendaitems[$agendaday] = '';
             }
             sort($tmpitems);
-            while (list($key, $val) = each($tmpitems)) {
+            foreach ($tmpitems as $val) {
                 $agendaitems[$agendaday] .= $val;
             }
         }

+ 76 - 19
main/inc/lib/api.lib.php

@@ -104,6 +104,7 @@ define('SURVEY_VISIBLE_PUBLIC', 2);
 /* When you add a new tool you must add it into function api_get_tools_lists() too */
 define('TOOL_DOCUMENT', 'document');
 define('TOOL_LP_FINAL_ITEM', 'final_item');
+define('TOOL_READOUT_TEXT', 'readout_text');
 define('TOOL_THUMBNAIL', 'thumbnail');
 define('TOOL_HOTPOTATOES', 'hotpotatoes');
 define('TOOL_CALENDAR_EVENT', 'calendar_event');
@@ -248,6 +249,8 @@ define('LOG_USER_ID', 'user_id');
 define('LOG_USER_OBJECT', 'user_object');
 define('LOG_USER_FIELD_VARIABLE', 'user_field_variable');
 define('LOG_SESSION_ID', 'session_id');
+
+define('LOG_QUESTION_ID', 'question_id');
 define('LOG_SESSION_CATEGORY_ID', 'session_category_id');
 define('LOG_CONFIGURATION_SETTINGS_CATEGORY', 'settings_category');
 define('LOG_CONFIGURATION_SETTINGS_VARIABLE', 'settings_variable');
@@ -277,6 +280,9 @@ define('LOG_USER_REMOVED_LEGAL_ACCEPT', 'user_removed_legal_accept');
 
 define('LOG_USER_DELETE_ACCOUNT_REQUEST', 'user_delete_account_request');
 
+define('LOG_QUESTION_CREATED', 'question_created');
+define('LOG_QUESTION_UPDATED', 'question_updated');
+
 define('USERNAME_PURIFIER', '/[^0-9A-Za-z_\.]/');
 
 //used when login_is_email setting is true
@@ -468,6 +474,8 @@ define('RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS', 1); //Do not show score n
 define('RESULT_DISABLE_SHOW_SCORE_ONLY', 2); //Show score only
 define('RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES', 3); //Show final score only with categories
 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);
 // 4: Show final score only with categories and show expected answers only on the last attempt
 
 define('EXERCISE_MAX_NAME_SIZE', 80);
@@ -1462,7 +1470,7 @@ function _api_format_user($user, $add_password = false, $loadAvatars = true)
     $result['complete_name'] = api_get_person_name($result['firstname'], $result['lastname']);
     $result['complete_name_with_username'] = $result['complete_name'];
 
-    if (!empty($user['username'])) {
+    if (!empty($user['username']) && !api_get_configuration_value('hide_username_with_complete_name')) {
         $result['complete_name_with_username'] = $result['complete_name'].' ('.$user['username'].')';
     }
 
@@ -3277,6 +3285,7 @@ function api_display_tool_view_option()
         $output_string .= '<a class="btn btn-default btn-sm" href="'.$sourceurl.'&isStudentView=true" target="_self">'.
             Display::returnFontAwesomeIcon('eye').' '.get_lang('SwitchToStudentView').'</a>';
     }
+    $output_string = Security::remove_XSS($output_string);
     $html = Display::tag('div', $output_string, ['class' => 'view-options']);
 
     return $html;
@@ -3309,8 +3318,10 @@ function api_is_allowed_to_edit(
     $session_coach = false,
     $check_student_view = true
 ) {
+    $allowSessionAdminEdit = api_get_configuration_value('session_admins_edit_courses_content') === true;
     // Admins can edit anything.
-    if (api_is_platform_admin(false)) {
+    // Admins can edit anything.
+    if (api_is_platform_admin($allowSessionAdminEdit)) {
         //The student preview was on
         if ($check_student_view && api_is_student_view_active()) {
             return false;
@@ -4632,7 +4643,7 @@ function api_get_languages_combo($name = 'language')
  * @param  bool Hide form if only one language available (defaults to false = show the box anyway)
  * @param bool $showAsButton
  *
- * @return null|string Display the box directly
+ * @return string|null Display the box directly
  */
 function api_display_language_form($hide_if_no_choice = false, $showAsButton = false)
 {
@@ -5085,7 +5096,7 @@ function api_max_sort_value($user_course_category, $user_id)
  *
  * @return string the formated time
  */
-function api_time_to_hms($seconds)
+function api_time_to_hms($seconds, $space = ':')
 {
     // $seconds = -1 means that we have wrong data in the db.
     if ($seconds == -1) {
@@ -5107,6 +5118,10 @@ function api_time_to_hms($seconds)
     // How many seconds
     $sec = floor($seconds - ($hours * 3600) - ($min * 60));
 
+    if ($hours < 10) {
+        $hours = "0$hours";
+    }
+
     if ($sec < 10) {
         $sec = "0$sec";
     }
@@ -5115,7 +5130,7 @@ function api_time_to_hms($seconds)
         $min = "0$min";
     }
 
-    return "$hours:$min:$sec";
+    return $hours.$space.$min.$space.$sec;
 }
 
 /* FILE SYSTEM RELATED FUNCTIONS */
@@ -6213,6 +6228,12 @@ function api_is_element_in_the_session($tool, $element_id, $session_id = null)
         $session_id = api_get_session_id();
     }
 
+    $element_id = (int) $element_id;
+
+    if (empty($element_id)) {
+        return false;
+    }
+
     // Get information to build query depending of the tool.
     switch ($tool) {
         case TOOL_SURVEY:
@@ -6237,7 +6258,7 @@ function api_is_element_in_the_session($tool, $element_id, $session_id = null)
     $course_id = api_get_course_int_id();
 
     $sql = "SELECT session_id FROM $table_tool 
-            WHERE c_id = $course_id AND $key_field =  ".intval($element_id);
+            WHERE c_id = $course_id AND $key_field =  ".$element_id;
     $rs = Database::query($sql);
     if ($element_session_id = Database::result($rs, 0, 0)) {
         if ($element_session_id == intval($session_id)) {
@@ -6266,8 +6287,21 @@ function api_replace_dangerous_char($filename, $treat_spaces_as_hyphens = true)
     $encoding = api_detect_encoding($filename);
     if (empty($encoding)) {
         $encoding = 'ASCII';
+        if (!api_is_valid_ascii($filename)) {
+            // try iconv and try non standard ASCII a.k.a CP437
+            // see BT#15022
+            if (function_exists('iconv')) {
+                $result = iconv('CP437', 'UTF-8', $filename);
+                if (api_is_valid_utf8($result)) {
+                    $filename = $result;
+                    $encoding = 'UTF-8';
+                }
+            }
+        }
     }
+
     $filename = api_to_system_encoding($filename, $encoding);
+
     $url = URLify::filter(
         $filename,
         250,
@@ -6318,7 +6352,7 @@ function api_get_current_access_url_id()
             return -1;
         }
 
-        return $access_url_id;
+        return (int) $access_url_id;
     }
 
     //if the url in WEB_PATH was not found, it can only mean that there is
@@ -8020,7 +8054,7 @@ function api_get_user_info_from_official_code($officialCode)
  * @param string $usernameInputId
  * @param string $passwordInputId
  *
- * @return null|string
+ * @return string|null
  */
 function api_get_password_checker_js($usernameInputId, $passwordInputId)
 {
@@ -8262,7 +8296,7 @@ function api_can_login_as($loginAsUserId, $userId = null)
         }
     }
 
-    $userInfo = api_get_user_info($userId);
+    $userInfo = api_get_user_info($loginAsUserId);
     $isDrh = function () use ($loginAsUserId) {
         if (api_is_drh()) {
             if (api_drh_can_access_all_session_content()) {
@@ -8291,7 +8325,15 @@ function api_can_login_as($loginAsUserId, $userId = null)
         return false;
     };
 
-    return api_is_platform_admin() || (api_is_session_admin() && $userInfo['status'] == 5) || $isDrh();
+    $loginAsStatusForSessionAdmins = [STUDENT];
+
+    if (api_get_configuration_value('allow_session_admin_login_as_teacher')) {
+        $loginAsStatusForSessionAdmins[] = COURSEMANAGER;
+    }
+
+    return api_is_platform_admin() ||
+        (api_is_session_admin() && in_array($userInfo['status'], $loginAsStatusForSessionAdmins)) ||
+        $isDrh();
 }
 
 /**
@@ -8714,7 +8756,7 @@ function api_mail_html(
     $mail->Mailer = $platform_email['SMTP_MAILER'];
     $mail->Host = $platform_email['SMTP_HOST'];
     $mail->Port = $platform_email['SMTP_PORT'];
-    $mail->CharSet = $platform_email['SMTP_CHARSET'];
+    $mail->CharSet = isset($platform_email['SMTP_CHARSET']) ? $platform_email['SMTP_CHARSET'] : 'UTF-8';
     // Stay far below SMTP protocol 980 chars limit.
     $mail->WordWrap = 200;
 
@@ -8743,14 +8785,16 @@ function api_mail_html(
 
     // Reply to first
     if (isset($extra_headers['reply_to']) && empty($platform_email['SMTP_UNIQUE_REPLY_TO'])) {
-        $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']);
+        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);
     }
@@ -9000,6 +9044,19 @@ function api_protect_limit_for_session_admin()
     }
 }
 
+/**
+ * Limits that a session admin has access to list users.
+ * When limit_session_admin_list_users configuration variable is set to true.
+ */
+function api_protect_session_admin_list_users()
+{
+    $limitAdmin = api_get_configuration_value('limit_session_admin_list_users');
+
+    if (api_is_session_admin() && true === $limitAdmin) {
+        api_not_allowed(true);
+    }
+}
+
 /**
  * @return bool
  */

+ 46 - 38
main/inc/lib/attendance.lib.php

@@ -52,7 +52,9 @@ class Attendance
         $sql = "SELECT COUNT(att.id) AS total_number_of_items
                 FROM $tbl_attendance att
                 WHERE c_id = $course_id $condition_session ";
-        if ($active == 1 || $active == 0) {
+
+        $active = (int) $active;
+        if ($active === 1 || $active === 0) {
             $sql .= "AND att.active = $active";
         }
         $res = Database::query($sql);
@@ -72,12 +74,12 @@ class Attendance
     public function get_attendances_list($course_id = 0, $session_id = 0)
     {
         $table = Database::get_course_table(TABLE_ATTENDANCE);
-        $course_id = intval($course_id);
+        $course_id = (int) $course_id;
         if (empty($course_id)) {
             $course_id = api_get_course_int_id();
         }
 
-        $session_id = !empty($session_id) ? intval($session_id) : api_get_session_id();
+        $session_id = !empty($session_id) ? (int) $session_id : api_get_session_id();
         $condition_session = api_get_session_condition($session_id);
 
         // Get attendance data
@@ -117,9 +119,9 @@ class Attendance
         $course_id = api_get_course_int_id();
         $session_id = api_get_session_id();
         $condition_session = api_get_session_condition($session_id);
-        $column = intval($column);
-        $from = intval($from);
-        $number_of_items = intval($number_of_items);
+        $column = (int) $column;
+        $from = (int) $from;
+        $number_of_items = (int) $number_of_items;
 
         if (!in_array($direction, ['ASC', 'DESC'])) {
             $direction = 'ASC';
@@ -152,22 +154,23 @@ class Attendance
         $user_info = api_get_user_info();
         $allowDelete = api_get_setting('allow_delete_attendance');
 
-        while ($attendance = Database::fetch_row($res)) {
-            $student_param = '';
-            if (api_is_drh() && $_GET['student_id']) {
-                $student_param = '&student_id='.intval($_GET['student_id']);
-            }
+        $student_param = '';
+        $studentRequestId = isset($_GET['student_id']) ? (int) $_GET['student_id'] : 0;
+        if (api_is_drh() && !empty($studentRequestId)) {
+            $student_param = '&student_id='.$studentRequestId;
+        }
 
+        while ($attendance = Database::fetch_row($res)) {
             $session_star = '';
             if (api_get_session_id() == $attendance[6]) {
                 $session_star = api_get_session_image(api_get_session_id(), $user_info['status']);
             }
+
             if ($attendance[5] == 1) {
                 $isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh(
                     api_get_user_id(),
                     api_get_course_info()
-                );
-
+                ) || api_is_drh();
                 if (api_is_allowed_to_edit(null, true) || $isDrhOfCourse) {
                     // Link to edit
                     $attendance[1] = '<a href="index.php?'.api_get_cidreq().'&action=attendance_sheet_list&attendance_id='.$attendance[0].$student_param.'">'.$attendance[1].'</a>'.$session_star;
@@ -285,7 +288,7 @@ class Attendance
     public function get_attendance_by_id($attendanceId)
     {
         $tbl_attendance = Database::get_course_table(TABLE_ATTENDANCE);
-        $attendanceId = intval($attendanceId);
+        $attendanceId = (int) $attendanceId;
         $course_id = api_get_course_int_id();
         $attendance_data = [];
         $sql = "SELECT * FROM $tbl_attendance
@@ -393,7 +396,7 @@ class Attendance
 
         $session_id = api_get_session_id();
         $user_id = api_get_user_id();
-        $attendanceId = intval($attendanceId);
+        $attendanceId = (int) $attendanceId;
         $course_code = $_course['code'];
         $course_id = $_course['real_id'];
         $title_gradebook = $this->attendance_qualify_title;
@@ -469,7 +472,7 @@ class Attendance
         $course_id = $_course['real_id'];
         if (is_array($attendanceId)) {
             foreach ($attendanceId as $id) {
-                $id = intval($id);
+                $id = (int) $id;
                 $sql = "UPDATE $tbl_attendance SET active = 1
                         WHERE c_id = $course_id AND id = '$id'";
                 $result = Database::query($sql);
@@ -486,7 +489,7 @@ class Attendance
                 }
             }
         } else {
-            $attendanceId = intval($attendanceId);
+            $attendanceId = (int) $attendanceId;
             $sql = "UPDATE $tbl_attendance SET active = 1
                     WHERE c_id = $course_id AND id = '$attendanceId'";
             $result = Database::query($sql);
@@ -574,7 +577,7 @@ class Attendance
         $tbl_attendance = Database::get_course_table(TABLE_ATTENDANCE);
         $user_id = api_get_user_id();
         $course_id = $_course['real_id'];
-        $status = intval($status);
+        $status = (int) $status;
 
         $action = 'visible';
         if ($status == 0) {
@@ -583,7 +586,7 @@ class Attendance
 
         if (is_array($attendanceId)) {
             foreach ($attendanceId as $id) {
-                $id = intval($id);
+                $id = (int) $id;
                 $sql = "UPDATE $tbl_attendance SET active = $status
                         WHERE c_id = $course_id AND id = '$id'";
                 $result = Database::query($sql);
@@ -600,7 +603,7 @@ class Attendance
                 }
             }
         } else {
-            $attendanceId = intval($attendanceId);
+            $attendanceId = (int) $attendanceId;
             $sql = "UPDATE $tbl_attendance SET active = $status
                     WHERE c_id = $course_id AND id = '$attendanceId'";
             $result = Database::query($sql);
@@ -632,8 +635,8 @@ class Attendance
     {
         $tbl_attendance = Database::get_course_table(TABLE_ATTENDANCE);
         $course_id = api_get_course_int_id();
-        $attendanceId = intval($attendanceId);
-        $locked = ($lock) ? 1 : 0;
+        $attendanceId = (int) $attendanceId;
+        $locked = $lock ? 1 : 0;
         $upd = "UPDATE $tbl_attendance SET locked = $locked
                 WHERE c_id = $course_id AND id = $attendanceId";
         $result = Database::query($upd);
@@ -775,8 +778,8 @@ class Attendance
         $tbl_attendance_sheet = Database::get_course_table(TABLE_ATTENDANCE_SHEET);
         $tbl_attendance_calendar = Database::get_course_table(TABLE_ATTENDANCE_CALENDAR);
 
-        $calendar_id = intval($calendar_id);
-        $attendanceId = intval($attendanceId);
+        $calendar_id = (int) $calendar_id;
+        $attendanceId = (int) $attendanceId;
         $users = $this->get_users_rel_course();
         $course_id = api_get_course_int_id();
 
@@ -793,7 +796,7 @@ class Attendance
 
         // save users present in class
         foreach ($users_present as $user_present) {
-            $uid = intval($user_present);
+            $uid = (int) $user_present;
             // check if user already was registered with the $calendar_id
             $sql = "SELECT user_id FROM $tbl_attendance_sheet
                     WHERE c_id = $course_id AND user_id='$uid' AND attendance_calendar_id = '$calendar_id'";
@@ -900,6 +903,7 @@ class Attendance
         // get count of presences by users inside current attendance and save like results
         if (count($user_ids) > 0) {
             foreach ($user_ids as $uid) {
+                $uid = (int) $uid;
                 $count_presences = 0;
                 if (count($calendar_ids) > 0) {
                     $sql = "SELECT count(presence) as count_presences
@@ -1238,6 +1242,7 @@ class Attendance
             $user_ids = array_keys($users);
             if (count($calendar_ids) > 0 && count($user_ids) > 0) {
                 foreach ($user_ids as $uid) {
+                    $uid = (int) $uid;
                     $sql = "SELECT * FROM $tbl_attendance_sheet
                             WHERE
                                 c_id = $course_id AND
@@ -1254,7 +1259,7 @@ class Attendance
             }
         } else {
             // Get attendance for current user
-            $user_id = intval($user_id);
+            $user_id = (int) $user_id;
             if (count($calendar_ids) > 0) {
                 $sql = "SELECT cal.date_time, att.presence
                         FROM $tbl_attendance_sheet att
@@ -1289,7 +1294,7 @@ class Attendance
     public function get_next_attendance_calendar_id($attendanceId)
     {
         $table = Database::get_course_table(TABLE_ATTENDANCE_CALENDAR);
-        $attendanceId = intval($attendanceId);
+        $attendanceId = (int) $attendanceId;
         $course_id = api_get_course_int_id();
 
         $sql = "SELECT id FROM $table
@@ -1320,7 +1325,7 @@ class Attendance
     {
         $table = Database::get_course_table(TABLE_ATTENDANCE_CALENDAR);
         $course_id = api_get_course_int_id();
-        $attendanceId = intval($attendanceId);
+        $attendanceId = (int) $attendanceId;
         $sql = "SELECT id, date_time FROM $table
                 WHERE
                     c_id = $course_id AND
@@ -1357,6 +1362,7 @@ class Attendance
         $attendanceId = intval($attendanceId);
         $groupId = (int) $groupId;
         $course_id = api_get_course_int_id();
+
         if (empty($groupId)) {
             $sql = "SELECT score FROM $tbl_attendance_result
                     WHERE
@@ -1646,13 +1652,13 @@ class Attendance
      */
     public static function is_all_attendance_calendar_done($attendanceId)
     {
-        $attendanceId = intval($attendanceId);
+        $attendanceId = (int) $attendanceId;
         $done_calendar = self::get_done_attendance_calendar($attendanceId);
         $count_dates_in_calendar = self::get_count_dates_inside_attendance_calendar($attendanceId);
         $number_of_dates = self::get_number_of_attendance_calendar($attendanceId);
 
         $result = false;
-        if ($number_of_dates && (intval($count_dates_in_calendar) == intval($done_calendar))) {
+        if ($number_of_dates && intval($count_dates_in_calendar) == intval($done_calendar)) {
             $result = true;
         }
 
@@ -1738,7 +1744,6 @@ class Attendance
         }
 
         $table = Database::get_course_table(TABLE_ATTENDANCE_CALENDAR_REL_GROUP);
-
         foreach ($groupList as $groupId) {
             if (empty($groupId)) {
                 continue;
@@ -1776,7 +1781,8 @@ class Attendance
         return Database::select(
             '*',
             $table,
-            ['where' => [
+            [
+                'where' => [
                     'calendar_id = ? AND c_id = ?' => [$calendarId, $courseId],
                 ],
             ]
@@ -1797,7 +1803,8 @@ class Attendance
         return Database::select(
             '*',
             $table,
-            ['where' => [
+            [
+                'where' => [
                     'calendar_id = ? AND c_id = ? AND group_id = ?' => [$calendarId, $courseId, $groupId],
                 ],
             ]
@@ -1934,6 +1941,8 @@ class Attendance
         $tbl_attendance_sheet = Database::get_course_table(TABLE_ATTENDANCE_SHEET);
 
         $attendanceId = intval($attendanceId);
+        $calendar_id = (int) $calendar_id;
+
         // get all registered users inside current course
         $users = $this->get_users_rel_course();
         $user_ids = array_keys($users);
@@ -1960,11 +1969,11 @@ class Attendance
         } else {
             // delete just one row from attendance sheet by the calendar id
             $sql = "DELETE FROM $tbl_attendance_sheet
-                    WHERE c_id = $course_id AND attendance_calendar_id = '".intval($calendar_id)."'";
+                    WHERE c_id = $course_id AND attendance_calendar_id = '".$calendar_id."'";
             Database::query($sql);
             // delete data from attendance calendar
             $sql = "DELETE FROM $tbl_attendance_calendar
-                    WHERE c_id = $course_id AND id = '".intval($calendar_id)."'";
+                    WHERE c_id = $course_id AND id = '".$calendar_id."'";
             Database::query($sql);
 
             $this->deleteAttendanceCalendarGroup($calendar_id, $course_id);
@@ -1977,10 +1986,9 @@ class Attendance
         return $affected_rows;
     }
 
-    /** Setters for fields of attendances tables */
-    public function set_session_id($session_id)
+    public function set_session_id($sessionId)
     {
-        $this->session_id = $session_id;
+        $this->session_id = $sessionId;
     }
 
     public function set_course_id($course_id)

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

@@ -827,7 +827,7 @@ function return_breadcrumb($interbreadcrumb, $language_file, $nameTools)
             }
         }
 
-	if (!empty($lis)) {
+        if (!empty($lis)) {
             //Ofaj
             $html .= Display::tag('ul', get_lang('YouAreHere').': '.$lis, ['class' => 'breadcrumb']);
         }

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

@@ -3212,7 +3212,7 @@ class Blog
      * @param string $fullText
      * @param int    $length
      *
-     * @return null|string
+     * @return string|null
      */
     private static function getPostExtract($fullText, $length = BLOG_MAX_PREVIEW_CHARS)
     {

+ 6 - 1
main/inc/lib/certificate.lib.php

@@ -391,6 +391,7 @@ class Certificate extends Model
             '((author_last_name))',
             '((score))',
             '((portal_name))',
+            '((certificate_link))',
         ];
 
         return $tags;
@@ -417,6 +418,9 @@ class Certificate extends Model
         }
 
         $currentUserInfo = api_get_user_info();
+        $url = api_get_path(WEB_PATH).
+            'certificates/index.php?id='.$certificateInfo['id'].'&user_id='.$certificateInfo['user_id'];
+        $link = Display::url($url, $url);
 
         $replace = [
             $courseInfo['title'],
@@ -426,9 +430,10 @@ class Certificate extends Model
             $currentUserInfo['lastname'],
             $certificateInfo['score_certificate'],
             api_get_setting('Institution'),
+            $link,
         ];
-        $message = str_replace(self::notificationTags(), $replace, $message);
 
+        $message = str_replace(self::notificationTags(), $replace, $message);
         MessageManager::send_message(
             $userInfo['id'],
             $subject,

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

@@ -116,7 +116,6 @@ class Chat extends Model
 
         $chats = [];
         foreach ($chatHistory as $userId => $time) {
-
             /*$items = $this->getPreviousMessages(
                 $userId,
                 api_get_user_id(),
@@ -138,7 +137,6 @@ class Chat extends Model
             $chats[$userId]['window_user_info'] = api_get_user_info($userId);
         }
 
-
         /*
             foreach ($chatHistory as $chat) {
                 $userId = $chat['user_info']['user_id'];
@@ -351,7 +349,7 @@ class Chat extends Model
                     'online' => $user_info['user_is_online'],
                     'avatar' => $user_info['avatar_small'],
                     'user_id' => $user_info['user_id'],
-                ]
+                ],
             ];
 
             $items[$fromUserId] = $item;
@@ -383,7 +381,7 @@ class Chat extends Model
                     'online' => $user_info['user_is_online'],
                     'avatar' => $user_info['avatar_small'],
                     'user_id' => $user_info['user_id'],
-                ]
+                ],
             ];
 
             $items[$fromUserId] = $item;

+ 271 - 192
main/inc/lib/course.lib.php

@@ -34,29 +34,29 @@ class CourseManager
     /**
      * Creates a course.
      *
-     * @param array $params   columns in the main.course table
-     * @param int   $authorId
+     * @param array $params      Columns in the main.course table.
+     * @param int   $authorId    optional
+     * @param int   $accessUrlId optional
      *
      * @return mixed false if the course was not created, array with the course info
      */
-    public static function create_course($params, $authorId = 0)
+    public static function create_course($params, $authorId = 0, $accessUrlId = 0)
     {
         global $_configuration;
 
         $hook = HookCreateCourse::create();
 
         // Check portal limits
-        $access_url_id = 1;
-        if (api_get_multiple_access_url()) {
-            $access_url_id = api_get_current_access_url_id();
-        }
+        $accessUrlId = empty($accessUrlId)
+            ? (api_get_multiple_access_url() ? api_get_current_access_url_id() : 1)
+            : $accessUrlId;
 
         $authorId = empty($authorId) ? api_get_user_id() : (int) $authorId;
 
-        if (isset($_configuration[$access_url_id]) && is_array($_configuration[$access_url_id])) {
+        if (isset($_configuration[$accessUrlId]) && is_array($_configuration[$accessUrlId])) {
             $return = self::checkCreateCourseAccessUrlParam(
                 $_configuration,
-                $access_url_id,
+                $accessUrlId,
                 'hosting_limit_courses',
                 'PortalCoursesLimitReached'
             );
@@ -65,7 +65,7 @@ class CourseManager
             }
             $return = self::checkCreateCourseAccessUrlParam(
                 $_configuration,
-                $access_url_id,
+                $accessUrlId,
                 'hosting_limit_active_courses',
                 'PortalActiveCoursesLimitReached'
             );
@@ -99,7 +99,7 @@ class CourseManager
             $params['directory'] = $keys['currentCourseRepository'];
             $course_info = api_get_course_info($params['code']);
             if (empty($course_info)) {
-                $course_id = AddCourse::register_course($params);
+                $course_id = AddCourse::register_course($params, $accessUrlId);
                 $course_info = api_get_course_info_by_id($course_id);
 
                 if ($hook) {
@@ -169,7 +169,9 @@ class CourseManager
         $conditionsLike = [],
         $onlyThisCourseList = []
     ) {
-        $sql = "SELECT course.* FROM ".Database::get_main_table(TABLE_MAIN_COURSE)." course ";
+        $courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
+        $sql = "SELECT course.*, course.id as real_id 
+                FROM $courseTable course ";
 
         if (!empty($urlId)) {
             $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
@@ -188,7 +190,7 @@ class CourseManager
                 $sql .= " AND visibility = $visibility ";
             }
         } else {
-            $sql .= "WHERE 1 ";
+            $sql .= 'WHERE 1 ';
             if ($visibility !== -1) {
                 $sql .= " AND visibility = $visibility ";
             }
@@ -235,7 +237,7 @@ class CourseManager
         if (!empty($orderby)) {
             $sql .= " ORDER BY ".Database::escape_string($orderby)." ";
         } else {
-            $sql .= " ORDER BY 1 ";
+            $sql .= ' ORDER BY 1 ';
         }
 
         if (!in_array($orderdirection, ['ASC', 'DESC'])) {
@@ -278,6 +280,7 @@ class CourseManager
     public static function getUserInCourseStatus($userId, $courseId)
     {
         $courseId = (int) $courseId;
+        $userId = (int) $userId;
         if (empty($courseId)) {
             return false;
         }
@@ -287,7 +290,7 @@ class CourseManager
                 "SELECT status FROM ".Database::get_main_table(TABLE_MAIN_COURSE_USER)."
                 WHERE
                     c_id  = $courseId AND
-                    user_id = ".intval($userId)
+                    user_id = $userId"
             )
         );
 
@@ -368,30 +371,36 @@ class CourseManager
      * @param   mixed   user_id or an array with user ids
      * @param   string  course code
      * @param   int     session id
+     *
+     * @return bool
+     *
      * @assert ('', '') === false
      */
     public static function unsubscribe_user($user_id, $course_code, $session_id = 0)
     {
         if (empty($user_id)) {
-            return;
+            return false;
         }
         if (!is_array($user_id)) {
             $user_id = [$user_id];
         }
 
         if (count($user_id) == 0) {
-            return;
+            return false;
         }
 
         if (!empty($session_id)) {
-            $session_id = intval($session_id);
+            $session_id = (int) $session_id;
         } else {
             $session_id = api_get_session_id();
         }
 
+        if (empty($course_code)) {
+            return false;
+        }
+
         $userList = [];
 
-        // Cleaning the $user_id variable
         if (is_array($user_id)) {
             $new_user_id_list = [];
             foreach ($user_id as $my_user_id) {
@@ -401,7 +410,7 @@ class CourseManager
             $userList = $new_user_id_list;
             $user_ids = implode(',', $new_user_id_list);
         } else {
-            $user_ids = intval($user_id);
+            $user_ids = (int) $user_id;
             $userList[] = $user_id;
         }
 
@@ -415,7 +424,6 @@ class CourseManager
         $sql = "DELETE FROM ".Database::get_course_table(TABLE_GROUP_TUTOR)."
                 WHERE c_id = $course_id AND user_id IN (".$user_ids.")";
         Database::query($sql);
-
         // Erase user student publications (works) in the course - by André Boivin
 
         if (!empty($userList)) {
@@ -547,76 +555,107 @@ class CourseManager
         }
     }
 
+    /**
+     * @param string $courseCode
+     * @param int    $status
+     *
+     * @return bool
+     */
+    public static function autoSubscribeToCourse($courseCode, $status = STUDENT)
+    {
+        $courseInfo = api_get_course_info($courseCode);
+
+        if (empty($courseInfo)) {
+            return false;
+        }
+
+        if (api_is_anonymous()) {
+            return false;
+        }
+
+        if (in_array(
+            $courseInfo['visibility'],
+            [
+                COURSE_VISIBILITY_CLOSED,
+                COURSE_VISIBILITY_REGISTERED,
+                COURSE_VISIBILITY_HIDDEN,
+            ]
+        )
+        ) {
+            Display::addFlash(
+                Display::return_message(
+                    get_lang('SubscribingNotAllowed'),
+                    'warning'
+                )
+            );
+
+            return false;
+        }
+
+        return self::subscribeUser(api_get_user_id(), $courseInfo['code'], $status);
+    }
+
     /**
      * Subscribe a user to a course. No checks are performed here to see if
      * course subscription is allowed.
      *
-     * @param int    $user_id
-     * @param string $course_code
-     * @param int    $status               (STUDENT, COURSEMANAGER, COURSE_ADMIN, NORMAL_COURSE_MEMBER)
-     * @param int    $session_id
+     * @param int    $userId
+     * @param string $courseCode
+     * @param int    $status                 (STUDENT, COURSEMANAGER, COURSE_ADMIN, NORMAL_COURSE_MEMBER)
+     * @param int    $sessionId
      * @param int    $userCourseCategoryId
+     * @param bool   $checkTeacherPermission
      *
      * @return bool True on success, false on failure
      *
-     * @see add_user_to_course
      * @assert ('', '') === false
      */
-    public static function subscribe_user(
-        $user_id,
-        $course_code,
+    public static function subscribeUser(
+        $userId,
+        $courseCode,
         $status = STUDENT,
-        $session_id = 0,
-        $userCourseCategoryId = 0
+        $sessionId = 0,
+        $userCourseCategoryId = 0,
+        $checkTeacherPermission = true
     ) {
-        if ($user_id != strval(intval($user_id))) {
+        $userId = (int) $userId;
+        $status = (int) $status;
+
+        if (empty($userId) || empty($courseCode)) {
             return false; //detected possible SQL injection
         }
 
-        $courseInfo = api_get_course_info($course_code);
-        $courseId = $courseInfo['real_id'];
-        $courseCode = $courseInfo['code'];
-        $userCourseCategoryId = intval($userCourseCategoryId);
+        $courseInfo = api_get_course_info($courseCode);
+
+        if (empty($courseInfo)) {
+            Display::addFlash(Display::return_message(get_lang('CourseDoesNotExist'), 'warning'));
 
-        if (empty($user_id) || empty($courseCode)) {
             return false;
         }
 
-        if (!empty($session_id)) {
-            $session_id = intval($session_id);
-        } else {
-            $session_id = api_get_session_id();
-        }
+        $userInfo = api_get_user_info($userId);
 
-        $status = ($status == STUDENT || $status == COURSEMANAGER) ? $status : STUDENT;
+        if (empty($userInfo)) {
+            Display::addFlash(Display::return_message(get_lang('UserDoesNotExist'), 'warning'));
 
-        // A preliminary check whether the user has bben already registered on the platform.
-        $sql = "SELECT status FROM ".Database::get_main_table(TABLE_MAIN_USER)."
-                WHERE user_id = $user_id";
-        if (Database::num_rows(Database::query($sql)) == 0) {
             return false; // The user has not been registered to the platform.
         }
 
+        $courseId = $courseInfo['real_id'];
+        $courseCode = $courseInfo['code'];
+        $userCourseCategoryId = (int) $userCourseCategoryId;
         // Check whether the user has not been already subscribed to the course.
-        $sql = "SELECT * FROM ".Database::get_main_table(TABLE_MAIN_COURSE_USER)."                    
-                WHERE 
-                    user_id = $user_id AND 
-                    relation_type <> ".COURSE_RELATION_TYPE_RRHH." AND 
-                    c_id = $courseId
-                ";
-        if (empty($session_id)) {
-            if (Database::num_rows(Database::query($sql)) > 0) {
-                // The user has been already subscribed to the course.
-                return false;
-            }
-        }
-
-        if (!empty($session_id)) {
+        $sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
+        $status = $status === STUDENT || $status === COURSEMANAGER ? $status : STUDENT;
+        $courseUserTable = Database::get_main_table(TABLE_MAIN_COURSE_USER);
+        // The user has been already subscribed to the course.
+        if (!empty($sessionId)) {
             SessionManager::subscribe_users_to_session_course(
-                [$user_id],
-                $session_id,
+                [$userId],
+                $sessionId,
                 $courseCode
             );
+
             // Add event to the system log
             Event::addEvent(
                 LOG_SUBSCRIBE_USER_TO_COURSE,
@@ -625,28 +664,109 @@ class CourseManager
                 api_get_utc_datetime(),
                 api_get_user_id(),
                 $courseId,
-                $session_id
+                $sessionId
             );
-            $user_info = api_get_user_info($user_id);
             Event::addEvent(
                 LOG_SUBSCRIBE_USER_TO_COURSE,
                 LOG_USER_OBJECT,
-                $user_info,
+                $userInfo,
                 api_get_utc_datetime(),
                 api_get_user_id(),
                 $courseId,
-                $session_id
+                $sessionId
             );
+
+            return true;
         } else {
-            self::add_user_to_course(
-                $user_id,
+            // Check whether the user has not been already subscribed to the course.
+            $sql = "SELECT * FROM ".Database::get_main_table(TABLE_MAIN_COURSE_USER)."                    
+                    WHERE 
+                        user_id = $userId AND 
+                        relation_type <> ".COURSE_RELATION_TYPE_RRHH." AND 
+                        c_id = $courseId
+                    ";
+            if (Database::num_rows(Database::query($sql)) > 0) {
+                Display::addFlash(Display::return_message(get_lang('AlreadyRegisteredToCourse'), 'warning'));
+
+                return false;
+            }
+
+            if ($checkTeacherPermission && !api_is_course_admin()) {
+                // Check in advance whether subscription is allowed or not for this course.
+                if ((int) $courseInfo['subscribe'] === SUBSCRIBE_NOT_ALLOWED) {
+                    Display::addFlash(Display::return_message(get_lang('SubscriptionNotAllowed'), 'warning'));
+
+                    return false;
+                }
+            }
+
+            if ($status === STUDENT) {
+                // Check if max students per course extra field is set
+                $extraFieldValue = new ExtraFieldValue('course');
+                $value = $extraFieldValue->get_values_by_handler_and_field_variable($courseId, 'max_subscribed_students');
+                if (!empty($value) && isset($value['value'])) {
+                    $maxStudents = $value['value'];
+                    if ($maxStudents !== '') {
+                        $maxStudents = (int) $maxStudents;
+                        $count = self::get_user_list_from_course_code(
                 $courseCode,
-                $status,
-                $userCourseCategoryId
+                            0,
+                            null,
+                            null,
+                            STUDENT,
+                            true,
+                            false
             );
 
-            // Add event to the system log
-            Event::addEvent(
+                        if ($count >= $maxStudents) {
+                            Display::addFlash(Display::return_message(get_lang('MaxNumberSubscribedStudentsReached'), 'warning'));
+
+                            return false;
+                        }
+                    }
+                }
+            }
+
+            $maxSort = api_max_sort_value('0', $userId);
+            $params = [
+                'c_id' => $courseId,
+                'user_id' => $userId,
+                'status' => $status,
+                'sort' => $maxSort + 1,
+                'relation_type' => 0,
+                'user_course_cat' => (int) $userCourseCategoryId,
+            ];
+            $insertId = Database::insert($courseUserTable, $params);
+
+            if ($insertId) {
+                Display::addFlash(
+                    Display::return_message(
+                        sprintf(
+                            get_lang('UserXAddedToCourseX'),
+                            $userInfo['complete_name_with_username'],
+                            $courseInfo['title']
+                        )
+                    )
+                );
+
+                $send = api_get_course_setting('email_alert_to_teacher_on_new_user_in_course', $courseCode);
+
+                if ($send == 1) {
+                    self::email_to_tutor(
+                        $userId,
+                        $courseInfo['real_id'],
+                        false
+                    );
+                } elseif ($send == 2) {
+                    self::email_to_tutor(
+                        $userId,
+                        $courseInfo['real_id'],
+                        true
+                    );
+                }
+
+                // Add event to the system log
+                Event::addEvent(
                 LOG_SUBSCRIBE_USER_TO_COURSE,
                 LOG_COURSE_CODE,
                 $courseCode,
@@ -655,18 +775,20 @@ class CourseManager
                 $courseId
             );
 
-            $user_info = api_get_user_info($user_id);
-            Event::addEvent(
+                Event::addEvent(
                 LOG_SUBSCRIBE_USER_TO_COURSE,
                 LOG_USER_OBJECT,
-                $user_info,
+                    $userInfo,
                 api_get_utc_datetime(),
                 api_get_user_id(),
                 $courseId
             );
-        }
 
-        return true;
+                return true;
+            }
+
+            return false;
+        }
     }
 
     /**
@@ -730,94 +852,6 @@ class CourseManager
         }
     }
 
-    /**
-     * Subscribe a user $user_id to a course defined by $courseCode.
-     *
-     * @author Hugues Peeters
-     * @author Roan Embrechts
-     *
-     * @param int    $user_id                the id of the user
-     * @param string $courseCode             the course code
-     * @param int    $status                 (optional) The user's status in the course
-     * @param int    $userCourseCategoryId   The user category in which this subscription will be classified
-     * @param bool   $checkTeacherPermission
-     *
-     * @return false|string true if subscription succeeds, boolean false otherwise
-     * @assert ('', '') === false
-     */
-    public static function add_user_to_course(
-        $user_id,
-        $courseCode,
-        $status = STUDENT,
-        $userCourseCategoryId = 0,
-        $checkTeacherPermission = true
-    ) {
-        $debug = false;
-        $user_table = Database::get_main_table(TABLE_MAIN_USER);
-        $course_table = Database::get_main_table(TABLE_MAIN_COURSE);
-        $course_user_table = Database::get_main_table(TABLE_MAIN_COURSE_USER);
-
-        $status = ($status == STUDENT || $status == COURSEMANAGER) ? $status : STUDENT;
-        if (empty($user_id) || empty($courseCode) || ($user_id != strval(intval($user_id)))) {
-            return false;
-        }
-
-        $courseCode = Database::escape_string($courseCode);
-        $courseInfo = api_get_course_info($courseCode);
-        $courseId = $courseInfo['real_id'];
-
-        // Check in advance whether the user has already been registered on the platform.
-        $sql = "SELECT status FROM $user_table WHERE user_id = $user_id ";
-        if (Database::num_rows(Database::query($sql)) == 0) {
-            if ($debug) {
-                error_log('The user has not been registered to the platform');
-            }
-
-            return false; // The user has not been registered to the platform.
-        }
-
-        // Check whether the user has already been subscribed to this course.
-        $sql = "SELECT * FROM $course_user_table
-                WHERE
-                    user_id = $user_id AND
-                    relation_type <> ".COURSE_RELATION_TYPE_RRHH." AND
-                    c_id = $courseId";
-        if (Database::num_rows(Database::query($sql)) > 0) {
-            if ($debug) {
-                error_log('The user has been already subscribed to the course');
-            }
-
-            return false; // The user has been subscribed to the course.
-        }
-
-        if ($checkTeacherPermission && !api_is_course_admin()) {
-            // Check in advance whether subscription is allowed or not for this course.
-            $sql = "SELECT code, visibility FROM $course_table
-                    WHERE id = $courseId AND subscribe = '".SUBSCRIBE_NOT_ALLOWED."'";
-            if (Database::num_rows(Database::query($sql)) > 0) {
-                if ($debug) {
-                    error_log('Subscription is not allowed for this course');
-                }
-
-                return false; // Subscription is not allowed for this course.
-            }
-        }
-
-        // Ok, subscribe the user.
-        $max_sort = api_max_sort_value('0', $user_id);
-        $params = [
-            'c_id' => $courseId,
-            'user_id' => $user_id,
-            'status' => $status,
-            'sort' => $max_sort + 1,
-            'relation_type' => 0,
-            'user_course_cat' => (int) $userCourseCategoryId,
-        ];
-        $insertId = Database::insert($course_user_table, $params);
-
-        return $insertId;
-    }
-
     /**
      * Add the user $userId visibility to the course $courseCode in the catalogue.
      *
@@ -1060,6 +1094,7 @@ class CourseManager
      * @param int    $user_id
      * @param string $course_code  , if this parameter is null, it'll check for all courses
      * @param bool   $in_a_session True for checking inside sessions too, by default is not checked
+     * @param int    $session_id
      *
      * @return bool $session_id true if the user is registered in the course, false otherwise
      */
@@ -1069,12 +1104,12 @@ class CourseManager
         $in_a_session = false,
         $session_id = 0
     ) {
-        $user_id = intval($user_id);
+        $user_id = (int) $user_id;
 
         if (empty($session_id)) {
             $session_id = api_get_session_id();
         } else {
-            $session_id = intval($session_id);
+            $session_id = (int) $session_id;
         }
 
         $condition_course = '';
@@ -1106,13 +1141,16 @@ class CourseManager
         }
 
         $tableSessionCourseUser = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
-        $sql = 'SELECT 1 FROM '.$tableSessionCourseUser.
-            ' WHERE user_id = '.$user_id.' '.$condition_course;
+        $sql = 'SELECT 1 FROM '.$tableSessionCourseUser.'
+            WHERE user_id = '.$user_id.' AND session_id = '.$session_id.' '.$condition_course;
+
         if (Database::num_rows(Database::query($sql)) > 0) {
             return true;
         }
 
-        $sql = 'SELECT 1 FROM '.$tableSessionCourseUser.' WHERE user_id = '.$user_id.' AND status = 2 '.$condition_course;
+        $sql = 'SELECT 1 FROM '.$tableSessionCourseUser.'
+            WHERE user_id = '.$user_id.' AND session_id = '.$session_id.' AND status = 2 '.$condition_course;
+
         if (Database::num_rows(Database::query($sql)) > 0) {
             return true;
         }
@@ -1305,6 +1343,7 @@ class CourseManager
                         session_id,
                         user.*,
                         course.*,
+                        course.id AS c_id,
                         session.name as session_name
                     ';
             if ($return_count) {
@@ -1351,6 +1390,7 @@ class CourseManager
                     $sql = 'SELECT DISTINCT
                                 course.title,
                                 course.code,
+                                course.id AS c_id,
                                 course_rel_user.status as status_rel,
                                 user.id as user_id,
                                 user.email,
@@ -1514,12 +1554,24 @@ class CourseManager
                             }
                         }
 
+                        if (empty($users[$row_key])) {
+                            $users[$row_key] = [];
+                        }
+
+                        if (!array_key_exists('training_hours', $users[$row_key])) {
+                            $users[$row_key]['training_hours'] = 0;
+                        }
+
                         $users[$row_key]['training_hours'] += Tracking::get_time_spent_on_the_course(
                             $user['user_id'],
                             $courseId,
                             $sessionId
                         );
 
+                        if (!array_key_exists('count_users', $users[$row_key])) {
+                            $users[$row_key]['count_users'] = 0;
+                        }
+
                         $users[$row_key]['count_users'] += $counter;
 
                         $registered_users_with_extra_field = self::getCountRegisteredUsersWithCourseExtraField(
@@ -1568,7 +1620,7 @@ class CourseManager
                         $report_info['time'] = api_time_to_hms(
                             Tracking::get_time_spent_on_the_course(
                                 $user['user_id'],
-                                $courseId,
+                                empty($user['c_id']) ? $courseId : $user['c_id'],
                                 $sessionId
                             )
                         );
@@ -2360,6 +2412,9 @@ class CourseManager
             // To prevent fK mix up on some tables
             GroupManager::deleteAllGroupsFromCourse($courseId);
 
+            $app_plugin = new AppPlugin();
+            $app_plugin->performActionsWhenDeletingItem('course', $courseId);
+
             // Delete the course from the database
             $sql = "DELETE FROM $table_course WHERE id = $courseId";
             Database::query($sql);
@@ -2549,10 +2604,8 @@ class CourseManager
      */
     public static function email_to_tutor($user_id, $courseId, $send_to_tutor_also = false)
     {
-        if ($user_id != strval(intval($user_id))) {
-            return false;
-        }
-        $courseId = intval($courseId);
+        $user_id = (int) $user_id;
+        $courseId = (int) $courseId;
         $information = api_get_course_info_by_id($courseId);
         $course_code = $information['code'];
         $student = api_get_user_info($user_id);
@@ -2565,9 +2618,9 @@ class CourseManager
         //if ($send_to_tutor_also = true)
         // Proposed change:
         if ($send_to_tutor_also) {
-            $sql .= " AND is_tutor = 1";
+            $sql .= ' AND is_tutor = 1';
         } else {
-            $sql .= " AND status = 1";
+            $sql .= ' AND status = 1';
         }
 
         $result = Database::query($sql);
@@ -3387,9 +3440,7 @@ class CourseManager
                 // Teacher of course or teacher inside session
                 $whereConditions = " AND (cru.status = ".COURSEMANAGER." OR srcru.status = 2) ";
             }
-            $courseList = SessionManager::get_course_list_by_session_id(
-                $sessionId
-            );
+            $courseList = SessionManager::get_course_list_by_session_id($sessionId);
             if (!empty($courseList)) {
                 $courseListToString = implode("','", array_keys($courseList));
                 $whereConditions .= " AND c.id IN ('".$courseListToString."')";
@@ -3777,13 +3828,13 @@ class CourseManager
         $load_dirs = false,
         $useUserLanguageFilterIfAvailable = true
     ) {
-        $user_id = intval($user_id);
+        $user_id = (int) $user_id;
         if (empty($user_id)) {
             $user_id = api_get_user_id();
         }
         // Step 1: We get all the categories of the user
         $table = Database::get_main_table(TABLE_USER_COURSE_CATEGORY);
-        $sql = "SELECT id, title FROM $table
+        $sql = "SELECT * FROM $table
                 WHERE user_id = '".$user_id."'
                 ORDER BY sort ASC";
 
@@ -3792,6 +3843,8 @@ class CourseManager
             'in_category' => [],
             'not_category' => [],
         ];
+        $collapsable = api_get_configuration_value('allow_user_course_category_collapsable');
+        $stok = Security::get_token();
         while ($row = Database::fetch_array($result)) {
             // We simply display the title of the category.
             $courseInCategory = self::returnCoursesCategories(
@@ -3801,9 +3854,30 @@ class CourseManager
                 $useUserLanguageFilterIfAvailable
             );
 
+            $collapsed = 0;
+            $collapsableLink = '';
+            if ($collapsable) {
+                $url = api_get_path(WEB_CODE_PATH).
+                    'auth/courses.php?categoryid='.$row['id'].'&sec_token='.$stok.'&redirect=home';
+                $collapsed = isset($row['collapsed']) && $row['collapsed'] ? 1 : 0;
+                if ($collapsed === 0) {
+                    $collapsableLink = Display::url(
+                        '<i class="fa fa-folder-open"></i>',
+                        $url.'&action=set_collapsable&option=1'
+                    );
+                } else {
+                    $collapsableLink = Display::url(
+                        '<i class="fa fa-folder"></i>',
+                        $url.'&action=set_collapsable&option=0'
+                    );
+                }
+            }
+
             $params = [
                 'id_category' => $row['id'],
                 'title_category' => $row['title'],
+                'collapsed' => $collapsed,
+                'collapsable_link' => $collapsableLink,
                 'courses' => $courseInCategory,
             ];
             $listItems['in_category'][] = $params;
@@ -4284,7 +4358,7 @@ class CourseManager
         $now = date('Y-m-d h:i:s');
         $user_id = api_get_user_id();
         $course_info = api_get_course_info_by_id($course['real_id']);
-        $course_visibility = $course_info['visibility'];
+        $course_visibility = (int) $course_info['visibility'];
 
         if ($course_visibility === COURSE_VISIBILITY_HIDDEN) {
             return '';
@@ -6025,10 +6099,10 @@ class CourseManager
 
                 switch ($type) {
                     case 'GROUP':
-                        $groupList[] = intval($id);
+                        $groupList[] = (int) $id;
                         break;
                     case 'USER':
-                        $userList[] = intval($id);
+                        $userList[] = (int) $id;
                         break;
                 }
             }
@@ -6051,8 +6125,8 @@ class CourseManager
      */
     public static function addGroupMultiSelect($form, $groupInfo, $to = [])
     {
-        $group_users = GroupManager::get_subscribed_users($groupInfo);
-        $array = self::buildSelectOptions(null, $group_users, $to);
+        $groupUsers = GroupManager::get_subscribed_users($groupInfo);
+        $array = self::buildSelectOptions([$groupInfo], $groupUsers, $to);
 
         $result = [];
         foreach ($array as $content) {
@@ -6091,13 +6165,18 @@ class CourseManager
                         $alreadySelected
                     )
                     ) { // $alreadySelected is the array containing the groups (and users) that are already selected
-                        $user_label = ($thisGroup['userNb'] > 0) ? get_lang('Users') : get_lang('LowerCaseUser');
-                        $user_disabled = ($thisGroup['userNb'] > 0) ? "" : "disabled=disabled";
+                        $userCount = isset($thisGroup['userNb']) ? $thisGroup['userNb'] : 0;
+                        if (empty($userCount)) {
+                            $userCount = isset($thisGroup['count_users']) ? $thisGroup['count_users'] : 0;
+                        }
+                        // $alreadySelected is the array containing the groups (and users) that are already selected
+                        $user_label = ($userCount > 0) ? get_lang('Users') : get_lang('LowerCaseUser');
+                        $user_disabled = ($userCount > 0) ? "" : "disabled=disabled";
                         $result[] = [
                             'disabled' => $user_disabled,
                             'value' => "GROUP:".$groupId,
                             // The space before "G" is needed in order to advmultiselect.php js puts groups first
-                            'content' => " G: ".$thisGroup['name']." - ".$thisGroup['userNb']." ".$user_label,
+                            'content' => " G: ".$thisGroup['name']." - ".$userCount." ".$user_label,
                         ];
                     }
                 }
@@ -6661,7 +6740,7 @@ class CourseManager
      * @param Course $course
      * @param bool   $fullSize
      *
-     * @return null|string
+     * @return string|null
      */
     public static function getPicturePath(Course $course, $fullSize = false)
     {

+ 49 - 2
main/inc/lib/course_category.lib.php

@@ -69,7 +69,6 @@ class CourseCategory
         $conditions = null;
 
         $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE_CATEGORY);
-
         $conditions = " INNER JOIN $table a ON (t1.id = a.course_category_id)";
         $whereCondition = " AND a.access_url_id = ".api_get_current_access_url_id();
         $allowBaseCategories = api_get_configuration_value('allow_base_course_category');
@@ -113,6 +112,54 @@ class CourseCategory
         return $categories;
     }
 
+    /**
+     * Returns a flat list of all course categories in this URL. If the
+     * allow_base_course_category option is true, then also show the
+     * course categories of the base URL.
+     *
+     * @return array [id, name, code, parent_id, tree_pos, children_count, number_courses]
+     */
+    public static function getAllCategories()
+    {
+        $tbl_category = Database::get_main_table(TABLE_MAIN_CATEGORY);
+        $tbl_course = Database::get_main_table(TABLE_MAIN_COURSE);
+
+        $table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE_CATEGORY);
+        $conditions = " INNER JOIN $table a ON (t1.id = a.course_category_id)";
+        $whereCondition = " AND a.access_url_id = ".api_get_current_access_url_id();
+        $allowBaseCategories = api_get_configuration_value('allow_base_course_category');
+        if ($allowBaseCategories) {
+            $whereCondition = " AND (a.access_url_id = ".api_get_current_access_url_id()." OR a.access_url_id = 1) ";
+        }
+
+        $sql = "SELECT
+                t1.id, 
+                t1.name, 
+                t1.code, 
+                t1.parent_id, 
+                t1.tree_pos, 
+                t1.children_count, 
+                COUNT(DISTINCT t3.code) AS number_courses 
+                FROM $tbl_category t1 
+                $conditions
+                LEFT JOIN $tbl_course t3 
+                ON t3.category_code=t1.code
+                WHERE 1=1
+                    $whereCondition 
+                GROUP BY
+                    t1.name, 
+                    t1.code, 
+                    t1.parent_id, 
+                    t1.tree_pos, 
+                    t1.children_count 
+                ORDER BY t1.parent_id, t1.tree_pos";
+
+        $result = Database::query($sql);
+        $categories = Database::store_result($result, 'ASSOC');
+
+        return $categories;
+    }
+
     /**
      * @param string $code
      * @param string $name
@@ -408,7 +455,7 @@ class CourseCategory
     /**
      * @param string $categoryCode
      *
-     * @return null|string
+     * @return string|null
      */
     public static function getParentsToString($categoryCode)
     {

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

@@ -55,6 +55,7 @@ define('TABLE_MAIN_GRADEBOOK_LINK', 'gradebook_link');
 define('TABLE_MAIN_GRADEBOOK_SCORE_DISPLAY', 'gradebook_score_display');
 define('TABLE_MAIN_GRADEBOOK_CERTIFICATE', 'gradebook_certificate');
 define('TABLE_MAIN_GRADEBOOK_SCORE_LOG', 'gradebook_score_log');
+define('TABLE_MAIN_GRADEBOOK_RESULT_ATTEMPT', 'gradebook_result_attempt');
 
 // Profiling
 define('TABLE_EXTRA_FIELD', 'extra_field');
@@ -203,6 +204,7 @@ define('TABLE_LP_VIEW', 'lp_view');
 define('TABLE_LP_ITEM_VIEW', 'lp_item_view');
 define('TABLE_LP_IV_INTERACTION', 'lp_iv_interaction'); // IV = Item View
 define('TABLE_LP_IV_OBJECTIVE', 'lp_iv_objective'); // IV = Item View
+define('TABLE_LP_CATEGORY', 'lp_category');
 
 // Smartblogs (Kevin Van Den Haute::kevin@develop-it.be)
 // Permission tables

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

@@ -180,7 +180,6 @@ class Database
             $sysPath."vendor/gedmo/doctrine-extensions/lib"
         );
 
-
         Type::overrideType(
             Type::DATETIME,
             self::getUTCDateTimeTypeClass()

+ 32 - 16
main/inc/lib/display.lib.php

@@ -67,7 +67,11 @@ class Display
             $showHeader = false;
         }
 
-        self::$global_template = new Template($tool_name, $showHeader, $showHeader);
+        /* USER_IN_ANON_SURVEY is defined in fillsurvey.php when survey is marked as anonymous survey */
+        $userInAnonSurvey = defined('USER_IN_ANON_SURVEY') && USER_IN_ANON_SURVEY;
+
+        self::$global_template = new Template($tool_name, $showHeader, $showHeader, false, $userInAnonSurvey);
+        self::$global_template->assign('user_in_anon_survey', $userInAnonSurvey);
 
         // Fixing tools with any help it takes xxx part of main/xxx/index.php
         if (empty($help)) {
@@ -1063,6 +1067,7 @@ class Display
         $html = '';
         $extra = '';
         $default_id = 'id="'.$name.'" ';
+        $extra_attributes = array_merge(['class' => 'form-control'], $extra_attributes);
         foreach ($extra_attributes as $key => $parameter) {
             if ($key == 'id') {
                 $default_id = '';
@@ -1320,9 +1325,23 @@ class Display
         $obj->viewrecords = 'true';
         $all_value = 10000000;
 
+        // Default row quantity
+        $obj->rowNum = 20;
+
         // Default row quantity
         if (!isset($extra_params['rowList'])) {
             $extra_params['rowList'] = [20, 50, 100, 500, 1000, $all_value];
+            $rowList = api_get_configuration_value('table_row_list');
+            if (!empty($rowList) && isset($rowList['options'])) {
+                $rowList = $rowList['options'];
+                $rowList[] = $all_value;
+            }
+            $extra_params['rowList'] = $rowList;
+        }
+
+        $defaultRow = api_get_configuration_value('table_default_row');
+        if (!empty($defaultRow)) {
+            $obj->rowNum = (int) $defaultRow;
         }
 
         $json = '';
@@ -1348,8 +1367,6 @@ class Display
             $obj->rowList = $extra_params['rowList'];
         }
 
-        // Sets how many records we want to view in the grid
-        $obj->rowNum = 20;
         if (!empty($extra_params['rowNum'])) {
             $obj->rowNum = $extra_params['rowNum'];
         } else {
@@ -1883,7 +1900,7 @@ class Display
     /**
      * @param array $list
      *
-     * @return null|string
+     * @return string|null
      */
     public static function description($list)
     {
@@ -1901,15 +1918,15 @@ class Display
     }
 
     /**
-     * @param $percentage
-     * @param bool $show_percentage
-     * @param null $extra_info
+     * @param int    $percentage
+     * @param bool   $show_percentage
+     * @param string $extra_info
      *
      * @return string
      */
-    public static function bar_progress($percentage, $show_percentage = true, $extra_info = null)
+    public static function bar_progress($percentage, $show_percentage = true, $extra_info = '')
     {
-        $percentage = intval($percentage);
+        $percentage = (int) $percentage;
         $div = '<div class="progress">
                 <div
                     class="progress-bar progress-bar-striped"
@@ -1926,7 +1943,7 @@ class Display
                 $div .= $extra_info;
             }
         }
-        $div .= '</div>';
+        $div .= '</div></div>';
 
         return $div;
     }
@@ -1935,7 +1952,7 @@ class Display
      * @param string $count
      * @param string $type
      *
-     * @return null|string
+     * @return string|null
      */
     public static function badge($count, $type = "warning")
     {
@@ -2026,7 +2043,7 @@ class Display
      * @param array  $items
      * @param string $class
      *
-     * @return null|string
+     * @return string|null
      */
     public static function actions($items, $class = 'new_actions')
     {
@@ -2079,7 +2096,7 @@ class Display
      * @param string $type
      * @param null   $id
      *
-     * @return null|string
+     * @return string|null
      */
     public static function generate_accordion($items, $type = 'jquery', $id = null)
     {
@@ -2160,7 +2177,7 @@ class Display
      * @param string $file
      * @param array  $params
      *
-     * @return null|string
+     * @return string|null
      */
     public static function getMediaPlayer($file, $params = [])
     {
@@ -2637,7 +2654,7 @@ class Display
      * @param bool|true  $open
      * @param bool|false $fullClickable
      *
-     * @return null|string
+     * @return string|null
      *
      * @todo rework function to easy use
      */
@@ -2784,7 +2801,6 @@ HTML;
             default:
                 //$html = self::url($data['basename'], $fileUrl);
                 break;
-
         }
         //$html = self::url($content, $fileUrl, ['ajax']);
 

+ 152 - 86
main/inc/lib/document.lib.php

@@ -65,7 +65,7 @@ class DocumentManager
     {
         // All MIME types in an array (from 1.6, this is the authorative source)
         // Please, keep this alphabetical if you add something to this list!
-        $mime_types = [
+        $mimeTypes = [
             'ai' => 'application/postscript',
             'aif' => 'audio/x-aiff',
             'aifc' => 'audio/x-aiff',
@@ -230,6 +230,7 @@ class DocumentManager
             'wav' => 'audio/x-wav',
             'wbmp' => 'image/vnd.wap.wbmp',
             'wbxml' => 'application/vnd.wap.wbxml',
+            'webp' => 'image/webp',
             'wml' => 'text/vnd.wap.wml',
             'wmlc' => 'application/vnd.wap.wmlc',
             'wmls' => 'text/vnd.wap.wmlscript',
@@ -255,7 +256,7 @@ class DocumentManager
         ];
 
         if ($filename === true) {
-            return $mime_types;
+            return $mimeTypes;
         }
 
         // Get the extension of the file
@@ -270,14 +271,79 @@ class DocumentManager
         }
 
         //if the extension is found, return the content type
-        if (isset($mime_types[$extension])) {
-            return $mime_types[$extension];
+        if (isset($mimeTypes[$extension])) {
+            return $mimeTypes[$extension];
         }
-
         //else return octet-stream
         return 'application/octet-stream';
     }
 
+    /**
+     * This function smart streams a file to the client using HTTP headers.
+     *
+     * @param string $fullFilename The full path of the file to be sent
+     * @param string $filename     The name of the file as shown to the client
+     * @param string $contentType  The MIME type of the file
+     *
+     * @return bool false if file doesn't exist, true if stream succeeded
+     */
+    public static function smartReadFile($fullFilename, $filename, $contentType = 'application/octet-stream')
+    {
+        if (!file_exists($fullFilename)) {
+            header("HTTP/1.1 404 Not Found");
+
+            return false;
+        }
+
+        $size = filesize($fullFilename);
+        $time = date('r', filemtime($fullFilename));
+
+        $fm = @fopen($fullFilename, 'rb');
+        if (!$fm) {
+            header("HTTP/1.1 505 Internal server error");
+
+            return false;
+        }
+
+        $begin = 0;
+        $end = $size - 1;
+
+        if (isset($_SERVER['HTTP_RANGE'])) {
+            if (preg_match('/bytes=\h*(\d+)-(\d*)[\D.*]?/i', $_SERVER['HTTP_RANGE'], $matches)) {
+                $begin = intval($matches[1]);
+                if (!empty($matches[2])) {
+                    $end = intval($matches[2]);
+                }
+            }
+        }
+
+        if (isset($_SERVER['HTTP_RANGE'])) {
+            header('HTTP/1.1 206 Partial Content');
+        } else {
+            header('HTTP/1.1 200 OK');
+        }
+
+        header("Content-Type: $contentType");
+        header('Cache-Control: public, must-revalidate, max-age=0');
+        header('Pragma: no-cache');
+        header('Accept-Ranges: bytes');
+        header('Content-Length:'.(($end - $begin) + 1));
+        if (isset($_SERVER['HTTP_RANGE'])) {
+            header("Content-Range: bytes $begin-$end/$size");
+        }
+        header("Content-Disposition: inline; filename=$filename");
+        header("Content-Transfer-Encoding: binary");
+        header("Last-Modified: $time");
+
+        $cur = $begin;
+        fseek($fm, $begin, 0);
+
+        while (!feof($fm) && $cur <= $end && (connection_status() == 0)) {
+            echo fread($fm, min(1024 * 16, ($end - $cur) + 1));
+            $cur += 1024 * 16;
+        }
+    }
+
     /**
      * This function streams a file to the client.
      *
@@ -374,6 +440,16 @@ class DocumentManager
                         }
                     }
                     break;
+                case 'video/mp4':
+                case 'audio/mpeg':
+                case 'audio/mp4':
+                case 'audio/ogg':
+                case 'audio/webm':
+                case 'audio/wav':
+                case 'video/ogg':
+                case 'video/webm':
+                    self::smartReadFile($full_file_name, $filename, $contentType);
+                    exit;
                 case 'application/vnd.dwg':
                 case 'application/vnd.dwf':
                     header('Content-type: application/octet-stream');
@@ -419,7 +495,7 @@ class DocumentManager
      * @param string $path
      * @param int    $sessionId
      *
-     * @return null|string
+     * @return string|null
      */
     public static function getSessionFolderFilters($path, $sessionId)
     {
@@ -537,6 +613,7 @@ class DocumentManager
                     $sharedCondition
                 ORDER BY last.iid DESC, last.session_id DESC
                 ";
+
         $result = Database::query($sql);
 
         $documentData = [];
@@ -1323,6 +1400,8 @@ class DocumentManager
         }
 
         $session_id = empty($session_id) ? api_get_session_id() : (int) $session_id;
+        $groupId = api_get_group_id();
+
         $www = api_get_path(WEB_COURSE_PATH).$course_info['path'].'/document';
         $TABLE_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
         $id = (int) $id;
@@ -1336,6 +1415,7 @@ class DocumentManager
         }
 
         $result = Database::query($sql);
+        $courseParam = '&cidReq='.$course_code.'&id='.$id.'&id_session='.$session_id.'&gidReq='.$groupId;
         if ($result && Database::num_rows($result) == 1) {
             $row = Database::fetch_array($result, 'ASSOC');
             //@todo need to clarify the name of the URLs not nice right now
@@ -1343,8 +1423,8 @@ class DocumentManager
             $path = str_replace('%2F', '/', $url_path);
             $pathinfo = pathinfo($row['path']);
 
-            $row['url'] = api_get_path(WEB_CODE_PATH).'document/showinframes.php?cidReq='.$course_code.'&id='.$id;
-            $row['document_url'] = api_get_path(WEB_CODE_PATH).'document/document.php?cidReq='.$course_code.'&id='.$id;
+            $row['url'] = api_get_path(WEB_CODE_PATH).'document/showinframes.php?id='.$id.$courseParam;
+            $row['document_url'] = api_get_path(WEB_CODE_PATH).'document/document.php?id='.$id.$courseParam;
             $row['absolute_path'] = api_get_path(SYS_COURSE_PATH).$course_info['path'].'/document'.$row['path'];
             $row['absolute_path_from_document'] = '/document'.$row['path'];
             $row['absolute_parent_path'] = api_get_path(SYS_COURSE_PATH).$course_info['path'].'/document'.$pathinfo['dirname'].'/';
@@ -1355,6 +1435,10 @@ class DocumentManager
                 $row['parent_id'] = '0';
             } else {
                 $row['parent_id'] = self::get_document_id($course_info, dirname($row['path']), $session_id);
+                if (empty($row['parent_id'])) {
+                    // Try one more with session id = 0
+                    $row['parent_id'] = self::get_document_id($course_info, dirname($row['path']), 0);
+                }
             }
             $parents = [];
 
@@ -3158,6 +3242,7 @@ class DocumentManager
      * @param bool   $showInvisibleFiles
      * @param bool   $showOnlyFolders
      * @param int    $folderId
+     * @param bool   $addCloseButton
      *
      * @return string
      */
@@ -3171,7 +3256,8 @@ class DocumentManager
         $overwrite_url = '',
         $showInvisibleFiles = false,
         $showOnlyFolders = false,
-        $folderId = false
+        $folderId = false,
+        $addCloseButton = true
     ) {
         if (empty($course_info['real_id']) || empty($course_info['code']) || !is_array($course_info)) {
             return '';
@@ -3179,12 +3265,7 @@ class DocumentManager
 
         $user_id = api_get_user_id();
         $userInfo = api_get_user_info();
-
-        $user_in_course = false;
-        if (api_is_platform_admin()) {
-            $user_in_course = true;
-        }
-
+        $user_in_course = api_is_platform_admin();
         if (!$user_in_course) {
             if (CourseManager::is_course_teacher($user_id, $course_info['code'])) {
                 $user_in_course = true;
@@ -3192,7 +3273,7 @@ class DocumentManager
         }
 
         // Condition for the session
-        $session_id = intval($session_id);
+        $session_id = (int) $session_id;
 
         if (!$user_in_course) {
             if (empty($session_id)) {
@@ -3228,19 +3309,12 @@ class DocumentManager
         // If we are in LP display hidden folder https://support.chamilo.org/issues/6679
         $lp_visibility_condition = null;
         if ($lp_id) {
-            // $lp_visibility_condition = " OR filetype='folder'";
             if ($showInvisibleFiles) {
                 $lp_visibility_condition .= ' OR last.visibility = 0';
             }
         }
 
-        $showOnlyFoldersCondition = null;
-        if ($showOnlyFolders) {
-            //$showOnlyFoldersCondition = " AND docs.filetype = 'folder' ";
-        }
-
         $folderCondition = " AND docs.path LIKE '/%' ";
-
         if (!api_is_allowed_to_edit()) {
             $protectedFolders = self::getProtectedFolderFromStudent();
             foreach ($protectedFolders as $folder) {
@@ -3277,7 +3351,7 @@ class DocumentManager
             }
         }
 
-        $levelCondition = null;
+        $levelCondition = '';
         if ($folderId === false) {
             $levelCondition = " AND docs.path NOT LIKE'/%/%'";
         }
@@ -3303,25 +3377,17 @@ class DocumentManager
         $resources = Database::store_result($res_doc, 'ASSOC');
 
         $return = '';
-        if ($lp_id) {
+        if ($lp_id == false && $addCloseButton) {
             if ($folderId === false) {
-                /*$return .= '<div class="lp_resource_element">';
-                $return .= Display::return_icon('new_doc.gif', '', [], ICON_SIZE_SMALL);
-                $return .= Display::url(
-                    get_lang('CreateTheDocument'),
-                    api_get_self().'?'.api_get_cidreq().'&action=add_item&type='.TOOL_DOCUMENT.'&lp_id='.$_SESSION['oLP']->lp_id
+                $return .= Display::div(
+                    Display::url(
+                        Display::return_icon('close.png', get_lang('Close'), [], ICON_SIZE_SMALL),
+                        ' javascript:void(0);',
+                        ['id' => 'close_div_'.$course_info['real_id'].'_'.$session_id, 'class' => 'close_div']
+                    ),
+                    ['style' => 'position:absolute;right:10px']
                 );
-                $return .= '</div>';*/
             }
-        } else {
-            $return .= Display::div(
-                Display::url(
-                    Display::return_icon('close.png', get_lang('Close'), [], ICON_SIZE_SMALL),
-                    ' javascript:void(0);',
-                    ['id' => 'close_div_'.$course_info['real_id'].'_'.$session_id, 'class' => 'close_div']
-                ),
-                ['style' => 'position:absolute;right:10px']
-            );
         }
 
         // If you want to debug it, I advise you to do "echo" on the eval statements.
@@ -3529,31 +3595,44 @@ class DocumentManager
     }
 
     /**
-     * @param int    $doc_id
-     * @param string $course_code
-     * @param int    $session_id
-     * @param int    $user_id
-     * @param int    $groupId     iid
+     * @param int   $doc_id
+     * @param array $courseInfo
+     * @param int   $sessionId
+     * @param int   $user_id
+     * @param int   $groupId               iid
+     * @param bool  $checkParentVisibility
      *
      * @return bool
      */
     public static function check_visibility_tree(
         $doc_id,
-        $course_code,
-        $session_id,
+        $courseInfo,
+        $sessionId,
         $user_id,
-        $groupId = 0
+        $groupId = 0,
+        $checkParentVisibility = true
     ) {
+        if (empty($courseInfo)) {
+            return false;
+        }
+
+        $courseCode = $courseInfo['code'];
+
+        if (empty($courseCode)) {
+            return false;
+        }
+
         $document_data = self::get_document_data_by_id(
             $doc_id,
-            $course_code,
+            $courseCode,
             null,
-            $session_id
+            $sessionId
         );
-        if ($session_id != 0 && !$document_data) {
+
+        if ($sessionId != 0 && !$document_data) {
             $document_data = self::get_document_data_by_id(
                 $doc_id,
-                $course_code,
+                $courseCode,
                 null,
                 0
             );
@@ -3561,30 +3640,34 @@ class DocumentManager
 
         if (!empty($document_data)) {
             // If admin or course teacher, allow anyway
-            if (api_is_platform_admin() || CourseManager::is_course_teacher($user_id, $course_code)) {
+            if (api_is_platform_admin() || CourseManager::is_course_teacher($user_id, $courseCode)) {
                 return true;
             }
-            $course_info = api_get_course_info($course_code);
+
             if ($document_data['parent_id'] == false || empty($document_data['parent_id'])) {
                 if (!empty($groupId)) {
                     return true;
                 }
-                $visible = self::is_visible_by_id($doc_id, $course_info, $session_id, $user_id);
+                $visible = self::is_visible_by_id($doc_id, $courseInfo, $sessionId, $user_id);
 
                 return $visible;
             } else {
-                $visible = self::is_visible_by_id($doc_id, $course_info, $session_id, $user_id);
+                $visible = self::is_visible_by_id($doc_id, $courseInfo, $sessionId, $user_id);
 
                 if (!$visible) {
                     return false;
                 } else {
-                    return self::check_visibility_tree(
+                    if ($checkParentVisibility) {
+                        return self::check_visibility_tree(
                         $document_data['parent_id'],
-                        $course_code,
-                        $session_id,
+                            $courseInfo,
+                            $sessionId,
                         $user_id,
                         $groupId
                     );
+                    }
+
+                    return true;
                 }
             }
         } else {
@@ -4135,24 +4218,6 @@ class DocumentManager
         }
     }
 
-    /**
-     * @param string $file
-     *
-     * @return string
-     */
-    public static function readNanogongFile($file)
-    {
-        $nanoGongJarFile = api_get_path(WEB_LIBRARY_PATH).'nanogong/nanogong.jar';
-        $html = '<applet id="applet" archive="'.$nanoGongJarFile.'" code="gong.NanoGong" width="160" height="95">';
-        $html .= '<param name="SoundFileURL" value="'.$file.'" />';
-        $html .= '<param name="ShowSaveButton" value="false" />';
-        $html .= '<param name="ShowTime" value="true" />';
-        $html .= '<param name="ShowRecordButton" value="false" />';
-        $html .= '</applet>';
-
-        return $html;
-    }
-
     /**
      * @param string $filePath
      * @param string $path
@@ -4280,7 +4345,6 @@ class DocumentManager
             file_exists($documentData['absolute_path'])
         ) {
             $mp3FilePath = self::convertWavToMp3($documentData['absolute_path']);
-            error_log($mp3FilePath);
 
             if (!empty($mp3FilePath) && file_exists($mp3FilePath)) {
                 $documentId = self::addFileToDocumentTool(
@@ -4376,7 +4440,7 @@ class DocumentManager
     }
 
     /**
-     * Check if the past is used in this course.
+     * Check if the path is used in this course.
      *
      * @param array  $courseInfo
      * @param string $path
@@ -4862,6 +4926,9 @@ class DocumentManager
             $attributes = ['onchange' => 'javascript: document.selector.submit();'];
         }
         $form->addElement('hidden', 'cidReq', api_get_course_id());
+        $form->addElement('hidden', 'id_session', api_get_session_id());
+        $form->addElement('hidden', 'gidReq', api_get_group_id());
+
         $parent_select = $form->addSelect(
             $selectName,
             get_lang('CurrentDirectory'),
@@ -5546,10 +5613,8 @@ class DocumentManager
                         ($folder != $move_file) &&
                         (substr($folder, 0, strlen($move_file) + 1) != $move_file.'/')
                     ) {
-                        $path_displayed = $folder;
                         // If document title is used, we have to display titles instead of real paths...
                         $path_displayed = self::get_titles_of_path($folder);
-
                         if (empty($path_displayed)) {
                             $path_displayed = get_lang('Untitled');
                         }
@@ -5566,8 +5631,7 @@ class DocumentManager
                     // Cannot copy dir into his own subdir
                     $path_displayed = self::get_titles_of_path($folder);
                     $display_folder = substr($path_displayed, strlen($group_dir));
-                    $display_folder = ($display_folder == '') ? get_lang('Documents') : $display_folder;
-                    //$form .= '<option value="'.$folder.'">'.$display_folder.'</option>';
+                    $display_folder = $display_folder == '' ? get_lang('Documents') : $display_folder;
                     $options[$folder] = $display_folder;
                 }
             }
@@ -6388,6 +6452,7 @@ class DocumentManager
             'dropboxusercontent.com',
             'fileserve.com',
             'drive.google.com',
+            'docs.google.com',
             'icloud.com',
             'mediafire.com',
             'mega.nz',
@@ -6455,7 +6520,7 @@ class DocumentManager
      * @param string $target
      * @param string $overwrite_url
      *
-     * @return null|string
+     * @return string|null
      */
     private static function parseFile(
         $userInfo,
@@ -6502,6 +6567,7 @@ class DocumentManager
             // Direct document URL
             $url = $web_code_path.'document/document.php?cidReq='.$course_info['code'].'&id_session='.$session_id.'&id='.$documentId;
             if (!empty($overwrite_url)) {
+                $overwrite_url = Security::remove_XSS($overwrite_url);
                 $url = $overwrite_url.'&cidReq='.$course_info['code'].'&id_session='.$session_id.'&document_id='.$documentId;
             }
         }
@@ -6556,7 +6622,7 @@ class DocumentManager
      * @param array $resource
      * @param int   $lp_id
      *
-     * @return null|string
+     * @return string|null
      */
     private static function parseFolder($folderId, $resource, $lp_id)
     {
@@ -6759,7 +6825,7 @@ class DocumentManager
      * @param bool  $isCertificateMode
      * @param int   $parentId
      *
-     * @return null|string
+     * @return string|null
      */
     private static function getButtonVisibility(
         $isReadOnly,

+ 26 - 7
main/inc/lib/events.lib.php

@@ -89,7 +89,7 @@ class Event
             $autoSubscribe = explode('|', $autoSubscribe);
             foreach ($autoSubscribe as $code) {
                 if (CourseManager::course_exists($code)) {
-                    CourseManager::subscribe_user($userId, $code);
+                    CourseManager::subscribeUser($userId, $code);
                 }
             }
         }
@@ -1144,6 +1144,8 @@ class Event
      * @param int   $lp_id
      * @param array $course
      * @param int   $session_id
+     *
+     * @return bool
      */
     public static function delete_student_lp_events(
         $user_id,
@@ -1156,7 +1158,14 @@ class Event
         $lpInteraction = Database::get_course_table(TABLE_LP_IV_INTERACTION);
         $lpObjective = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
 
+        if (empty($course)) {
+            return false;
+        }
+
         $course_id = $course['real_id'];
+        $user_id = (int) $user_id;
+        $lp_id = (int) $lp_id;
+        $session_id = (int) $session_id;
 
         if (empty($course_id)) {
             $course_id = api_get_course_int_id();
@@ -1172,10 +1181,6 @@ class Event
             TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING
         );
 
-        $user_id = (int) $user_id;
-        $lp_id = (int) $lp_id;
-        $session_id = (int) $session_id;
-
         //Make sure we have the exact lp_view_id
         $sql = "SELECT id FROM $lp_view_table
                 WHERE
@@ -1202,6 +1207,18 @@ class Event
             Database::query($sql);
         }
 
+        if (api_get_configuration_value('lp_minimum_time')) {
+            $sql = "DELETE FROM track_e_access_complete
+                    WHERE 
+                        tool = 'learnpath' AND 
+                        c_id = $course_id AND 
+                        tool_id = $lp_id AND
+                        user_id = $user_id AND
+                        session_id = $session_id
+                    ";
+            Database::query($sql);
+        }
+
         $sql = "DELETE FROM $lp_view_table
                 WHERE
                     c_id = $course_id AND
@@ -1247,6 +1264,8 @@ class Event
             $course_id,
             $session_id
         );
+
+        return true;
     }
 
     /**
@@ -2464,11 +2483,11 @@ class Event
      */
     public static function registerLog($logInfo)
     {
-        if (!api_get_configuration_value('lp_minimum_time')) {
+        if (!Tracking::minimunTimeAvailable(api_get_session_id(), api_get_course_int_id())) {
             return false;
         }
 
-        $loginAs = (int) (Session::read('login_as') === true);
+        $loginAs = (int) Session::read('login_as') === true;
 
         $logInfo['user_id'] = api_get_user_id();
         $logInfo['date_reg'] = api_get_utc_datetime();

+ 34 - 9
main/inc/lib/exercise.lib.php

@@ -49,7 +49,7 @@ class ExerciseLib
         // 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)) {
+        if (!$objQuestionTmp = Question::read($questionId, $course_id)) {
             // Question not found
             return false;
         }
@@ -98,12 +98,9 @@ class ExerciseLib
 
             echo '<div class="question_options">';
             // construction of the Answer object (also gets all answers details)
-            $objAnswerTmp = new Answer($questionId, api_get_course_int_id(), $exercise);
+            $objAnswerTmp = new Answer($questionId, $course_id, $exercise);
             $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
-            $quizQuestionOptions = Question::readQuestionOption(
-                $questionId,
-                $course_id
-            );
+            $quizQuestionOptions = Question::readQuestionOption($questionId, $course_id);
 
             // For "matching" type here, we need something a little bit special
             // because the match between the suggestions and the answers cannot be
@@ -211,7 +208,7 @@ class ExerciseLib
 
                 $form->addHtml('<div id="'.'hide_description_'.$questionId.'_options" style="display: none;">');
                 $form->addHtmlEditor(
-                    "choice[".$questionId."]",
+                    "choice[$questionId]",
                     null,
                     false,
                     false,
@@ -446,6 +443,7 @@ class ExerciseLib
 
             $hidingClass = '';
             if ($answerType == READING_COMPREHENSION) {
+                $objQuestionTmp->setExerciseType($exercise->selectType());
                 $objQuestionTmp->processText($objQuestionTmp->selectDescription());
                 $hidingClass = 'hide-reading-answers';
                 $s .= Display::div(
@@ -598,7 +596,9 @@ class ExerciseLib
                             if (!empty($userChoiceList)) {
                                 foreach ($userChoiceList as $item) {
                                     $item = explode(':', $item);
-                                    $myChoice[$item[0]] = $item[1];
+                                    if (!empty($item)) {
+                                        $myChoice[$item[0]] = isset($item[1]) ? $item[1] : '';
+                                    }
                                 }
                             }
 
@@ -1393,7 +1393,7 @@ HTML;
             $questionDescription = $objQuestionTmp->selectDescription();
 
             // Get the answers, make a list
-            $objAnswerTmp = new Answer($questionId);
+            $objAnswerTmp = new Answer($questionId, $course_id);
             $nbrAnswers = $objAnswerTmp->selectNbrAnswers();
 
             // get answers of hotpost
@@ -2746,6 +2746,31 @@ HOTSPOT;
         return $listInfo;
     }
 
+    /**
+     * @param $score
+     * @param $weight
+     *
+     * @return array
+     */
+    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');
+
+        if ($maxNote != '' && $minNote != '') {
+            if (!empty($weight) && intval($weight) != 0) {
+                $score = $minNote + ($maxNote - $minNote) * $score / $weight;
+            } else {
+                $score = $minNote;
+            }
+            $weight = $maxNote;
+        }
+
+        return ['score' => $score, 'weight' => $weight];
+    }
+
     /**
      * Converts the score with the exercise_max_note and exercise_min_score
      * the platform settings + formats the results using the float_format function.

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

@@ -243,9 +243,6 @@ class ExtraField extends Model
             case 'filter':
                 $sidx = 'e.filter';
                 break;
-            case 'display_text':
-                $sidx = 'e.fieldType';
-                break;
         }
         $em = Database::getManager();
         $query = $em->getRepository('ChamiloCoreBundle:ExtraField')->createQueryBuilder('e');
@@ -520,7 +517,6 @@ class ExtraField extends Model
         if (empty($extraData)) {
             if (!empty($itemId)) {
                 $extraData = self::get_handler_extra_data($itemId);
-
                 if ($form) {
                     if (!empty($showOnlyTheseFields)) {
                         $setData = [];
@@ -1395,7 +1391,6 @@ class ExtraField extends Model
                             $options[$option_details['option_value']] = $option_details['display_text'];
                         }
 
-
                         if ($orderDependingDefaults) {
                             $defaultOptions = $extraData['extra_'.$field_details['variable']];
                             if (!empty($defaultOptions)) {

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

@@ -136,7 +136,7 @@ class ExtraFieldOption extends Model
      * @param array $params
      * @param bool  $showQuery
      *
-     * @return int
+     * @return int|bool
      */
     public function saveOptions($params, $showQuery = false)
     {
@@ -721,7 +721,7 @@ class ExtraFieldOption extends Model
     /**
      * @param $priority
      *
-     * @return null|string
+     * @return string|null
      */
     public function getPriorityMessageType($priority)
     {

+ 2 - 0
main/inc/lib/extra_field_value.lib.php

@@ -234,6 +234,7 @@ class ExtraFieldValue extends Model
                         $tag->setCount(count($tagUses) + 1);
                         $em->persist($tag);
                     }
+
                     $em->flush();
 
                     foreach ($tags as $tag) {
@@ -243,6 +244,7 @@ class ExtraFieldValue extends Model
                         $fieldRelTag->setTagId($tag->getId());
                         $em->persist($fieldRelTag);
                     }
+
                     $em->flush();
                     break;
                 case ExtraField::FIELD_TYPE_FILE_IMAGE:

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

@@ -109,6 +109,7 @@ $(document).ready(function() {
             src = url+'&type=link&src='+src;
             src = src.replace('https', 'http');
             $(this).attr('href', src);
+            $(this).attr('target', '_blank');
             var myAnchor = $('<a><img width="16px" src="'+iconPath+'link-external.png "/></a>').attr("href", src).attr('target', '_blank').attr('class', 'generated');
             $(this).after(myAnchor);
             $(this).after('-');

+ 67 - 67
main/inc/lib/formvalidator/Element/DateTimeRangePicker.php

@@ -44,70 +44,6 @@ class DateTimeRangePicker extends DateRangePicker
         '.$this->getElementJS();
     }
 
-    /**
-     * Get the necessary javascript for this datepicker.
-     *
-     * @return string
-     */
-    private function getElementJS()
-    {
-        $js = null;
-        $id = $this->getAttribute('id');
-
-        $js .= "<script>                    
-            $(function() {
-                var txtDate = $('#$id'),
-                    inputGroup = txtDate.parents('.input-group'),
-                    txtDateAlt = $('#{$id}_alt'),
-                    txtDateAltText = $('#{$id}_alt_text');
-                    
-                txtDate
-                    .hide()
-                    .datepicker({
-                        defaultDate: '".$this->getValue()."',
-                        dateFormat: 'yy-mm-dd',
-                        altField: '#{$id}_alt',
-                        altFormat: \"".get_lang('DateFormatLongNoDayJS')."\",
-                        showOn: 'both',
-                        buttonImage: '".Display::return_icon('attendance.png', null, [], ICON_SIZE_TINY, true, true)."',
-                        buttonImageOnly: true,
-                        buttonText: '".get_lang('SelectDate')."',
-                        changeMonth: true,
-                        changeYear: true,
-                        yearRange: 'c-60y:c+5y'
-                    })
-                    .on('change', function (e) {
-                        txtDateAltText.text(txtDateAlt.val());
-                    });
-                    
-                txtDateAltText.on('click', function () {
-                    txtDate.datepicker('show');
-                });
-
-                inputGroup
-                    .find('button')
-                    .on('click', function (e) {
-                        e.preventDefault();
-
-                        $('#$id, #{$id}_alt').val('');
-                        $('#{$id}_alt_text').html('');
-                    });
-                
-                $('#".$id."_time_range .time').timepicker({
-                    'showDuration': true,
-                    'timeFormat': 'H:i:s',
-                    'scrollDefault': 'now',
-                    
-                });
-                var timeOnlyExampleEl = document.getElementById('".$id."_time_range');
-                var timeOnlyDatepair = new Datepair(timeOnlyExampleEl);
-                
-            });
-        </script>";
-
-        return $js;
-    }
-
     /**
      * @param string $layout
      *
@@ -122,7 +58,7 @@ class DateTimeRangePicker extends DateRangePicker
             if (empty($size)) {
                 $sizeTemp = 8;
             }
-            $size = array(2, $sizeTemp, 2);
+            $size = [2, $sizeTemp, 2];
         } else {
             if (is_array($size)) {
                 if (count($size) != 3) {
@@ -130,11 +66,11 @@ class DateTimeRangePicker extends DateRangePicker
                     if (empty($size)) {
                         $sizeTemp = 8;
                     }
-                    $size = array(2, $sizeTemp, 2);
+                    $size = [2, $sizeTemp, 2];
                 }
                 // else just keep the $size array as received
             } else {
-                $size = array(2, intval($size), 2);
+                $size = [2, intval($size), 2];
             }
         }
 
@@ -206,4 +142,68 @@ class DateTimeRangePicker extends DateRangePicker
                 break;
         }
     }
+
+    /**
+     * Get the necessary javascript for this datepicker.
+     *
+     * @return string
+     */
+    private function getElementJS()
+    {
+        $js = null;
+        $id = $this->getAttribute('id');
+
+        $js .= "<script>                    
+            $(function() {
+                var txtDate = $('#$id'),
+                    inputGroup = txtDate.parents('.input-group'),
+                    txtDateAlt = $('#{$id}_alt'),
+                    txtDateAltText = $('#{$id}_alt_text');
+                    
+                txtDate
+                    .hide()
+                    .datepicker({
+                        defaultDate: '".$this->getValue()."',
+                        dateFormat: 'yy-mm-dd',
+                        altField: '#{$id}_alt',
+                        altFormat: \"".get_lang('DateFormatLongNoDayJS')."\",
+                        showOn: 'both',
+                        buttonImage: '".Display::return_icon('attendance.png', null, [], ICON_SIZE_TINY, true, true)."',
+                        buttonImageOnly: true,
+                        buttonText: '".get_lang('SelectDate')."',
+                        changeMonth: true,
+                        changeYear: true,
+                        yearRange: 'c-60y:c+5y'
+                    })
+                    .on('change', function (e) {
+                        txtDateAltText.text(txtDateAlt.val());
+                    });
+                    
+                txtDateAltText.on('click', function () {
+                    txtDate.datepicker('show');
+                });
+
+                inputGroup
+                    .find('button')
+                    .on('click', function (e) {
+                        e.preventDefault();
+
+                        $('#$id, #{$id}_alt').val('');
+                        $('#{$id}_alt_text').html('');
+                    });
+                
+                $('#".$id."_time_range .time').timepicker({
+                    'showDuration': true,
+                    'timeFormat': 'H:i:s',
+                    'scrollDefault': 'now',
+                    
+                });
+                var timeOnlyExampleEl = document.getElementById('".$id."_time_range');
+                var timeOnlyDatepair = new Datepair(timeOnlyExampleEl);
+                
+            });
+        </script>";
+
+        return $js;
+    }
 }

+ 2 - 4
main/inc/lib/formvalidator/Element/HtmlEditor.php

@@ -54,10 +54,9 @@ class HtmlEditor extends HTML_QuickForm_textarea
      */
     public function toHtml()
     {
-        $value = $this->getValue();
-
         if ($this->editor) {
             if ($this->editor->getConfigAttribute('fullPage')) {
+                $value = $this->getValue();
                 if (strlen(trim($value)) == 0) {
                     // TODO: To be considered whether here to add
                     // language and character set declarations.
@@ -71,10 +70,9 @@ class HtmlEditor extends HTML_QuickForm_textarea
             return $this->getFrozenHtml();
         } else {
             $styleCss = $this->editor->getConfigAttribute('style');
+            $style = false;
             if ($styleCss) {
                 $style = true;
-            } else {
-                $style = false;
             }
 
             return $this->buildEditor($style);

File diff suppressed because it is too large
+ 218 - 254
main/inc/lib/formvalidator/FormValidator.class.php


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

@@ -419,7 +419,7 @@ class GlossaryManager
                 Display::return_icon('view_text.png', get_lang('TableView'), '', ICON_SIZE_MEDIUM).'</a>';
         }
 
-        if (!api_is_anonymous()) {
+        if (api_is_allowed_to_edit(true, true, true)) {
             $actionsLeft .= Display::url(
                 Display::return_icon('export_to_documents.png', get_lang('ExportToDocArea'), [], ICON_SIZE_MEDIUM),
                 api_get_self().'?'.api_get_cidreq().'&'.http_build_query(['action' => 'export_documents'])

+ 28 - 16
main/inc/lib/groupmanager.lib.php

@@ -108,7 +108,7 @@ class GroupManager
         $course_id = $course_info['real_id'];
         $table_group = Database::get_course_table(TABLE_GROUP);
 
-        $select = " g.id,
+        $select = ' g.id,
                     g.iid,
                     g.name,
                     g.description,
@@ -118,9 +118,9 @@ class GroupManager
                     g.self_registration_allowed,
                     g.self_unregistration_allowed,
                     g.session_id,
-                    g.status";
+                    g.status';
         if ($getCount) {
-            $select = " DISTINCT count(g.iid) as count ";
+            $select = ' DISTINCT count(g.iid) as count ';
         }
 
         $sql = "SELECT 
@@ -147,7 +147,7 @@ class GroupManager
         if (!empty($session_condition)) {
             $sql .= $session_condition;
         }
-        $sql .= "ORDER BY UPPER(g.name)";
+        $sql .= ' ORDER BY UPPER(g.name)';
 
         $result = Database::query($sql);
 
@@ -1313,21 +1313,26 @@ class GroupManager
     /**
      * Get only students from a group (not tutors).
      *
-     * @param int $group_id iid
+     * @param int  $group_id         iid
+     * @param bool $filterOnlyActive
      *
      * @return array
      */
-    public static function getStudents($group_id)
+    public static function getStudents($group_id, $filterOnlyActive = false)
     {
+        $activeCondition = $filterOnlyActive ? 'AND u.active = 1' : '';
+
         $em = Database::getManager();
         $subscriptions = $em
-            ->createQuery('
-                SELECT gu
-                FROM ChamiloCourseBundle:CGroupRelUser gu
+            ->createQuery("
+                SELECT u.id FROM ChamiloUserBundle:User u
+                INNER JOIN ChamiloCourseBundle:CGroupRelUser gu
+                    WITH u.id = gu.userId
                 INNER JOIN ChamiloCourseBundle:CGroupInfo g
                 WITH gu.groupId = g.id AND g.cId = gu.cId
                 WHERE gu.cId = :course AND g.id = :group
-            ')
+                    $activeCondition
+            ")
             ->setParameters([
                 'course' => api_get_course_int_id(),
                 'group' => intval($group_id),
@@ -1335,10 +1340,9 @@ class GroupManager
             ->getResult();
 
         $users = [];
-
         /** @var CGroupRelUser $subscription */
         foreach ($subscriptions as $subscription) {
-            $users[] = api_get_user_info($subscription->getUserId());
+            $users[] = api_get_user_info($subscription['id']);
         }
 
         return $users;
@@ -1684,15 +1688,19 @@ class GroupManager
      */
     public static function get_subscribed_users($groupInfo)
     {
+        if (empty($groupInfo)) {
+            return [];
+        }
+
         $table_user = Database::get_main_table(TABLE_MAIN_USER);
         $table_group_user = Database::get_course_table(TABLE_GROUP_USER);
         $order_clause = api_sort_by_first_name() ? ' ORDER BY u.firstname, u.lastname' : ' ORDER BY u.lastname, u.firstname';
         $orderListByOfficialCode = api_get_setting('order_user_list_by_official_code');
         if ($orderListByOfficialCode === 'true') {
-            $order_clause = " ORDER BY u.official_code, u.firstname, u.lastname";
+            $order_clause = ' ORDER BY u.official_code, u.firstname, u.lastname';
         }
 
-        $group_id = intval($groupInfo['id']);
+        $group_id = (int) $groupInfo['iid'];
 
         if (empty($group_id)) {
             return [];
@@ -1740,16 +1748,20 @@ class GroupManager
      */
     public static function get_subscribed_tutors($groupInfo, $id_only = false)
     {
+        if (empty($groupInfo)) {
+            return [];
+        }
+
         $table_user = Database::get_main_table(TABLE_MAIN_USER);
         $table_group_tutor = Database::get_course_table(TABLE_GROUP_TUTOR);
         $order_clause = api_sort_by_first_name() ? ' ORDER BY u.firstname, u.lastname' : ' ORDER BY u.lastname, u.firstname';
 
         $orderListByOfficialCode = api_get_setting('order_user_list_by_official_code');
         if ($orderListByOfficialCode === 'true') {
-            $order_clause = " ORDER BY u.official_code, u.firstname, u.lastname";
+            $order_clause = ' ORDER BY u.official_code, u.firstname, u.lastname';
         }
 
-        $group_id = intval($groupInfo['id']);
+        $group_id = (int) $groupInfo['iid'];
         $course_id = api_get_course_int_id();
 
         $sql = "SELECT tg.id, u.user_id, u.lastname, u.firstname, u.email

+ 2 - 4
main/inc/lib/internationalization.lib.php

@@ -721,9 +721,10 @@ function api_format_date($time, $format = null, $language = null)
  * You can use it like this:
  * Display::dateToStringAgoAndLongDate($dateInUtc);.
  *
- * @param string $date     Result of a date function in this format -> date('Y-m-d H:i:s', time());
+ * @param string $date                 Result of a date function in this format -> date('Y-m-d H:i:s', time());
  * @param string $timeZone
  * @param bool   $returnDateDifference
+ *
  * @return string
  *
  * @author Julio Montoya
@@ -753,9 +754,6 @@ function date_to_str_ago($date, $timeZone = 'UTC', $returnDateDifference = false
     }
 
     return $value;
-
-
-
 }
 
 /**

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

@@ -386,11 +386,11 @@ class LegalManager
         $webPath = api_get_path(WEB_PATH);
 
         // @ofaj
-	    $link = '<a href="'.$webPath.'contrat.php">'.$webPath.'contrat.php</a>';
+        $link = '<a href="'.$webPath.'contrat.php">'.$webPath.'contrat.php</a>';
         $content = sprintf(
             get_lang('SendTermsDescriptionToUrlX'),
             $userInfo['firstName'],
-	        $link,
+            $link,
             $senderInfo['firstName']
         );
         MessageManager::send_message_simple($userId, $subject, $content);

+ 15 - 0
main/inc/lib/link.lib.php

@@ -1564,6 +1564,21 @@ class Link extends Model
         return $is_youtube_link;
     }
 
+    /**
+     * This function checks if the url is a PDF File link.
+     *
+     * @author Jorge Frisancho
+     * @author Alex Aragón - Fixing code
+     *
+     * @version 1.0
+     */
+    public static function isPdfLink($url)
+    {
+        $isPdfLink = strrpos(strtolower($url), '.pdf');
+
+        return $isPdfLink;
+    }
+
     /**
      * Get youtube id from an URL.
      *

+ 112 - 112
main/inc/lib/lp_item.lib.php

@@ -1,112 +1,112 @@
-<?php
-/* For licensing terms, see /license.txt */
-
-/**
- * Class lp_item
- *  made to manipulate data of lp_item table.
- *
- * This class is still incomplete
- * You can add lp_item database manipulation function here
- */
-class LpItem
-{
-    public $c_id = 0;
-    public $id = 0;
-    public $lp_id = 0;
-    public $item_type = '';
-    public $ref = '';
-    public $title = '';
-    public $description = '';
-    public $path = '';
-    public $min_score = 0;
-    public $max_score = 0;
-    public $mastery_score = 0;
-    public $parent_item_id = 0;
-    public $previous_item_id = 0;
-    public $next_item_id = 0;
-    public $display_order = 0;
-    public $prerequisite = '';
-    public $parameters = '';
-    public $launch_data = '';
-    public $max_time_allowed = '';
-    public $terms = '';
-    public $search_did = 0;
-    public $audio = '';
-
-    /**
-     * LpItem constructor.
-     *
-     * @param int $in_c_id
-     * @param int $in_id
-     */
-    public function __construct($in_c_id = 0, $in_id = 0)
-    {
-        if ($in_c_id > 0 && $in_id > 0) {
-            $item_view_table = Database::get_course_table(TABLE_LP_ITEM);
-            $sql = "SELECT * FROM $item_view_table
-                    WHERE
-                        c_id=".intval($in_c_id)." AND
-                        id=".intval($in_id);
-
-            $res = Database::query($sql);
-            $data = Database::fetch_array($res);
-            if (Database::num_rows($res) > 0) {
-                $this->c_id = $data['c_id'];
-                $this->id = $data['id'];
-                $this->lp_id = $data['lp_id'];
-                $this->item_type = $data['item_type'];
-                $this->ref = $data['ref'];
-                $this->title = $data['title'];
-                $this->description = $data['description'];
-                $this->path = $data['path'];
-                $this->min_score = $data['min_score'];
-                $this->max_score = $data['max_score'];
-                $this->mastery_score = $data['mastery_score'];
-                $this->parent_item_id = $data['parent_item_id'];
-                $this->previous_item_id = $data['previous_item_id'];
-                $this->next_item_id = $data['next_item_id'];
-                $this->display_order = $data['display_order'];
-                $this->prerequisite = $data['prerequisite'];
-                $this->parameters = $data['parameters'];
-                $this->launch_data = $data['launch_data'];
-                $this->max_time_allowed = $data['max_time_allowed'];
-                $this->terms = $data['terms'];
-                $this->search_did = $data['search_did'];
-                $this->audio = $data['audio'];
-            }
-        }
-    }
-
-    /**
-     * Update in database.
-     */
-    public function update()
-    {
-        $table = Database::get_course_table(TABLE_LP_ITEM);
-        if ($this->c_id > 0 && $this->id > 0) {
-            $sql = "UPDATE $table SET
-                        lp_id = '".intval($this->lp_id)."' ,
-                        item_type = '".Database::escape_string($this->item_type)."' ,
-                        ref = '".Database::escape_string($this->ref)."' ,
-                        title = '".Database::escape_string($this->title)."' ,
-                        description = '".Database::escape_string($this->description)."' ,
-                        path = '".Database::escape_string($this->path)."' ,
-                        min_score = '".Database::escape_string($this->min_score)."' ,
-                        max_score = '".Database::escape_string($this->max_score)."' ,
-                        mastery_score = '".Database::escape_string($this->mastery_score)."' ,
-                        parent_item_id = '".Database::escape_string($this->parent_item_id)."' ,
-                        previous_item_id = '".Database::escape_string($this->previous_item_id)."' ,
-                        next_item_id = '".Database::escape_string($this->next_item_id)."' ,
-                        display_order = '".Database::escape_string($this->display_order)."' ,
-                        prerequisite = '".Database::escape_string($this->prerequisite)."' ,
-                        parameters = '".Database::escape_string($this->parameters)."' ,
-                        launch_data = '".Database::escape_string($this->launch_data)."' ,
-                        max_time_allowed = '".Database::escape_string($this->max_time_allowed)."' ,
-                        terms = '".Database::escape_string($this->terms)."' ,
-                        search_did = '".Database::escape_string($this->search_did)."' ,
-                        audio = '".Database::escape_string($this->audio)."'
-                    WHERE c_id=".$this->c_id." AND id=".$this->id;
-            Database::query($sql);
-        }
-    }
-}
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Class lp_item
+ *  made to manipulate data of lp_item table.
+ *
+ * This class is still incomplete
+ * You can add lp_item database manipulation function here
+ */
+class LpItem
+{
+    public $c_id = 0;
+    public $id = 0;
+    public $lp_id = 0;
+    public $item_type = '';
+    public $ref = '';
+    public $title = '';
+    public $description = '';
+    public $path = '';
+    public $min_score = 0;
+    public $max_score = 0;
+    public $mastery_score = 0;
+    public $parent_item_id = 0;
+    public $previous_item_id = 0;
+    public $next_item_id = 0;
+    public $display_order = 0;
+    public $prerequisite = '';
+    public $parameters = '';
+    public $launch_data = '';
+    public $max_time_allowed = '';
+    public $terms = '';
+    public $search_did = 0;
+    public $audio = '';
+
+    /**
+     * LpItem constructor.
+     *
+     * @param int $in_c_id
+     * @param int $in_id
+     */
+    public function __construct($in_c_id = 0, $in_id = 0)
+    {
+        if ($in_c_id > 0 && $in_id > 0) {
+            $item_view_table = Database::get_course_table(TABLE_LP_ITEM);
+            $sql = "SELECT * FROM $item_view_table
+                    WHERE
+                        c_id=".intval($in_c_id)." AND
+                        iid=".intval($in_id);
+
+            $res = Database::query($sql);
+            $data = Database::fetch_array($res);
+            if (Database::num_rows($res) > 0) {
+                $this->c_id = $data['c_id'];
+                $this->id = $data['id'];
+                $this->lp_id = $data['lp_id'];
+                $this->item_type = $data['item_type'];
+                $this->ref = $data['ref'];
+                $this->title = $data['title'];
+                $this->description = $data['description'];
+                $this->path = $data['path'];
+                $this->min_score = $data['min_score'];
+                $this->max_score = $data['max_score'];
+                $this->mastery_score = $data['mastery_score'];
+                $this->parent_item_id = $data['parent_item_id'];
+                $this->previous_item_id = $data['previous_item_id'];
+                $this->next_item_id = $data['next_item_id'];
+                $this->display_order = $data['display_order'];
+                $this->prerequisite = $data['prerequisite'];
+                $this->parameters = $data['parameters'];
+                $this->launch_data = $data['launch_data'];
+                $this->max_time_allowed = $data['max_time_allowed'];
+                $this->terms = $data['terms'];
+                $this->search_did = $data['search_did'];
+                $this->audio = $data['audio'];
+            }
+        }
+    }
+
+    /**
+     * Update in database.
+     */
+    public function update()
+    {
+        $table = Database::get_course_table(TABLE_LP_ITEM);
+        if ($this->c_id > 0 && $this->id > 0) {
+            $sql = "UPDATE $table SET
+                        lp_id = '".intval($this->lp_id)."' ,
+                        item_type = '".Database::escape_string($this->item_type)."' ,
+                        ref = '".Database::escape_string($this->ref)."' ,
+                        title = '".Database::escape_string($this->title)."' ,
+                        description = '".Database::escape_string($this->description)."' ,
+                        path = '".Database::escape_string($this->path)."' ,
+                        min_score = '".Database::escape_string($this->min_score)."' ,
+                        max_score = '".Database::escape_string($this->max_score)."' ,
+                        mastery_score = '".Database::escape_string($this->mastery_score)."' ,
+                        parent_item_id = '".Database::escape_string($this->parent_item_id)."' ,
+                        previous_item_id = '".Database::escape_string($this->previous_item_id)."' ,
+                        next_item_id = '".Database::escape_string($this->next_item_id)."' ,
+                        display_order = '".Database::escape_string($this->display_order)."' ,
+                        prerequisite = '".Database::escape_string($this->prerequisite)."' ,
+                        parameters = '".Database::escape_string($this->parameters)."' ,
+                        launch_data = '".Database::escape_string($this->launch_data)."' ,
+                        max_time_allowed = '".Database::escape_string($this->max_time_allowed)."' ,
+                        terms = '".Database::escape_string($this->terms)."' ,
+                        search_did = '".Database::escape_string($this->search_did)."' ,
+                        audio = '".Database::escape_string($this->audio)."'
+                    WHERE c_id=".$this->c_id." AND id=".$this->id;
+            Database::query($sql);
+        }
+    }
+}

+ 16 - 5
main/inc/lib/message.lib.php

@@ -78,7 +78,11 @@ class MessageManager
         $result = Database::query($sql);
         $result = Database::fetch_array($result);
 
-        return $result['number_messages'];
+        if ($result) {
+            return (int) $result['number_messages'];
+        }
+
+        return 0;
     }
 
     /**
@@ -907,6 +911,7 @@ class MessageManager
                     'message_id' => $message_id,
                     'size' => $file_attach['size'],
                 ];
+
                 return Database::insert($table, $params);
             }
         }
@@ -1298,12 +1303,13 @@ class MessageManager
     {
         $table = Database::get_main_table(TABLE_MESSAGE);
         $messageId = (int) $messageId;
+        $currentUserId = api_get_user_id();
 
         if ($source == 'outbox') {
             if (isset($messageId) && is_numeric($messageId)) {
                 $query = "SELECT * FROM $table
                           WHERE
-                            user_sender_id = ".api_get_user_id()." AND
+                            user_sender_id = ".$currentUserId." AND
                             id = $messageId AND
                             msg_status = ".MESSAGE_STATUS_OUTBOX;
                 $result = Database::query($query);
@@ -1313,19 +1319,24 @@ class MessageManager
                 $query = "UPDATE $table SET
                           msg_status = '".MESSAGE_STATUS_NEW."'
                           WHERE
-                            user_receiver_id=".api_get_user_id()." AND
+                            user_receiver_id=".$currentUserId." AND
                             id='".$messageId."'";
                 Database::query($query);
 
                 $query = "SELECT * FROM $table
                           WHERE
                             msg_status<> ".MESSAGE_STATUS_OUTBOX." AND
-                            user_receiver_id=".api_get_user_id()." AND
+                            user_receiver_id=".$currentUserId." AND
                             id='".$messageId."'";
                 $result = Database::query($query);
             }
         }
         $row = Database::fetch_array($result, 'ASSOC');
+
+        if (empty($row)) {
+            return '';
+        }
+
         $user_sender_id = $row['user_sender_id'];
 
         // get file attachments by message id
@@ -2370,7 +2381,7 @@ class MessageManager
         );
 
         if (!empty($result)) {
-           return $result;
+            return $result;
         }
 
         return false;

+ 27 - 72
main/inc/lib/myspace.lib.php

@@ -66,7 +66,7 @@ class MySpace
         $menu_items = [];
         $menu_items[] = Display::url(
             Display::return_icon(
-                'stats.png',
+                'statistics.png',
                 get_lang('MyStats'),
                 '',
                 ICON_SIZE_MEDIUM
@@ -2359,7 +2359,7 @@ class MySpace
      *
      * @author Julio Montoya Armas
      */
-    public function check_all_usernames($users, $course_list, $id_session)
+    public static function check_all_usernames($users, $course_list, $id_session)
     {
         $table_user = Database::get_main_table(TABLE_MAIN_USER);
         $usernames = [];
@@ -2409,7 +2409,7 @@ class MySpace
      *
      * @author Julio Montoya Armas
      */
-    public function get_user_creator($users)
+    public static function get_user_creator($users)
     {
         $errors = [];
         foreach ($users as $index => $user) {
@@ -2437,7 +2437,7 @@ class MySpace
      *
      * @param array $users list of users
      */
-    public function validate_data($users, $id_session = null)
+    public static function validate_data($users, $id_session = null)
     {
         $errors = [];
         $new_users = [];
@@ -2475,7 +2475,7 @@ class MySpace
     /**
      * Adds missing user-information (which isn't required, like password, etc).
      */
-    public function complete_missing_data($user)
+    public static function complete_missing_data($user)
     {
         // 1. Generate a password if it is necessary.
         if (!isset($user['Password']) || strlen($user['Password']) == 0) {
@@ -2488,14 +2488,14 @@ class MySpace
     /**
      * Saves imported data.
      */
-    public function save_data($users, $course_list, $id_session)
+    public static function save_data($users, $course_list, $id_session)
     {
         $tbl_session = Database::get_main_table(TABLE_MAIN_SESSION);
         $tbl_session_rel_course = Database::get_main_table(TABLE_MAIN_SESSION_COURSE);
         $tbl_session_rel_course_rel_user = Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
         $tbl_session_rel_user = Database::get_main_table(TABLE_MAIN_SESSION_USER);
 
-        $id_session = intval($id_session);
+        $id_session = (int) $id_session;
         $sendMail = $_POST['sendMail'] ? 1 : 0;
 
         // Adding users to the platform.
@@ -2612,7 +2612,7 @@ class MySpace
                     $addedto = get_lang('UserNotAdded');
                 }
 
-                $registered_users .= UserManager::getUserProfileLink($userInfo)." - ".$addedto.'<br />';
+                $registered_users .= UserManager::getUserProfileLink($userInfo).' - '.$addedto.'<br />';
             }
         } else {
             $i = 0;
@@ -2632,10 +2632,10 @@ class MySpace
                     $addedto = get_lang('UserNotAdded');
                 }
                 $registered_users .= "<a href=\"../user/userInfo.php?uInfo=".$user['id']."\">".
-                    api_get_person_name($user['FirstName'], $user['LastName'])."</a> - ".$addedto.'<br />';
+                    Security::remove_XSS($userInfo['complete_user_name'])."</a> - ".$addedto.'<br />';
             }
         }
-        Display::addFlash(Display::return_message($registered_users));
+        Display::addFlash(Display::return_message($registered_users, 'normal', false));
         header('Location: course.php?id_session='.$id_session);
         exit;
     }
@@ -2660,55 +2660,6 @@ class MySpace
         return $users;
     }
 
-    /**
-     * XML-parser: the handler at the beginning of element.
-     */
-    public function element_start($parser, $data)
-    {
-        $data = api_utf8_decode($data);
-        global $user;
-        global $current_tag;
-        switch ($data) {
-            case 'Contact':
-                $user = [];
-                break;
-            default:
-                $current_tag = $data;
-        }
-    }
-
-    /**
-     * XML-parser: the handler at the end of element.
-     */
-    public function element_end($parser, $data)
-    {
-        $data = api_utf8_decode($data);
-        global $user;
-        global $users;
-        global $current_value;
-        global $purification_option_for_usernames;
-        $user[$data] = $current_value;
-        switch ($data) {
-            case 'Contact':
-                $user['UserName'] = UserManager::purify_username($user['UserName'], $purification_option_for_usernames);
-                $users[] = $user;
-                break;
-            default:
-                $user[$data] = $current_value;
-                break;
-        }
-    }
-
-    /**
-     * XML-parser: the handler for character data.
-     */
-    public function character_data($parser, $data)
-    {
-        $data = trim(api_utf8_decode($data));
-        global $current_value;
-        $current_value = $data;
-    }
-
     /**
      * Reads XML-file.
      *
@@ -2716,21 +2667,25 @@ class MySpace
      *
      * @return array All userinformation read from the file
      */
-    public function parse_xml_data($file)
+    public static function parse_xml_data($file)
     {
-        global $current_tag;
-        global $current_value;
-        global $user;
-        global $users;
-        $users = [];
-        $parser = xml_parser_create('UTF-8');
-        xml_set_element_handler($parser, ['MySpace', 'element_start'], ['MySpace', 'element_end']);
-        xml_set_character_data_handler($parser, "character_data");
-        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
-        xml_parse($parser, api_utf8_encode_xml(file_get_contents($file)));
-        xml_parser_free($parser);
+        $crawler = new \Symfony\Component\DomCrawler\Crawler();
+        $crawler->addXmlContent(file_get_contents($file));
+        $crawler = $crawler->filter('Contacts > Contact ');
+        $array = [];
+        foreach ($crawler as $domElement) {
+            $row = [];
+            foreach ($domElement->childNodes as $node) {
+                if ($node->nodeName != '#text') {
+                    $row[$node->nodeName] = $node->nodeValue;
+                }
+            }
+            if (!empty($row)) {
+                $array[] = $row;
+            }
+        }
 
-        return $users;
+        return $array;
     }
 
     /**

+ 0 - 201
main/inc/lib/nanogong/LICENSE

@@ -1,201 +0,0 @@
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.

+ 0 - 11
main/inc/lib/nanogong/NOTICE

@@ -1,11 +0,0 @@
-The NanoGong Applet 5.0
-Copyright 2002-2014 The Gong Project
-
-This product includes software developed by the Gong Project
-(http://gong.ust.hk/) at the Department of Computer Science and Engineering at
-the Hong Kong University of Science and Technology.
-
-The Speex speech codec is provided by the JSpeex library package, which is open
-source software, written by Marc Gimpel, and copyright by Wimba S.A. The
-original software is available from http://jspeex.sourceforge.net/
-

+ 0 - 39
main/inc/lib/nanogong/README

@@ -1,39 +0,0 @@
-Thank you for downloading the NanoGong applet!
-
-Use the NanoGong Applet
------------------------
-
-NanoGong is a 'simple' Java applet which you can put in any web page. We have
-included an example index.html at the root directory so that you can quickly
-make a web page with NanoGong by modifying the given page.
-
-Here are the brief steps to create your NanoGong page:
-
-1) Modify the index.html with appropriate content, you may change the filename
-   as well if you want to
-2) Copy both index.html and nanogong.jar to the location where you usually put
-   your web pages in
-3) Open a browser and type in the location of the index.html you have just
-   uploaded
-
-Done! In just 3 steps you will have a NanoGong page ready on the Web.
-
-If you want to look at more examples on using the NanoGong applet you can visit
-the NanoGong project website at
-
-	http://nanogong.ust.hk/
-
-
-Build the NanoGong Applet
--------------------------
-
-This package contains the necessary source code for creating your own version of
-NanoGong applet. The JSpeex library is provided in binary form inside the jspeex
-directory. The build file, build.xml, at the root directory can be used to
-compile the source code as well as pack the resulting class files together with
-the JSpeex library in a Jar file.
-
-To sign the Jar file, which is necessary for the proper execution of the
-NanoGong applet, you can either provide a keystore file called 'keystore.jks',
-or sign the Jar file manually. Please refer to the build file for details.
-

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