Merge branch 'MDL-61033-master' of https://github.com/nashtechdev01/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Mon, 9 Apr 2018 02:37:23 +0000 (10:37 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Mon, 9 Apr 2018 02:37:23 +0000 (10:37 +0800)
268 files changed:
.travis.yml
admin/cli/install.php
admin/searchareas.php
admin/settings/appearance.php
admin/settings/plugins.php
admin/tests/behat/search_areas.feature [new file with mode: 0644]
admin/tool/log/store/database/classes/helper.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/external.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/usertours/classes/manager.php
admin/tool/usertours/classes/step.php
admin/user.php
analytics/tests/course_test.php
auth/db/tests/db_test.php
auth/email/auth.php
backup/moodle2/backup_plan_builder.class.php
backup/moodle2/backup_qtype_extrafields_plugin.class.php [new file with mode: 0644]
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_plan_builder.class.php
backup/moodle2/restore_qtype_extrafields_plugin.class.php [new file with mode: 0644]
backup/moodle2/restore_stepslib.php
backup/util/plan/base_task.class.php
calendar/templates/month_mini.mustache
calendar/view.php
cohort/classes/external/cohort_summary_exporter.php
cohort/edit_form.php
cohort/externallib.php
cohort/lib.php
cohort/tests/behat/upload_cohorts.feature
cohort/tests/cohortlib_test.php
cohort/tests/externallib_test.php
cohort/tests/fixtures/uploadcohorts4.csv [new file with mode: 0644]
cohort/upload_form.php
config-dist.php
course/classes/search/section.php [new file with mode: 0644]
course/tests/behat/course_controls.feature
course/tests/behat/paged_course_navigation.feature
course/tests/search_test.php
enrol/database/tests/sync_test.php
enrol/self/bulkchangeforms.php [new file with mode: 0644]
enrol/self/classes/deleteselectedusers_form.php [new file with mode: 0644]
enrol/self/classes/deleteselectedusers_operation.php [new file with mode: 0644]
enrol/self/classes/editselectedusers_form.php [new file with mode: 0644]
enrol/self/classes/editselectedusers_operation.php [new file with mode: 0644]
enrol/self/lang/en/enrol_self.php
enrol/self/lib.php
enrol/self/locallib.php
files/renderer.php
grade/export/lib.php
group/index.php
install.php
install/lang/fo/moodle.php
install/lang/sk/install.php
iplookup/tests/geoip_test.php
lang/en/admin.php
lang/en/cohort.php
lang/en/install.php
lang/en/message.php
lang/en/moodle.php
lang/en/search.php
lib/adminlib.php
lib/amd/build/auto_rows.min.js
lib/amd/src/auto_rows.js
lib/antivirus/clamav/classes/privacy/provider.php [new file with mode: 0644]
lib/antivirus/clamav/lang/en/antivirus_clamav.php
lib/classes/event/course_module_completion_updated.php
lib/classes/event/message_deleted.php
lib/classes/event/message_sent.php
lib/classes/event/message_viewed.php
lib/classes/event/notification_sent.php [new file with mode: 0644]
lib/classes/event/notification_viewed.php [new file with mode: 0644]
lib/classes/message/manager.php
lib/classes/plugin_manager.php
lib/classes/task/messaging_cleanup_task.php
lib/classes/task/tag_cron_task.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/deprecatedlib.php
lib/dml/mariadb_native_moodle_database.php
lib/dml/moodle_database.php
lib/dml/mssql_native_moodle_database.php [deleted file]
lib/dml/mssql_native_moodle_recordset.php [deleted file]
lib/dml/mssql_native_moodle_temptables.php [deleted file]
lib/dml/mysqli_native_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_temptables.php
lib/dmllib.php
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js
lib/editor/atto/plugins/table/yui/src/button/js/button.js
lib/editor/tinymce/plugins/ctrlhelp/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/ctrlhelp/lang/en/tinymce_ctrlhelp.php
lib/editor/tinymce/plugins/managefiles/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/lang/en/tinymce_managefiles.php
lib/editor/tinymce/plugins/moodleemoticon/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/moodleemoticon/lang/en/tinymce_moodleemoticon.php
lib/editor/tinymce/plugins/moodleimage/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/moodleimage/lang/en/tinymce_moodleimage.php
lib/editor/tinymce/plugins/moodlemedia/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/moodlemedia/lang/en/tinymce_moodlemedia.php
lib/editor/tinymce/plugins/moodlenolink/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/moodlenolink/lang/en/tinymce_moodlenolink.php
lib/editor/tinymce/plugins/pdw/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/pdw/lang/en/tinymce_pdw.php
lib/editor/tinymce/plugins/spellchecker/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/spellchecker/lang/en/tinymce_spellchecker.php
lib/editor/tinymce/plugins/wrap/classes/privacy/provider.php [new file with mode: 0644]
lib/editor/tinymce/plugins/wrap/lang/en/tinymce_wrap.php
lib/externallib.php
lib/messagelib.php
lib/moodlelib.php
lib/pagelib.php
lib/phpunit/classes/message_sink.php
lib/phpunit/classes/phpmailer_sink.php
lib/phpunit/classes/util.php
lib/questionlib.php
lib/testing/generator/data_generator.php
lib/tests/message_test.php
lib/tests/messagelib_test.php
lib/tests/moodle_page_test.php
lib/tests/moodlelib_test.php
lib/tests/questionlib_test.php
lib/upgrade.txt
lib/xmldb/xmldb_structure.php
media/player/videojs/styles.css
message/classes/api.php
message/classes/helper.php
message/classes/search/base_message.php
message/classes/search/message_received.php
message/classes/search/message_sent.php
message/externallib.php
message/index.php
message/lib.php
message/output/airnotifier/classes/privacy/provider.php [new file with mode: 0644]
message/output/airnotifier/lang/en/message_airnotifier.php
message/output/airnotifier/tests/privacy_test.php [new file with mode: 0644]
message/output/jabber/classes/privacy/provider.php [new file with mode: 0644]
message/output/jabber/lang/en/message_jabber.php
message/output/jabber/tests/privacy_test.php [new file with mode: 0644]
message/output/popup/amd/build/notification_repository.min.js
message/output/popup/amd/src/notification_repository.js
message/output/popup/classes/api.php
message/output/popup/classes/output/popup_notification.php
message/output/popup/db/install.xml
message/output/popup/externallib.php
message/output/popup/message_output_popup.php
message/output/popup/tests/base.php
message/output/popup/tests/behat/message_popover_unread.feature
message/output/popup/version.php
message/tests/api_test.php
message/tests/events_test.php
message/tests/externallib_test.php
message/tests/messagelib_test.php
message/tests/search_received_test.php
message/tests/search_sent_test.php
message/upgrade.txt
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/renderer.php
mod/assign/tests/locallib_test.php
mod/book/edit.php
mod/feedback/classes/responses_table.php
mod/feedback/item/multichoicerated/lib.php
mod/feedback/lang/en/feedback.php
mod/folder/download_folder.php
mod/glossary/view.php
mod/lti/lang/en/lti.php
mod/lti/locallib.php
mod/lti/service/memberships/classes/local/resources/contextmemberships.php [moved from mod/lti/service/memberships/classes/local/resource/contextmemberships.php with 99% similarity]
mod/lti/service/memberships/classes/local/resources/linkmemberships.php [moved from mod/lti/service/memberships/classes/local/resource/linkmemberships.php with 98% similarity]
mod/lti/service/memberships/classes/local/service/memberships.php
mod/lti/service/memberships/db/renamedclasses.php [new file with mode: 0644]
mod/lti/service/profile/classes/local/resources/profile.php [moved from mod/lti/service/profile/classes/local/resource/profile.php with 98% similarity]
mod/lti/service/profile/classes/local/service/profile.php
mod/lti/service/profile/db/renamedclasses.php [new file with mode: 0644]
mod/lti/service/toolproxy/classes/local/resources/toolproxy.php [moved from mod/lti/service/toolproxy/classes/local/resource/toolproxy.php with 98% similarity]
mod/lti/service/toolproxy/classes/local/service/toolproxy.php
mod/lti/service/toolproxy/db/renamedclasses.php [new file with mode: 0644]
mod/lti/service/toolsettings/classes/local/resources/contextsettings.php [moved from mod/lti/service/toolsettings/classes/local/resource/contextsettings.php with 97% similarity]
mod/lti/service/toolsettings/classes/local/resources/linksettings.php [moved from mod/lti/service/toolsettings/classes/local/resource/linksettings.php with 96% similarity]
mod/lti/service/toolsettings/classes/local/resources/systemsettings.php [moved from mod/lti/service/toolsettings/classes/local/resource/systemsettings.php with 99% similarity]
mod/lti/service/toolsettings/classes/local/service/toolsettings.php
mod/lti/service/toolsettings/db/renamedclasses.php [new file with mode: 0644]
mod/quiz/locallib.php
mod/wiki/comments.php
mod/wiki/db/access.php
mod/wiki/diff.php
mod/wiki/files.php
mod/wiki/history.php
mod/wiki/lib.php
mod/wiki/map.php
mod/wiki/pagelib.php
mod/wiki/prettyview.php
mod/wiki/tests/externallib_test.php
mod/wiki/view.php
mod/wiki/viewversion.php
phpunit.xml.dist
plagiarism/classes/privacy/legacy_polyfill.php [new file with mode: 0644]
plagiarism/classes/privacy/plagiarism_provider.php
plagiarism/tests/privacy_legacy_polyfill_test.php [new file with mode: 0644]
privacy/classes/local/metadata/collection.php
privacy/classes/local/metadata/types/plugintype_link.php
privacy/classes/local/metadata/types/subsystem_link.php
privacy/classes/local/request/moodle_content_writer.php
privacy/classes/manager.php
privacy/classes/tests/request/content_writer.php
privacy/tests/collection_test.php
privacy/tests/legacy_polyfill_test.php
privacy/tests/manager_test.php
privacy/tests/moodle_content_writer_test.php
privacy/tests/types_plugintype_link_test.php
privacy/tests/types_subsystem_link_test.php
question/amd/build/edit_tags.min.js
question/amd/build/repository.min.js
question/amd/src/edit_tags.js
question/amd/src/repository.js
question/behaviour/manualgraded/behaviour.php
question/classes/bank/tags_action_column.php
question/classes/bank/view.php
question/classes/external.php
question/engine/tests/helpers.php
question/format.php
question/lib.php
question/question.php
question/tests/backup_test.php [new file with mode: 0644]
question/tests/externallib_test.php
question/tests/generator/lib.php
question/type/calculatedsimple/tests/helper.php
question/type/essay/backup/moodle2/backup_qtype_essay_plugin.class.php
question/type/essay/db/install.xml
question/type/essay/db/upgrade.php
question/type/essay/edit_essay_form.php
question/type/essay/lang/en/qtype_essay.php
question/type/essay/question.php
question/type/essay/questiontype.php
question/type/essay/renderer.php
question/type/essay/tests/behat/file_type_restriction.feature [new file with mode: 0644]
question/type/essay/tests/fixtures/testquestion.moodle.xml
question/type/essay/tests/helper.php
question/type/essay/tests/walkthrough_test.php
question/type/essay/version.php
question/type/gapselect/tests/edit_form_test.php
question/type/gapselect/tests/helper.php
question/type/gapselect/tests/walkthrough_test.php
question/type/questionbase.php
question/type/shortanswer/backup/moodle2/backup_qtype_shortanswer_plugin.class.php
question/type/shortanswer/backup/moodle2/restore_qtype_shortanswer_plugin.class.php
question/type/tags_form.php
question/type/upgrade.txt
report/security/locallib.php
repository/filepicker.js
search/engine/simpledb/classes/engine.php [new file with mode: 0644]
search/engine/simpledb/db/install.php [new file with mode: 0644]
search/engine/simpledb/db/install.xml [new file with mode: 0644]
search/engine/simpledb/db/uninstall.php [new file with mode: 0644]
search/engine/simpledb/lang/en/search_simpledb.php [new file with mode: 0644]
search/engine/simpledb/tests/engine_test.php [new file with mode: 0644]
search/engine/simpledb/version.php [moved from message/output/popup/db/events.php with 68% similarity]
search/engine/solr/lang/en/search_solr.php
search/engine/solr/tests/engine_test.php
theme/boost/templates/core/filemanager_modal_generallayout.mustache
user/index.php
version.php
webservice/lib.php

index 9e436f5..a1217e8 100644 (file)
@@ -248,7 +248,7 @@ script:
     - >
       if [ "$TASK" = 'PHPUNIT' ];
       then
-        vendor/bin/phpunit;
+        vendor/bin/phpunit --fail-on-risky --disallow-test-output --verbose;
       fi
 
     - >
index 240a819..1ff43fe 100644 (file)
@@ -219,7 +219,6 @@ $databases = array('mysqli' => moodle_database::get_driver_instance('mysqli', 'n
                    'pgsql'  => moodle_database::get_driver_instance('pgsql',  'native'),
                    'oci'    => moodle_database::get_driver_instance('oci',    'native'),
                    'sqlsrv' => moodle_database::get_driver_instance('sqlsrv', 'native'), // MS SQL*Server PHP driver
-                   'mssql'  => moodle_database::get_driver_instance('mssql',  'native'), // FreeTDS driver
                   );
 foreach ($databases as $type=>$database) {
     if ($database->driver_installed() !== true) {
index 9cac439..bf178b5 100644 (file)
@@ -35,10 +35,7 @@ try {
     // Continue, we return an error later depending on the requested action.
 }
 
-echo $OUTPUT->header();
-
 if ($action) {
-    require_sesskey();
 
     if ($areaid) {
         // We need to check that the area exists.
@@ -48,45 +45,76 @@ if ($action) {
         }
     }
 
-    // All actions but enable/disable need the search engine to be ready.
     if ($action !== 'enable' && $action !== 'disable') {
+        // All actions but enable/disable need the search engine to be ready.
         if (!empty($searchmanagererror)) {
             throw $searchmanagererror;
         }
+
+        // Show confirm prompt for all these actions as they may be inadvisable, or may cause
+        // an interruption in search functionality, on production systems.
+        if (!optional_param('confirm', 0, PARAM_INT)) {
+            // Display confirmation prompt.
+            $a = null;
+            if ($areaid) {
+                $a = html_writer::tag('strong', $area->get_visible_name());
+            }
+
+            $actionparams = ['sesskey' => sesskey(), 'action' => $action, 'confirm' => 1];
+            if ($areaid) {
+                $actionparams['areaid'] = $areaid;
+            }
+            $actionurl = new moodle_url('/admin/searchareas.php', $actionparams);
+            $cancelurl = new moodle_url('/admin/searchareas.php');
+            echo $OUTPUT->header();
+            echo $OUTPUT->confirm(get_string('confirm_' . $action, 'search', $a),
+                    new single_button($actionurl, get_string('continue'), 'post', true),
+                    new single_button($cancelurl, get_string('cancel'), 'get'));
+            echo $OUTPUT->footer();
+            exit;
+        }
     }
 
+    // We are now taking an actual action, so require sesskey.
+    require_sesskey();
+
     switch ($action) {
         case 'enable':
             $area->set_enabled(true);
-            echo $OUTPUT->notification(get_string('searchareaenabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
+            \core\notification::add(get_string('searchareaenabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
             break;
         case 'disable':
             $area->set_enabled(false);
-            echo $OUTPUT->notification(get_string('searchareadisabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
+            \core\notification::add(get_string('searchareadisabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
             break;
         case 'delete':
             $search = \core_search\manager::instance();
             $search->delete_index($areaid);
-            echo $OUTPUT->notification(get_string('searchindexdeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
+            \core\notification::add(get_string('searchindexdeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
             break;
         case 'indexall':
             $searchmanager->index();
-            echo $OUTPUT->notification(get_string('searchindexupdated', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
+            \core\notification::add(get_string('searchindexupdated', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
             break;
         case 'reindexall':
             $searchmanager->index(true);
-            echo $OUTPUT->notification(get_string('searchreindexed', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
+            \core\notification::add(get_string('searchreindexed', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
             break;
         case 'deleteall':
             $searchmanager->delete_index();
-            echo $OUTPUT->notification(get_string('searchalldeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
+            \core\notification::add(get_string('searchalldeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
             break;
         default:
             throw new moodle_exception('invalidaction');
             break;
     }
+
+    // Redirect back to the main page after taking action.
+    redirect(new moodle_url('/admin/searchareas.php'));
 }
 
+echo $OUTPUT->header();
+
 $searchareas = \core_search\manager::get_search_areas_list();
 if (empty($searchmanagererror)) {
     $areasconfig = $searchmanager->get_areas_config($searchareas);
@@ -192,9 +220,12 @@ echo $OUTPUT->footer();
  * @return moodle_url
  */
 function admin_searcharea_action_url($action, $areaid = false) {
-    $params = array('action' => $action, 'sesskey' => sesskey());
+    $params = array('action' => $action);
     if ($areaid) {
         $params['areaid'] = $areaid;
     }
+    if ($action === 'disable' || $action === 'enable') {
+        $params['sesskey'] = sesskey();
+    }
     return new moodle_url('/admin/searchareas.php', $params);
 }
index 5e13a2f..db05c1d 100644 (file)
@@ -22,6 +22,7 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) { // sp
     $temp->add(new admin_setting_configcheckbox('allowuserthemes', new lang_string('allowuserthemes', 'admin'), new lang_string('configallowuserthemes', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowcoursethemes', new lang_string('allowcoursethemes', 'admin'), new lang_string('configallowcoursethemes', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowcategorythemes',  new lang_string('allowcategorythemes', 'admin'), new lang_string('configallowcategorythemes', 'admin'), 0));
+    $temp->add(new admin_setting_configcheckbox('allowcohortthemes',  new lang_string('allowcohortthemes', 'admin'), new lang_string('configallowcohortthemes', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowthemechangeonurl',  new lang_string('allowthemechangeonurl', 'admin'), new lang_string('configallowthemechangeonurl', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('allowuserblockhiding', new lang_string('allowuserblockhiding', 'admin'), new lang_string('configallowuserblockhiding', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('allowblockstodock', new lang_string('allowblockstodock', 'admin'), new lang_string('configallowblockstodock', 'admin'), 1));
@@ -199,7 +200,7 @@ preferences,moodle|/user/preferences.php|preferences',
 
     // "documentation" settingpage
     $temp = new admin_settingpage('documentation', new lang_string('moodledocs'));
-    $temp->add(new admin_setting_configtext('docroot', new lang_string('docroot', 'admin'), new lang_string('configdocroot', 'admin'), 'http://docs.moodle.org', PARAM_URL));
+    $temp->add(new admin_setting_configtext('docroot', new lang_string('docroot', 'admin'), new lang_string('configdocroot', 'admin'), 'https://docs.moodle.org', PARAM_URL));
     $ltemp = array('' => get_string('forceno'));
     $ltemp += get_string_manager()->get_list_of_translations(true);
     $temp->add(new admin_setting_configselect('doclang', get_string('doclang', 'admin'), get_string('configdoclang', 'admin'), '', $ltemp));
index c55b1f1..9a2202f 100644 (file)
@@ -556,7 +556,7 @@ if ($hassiteconfig) {
     // Search engine selection.
     $temp->add(new admin_setting_heading('searchengineheading', new lang_string('searchengine', 'admin'), ''));
     $temp->add(new admin_setting_configselect('searchengine',
-                                new lang_string('selectsearchengine', 'admin'), '', 'solr', $engines));
+                                new lang_string('selectsearchengine', 'admin'), '', 'simpledb', $engines));
     $temp->add(new admin_setting_heading('searchoptionsheading', new lang_string('searchoptions', 'admin'), ''));
     $temp->add(new admin_setting_configcheckbox('searchindexwhendisabled',
             new lang_string('searchindexwhendisabled', 'admin'), new lang_string('searchindexwhendisabled_desc', 'admin'),
diff --git a/admin/tests/behat/search_areas.feature b/admin/tests/behat/search_areas.feature
new file mode 100644 (file)
index 0000000..c1c4909
--- /dev/null
@@ -0,0 +1,21 @@
+@core @core_admin
+Feature: Use the search areas admin screen
+  In order to control search indexing
+  As an admin
+  I need to use the search areas admin screen
+
+  Background:
+    Given I log in as "admin"
+    And I navigate to "Plugins > Search > Search areas" in site administration
+
+  Scenario: Disable and enable a search area
+    When I click on "Disable" "link" in the "Book - resource information" "table_row"
+    Then I should see "Search area disabled" in the ".alert-success" "css_element"
+    And I should see "Search area disabled" in the "Book - resource information" "table_row"
+
+    When I click on "Enable" "link" in the "Book - resource information" "table_row"
+    Then I should see "Search area enabled" in the ".alert-success" "css_element"
+    And I should not see "Search area disabled" in the "Book - resource information" "table_row"
+
+  # Note: Other scenarios are not currently easy to implement in Behat because there is no mock
+  # search engine - we could add testing once Moodle has an internal database search engine.
index 1acb99c..bad2c1b 100644 (file)
@@ -44,8 +44,7 @@ class helper {
             'native/mariadb' => \moodle_database::get_driver_instance('mariadb', 'native')->get_name(),
             'native/pgsql'   => \moodle_database::get_driver_instance('pgsql', 'native')->get_name(),
             'native/oci'     => \moodle_database::get_driver_instance('oci', 'native')->get_name(),
-            'native/sqlsrv'  => \moodle_database::get_driver_instance('sqlsrv', 'native')->get_name(),
-            'native/mssql'   => \moodle_database::get_driver_instance('mssql', 'native')->get_name()
+            'native/sqlsrv'  => \moodle_database::get_driver_instance('sqlsrv', 'native')->get_name()
         );
     }
 
index 4841569..160c4f0 100644 (file)
@@ -151,6 +151,11 @@ class api {
             'tool_mobile_disabledfeatures' => get_config('tool_mobile', 'disabledfeatures'),
             'country' => clean_param($CFG->country, PARAM_NOTAGS),
             'agedigitalconsentverification' => \core_auth\digital_consent::is_age_digital_consent_verification_enabled(),
+            'autolang' => $CFG->autolang,
+            'lang' => clean_param($CFG->lang, PARAM_LANG),  // Avoid breaking WS because of incorrect package langs.
+            'langmenu' => $CFG->langmenu,
+            'langlist' => $CFG->langlist,
+            'locale' => $CFG->locale,
         );
 
         $typeoflogin = get_config('tool_mobile', 'typeoflogin');
index 65a5ccf..7df769b 100644 (file)
@@ -168,6 +168,12 @@ class external extends external_api {
                     (only if age verification is enabled).', VALUE_OPTIONAL),
                 'supportemail' => new external_value(PARAM_EMAIL, 'Site support contact email
                     (only if age verification is enabled).', VALUE_OPTIONAL),
+                'autolang' => new external_value(PARAM_INT, 'Whether to detect default language
+                    from browser setting.', VALUE_OPTIONAL),
+                'lang' => new external_value(PARAM_LANG, 'Default language for the site.', VALUE_OPTIONAL),
+                'langmenu' => new external_value(PARAM_INT, 'Whether the language menu should be displayed.', VALUE_OPTIONAL),
+                'langlist' => new external_value(PARAM_RAW, 'Languages on language menu.', VALUE_OPTIONAL),
+                'locale' => new external_value(PARAM_RAW, 'Sitewide locale.', VALUE_OPTIONAL),
                 'warnings' => new external_warnings(),
             )
         );
index 19483ee..e61657c 100644 (file)
@@ -88,6 +88,11 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
             'launchurl' => "$CFG->wwwroot/$CFG->admin/tool/mobile/launch.php",
             'country' => $CFG->country,
             'agedigitalconsentverification' => \core_auth\digital_consent::is_age_digital_consent_verification_enabled(),
+            'autolang' => $CFG->autolang,
+            'lang' => $CFG->lang,
+            'langmenu' => $CFG->langmenu,
+            'langlist' => $CFG->langlist,
+            'locale' => $CFG->locale,
             'warnings' => array()
         );
         $this->assertEquals($expected, $result);
@@ -101,6 +106,8 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         set_config('logocompact', 'mock.png', 'core_admin');
         set_config('forgottenpasswordurl', 'mailto:fake@email.zy'); // Test old hack.
         set_config('agedigitalconsentverification', 1);
+        set_config('autolang', 1);
+        set_config('lang', 'a_b');  // Set invalid lang.
 
         list($authinstructions, $notusedformat) = external_format_text($authinstructions, FORMAT_MOODLE, $context->id);
         $expected['registerauth'] = 'email';
@@ -110,6 +117,8 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $expected['agedigitalconsentverification'] = true;
         $expected['supportname'] = $CFG->supportname;
         $expected['supportemail'] = $CFG->supportemail;
+        $expected['autolang'] = '1';
+        $expected['lang'] = ''; // Expect empty because it was set to an invalid lang.
 
         if ($logourl = $OUTPUT->get_logo_url()) {
             $expected['logourl'] = $logourl->out(false);
index b481d2b..15f142c 100644 (file)
@@ -819,7 +819,7 @@ class manager {
         $existingtourrecords->close();
 
         foreach ($shippedtours as $filename => $version) {
-            $filepath = $CFG->dirroot . '/admin/tool/usertours/tours/' . $filename;
+            $filepath = $CFG->dirroot . "/{$CFG->admin}/tool/usertours/tours/" . $filename;
             $tourjson = file_get_contents($filepath);
             $tour = self::import_tour_from_json($tourjson);
 
index 6049e8f..4819ac0 100644 (file)
@@ -483,10 +483,9 @@ class step {
             $record = $this->to_record();
             unset($record->id);
             $this->id = $DB->insert_record('tool_usertours_steps', $record);
+            $this->get_tour()->reset_step_sortorder();
         }
 
-        $this->get_tour()->reset_step_sortorder();
-
         $this->reload();
 
         // Notify of a change to the step configuration.
index 80e462d..0caf98f 100644 (file)
 
     // Carry on with the user listing
     $context = context_system::instance();
-    $extracolumns = get_extra_user_fields($context);
+    // These columns are always shown in the users list.
+    $requiredcolumns = array('city', 'country', 'lastaccess');
+    // Extra columns containing the extra user fields, excluding the required columns (city and country, to be specific).
+    $extracolumns = get_extra_user_fields($context, $requiredcolumns);
     // Get all user name fields as an array.
     $allusernamefields = get_all_user_name_fields(false, null, null, null, true);
-    $columns = array_merge($allusernamefields, $extracolumns, array('city', 'country', 'lastaccess'));
+    $columns = array_merge($allusernamefields, $extracolumns, $requiredcolumns);
 
     foreach ($columns as $column) {
         $string[$column] = get_user_field_name($column);
index 0383c08..00cfea3 100644 (file)
@@ -36,7 +36,7 @@ class core_analytics_course_testcase extends advanced_testcase {
     public function setUp() {
         global $DB;
 
-        $this->course = $this->getDataGenerator()->create_course();
+        $this->course = $this->getDataGenerator()->create_course(['startdate' => 0]);
         $this->stu1 = $this->getDataGenerator()->create_user();
         $this->stu2 = $this->getDataGenerator()->create_user();
         $this->both = $this->getDataGenerator()->create_user();
index 2900719..fd6a62c 100644 (file)
@@ -97,11 +97,7 @@ class auth_db_testcase extends advanced_testcase {
                 break;
 
             case 'mssql':
-                if (get_class($DB) == 'mssql_native_moodle_database') {
-                    set_config('type', 'mssql_n', 'auth_db');
-                } else {
-                    set_config('type', 'mssqlnative', 'auth_db');
-                }
+                set_config('type', 'mssqlnative', 'auth_db');
                 set_config('sybasequoting', '1', 'auth_db');
                 break;
 
index f1cbc11..f207f03 100644 (file)
@@ -113,7 +113,7 @@ class auth_plugin_email extends auth_plugin_base {
      * @since Moodle 3.2
      */
     public function user_signup_with_confirmation($user, $notify=true, $confirmationurl = null) {
-        global $CFG, $DB;
+        global $CFG, $DB, $SESSION;
         require_once($CFG->dirroot.'/user/profile/lib.php');
         require_once($CFG->dirroot.'/user/lib.php');
 
@@ -130,6 +130,11 @@ class auth_plugin_email extends auth_plugin_base {
         // Save any custom profile field information.
         profile_save_data($user);
 
+        // Save wantsurl against user's profile, so we can return them there upon confirmation.
+        if (!empty($SESSION->wantsurl)) {
+            set_user_preference('auth_email_wantsurl', $SESSION->wantsurl, $user);
+        }
+
         // Trigger event.
         \core\event\user_created::create_from_userid($user->id)->trigger();
 
@@ -166,7 +171,7 @@ class auth_plugin_email extends auth_plugin_base {
      * @param string $confirmsecret
      */
     function user_confirm($username, $confirmsecret) {
-        global $DB;
+        global $DB, $SESSION;
         $user = get_complete_user_data('username', $username);
 
         if (!empty($user)) {
@@ -178,6 +183,13 @@ class auth_plugin_email extends auth_plugin_base {
 
             } else if ($user->secret == $confirmsecret) {   // They have provided the secret key to get in
                 $DB->set_field("user", "confirmed", 1, array("id"=>$user->id));
+
+                if ($wantsurl = get_user_preferences('auth_email_wantsurl', false, $user)) {
+                    // Ensure user gets returned to page they were trying to access before signing up.
+                    $SESSION->wantsurl = $wantsurl;
+                    unset_user_preference('auth_email_wantsurl', $user);
+                }
+
                 return AUTH_CONFIRM_OK;
             }
         } else {
index c359096..c3c200a 100644 (file)
@@ -37,6 +37,7 @@ require_once($CFG->dirroot . '/backup/moodle2/backup_default_block_task.class.ph
 require_once($CFG->dirroot . '/backup/moodle2/backup_xml_transformer.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php');
+require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_extrafields_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_gradingform_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_format_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_local_plugin.class.php');
diff --git a/backup/moodle2/backup_qtype_extrafields_plugin.class.php b/backup/moodle2/backup_qtype_extrafields_plugin.class.php
new file mode 100644 (file)
index 0000000..d7db6ff
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Defines backup_qtype_extrafields_plugin class
+ *
+ * @package    core_backup
+ * @copyright  2012 Oleg Sychev, Volgograd State Technical University
+ * @author     Valeriy Streltsov <vostreltsov@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/question/engine/bank.php');
+
+/**
+ * Class extending backup_qtype_plugin in order to use extra fields method
+ *
+ * See qtype_shortanswer for an example
+ *
+ * @copyright  2012 Oleg Sychev, Volgograd State Technical University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backup_qtype_extrafields_plugin extends backup_qtype_plugin {
+
+    /**
+     * Returns the qtype information to attach to question element.
+     */
+    protected function define_question_plugin_structure() {
+        $qtypeobj = question_bank::get_qtype($this->pluginname);
+
+        // Define the virtual plugin element with the condition to fulfill.
+        $plugin = $this->get_plugin_element(null, '../../qtype', $qtypeobj->name());
+
+        // Create one standard named plugin element (the visible container).
+        $pluginwrapper = new backup_nested_element($this->get_recommended_name());
+
+        // Connect the visible container ASAP.
+        $plugin->add_child($pluginwrapper);
+
+        // This qtype uses standard question_answers, add them here
+        // to the tree before any other information that will use them.
+        $this->add_question_question_answers($pluginwrapper);
+        $answers = $pluginwrapper->get_child('answers');
+        $answer = $answers->get_child('answer');
+
+        // Extra question fields.
+        $extraquestionfields = $qtypeobj->extra_question_fields();
+        if (!empty($extraquestionfields)) {
+            $tablename = array_shift($extraquestionfields);
+            $child = new backup_nested_element($qtypeobj->name(), array('id'), $extraquestionfields);
+            $pluginwrapper->add_child($child);
+            $child->set_source_table($tablename, array($qtypeobj->questionid_column_name() => backup::VAR_PARENTID));
+        }
+
+        // Extra answer fields.
+        $extraanswerfields = $qtypeobj->extra_answer_fields();
+        if (!empty($extraanswerfields)) {
+            $tablename = array_shift($extraanswerfields);
+            $child = new backup_nested_element('extraanswerdata', array('id'), $extraanswerfields);
+            $answer->add_child($child);
+            $child->set_source_table($tablename, array('answerid' => backup::VAR_PARENTID));
+        }
+
+        // Don't need to annotate ids nor files.
+        return $plugin;
+    }
+}
index af22ff5..a2f922a 100644 (file)
@@ -2234,7 +2234,7 @@ class backup_questions_structure_step extends backup_structure_step {
 
         $tags = new backup_nested_element('tags');
 
-        $tag = new backup_nested_element('tag', array('id'), array('name', 'rawname'));
+        $tag = new backup_nested_element('tag', array('id', 'contextid'), array('name', 'rawname'));
 
         // Build the tree
 
@@ -2266,7 +2266,7 @@ class backup_questions_structure_step extends backup_structure_step {
                 ORDER BY id',
                 array('questionid' => backup::VAR_PARENTID));
 
-        $tag->set_source_sql("SELECT t.id, t.name, t.rawname
+        $tag->set_source_sql("SELECT t.id, ti.contextid, t.name, t.rawname
                               FROM {tag} t
                               JOIN {tag_instance} ti ON ti.tagid = t.id
                               WHERE ti.itemid = ?
index ae48ec9..02dc062 100644 (file)
@@ -36,6 +36,7 @@ require_once($CFG->dirroot . '/backup/moodle2/restore_block_task.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_default_block_task.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_plugin.class.php');
+require_once($CFG->dirroot . '/backup/moodle2/restore_qtype_extrafields_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_format_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_local_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/restore_theme_plugin.class.php');
@@ -46,6 +47,7 @@ require_once($CFG->dirroot . '/backup/moodle2/restore_gradingform_plugin.class.p
 require_once($CFG->dirroot . '/backup/moodle2/restore_enrol_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_plugin.class.php');
+require_once($CFG->dirroot . '/backup/moodle2/backup_qtype_extrafields_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_format_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_local_plugin.class.php');
 require_once($CFG->dirroot . '/backup/moodle2/backup_theme_plugin.class.php');
diff --git a/backup/moodle2/restore_qtype_extrafields_plugin.class.php b/backup/moodle2/restore_qtype_extrafields_plugin.class.php
new file mode 100644 (file)
index 0000000..e976f38
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Defines restore_qtype_extrafields_plugin class
+ *
+ * @package    core_backup
+ * @copyright  2012 Oleg Sychev, Volgograd State Technical University
+ * @author     Valeriy Streltsov <vostreltsov@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/question/engine/bank.php');
+
+/**
+ * Class extending restore_qtype_plugin in order to use extra fields method
+ *
+ * See qtype_shortanswer for an example
+ *
+ * @copyright  2012 Oleg Sychev, Volgograd State Technical University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_qtype_extrafields_plugin extends restore_qtype_plugin {
+
+    /**
+     * Question type class for a particular question type
+     * @var question_type
+     */
+    protected $qtypeobj;
+
+    /**
+     * Constructor
+     *
+     * @param string $plugintype plugin type
+     * @param string $pluginname plugin name
+     * @param restore_step $step step
+     */
+    public function __construct($plugintype, $pluginname, $step) {
+        parent::__construct($plugintype, $pluginname, $step);
+        $this->qtypeobj = question_bank::get_qtype($this->pluginname);
+    }
+
+    /**
+     * Returns the paths to be handled by the plugin at question level.
+     */
+    protected function define_question_plugin_structure() {
+        $paths = array();
+
+        // This qtype uses question_answers, add them.
+        $this->add_question_question_answers($paths);
+
+        // Add own qtype stuff.
+        $elepath = $this->get_pathfor('/' . $this->qtypeobj->name());
+        $paths[] = new restore_path_element($this->qtypeobj->name(), $elepath);
+
+        $elepath = $this->get_pathfor('/answers/answer/extraanswerdata');
+        $paths[] = new restore_path_element('extraanswerdata', $elepath);
+
+        return $paths;
+    }
+
+    /**
+     * Processes the extra answer data
+     *
+     * @param array $data extra answer data
+     */
+    public function process_extraanswerdata($data) {
+        global $DB;
+
+        $extra = $this->qtypeobj->extra_answer_fields();
+        $tablename = array_shift($extra);
+
+        $oldquestionid = $this->get_old_parentid('question');
+        $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
+
+        if ($questioncreated) {
+            $data['answerid'] = $this->get_mappingid('question_answer', $data['id']);
+            $DB->insert_record($tablename, $data);
+        } else {
+            $DB->update_record($tablename, $data);
+        }
+    }
+
+    /**
+     * Process the qtype/... element.
+     *
+     * @param array $data question data
+     */
+    public function really_process_extra_question_fields($data) {
+        global $DB;
+
+        $oldid = $data['id'];
+
+        // Detect if the question is created or mapped.
+        $oldquestionid = $this->get_old_parentid('question');
+        $newquestionid = $this->get_new_parentid('question');
+        $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
+
+        // If the question has been created by restore, we need to create its qtype_... too.
+        if ($questioncreated) {
+            $extraquestionfields = $this->qtypeobj->extra_question_fields();
+            $tablename = array_shift($extraquestionfields);
+
+            // Adjust some columns.
+            $qtfield = $this->qtypeobj->questionid_column_name();
+            $data[$qtfield] = $newquestionid;
+
+            // Insert record.
+            $newitemid = $DB->insert_record($tablename, $data);
+
+            // Create mapping.
+            $this->set_mapping($tablename, $oldid, $newitemid);
+        }
+    }
+}
index b96a02e..127eab9 100644 (file)
@@ -4388,6 +4388,9 @@ class restore_create_categories_and_questions extends restore_structure_step {
 
         // Check we have to create the category (newitemid = 0)
         if ($mapping->newitemid) {
+            // By performing this set_mapping() we make get_old/new_parentid() to work for all the
+            // children elements of the 'question_category' one.
+            $this->set_mapping('question_category', $oldid, $mapping->newitemid);
             return; // newitemid != 0, this category is going to be mapped. Nothing to do
         }
 
@@ -4414,20 +4417,29 @@ class restore_create_categories_and_questions extends restore_structure_step {
             $data->parent = $top->id;
         }
 
-        // Before 3.1, the 'stamp' field could be erroneously duplicated.
-        // From 3.1 onwards, there's a unique index of (contextid, stamp).
-        // If we encounter a duplicate in an old restore file, just generate a new stamp.
-        // This is the same as what happens during an upgrade to 3.1+ anyway.
-        if ($DB->record_exists('question_categories', ['stamp' => $data->stamp, 'contextid' => $data->contextid])) {
-            $data->stamp = make_unique_id_code();
-        }
+        if (empty($data->parent)) {
+            if (!$top = question_get_top_category($data->contextid)) {
+                $top = question_get_top_category($data->contextid, true);
+                $this->set_mapping('question_category_created', $oldid, $top->id, false, null, $data->contextid);
+            }
+            $this->set_mapping('question_category', $oldid, $top->id);
+        } else {
+
+            // Before 3.1, the 'stamp' field could be erroneously duplicated.
+            // From 3.1 onwards, there's a unique index of (contextid, stamp).
+            // If we encounter a duplicate in an old restore file, just generate a new stamp.
+            // This is the same as what happens during an upgrade to 3.1+ anyway.
+            if ($DB->record_exists('question_categories', ['stamp' => $data->stamp, 'contextid' => $data->contextid])) {
+                $data->stamp = make_unique_id_code();
+            }
 
-        // Let's create the question_category and save mapping
-        $newitemid = $DB->insert_record('question_categories', $data);
-        $this->set_mapping('question_category', $oldid, $newitemid);
-        // Also annotate them as question_category_created, we need
-        // that later when remapping parents
-        $this->set_mapping('question_category_created', $oldid, $newitemid, false, null, $data->contextid);
+            // Let's create the question_category and save mapping.
+            $newitemid = $DB->insert_record('question_categories', $data);
+            $this->set_mapping('question_category', $oldid, $newitemid);
+            // Also annotate them as question_category_created, we need
+            // that later when remapping parents.
+            $this->set_mapping('question_category_created', $oldid, $newitemid, false, null, $data->contextid);
+        }
     }
 
     protected function process_question($data) {
@@ -4542,7 +4554,7 @@ class restore_create_categories_and_questions extends restore_structure_step {
     }
 
     protected function process_tag($data) {
-        global $CFG, $DB;
+        global $DB;
 
         $data = (object)$data;
         $newquestion = $this->get_new_parentid('question');
@@ -4554,14 +4566,19 @@ class restore_create_categories_and_questions extends restore_structure_step {
 
         if (core_tag_tag::is_enabled('core_question', 'question')) {
             $tagname = $data->rawname;
-            // Get the category, so we can then later get the context.
-            $categoryid = $this->get_new_parentid('question_category');
-            if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) {
-                $this->cachedcategory = $DB->get_record('question_categories', array('id' => $categoryid));
+            if (!empty($data->contextid) && $newcontextid = $this->get_mappingid('context', $data->contextid)) {
+                    $tagcontextid = $newcontextid;
+            } else {
+                // Get the category, so we can then later get the context.
+                $categoryid = $this->get_new_parentid('question_category');
+                if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) {
+                    $this->cachedcategory = $DB->get_record('question_categories', array('id' => $categoryid));
+                }
+                $tagcontextid = $this->cachedcategory->contextid;
             }
             // Add the tag to the question.
             core_tag_tag::add_item_tag('core_question', 'question', $newquestion,
-                    context::instance_by_id($this->cachedcategory->contextid),
+                    context::instance_by_id($tagcontextid),
                     $tagname);
         }
     }
@@ -4633,9 +4650,9 @@ class restore_move_module_questions_categories extends restore_execution_step {
         $backuprelease = floatval($this->task->get_info()->backup_release);
         preg_match('/(\d{8})/', $this->task->get_info()->moodle_release, $matches);
         $backupbuild = (int)$matches[1];
-        $before35 = false;
-        if ($backuprelease < 3.5 || $backupbuild < 20180205) {
-            $before35 = true;
+        $after35 = false;
+        if ($backuprelease >= 3.5 && $backupbuild > 20180205) {
+            $after35 = true;
         }
 
         $contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE);
@@ -4648,23 +4665,36 @@ class restore_move_module_questions_categories extends restore_execution_step {
                                                      WHERE backupid = ?
                                                        AND itemname = 'question_category'
                                                        AND parentitemid = ?", array($this->get_restoreid(), $contextid));
+                $top = question_get_top_category($newcontext->newitemid, true);
+                $oldtopid = 0;
                 foreach ($modulecats as $modulecat) {
-                    $cat = new stdClass();
-                    $cat->id = $modulecat->newitemid;
-                    $cat->contextid = $newcontext->newitemid;
-
                     // Before 3.5, question categories could be created at top level.
                     // From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
                     $info = backup_controller_dbops::decode_backup_temp_info($modulecat->info);
-                    if ($before35 && empty($info->parent)) {
-                        $top = question_get_top_category($newcontext->newitemid, true);
-                        $cat->parent = $top->id;
+                    if ($after35 && empty($info->parent)) {
+                        $oldtopid = $modulecat->newitemid;
+                        $modulecat->newitemid = $top->id;
+                    } else {
+                        $cat = new stdClass();
+                        $cat->id = $modulecat->newitemid;
+                        $cat->contextid = $newcontext->newitemid;
+                        if (empty($info->parent)) {
+                            $cat->parent = $top->id;
+                        }
+                        $DB->update_record('question_categories', $cat);
                     }
-                    $DB->update_record('question_categories', $cat);
 
-                    // And set new contextid also in question_category mapping (will be
-                    // used by {@link restore_create_question_files} later
-                    restore_dbops::set_backup_ids_record($this->get_restoreid(), 'question_category', $modulecat->itemid, $modulecat->newitemid, $newcontext->newitemid);
+                    // And set new contextid (and maybe update newitemid) also in question_category mapping (will be
+                    // used by {@link restore_create_question_files} later.
+                    restore_dbops::set_backup_ids_record($this->get_restoreid(), 'question_category', $modulecat->itemid,
+                            $modulecat->newitemid, $newcontext->newitemid);
+                }
+
+                // Now set the parent id for the question categories that were in the top category in the course context
+                // and have been moved now.
+                if ($oldtopid) {
+                    $DB->set_field('question_categories', 'parent', $top->id,
+                            array('contextid' => $newcontext->newitemid, 'parent' => $oldtopid));
                 }
             }
         }
index 19cb072..ce30c06 100644 (file)
@@ -209,7 +209,7 @@ abstract class base_task implements checksumable, executable, loggable {
         }
         // Everything has been destroyed recursively, now we can reset safely
         $this->steps = array();
-        $this->setting = array();
+        $this->settings = array();
         $this->plan = null;
     }
 
index d4a98a1..2a34335 100644 (file)
@@ -61,7 +61,7 @@
                 <a{{!
                     }} href="#"{{!
                     }} class="arrow_link next"{{!
-                    }} title="{{#str}}monthprev, calendar{{/str}}"{{!
+                    }} title="{{#str}}monthnext, calendar{{/str}}"{{!
                     }} data-year="{{nextperiod.year}}"{{!
                     }} data-month="{{nextperiod.mon}}"{{!
                 }}>
index b7fba4d..df616e1 100644 (file)
@@ -132,4 +132,5 @@ echo html_writer::end_tag('div');
 list($data, $template) = calendar_get_footer_options($calendar);
 echo $renderer->render_from_template($template, $data);
 
+echo $renderer->complete_layout();
 echo $OUTPUT->footer();
index 55e7529..cbd2101 100644 (file)
@@ -64,6 +64,10 @@ class cohort_summary_exporter extends \core\external\exporter {
             ),
             'visible' => array(
                 'type' => PARAM_BOOL,
+            ),
+            'theme' => array(
+                'type' => PARAM_THEME,
+                'null' => NULL_ALLOWED
             )
         );
     }
index d4abb88..83df3a7 100644 (file)
@@ -32,6 +32,7 @@ class cohort_edit_form extends moodleform {
      * Define the cohort edit form
      */
     public function definition() {
+        global $CFG;
 
         $mform = $this->_form;
         $editoroptions = $this->_customdata['editoroptions'];
@@ -54,6 +55,11 @@ class cohort_edit_form extends moodleform {
         $mform->addElement('editor', 'description_editor', get_string('description', 'cohort'), null, $editoroptions);
         $mform->setType('description_editor', PARAM_RAW);
 
+        if (!empty($CFG->allowcohortthemes)) {
+            $themes = array_merge(array('' => get_string('forceno')), cohort_get_list_of_themes());
+            $mform->addElement('select', 'theme', get_string('forcetheme'), $themes);
+        }
+
         $mform->addElement('hidden', 'id');
         $mform->setType('id', PARAM_INT);
 
index 7c38a8f..08f62ee 100644 (file)
@@ -54,6 +54,10 @@ class core_cohort_external extends external_api {
                             'description' => new external_value(PARAM_RAW, 'cohort description', VALUE_OPTIONAL),
                             'descriptionformat' => new external_format_value('description', VALUE_DEFAULT),
                             'visible' => new external_value(PARAM_BOOL, 'cohort visible', VALUE_OPTIONAL, true),
+                            'theme' => new external_value(PARAM_THEME,
+                                'the cohort theme. The allowcohortthemes setting must be enabled on Moodle',
+                                VALUE_OPTIONAL
+                            ),
                         )
                     )
                 )
@@ -74,6 +78,8 @@ class core_cohort_external extends external_api {
 
         $params = self::validate_parameters(self::create_cohorts_parameters(), array('cohorts' => $cohorts));
 
+        $availablethemes = cohort_get_list_of_themes();
+
         $transaction = $DB->start_delegated_transaction();
 
         $syscontext = context_system::instance();
@@ -107,6 +113,15 @@ class core_cohort_external extends external_api {
             self::validate_context($context);
             require_capability('moodle/cohort:manage', $context);
 
+            // Make sure theme is valid.
+            if (isset($cohort->theme)) {
+                if (!empty($CFG->allowcohortthemes)) {
+                    if (empty($availablethemes[$cohort->theme])) {
+                        throw new moodle_exception('errorinvalidparam', 'webservice', '', 'theme');
+                    }
+                }
+            }
+
             // Validate format.
             $cohort->descriptionformat = external_validate_format($cohort->descriptionformat);
             $cohort->id = cohort_add_cohort($cohort);
@@ -137,6 +152,7 @@ class core_cohort_external extends external_api {
                     'description' => new external_value(PARAM_RAW, 'cohort description'),
                     'descriptionformat' => new external_format_value('description'),
                     'visible' => new external_value(PARAM_BOOL, 'cohort visible'),
+                    'theme' => new external_value(PARAM_THEME, 'cohort theme', VALUE_OPTIONAL),
                 )
             )
         );
@@ -223,7 +239,7 @@ class core_cohort_external extends external_api {
      * @since Moodle 2.5
      */
     public static function get_cohorts($cohortids = array()) {
-        global $DB;
+        global $DB, $CFG;
 
         $params = self::validate_parameters(self::get_cohorts_parameters(), array('cohortids' => $cohortids));
 
@@ -245,6 +261,11 @@ class core_cohort_external extends external_api {
                 throw new required_capability_exception($context, 'moodle/cohort:view', 'nopermissions', '');
             }
 
+            // Only return theme when $CFG->allowcohortthemes is enabled.
+            if (!empty($cohort->theme) && empty($CFG->allowcohortthemes)) {
+                $cohort->theme = null;
+            }
+
             list($cohort->description, $cohort->descriptionformat) =
                 external_format_text($cohort->description, $cohort->descriptionformat,
                         $context->id, 'cohort', 'description', $cohort->id);
@@ -271,6 +292,7 @@ class core_cohort_external extends external_api {
                     'description' => new external_value(PARAM_RAW, 'cohort description'),
                     'descriptionformat' => new external_format_value('description'),
                     'visible' => new external_value(PARAM_BOOL, 'cohort visible'),
+                    'theme' => new external_value(PARAM_THEME, 'cohort theme', VALUE_OPTIONAL),
                 )
             )
         );
@@ -367,6 +389,12 @@ class core_cohort_external extends external_api {
         $cohorts = array();
         foreach ($results as $key => $cohort) {
             $cohortcontext = context::instance_by_id($cohort->contextid);
+
+            // Only return theme when $CFG->allowcohortthemes is enabled.
+            if (!empty($cohort->theme) && empty($CFG->allowcohortthemes)) {
+                $cohort->theme = null;
+            }
+
             if (!isset($cohort->description)) {
                 $cohort->description = '';
             }
@@ -399,6 +427,7 @@ class core_cohort_external extends external_api {
                     'description' => new external_value(PARAM_RAW, 'cohort description'),
                     'descriptionformat' => new external_format_value('description'),
                     'visible' => new external_value(PARAM_BOOL, 'cohort visible'),
+                    'theme' => new external_value(PARAM_THEME, 'cohort theme', VALUE_OPTIONAL),
                 ))
             )
         ));
@@ -432,6 +461,10 @@ class core_cohort_external extends external_api {
                             'description' => new external_value(PARAM_RAW, 'cohort description', VALUE_OPTIONAL),
                             'descriptionformat' => new external_format_value('description', VALUE_DEFAULT),
                             'visible' => new external_value(PARAM_BOOL, 'cohort visible', VALUE_OPTIONAL),
+                            'theme' => new external_value(PARAM_THEME,
+                                'the cohort theme. The allowcohortthemes setting must be enabled on Moodle',
+                                VALUE_OPTIONAL
+                            ),
                         )
                     )
                 )
@@ -452,6 +485,8 @@ class core_cohort_external extends external_api {
 
         $params = self::validate_parameters(self::update_cohorts_parameters(), array('cohorts' => $cohorts));
 
+        $availablethemes = cohort_get_list_of_themes();
+
         $transaction = $DB->start_delegated_transaction();
         $syscontext = context_system::instance();
 
@@ -490,6 +525,14 @@ class core_cohort_external extends external_api {
                 require_capability('moodle/cohort:manage', $context);
             }
 
+            // Make sure theme is valid.
+            if (!empty($cohort->theme) && !empty($CFG->allowcohortthemes)) {
+                if (empty($availablethemes[$cohort->theme])) {
+                    $debuginfo = 'The following cohort theme is not installed on this site: '.$cohort->theme;
+                    throw new moodle_exception('errorinvalidparam', 'webservice', '', 'theme', $debuginfo);
+                }
+            }
+
             if (!empty($cohort->description)) {
                 $cohort->descriptionformat = external_validate_format($cohort->descriptionformat);
             }
index f3c3a16..09d17dc 100644 (file)
@@ -38,7 +38,7 @@ define('COHORT_WITH_NOTENROLLED_MEMBERS_ONLY', 23);
  * @return int new cohort id
  */
 function cohort_add_cohort($cohort) {
-    global $DB;
+    global $DB, $CFG;
 
     if (!isset($cohort->name)) {
         throw new coding_exception('Missing cohort name in cohort_add_cohort().');
@@ -58,6 +58,12 @@ function cohort_add_cohort($cohort) {
     if (empty($cohort->component)) {
         $cohort->component = '';
     }
+    if (empty($CFG->allowcohortthemes) && isset($cohort->theme)) {
+        unset($cohort->theme);
+    }
+    if (empty($cohort->theme) || empty($CFG->allowcohortthemes)) {
+        $cohort->theme = '';
+    }
     if (!isset($cohort->timecreated)) {
         $cohort->timecreated = time();
     }
@@ -83,11 +89,15 @@ function cohort_add_cohort($cohort) {
  * @return void
  */
 function cohort_update_cohort($cohort) {
-    global $DB;
+    global $DB, $CFG;
     if (property_exists($cohort, 'component') and empty($cohort->component)) {
         // prevent NULLs
         $cohort->component = '';
     }
+    // Only unset the cohort theme if allowcohortthemes is enabled to prevent the value from being overwritten.
+    if (empty($CFG->allowcohortthemes) && isset($cohort->theme)) {
+        unset($cohort->theme);
+    }
     $cohort->timemodified = time();
     $DB->update_record('cohort', $cohort);
 
@@ -478,6 +488,47 @@ function cohort_get_all_cohorts($page = 0, $perpage = 25, $search = '') {
     return array('totalcohorts' => $totalcohorts, 'cohorts' => $cohorts, 'allcohorts' => $allcohorts);
 }
 
+/**
+ * Get all the cohorts where the given user is member of.
+ *
+ * @param int $userid
+ * @return array Array
+ */
+function cohort_get_user_cohorts($userid) {
+    global $DB;
+
+    $sql = 'SELECT c.*
+              FROM {cohort} c
+              JOIN {cohort_members} cm ON c.id = cm.cohortid
+             WHERE cm.userid = ? AND c.visible = 1';
+    return $DB->get_records_sql($sql, array($userid));
+}
+
+/**
+ * Get the user cohort theme.
+ *
+ * If the user is member of one cohort, will return this cohort theme (if defined).
+ * If the user is member of 2 or more cohorts, will return the theme if all them have the same
+ * theme (null themes are ignored).
+ *
+ * @param int $userid
+ * @return string|null
+ */
+function cohort_get_user_cohort_theme($userid) {
+    $cohorts = cohort_get_user_cohorts($userid);
+    $theme = null;
+    foreach ($cohorts as $cohort) {
+        if (!empty($cohort->theme)) {
+            if (null === $theme) {
+                $theme = $cohort->theme;
+            } else if ($theme != $cohort->theme) {
+                return null;
+            }
+        }
+    }
+    return $theme;
+}
+
 /**
  * Returns list of contexts where cohorts are present but current user does not have capability to view/manage them.
  *
@@ -568,3 +619,19 @@ function core_cohort_inplace_editable($itemtype, $itemid, $newvalue) {
         return \core_cohort\output\cohortidnumber::update($itemid, $newvalue);
     }
 }
+
+/**
+ * Returns a list of valid themes which can be displayed in a selector.
+ *
+ * @return array as (string)themename => (string)get_string_theme
+ */
+function cohort_get_list_of_themes() {
+    $themes = array();
+    $allthemes = get_list_of_themes();
+    foreach ($allthemes as $key => $theme) {
+        if (empty($theme->hidefromselector)) {
+            $themes[$key] = get_string('pluginname', 'theme_'.$theme->name);
+        }
+    }
+    return $themes;
+}
\ No newline at end of file
index a400b01..2cc5074 100644 (file)
@@ -50,7 +50,7 @@ Feature: A privileged user can create cohorts using a CSV file
     And the "class" attribute of "cohort name 5" "table_row" should contain "dimmed_text"
     And ".dimmed_text" "css_element" should not exist in the "cohort name 6" "table_row"
 
-  @javascript
+  @javascript @_file_upload
   Scenario: Upload cohorts with default category context as admin
     When I log in as "admin"
     And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
@@ -81,7 +81,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | Cat 2         | cohort name 5 | cohortid5 |                   | 0           | Created manually |
       | Cat 3         | cohort name 6 | cohortid6 |                   | 0           | Created manually |
 
-  @javascript
+  @javascript @_file_upload
   Scenario: Upload cohorts with default category context as manager
     Given the following "users" exist:
       | username | firstname | lastname | email                  |
@@ -107,7 +107,7 @@ Feature: A privileged user can create cohorts using a CSV file
     And I press "Upload cohorts"
     And I should see "Uploaded 6 cohorts"
 
-  @javascript
+  @javascript @_file_upload
   Scenario: Upload cohorts with conflicting id number
     Given the following "cohorts" exist:
       | name   | idnumber  |
@@ -128,7 +128,7 @@ Feature: A privileged user can create cohorts using a CSV file
       | cohort name 6 | cohortid6 |  | Cat 3 |  |
     And "Upload cohorts" "button" should not exist
 
-  @javascript
+  @javascript @_file_upload
   Scenario: Upload cohorts with different ways of specifying context
     When I log in as "admin"
     And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
@@ -161,3 +161,35 @@ Feature: A privileged user can create cohorts using a CSV file
     And I should not see "not found or you"
     And I press "Upload cohorts"
     And I should see "Uploaded 5 cohorts"
+
+  @javascript @_file_upload
+  Scenario: Upload cohorts with theme
+    When I log in as "admin"
+    And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+    And I follow "Upload cohorts"
+    And I upload "cohort/tests/fixtures/uploadcohorts4.csv" file to "File" filemanager
+    And I click on "Preview" "button"
+    Then the following should exist in the "previewuploadedcohorts" table:
+      | name          | idnumber  | description       | Context       | visible | theme  | Status |
+      | cohort name 1 | cohortid1 | first description | System        | 1       | boost  |        |
+      | cohort name 2 | cohortid2 |                   | System        | 1       |        |        |
+      | cohort name 3 | cohortid3 |                   | Miscellaneous | 0       | boost  |        |
+      | cohort name 4 | cohortid4 |                   | Cat 1         | 1       | clean  |        |
+      | cohort name 5 | cohortid5 |                   | Cat 2         | 0       |        |        |
+      | cohort name 6 | cohortid6 |                   | Cat 3         | 1       | clean  |        |
+    And I press "Upload cohorts"
+    And I should see "Uploaded 6 cohorts"
+    And I press "Continue"
+    And the following should exist in the "cohorts" table:
+      | Name          | Cohort ID | Description | Cohort size | Source           |
+      | cohort name 1 | cohortid1 | first description | 0           | Created manually |
+      | cohort name 2 | cohortid2 |             | 0           | Created manually |
+    And I follow "All cohorts"
+    And the following should exist in the "cohorts" table:
+      | Category      | Name          | Cohort ID | Description       | Cohort size | Source           |
+      | System        | cohort name 1 | cohortid1 | first description | 0           | Created manually |
+      | System        | cohort name 2 | cohortid2 |                   | 0           | Created manually |
+      | Miscellaneous | cohort name 3 | cohortid3 |                   | 0           | Created manually |
+      | Cat 1         | cohort name 4 | cohortid4 |                   | 0           | Created manually |
+      | Cat 2         | cohort name 5 | cohortid5 |                   | 0           | Created manually |
+      | Cat 3         | cohort name 6 | cohortid6 |                   | 0           | Created manually |
index 04b37b1..e92c264 100644 (file)
@@ -61,6 +61,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($cohort->descriptionformat, $newcohort->descriptionformat);
         $this->assertNotEmpty($newcohort->timecreated);
         $this->assertSame($newcohort->component, '');
+        $this->assertSame($newcohort->theme, '');
         $this->assertSame($newcohort->timecreated, $newcohort->timemodified);
     }
 
@@ -142,6 +143,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertSame($cohort->descriptionformat, $newcohort->descriptionformat);
         $this->assertSame($cohort->timecreated, $newcohort->timecreated);
         $this->assertSame($cohort->component, $newcohort->component);
+        $this->assertSame($newcohort->theme, '');
         $this->assertGreaterThan($newcohort->timecreated, $newcohort->timemodified);
         $this->assertLessThanOrEqual(time(), $newcohort->timemodified);
     }
@@ -158,6 +160,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $cohort->idnumber = 'testid';
         $cohort->description = 'test cohort desc';
         $cohort->descriptionformat = FORMAT_HTML;
+        $cohort->theme = '';
         $id = cohort_add_cohort($cohort);
         $this->assertNotEmpty($id);
 
@@ -168,6 +171,8 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
 
         // Peform the update.
         cohort_update_cohort($cohort);
+        // Add again theme property to the cohort object for comparing it to the event snapshop.
+        $cohort->theme = '';
 
         $events = $sink->get_events();
         $sink->close();
@@ -651,4 +656,87 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $result = cohort_get_available_cohorts($course1ctx, COHORT_ALL, 0, 0, '');
         $this->assertEquals(array($cohort1->id, $cohort2->id, $cohort4->id), array_keys($result));
     }
+
+    /**
+     * Create a cohort with allowcohortthemes enabled/disabled.
+     */
+    public function test_cohort_add_theme_cohort() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Theme is added when allowcohortthemes is enabled.
+        set_config('allowcohortthemes', 1);
+        set_config('theme', 'boost');
+
+        $systemctx = context_system::instance();
+        $cohort1 = $this->getDataGenerator()->create_cohort(array('contextid' => $systemctx->id, 'name' => 'test cohort 1',
+            'idnumber' => 'testid1', 'description' => 'test cohort desc', 'descriptionformat' => FORMAT_HTML, 'theme' => 'clean'));
+
+        $id = cohort_add_cohort($cohort1);
+        $this->assertNotEmpty($id);
+        $newcohort = $DB->get_record('cohort', array('id' => $id));
+        $this->assertEquals($cohort1->contextid, $newcohort->contextid);
+        $this->assertSame($cohort1->name, $newcohort->name);
+        $this->assertSame($cohort1->description, $newcohort->description);
+        $this->assertEquals($cohort1->descriptionformat, $newcohort->descriptionformat);
+        $this->assertNotEmpty($newcohort->theme);
+        $this->assertSame($cohort1->theme, $newcohort->theme);
+        $this->assertNotEmpty($newcohort->timecreated);
+        $this->assertSame($newcohort->component, '');
+        $this->assertSame($newcohort->timecreated, $newcohort->timemodified);
+
+        // Theme is not added when allowcohortthemes is disabled.
+        set_config('allowcohortthemes', 0);
+
+        $cohort2 = $this->getDataGenerator()->create_cohort(array('contextid' => $systemctx->id, 'name' => 'test cohort 2',
+            'idnumber' => 'testid2', 'description' => 'test cohort desc', 'descriptionformat' => FORMAT_HTML, 'theme' => 'clean'));
+
+        $id = cohort_add_cohort($cohort2);
+        $this->assertNotEmpty($id);
+        $newcohort = $DB->get_record('cohort', array('id' => $id));
+        $this->assertSame($cohort2->name, $newcohort->name);
+        $this->assertEmpty($newcohort->theme);
+    }
+
+    /**
+     * Update a cohort with allowcohortthemes enabled/disabled.
+     */
+    public function test_cohort_update_theme_cohort() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Enable cohort themes.
+        set_config('allowcohortthemes', 1);
+        set_config('theme', 'boost');
+
+        $systemctx = context_system::instance();
+        $cohort1 = $this->getDataGenerator()->create_cohort(array('contextid' => $systemctx->id, 'name' => 'test cohort 1',
+            'idnumber' => 'testid1', 'description' => 'test cohort desc', 'descriptionformat' => FORMAT_HTML, 'theme' => 'clean'));
+        $id = cohort_add_cohort($cohort1);
+        $this->assertNotEmpty($id);
+
+        // Theme is updated when allowcohortthemes is enabled.
+        $cohort1 = $DB->get_record('cohort', array('id' => $id));
+        $cohort1->name = 'test cohort 1 updated';
+        $cohort1->theme = 'more';
+        cohort_update_cohort($cohort1);
+        $updatedcohort = $DB->get_record('cohort', array('id' => $id));
+        $this->assertEquals($cohort1->contextid, $updatedcohort->contextid);
+        $this->assertSame($cohort1->name, $updatedcohort->name);
+        $this->assertSame($cohort1->description, $updatedcohort->description);
+        $this->assertNotEmpty($updatedcohort->theme);
+        $this->assertSame($cohort1->theme, $updatedcohort->theme);
+
+        // Theme is not updated neither overwritten when allowcohortthemes is disabled.
+        set_config('allowcohortthemes', 0);
+        $cohort2 = $DB->get_record('cohort', array('id' => $id));
+        $cohort2->theme = 'clean';
+        cohort_update_cohort($cohort2);
+        $updatedcohort = $DB->get_record('cohort', array('id' => $id));
+        $this->assertEquals($cohort2->contextid, $updatedcohort->contextid);
+        $this->assertNotEmpty($updatedcohort->theme);
+        $this->assertSame($cohort1->theme, $updatedcohort->theme);
+    }
 }
index 85b2f02..a6717b8 100644 (file)
@@ -42,6 +42,8 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
 
         $this->resetAfterTest(true);
 
+        set_config('allowcohortthemes', 1);
+
         $contextid = context_system::instance()->id;
         $category = $this->getDataGenerator()->create_category();
 
@@ -49,7 +51,8 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
             'categorytype' => array('type' => 'id', 'value' => $category->id),
             'name' => 'cohort test 1',
             'idnumber' => 'cohorttest1',
-            'description' => 'This is a description for cohorttest1'
+            'description' => 'This is a description for cohorttest1',
+            'theme' => 'clean'
             );
 
         $cohort2 = array(
@@ -68,6 +71,14 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
             );
         $roleid = $this->assignUserCapability('moodle/cohort:manage', $contextid);
 
+        $cohort4 = array(
+            'categorytype' => array('type' => 'id', 'value' => $category->id),
+            'name' => 'cohort test 4',
+            'idnumber' => 'cohorttest4',
+            'description' => 'This is a description for cohorttest4',
+            'theme' => 'clean'
+            );
+
         // Call the external function.
         $this->setCurrentTimeStart();
         $createdcohorts = core_cohort_external::create_cohorts(array($cohort1, $cohort2));
@@ -85,11 +96,15 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
                 $this->assertEquals($dbcohort->name, $cohort1['name']);
                 $this->assertEquals($dbcohort->description, $cohort1['description']);
                 $this->assertEquals($dbcohort->visible, 1); // Field was not specified, ensure it is visible by default.
+                // As $CFG->allowcohortthemes is enabled, theme must be initialised.
+                $this->assertEquals($dbcohort->theme, $cohort1['theme']);
             } else if ($createdcohort['idnumber'] == $cohort2['idnumber']) {
                 $this->assertEquals($dbcohort->contextid, context_system::instance()->id);
                 $this->assertEquals($dbcohort->name, $cohort2['name']);
                 $this->assertEquals($dbcohort->description, $cohort2['description']);
                 $this->assertEquals($dbcohort->visible, $cohort2['visible']);
+                // Although $CFG->allowcohortthemes is enabled, no theme is defined for this cohort.
+                $this->assertEquals($dbcohort->theme, '');
             } else {
                 $this->fail('Unrecognised cohort found');
             }
@@ -97,6 +112,23 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
             $this->assertTimeCurrent($dbcohort->timemodified);
         }
 
+        // Call when $CFG->allowcohortthemes is disabled.
+        set_config('allowcohortthemes', 0);
+        $createdcohorts = core_cohort_external::create_cohorts(array($cohort4));
+        $createdcohorts = external_api::clean_returnvalue(core_cohort_external::create_cohorts_returns(), $createdcohorts);
+        foreach ($createdcohorts as $createdcohort) {
+            $dbcohort = $DB->get_record('cohort', array('id' => $createdcohort['id']));
+            if ($createdcohort['idnumber'] == $cohort4['idnumber']) {
+                $conid = $DB->get_field('context', 'id', array('instanceid' => $cohort4['categorytype']['value'],
+                        'contextlevel' => CONTEXT_COURSECAT));
+                $this->assertEquals($dbcohort->contextid, $conid);
+                $this->assertEquals($dbcohort->name, $cohort4['name']);
+                $this->assertEquals($dbcohort->description, $cohort4['description']);
+                $this->assertEquals($dbcohort->visible, 1); // Field was not specified, ensure it is visible by default.
+                $this->assertEquals($dbcohort->theme, ''); // As $CFG->allowcohortthemes is disabled, theme must be empty.
+            }
+        }
+
         // Call without required capability.
         $this->unassignUserCapability('moodle/cohort:manage', $contextid, $roleid);
         $createdcohorts = core_cohort_external::create_cohorts(array($cohort3));
@@ -143,11 +175,14 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
 
         $this->resetAfterTest(true);
 
+        set_config('allowcohortthemes', 1);
+
         $cohort1 = array(
             'contextid' => 1,
             'name' => 'cohortnametest1',
             'idnumber' => 'idnumbertest1',
-            'description' => 'This is a description for cohort 1'
+            'description' => 'This is a description for cohort 1',
+            'theme' => 'clean'
             );
         $cohort1 = self::getDataGenerator()->create_cohort($cohort1);
         $cohort2 = self::getDataGenerator()->create_cohort();
@@ -168,6 +203,7 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
                 $this->assertEquals($cohort1->name, $enrolledcohort['name']);
                 $this->assertEquals($cohort1->description, $enrolledcohort['description']);
                 $this->assertEquals($cohort1->visible, $enrolledcohort['visible']);
+                $this->assertEquals($cohort1->theme, $enrolledcohort['theme']);
             }
         }
 
@@ -181,6 +217,17 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
 
         // Check we retrieve the good total number of enrolled cohorts + no error on capability.
         $this->assertEquals(2, count($returnedcohorts));
+
+        // Check when allowcohortstheme is disabled, theme is not returned.
+        set_config('allowcohortthemes', 0);
+        $returnedcohorts = core_cohort_external::get_cohorts(array(
+            $cohort1->id));
+        $returnedcohorts = external_api::clean_returnvalue(core_cohort_external::get_cohorts_returns(), $returnedcohorts);
+        foreach ($returnedcohorts as $enrolledcohort) {
+            if ($enrolledcohort['idnumber'] == $cohort1->idnumber) {
+                $this->assertNull($enrolledcohort['theme']);
+            }
+        }
     }
 
     /**
@@ -193,6 +240,8 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
 
         $this->resetAfterTest(true);
 
+        set_config('allowcohortthemes', 0);
+
         $cohort1 = self::getDataGenerator()->create_cohort(array('visible' => 0));
 
         $cohort1 = array(
@@ -200,7 +249,8 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
             'categorytype' => array('type' => 'id', 'value' => '1'),
             'name' => 'cohortnametest1',
             'idnumber' => 'idnumbertest1',
-            'description' => 'This is a description for cohort 1'
+            'description' => 'This is a description for cohort 1',
+            'theme' => 'clean'
             );
 
         $context = context_system::instance();
@@ -217,6 +267,7 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($dbcohort->idnumber, $cohort1['idnumber']);
         $this->assertEquals($dbcohort->description, $cohort1['description']);
         $this->assertEquals($dbcohort->visible, 0);
+        $this->assertEmpty($dbcohort->theme);
 
         // Since field 'visible' was added in 2.8, make sure that update works correctly with and without this parameter.
         core_cohort_external::update_cohorts(array($cohort1 + array('visible' => 1)));
@@ -226,6 +277,18 @@ class core_cohort_externallib_testcase extends externallib_advanced_testcase {
         $dbcohort = $DB->get_record('cohort', array('id' => $cohort1['id']));
         $this->assertEquals(1, $dbcohort->visible);
 
+        // Call when $CFG->allowcohortthemes is enabled.
+        set_config('allowcohortthemes', 1);
+        core_cohort_external::update_cohorts(array($cohort1 + array('theme' => 'clean')));
+        $dbcohort = $DB->get_record('cohort', array('id' => $cohort1['id']));
+        $this->assertEquals('clean', $dbcohort->theme);
+
+        // Call when $CFG->allowcohortthemes is disabled.
+        set_config('allowcohortthemes', 0);
+        core_cohort_external::update_cohorts(array($cohort1 + array('theme' => 'more')));
+        $dbcohort = $DB->get_record('cohort', array('id' => $cohort1['id']));
+        $this->assertEquals('clean', $dbcohort->theme);
+
         // Call without required capability.
         $this->unassignUserCapability('moodle/cohort:manage', $context->id, $roleid);
         core_cohort_external::update_cohorts(array($cohort1));
diff --git a/cohort/tests/fixtures/uploadcohorts4.csv b/cohort/tests/fixtures/uploadcohorts4.csv
new file mode 100644 (file)
index 0000000..1aba4cc
--- /dev/null
@@ -0,0 +1,7 @@
+name,idnumber,description,category,visible,theme
+cohort name 1,cohortid1,first description,,,boost
+cohort name 2,cohortid2,,,,
+cohort name 3,cohortid3,,Miscellaneous,no,boost
+cohort name 4,cohortid4,,CAT1,yes,clean
+cohort name 5,cohortid5,,CAT2,0,
+cohort name 6,cohortid6,,CAT3,1,clean
index a7ee3a8..dddb26e 100644 (file)
@@ -359,7 +359,7 @@ class cohort_upload_form extends moodleform {
         $columns = $cir->get_columns();
 
         // Check that columns include 'name' and warn about extra columns.
-        $allowedcolumns = array('contextid', 'name', 'idnumber', 'description', 'descriptionformat', 'visible');
+        $allowedcolumns = array('contextid', 'name', 'idnumber', 'description', 'descriptionformat', 'visible', 'theme');
         $additionalcolumns = array('context', 'category', 'category_id', 'category_idnumber', 'category_path');
         $displaycolumns = array();
         $extracolumns = array();
@@ -424,6 +424,13 @@ class cohort_upload_form extends moodleform {
                 $cohorts[$rownum]['errors'][] = new lang_string('namefieldempty', 'cohort');
             }
 
+            if (!empty($hash['theme']) && !empty($CFG->allowcohortthemes)) {
+                $availablethemes = cohort_get_list_of_themes();
+                if (empty($availablethemes[$hash['theme']])) {
+                    $cohorts[$rownum]['errors'][] = new lang_string('invalidtheme', 'cohort');
+                }
+            }
+
             $cohorts[$rownum]['data'] = array_intersect_key($hash, $cohorts[0]['data']);
             $haserrors = $haserrors || !empty($cohorts[$rownum]['errors']);
             $haswarnings = $haswarnings || !empty($cohorts[$rownum]['warnings']);
@@ -466,6 +473,9 @@ class cohort_upload_form extends moodleform {
                         $hash[$key] = clean_param($value, PARAM_BOOL) ? 1 : 0;
                     }
                     break;
+                case 'theme':
+                    $hash[$key] = core_text::substr(clean_param($value, PARAM_TEXT), 0, 50);
+                    break;
             }
         }
     }
index 5308052..e3156b0 100644 (file)
@@ -38,7 +38,7 @@ $CFG = new stdClass();
 // will be stored.  This database must already have been created         //
 // and a username/password created to access it.                         //
 
-$CFG->dbtype    = 'pgsql';      // 'pgsql', 'mariadb', 'mysqli', 'mssql', 'sqlsrv' or 'oci'
+$CFG->dbtype    = 'pgsql';      // 'pgsql', 'mariadb', 'mysqli', 'sqlsrv' or 'oci'
 $CFG->dblibrary = 'native';     // 'native' only at the moment
 $CFG->dbhost    = 'localhost';  // eg 'localhost' or 'db.isp.com' or IP
 $CFG->dbname    = 'moodle';     // database name, eg moodle
@@ -418,8 +418,10 @@ $CFG->admin = 'admin';
 // example) in sites where the user theme should override all other theme
 // settings for accessibility reasons. You can also disable types of themes
 // (other than site)  by removing them from the array. The default setting is:
-//      $CFG->themeorder = array('course', 'category', 'session', 'user', 'site');
-// NOTE: course, category, session, user themes still require the
+//
+//     $CFG->themeorder = array('course', 'category', 'session', 'user', 'cohort', 'site');
+//
+// NOTE: course, category, session, user, cohort themes still require the
 // respective settings to be enabled
 //
 // It is possible to add extra themes directory stored outside of $CFG->dirroot.
diff --git a/course/classes/search/section.php b/course/classes/search/section.php
new file mode 100644 (file)
index 0000000..11f6866
--- /dev/null
@@ -0,0 +1,195 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search area for course sections (title and summary).
+ *
+ * @package core_course
+ * @copyright 2018 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_course\search;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Search area for course sections (title and summary).
+ *
+ * Note this does not include the activities within the section, as these have their own search
+ * areas.
+ *
+ * @package core_course
+ * @copyright 2018 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class section extends \core_search\base {
+    /**
+     * Sections are indexed at course context.
+     *
+     * @var array
+     */
+    protected static $levels = [CONTEXT_COURSE];
+
+    /**
+     * Returns recordset containing required data for indexing course sections.
+     *
+     * @param int $modifiedfrom timestamp
+     * @param \context|null $context Restriction context
+     * @return \moodle_recordset|null Recordset or null if no change possible
+     */
+    public function get_document_recordset($modifiedfrom = 0, \context $context = null) {
+        global $DB;
+
+        list ($contextjoin, $contextparams) = $this->get_course_level_context_restriction_sql($context, 'c');
+        if ($contextjoin === null) {
+            return null;
+        }
+
+        $comparetext = $DB->sql_compare_text('cs.summary', 1);
+
+        return $DB->get_recordset_sql("
+                SELECT cs.id,
+                       cs.course,
+                       cs.section,
+                       cs.name,
+                       cs.summary,
+                       cs.summaryformat,
+                       cs.timemodified
+                  FROM {course_sections} cs
+                  JOIN {course} c ON c.id = cs.course
+          $contextjoin
+                 WHERE cs.timemodified >= ?
+                   AND (cs.name != ? OR $comparetext != ?)
+              ORDER BY cs.timemodified ASC", array_merge($contextparams, [$modifiedfrom, '', '']));
+    }
+
+    /**
+     * Returns the document associated with this section.
+     *
+     * @param \stdClass $record
+     * @param array $options
+     * @return \core_search\document
+     */
+    public function get_document($record, $options = array()) {
+        // Get the context, modinfo, and section.
+        try {
+            $context = \context_course::instance($record->course);
+        } catch (\moodle_exception $ex) {
+            // Notify it as we run here as admin, we should see everything.
+            debugging('Error retrieving ' . $this->areaid . ' ' . $record->id .
+                    ' document, not all required data is available: ' . $ex->getMessage(),
+                    DEBUG_DEVELOPER);
+            return false;
+        }
+
+        // Title - use default if none given.
+        $title = get_section_name($record->course, $record->section);
+
+        // Prepare associative array with data from DB.
+        $doc = \core_search\document_factory::instance($record->id, $this->componentname, $this->areaname);
+        $doc->set('title', content_to_text($title, false));
+        $doc->set('content', content_to_text($record->summary, $record->summaryformat));
+        $doc->set('contextid', $context->id);
+        $doc->set('courseid', $record->course);
+        $doc->set('owneruserid', \core_search\manager::NO_OWNER_ID);
+        $doc->set('modified', $record->timemodified);
+
+        return $doc;
+    }
+
+    /**
+     * Whether the user can access the section or not.
+     *
+     * @param int $id The course section id.
+     * @return int One of the \core_search\manager:ACCESS_xx constants
+     */
+    public function check_access($id) {
+        global $DB;
+
+        // Check we can get the section and the course modinfo.
+        $sectionrec = $DB->get_record('course_sections', ['id' => $id], '*', IGNORE_MISSING);
+        if (!$sectionrec) {
+            return \core_search\manager::ACCESS_DELETED;
+        }
+        try {
+            $modinfo = get_fast_modinfo($sectionrec->course);
+        } catch (\moodle_exception $e) {
+            return \core_search\manager::ACCESS_DELETED;
+        }
+        $section = $modinfo->get_section_info($sectionrec->section, IGNORE_MISSING);
+        if (!$section) {
+            return \core_search\manager::ACCESS_DELETED;
+        }
+
+        // Check access to course and that the section is visible to current user.
+        if (can_access_course($modinfo->get_course()) && $section->uservisible) {
+            return \core_search\manager::ACCESS_GRANTED;
+        }
+
+        return \core_search\manager::ACCESS_DENIED;
+    }
+
+    /**
+     * Gets a link to the section.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_doc_url(\core_search\document $doc) {
+        global $DB;
+        $section = $DB->get_field('course_sections', 'section', ['id' => $doc->get('itemid')], MUST_EXIST);
+        $format = course_get_format($doc->get('courseid'));
+        return $format->get_view_url($section);
+    }
+
+    /**
+     * Gets a link to the section.
+     *
+     * @param \core_search\document $doc
+     * @return \moodle_url
+     */
+    public function get_context_url(\core_search\document $doc) {
+        return $this->get_doc_url($doc);
+    }
+
+    /**
+     * Returns true to include summary files in the index.
+     *
+     * @return bool True
+     */
+    public function uses_file_indexing() {
+        return true;
+    }
+
+    /**
+     * Return the file area that is used for summary files.
+     *
+     * @return array File area name
+     */
+    public function get_search_fileareas() {
+        return ['section'];
+    }
+
+    /**
+     * Returns the moodle component name, as used in the files table.
+     *
+     * @return string Component name
+     */
+    public function get_component_name() {
+        return 'course';
+    }
+}
index f28f75e..0cb64fe 100644 (file)
@@ -21,8 +21,8 @@ Feature: Course activity controls works as expected
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@example.com |
     And the following "courses" exist:
-      | fullname | shortname | format | coursedisplay | numsections |
-      | Course 1 | C1 | <courseformat> | <coursedisplay> | 5 |
+      | fullname | shortname | format | coursedisplay | numsections | startdate |
+      | Course 1 | C1 | <courseformat> | <coursedisplay> | 5 | 0 |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
@@ -93,8 +93,8 @@ Feature: Course activity controls works as expected
       | username | firstname | lastname | email |
       | teacher1 | Teacher | 1 | teacher1@example.com |
     And the following "courses" exist:
-      | fullname | shortname | format | coursedisplay | numsections |
-      | Course 1 | C1 | <courseformat> | <coursedisplay> | 5 |
+      | fullname | shortname | format | coursedisplay | numsections | startdate |
+      | Course 1 | C1 | <courseformat> | <coursedisplay> | 5 | 0 |
     And the following "course enrolments" exist:
       | user | course | role |
       | teacher1 | C1 | editingteacher |
index 3e88929..92fd475 100644 (file)
@@ -7,8 +7,8 @@ Feature: Course paged mode
   @javascript @_cross_browser
   Scenario Outline: Weekly and topics course formats with Javascript enabled
     Given the following "courses" exist:
-      | fullname | shortname | category | format | coursedisplay | numsections |
-      | Course 1 | C1 | 0 | <courseformat> | 1 | 3 |
+      | fullname | shortname | category | format | coursedisplay | numsections | startdate |
+      | Course 1 | C1 | 0 | <courseformat> | 1 | 3 | 0 |
     And I log in as "admin"
     And I am on "Course 1" course homepage
     Then I click on <section2> "link" in the <section2> "section"
@@ -40,8 +40,8 @@ Feature: Course paged mode
 
   Scenario Outline: Weekly and topics course formats with Javascript disabled
     Given the following "courses" exist:
-      | fullname | shortname | category | format | coursedisplay | numsections |
-      | Course 1 | C1 | 0 | <courseformat> | 1 | 3 |
+      | fullname | shortname | category | format | coursedisplay | numsections | startdate |
+      | Course 1 | C1 | 0 | <courseformat> | 1 | 3 | 0 |
     And I log in as "admin"
     And I am on "Course 1" course homepage
     Then I click on <section2> "link" in the <section2> "section"
index b609280..9751f1a 100644 (file)
@@ -43,11 +43,17 @@ class course_search_testcase extends advanced_testcase {
      */
     protected $mycoursesareaid = null;
 
+    /**
+     * @var string Area id for sections
+     */
+    protected $sectionareaid = null;
+
     public function setUp() {
         $this->resetAfterTest(true);
         set_config('enableglobalsearch', true);
 
         $this->mycoursesareaid = \core_search\manager::generate_areaid('core_course', 'mycourse');
+        $this->sectionareaid = \core_search\manager::generate_areaid('core_course', 'section');
 
         // Set \core_search::instance to the mock_search_engine as we don't require the search engine to be working to test this.
         $search = testable_core_search::instance();
@@ -240,4 +246,204 @@ class course_search_testcase extends advanced_testcase {
         $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course2->id));
         $this->assertEquals(\core_search\manager::ACCESS_DENIED, $searcharea->check_access($course3->id));
     }
+
+    /**
+     * Indexing section contents.
+     */
+    public function test_section_indexing() {
+        global $DB, $USER;
+
+        // Returns the instance as long as the area is supported.
+        $searcharea = \core_search\manager::get_search_area($this->sectionareaid);
+        $this->assertInstanceOf('\core_course\search\section', $searcharea);
+
+        // Create some courses in categories, and a forum.
+        $generator = $this->getDataGenerator();
+        $cat1 = $generator->create_category();
+        $cat2 = $generator->create_category(['parent' => $cat1->id]);
+        $course1 = $generator->create_course(['category' => $cat1->id]);
+        $course2 = $generator->create_course(['category' => $cat2->id]);
+        $forum = $generator->create_module('forum', ['course' => $course1->id]);
+
+        // Edit 2 sections on course 1 and one on course 2.
+        $existing = $DB->get_record('course_sections', ['course' => $course1->id, 'section' => 2]);
+        $course1section2id = $existing->id;
+        $new = clone($existing);
+        $new->name = 'Frogs';
+        course_update_section($course1->id, $existing, $new);
+
+        $existing = $DB->get_record('course_sections', ['course' => $course1->id, 'section' => 3]);
+        $course1section3id = $existing->id;
+        $new = clone($existing);
+        $new->summary = 'Frogs';
+        $new->summaryformat = FORMAT_HTML;
+        course_update_section($course1->id, $existing, $new);
+
+        $existing = $DB->get_record('course_sections', ['course' => $course2->id, 'section' => 1]);
+        $course2section1id = $existing->id;
+        $new = clone($existing);
+        $new->summary = 'Frogs';
+        $new->summaryformat = FORMAT_HTML;
+        course_update_section($course2->id, $existing, $new);
+
+        // Bodge timemodified into a particular order.
+        $DB->set_field('course_sections', 'timemodified', 1, ['id' => $course1section3id]);
+        $DB->set_field('course_sections', 'timemodified', 2, ['id' => $course1section2id]);
+        $DB->set_field('course_sections', 'timemodified', 3, ['id' => $course2section1id]);
+
+        // All records.
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(0));
+        $this->assertEquals([$course1section3id, $course1section2id, $course2section1id], $results);
+
+        // Records after time 2.
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(2));
+        $this->assertEquals([$course1section2id, $course2section1id], $results);
+
+        // Records after time 10 (there aren't any).
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(10));
+        $this->assertEquals([], $results);
+
+        // Find the first block to use for a block context.
+        $blockid = array_values($DB->get_records('block_instances', null, 'id', 'id', 0, 1))[0]->id;
+        $blockcontext = context_block::instance($blockid);
+
+        // Check with block context - should be null.
+        $this->assertNull($searcharea->get_document_recordset(0, $blockcontext));
+
+        // Check with user context - should be null.
+        $this->setAdminUser();
+        $usercontext = context_user::instance($USER->id);
+        $this->assertNull($searcharea->get_document_recordset(0, $usercontext));
+
+        // Check with module context - should be null.
+        $modcontext = context_module::instance($forum->cmid);
+        $this->assertNull($searcharea->get_document_recordset(0, $modcontext));
+
+        // Check with course context - should return specific course entries.
+        $coursecontext = context_course::instance($course1->id);
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $coursecontext));
+        $this->assertEquals([$course1section3id, $course1section2id], $results);
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(2, $coursecontext));
+        $this->assertEquals([$course1section2id], $results);
+
+        // Check with category context - should return course in categories and subcategories.
+        $catcontext = context_coursecat::instance($cat1->id);
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $catcontext));
+        $this->assertEquals([$course1section3id, $course1section2id, $course2section1id], $results);
+        $catcontext = context_coursecat::instance($cat2->id);
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $catcontext));
+        $this->assertEquals([$course2section1id], $results);
+
+        // Check with system context - should return everything (same as null, tested first).
+        $systemcontext = context_system::instance();
+        $results = self::recordset_to_ids($searcharea->get_document_recordset(0, $systemcontext));
+        $this->assertEquals([$course1section3id, $course1section2id, $course2section1id], $results);
+    }
+
+    /**
+     * Document contents for sections.
+     */
+    public function test_section_document() {
+        global $DB;
+
+        $searcharea = \core_search\manager::get_search_area($this->sectionareaid);
+
+        // Create a course.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+
+        // Test with default title.
+        $sectionrec = (object)['id' => 123, 'course' => $course->id,
+                'section' => 3, 'timemodified' => 456,
+                'summary' => 'Kermit', 'summaryformat' => FORMAT_HTML];
+        $doc = $searcharea->get_document($sectionrec);
+        $this->assertInstanceOf('\core_search\document', $doc);
+        $this->assertEquals(123, $doc->get('itemid'));
+        $this->assertEquals($this->sectionareaid . '-123', $doc->get('id'));
+        $this->assertEquals($course->id, $doc->get('courseid'));
+        $this->assertFalse($doc->is_set('userid'));
+        $this->assertEquals(\core_search\manager::NO_OWNER_ID, $doc->get('owneruserid'));
+        $this->assertEquals('Topic 3', $doc->get('title'));
+        $this->assertEquals('Kermit', $doc->get('content'));
+
+        // Test with user-set title.
+        $DB->set_field('course_sections', 'name', 'Frogs',
+                ['course' => $course->id, 'section' => 3]);
+        rebuild_course_cache($course->id, true);
+        $doc = $searcharea->get_document($sectionrec);
+        $this->assertEquals('Frogs', $doc->get('title'));
+    }
+
+    /**
+     * Document access for sections.
+     */
+    public function test_section_access() {
+        global $DB;
+
+        $searcharea = \core_search\manager::get_search_area($this->sectionareaid);
+
+        // Create a course.
+        $generator = $this->getDataGenerator();
+        $course = $generator->create_course();
+
+        // Create 2 users - student and manager. Initially, student is not even enrolled.
+        $student = $generator->create_user();
+        $manager = $generator->create_user();
+        $generator->enrol_user($manager->id, $course->id, 'manager');
+
+        // Two sections have content - one is hidden.
+        $DB->set_field('course_sections', 'name', 'Frogs',
+                ['course' => $course->id, 'section' => 1]);
+        $DB->set_field('course_sections', 'name', 'Toads',
+                ['course' => $course->id, 'section' => 2]);
+        $DB->set_field('course_sections', 'visible', '0',
+                ['course' => $course->id, 'section' => 2]);
+
+        // Make the modified time be in order of sections.
+        $DB->execute('UPDATE {course_sections} SET timemodified = section');
+
+        // Get the two document objects.
+        $rs = $searcharea->get_document_recordset();
+        $documents = [];
+        $index = 0;
+        foreach ($rs as $rec) {
+            $documents[$index++] = $searcharea->get_document($rec);
+        }
+        $this->assertCount(2, $documents);
+
+        // Log in as admin and check access.
+        $this->setAdminUser();
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
+                $searcharea->check_access($documents[0]->get('itemid')));
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
+                $searcharea->check_access($documents[1]->get('itemid')));
+
+        // Log in as manager and check access.
+        $this->setUser($manager);
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
+                $searcharea->check_access($documents[0]->get('itemid')));
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
+                $searcharea->check_access($documents[1]->get('itemid')));
+
+        // Log in as student and check access - none yet.
+        $this->setUser($student);
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED,
+                $searcharea->check_access($documents[0]->get('itemid')));
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED,
+                $searcharea->check_access($documents[1]->get('itemid')));
+
+        // Enrol student - now they should get access but not to the hidden one.
+        $generator->enrol_user($student->id, $course->id, 'student');
+        $this->assertEquals(\core_search\manager::ACCESS_GRANTED,
+                $searcharea->check_access($documents[0]->get('itemid')));
+        $this->assertEquals(\core_search\manager::ACCESS_DENIED,
+                $searcharea->check_access($documents[1]->get('itemid')));
+
+        // Delete the course and check it returns deleted.
+        delete_course($course, false);
+        $this->assertEquals(\core_search\manager::ACCESS_DELETED,
+                $searcharea->check_access($documents[0]->get('itemid')));
+        $this->assertEquals(\core_search\manager::ACCESS_DELETED,
+                $searcharea->check_access($documents[1]->get('itemid')));
+    }
 }
index d7eacac..0165ba1 100644 (file)
@@ -96,11 +96,7 @@ class enrol_database_testcase extends advanced_testcase {
                 break;
 
             case 'mssql':
-                if (get_class($DB) == 'mssql_native_moodle_database') {
-                    set_config('dbtype', 'mssql_n', 'enrol_database');
-                } else {
-                    set_config('dbtype', 'mssqlnative', 'enrol_database');
-                }
+                set_config('dbtype', 'mssqlnative', 'enrol_database');
                 set_config('dbsybasequoting', '1', 'enrol_database');
                 break;
 
diff --git a/enrol/self/bulkchangeforms.php b/enrol/self/bulkchangeforms.php
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/enrol/self/classes/deleteselectedusers_form.php b/enrol/self/classes/deleteselectedusers_form.php
new file mode 100644 (file)
index 0000000..4ebc7ed
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The form to confirm the intention to bulk delete users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->dirroot/enrol/bulkchange_forms.php");
+
+/**
+ * The form to confirm the intention to bulk delete users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_self_deleteselectedusers_form extends enrol_bulk_enrolment_confirm_form {
+}
\ No newline at end of file
diff --git a/enrol/self/classes/deleteselectedusers_operation.php b/enrol/self/classes/deleteselectedusers_operation.php
new file mode 100644 (file)
index 0000000..81f0248
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A bulk operation for the self enrolment plugin to delete selected users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A bulk operation for the self enrolment plugin to delete selected users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_self_deleteselectedusers_operation extends enrol_bulk_enrolment_operation {
+
+    /**
+     * Returns the title to display for this bulk operation.
+     *
+     * @return string
+     */
+    public function get_identifier() {
+        return 'deleteselectedusers';
+    }
+
+    /**
+     * Returns the identifier for this bulk operation. This is the key used when the plugin
+     * returns an array containing all of the bulk operations it supports.
+     *
+     * @return string
+     */
+    public function get_title() {
+        return get_string('deleteselectedusers', 'enrol_manual');
+    }
+
+    /**
+     * Returns a enrol_bulk_enrolment_operation extension form to be used
+     * in collecting required information for this operation to be processed.
+     *
+     * @param string|moodle_url|null $defaultaction
+     * @param mixed $defaultcustomdata
+     * @return enrol_manual_editselectedusers_form
+     */
+    public function get_form($defaultaction = null, $defaultcustomdata = null) {
+        if (!array($defaultcustomdata)) {
+            $defaultcustomdata = array();
+        }
+        $defaultcustomdata['title'] = $this->get_title();
+        $defaultcustomdata['message'] = get_string('confirmbulkdeleteenrolment', 'enrol_self');
+        $defaultcustomdata['button'] = get_string('unenrolusers', 'enrol_self');
+
+        return new enrol_self_deleteselectedusers_form($defaultaction, $defaultcustomdata);
+    }
+
+    /**
+     * Processes the bulk operation request for the given userids with the provided properties.
+     *
+     * @param course_enrolment_manager $manager
+     * @param array $users
+     * @param stdClass $properties The data returned by the form.
+     */
+    public function process(course_enrolment_manager $manager, array $users, stdClass $properties) {
+        if (!has_capability("enrol/self:unenrol", $manager->get_context())) {
+            return false;
+        }
+
+        foreach ($users as $user) {
+            foreach ($user->enrolments as $enrolment) {
+                $plugin = $enrolment->enrolmentplugin;
+                $instance = $enrolment->enrolmentinstance;
+                if ($plugin->allow_unenrol_user($instance, $enrolment)) {
+                    $plugin->unenrol_user($instance, $user->id);
+                }
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/enrol/self/classes/editselectedusers_form.php b/enrol/self/classes/editselectedusers_form.php
new file mode 100644 (file)
index 0000000..7bc82f4
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The form to collect required information when bulk editing users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("$CFG->dirroot/enrol/bulkchange_forms.php");
+
+/**
+ * The form to collect required information when bulk editing users enrolments.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_self_editselectedusers_form extends enrol_bulk_enrolment_change_form {
+}
diff --git a/enrol/self/classes/editselectedusers_operation.php b/enrol/self/classes/editselectedusers_operation.php
new file mode 100644 (file)
index 0000000..800a0b8
--- /dev/null
@@ -0,0 +1,161 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * A bulk operation for the manual enrolment plugin to edit selected users.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * A bulk operation for the manual enrolment plugin to edit selected users.
+ *
+ * @package enrol_self
+ * @copyright 2018 Farhan Karmali
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_self_editselectedusers_operation extends enrol_bulk_enrolment_operation {
+
+    /**
+     * Returns the title to display for this bulk operation.
+     *
+     * @return string
+     */
+    public function get_title() {
+        return get_string('editselectedusers', 'enrol_self');
+    }
+
+    /**
+     * Returns the identifier for this bulk operation. This is the key used when the plugin
+     * returns an array containing all of the bulk operations it supports.
+     */
+    public function get_identifier() {
+        return 'editselectedusers';
+    }
+
+    /**
+     * Processes the bulk operation request for the given userids with the provided properties.
+     *
+     * @param course_enrolment_manager $manager
+     * @param array $users
+     * @param stdClass $properties The data returned by the form.
+     */
+    public function process(course_enrolment_manager $manager, array $users, stdClass $properties) {
+        global $DB, $USER;
+
+        if (!has_capability("enrol/self:manage", $manager->get_context())) {
+            return false;
+        }
+
+        // Get all of the user enrolment id's.
+        $ueids = array();
+        $instances = array();
+        foreach ($users as $user) {
+            foreach ($user->enrolments as $enrolment) {
+                $ueids[] = $enrolment->id;
+                if (!array_key_exists($enrolment->id, $instances)) {
+                    $instances[$enrolment->id] = $enrolment;
+                }
+            }
+        }
+
+        // Check that each instance is manageable by the current user.
+        foreach ($instances as $instance) {
+            if (!$this->plugin->allow_manage($instance)) {
+                return false;
+            }
+        }
+
+        // Collect the known properties.
+        $status = $properties->status;
+        $timestart = $properties->timestart;
+        $timeend = $properties->timeend;
+
+        list($ueidsql, $params) = $DB->get_in_or_equal($ueids, SQL_PARAMS_NAMED);
+
+        $updatesql = array();
+        if ($status == ENROL_USER_ACTIVE || $status == ENROL_USER_SUSPENDED) {
+            $updatesql[] = 'status = :status';
+            $params['status'] = (int)$status;
+        }
+        if (!empty($timestart)) {
+            $updatesql[] = 'timestart = :timestart';
+            $params['timestart'] = (int)$timestart;
+        }
+        if (!empty($timeend)) {
+            $updatesql[] = 'timeend = :timeend';
+            $params['timeend'] = (int)$timeend;
+        }
+        if (empty($updatesql)) {
+            return true;
+        }
+
+        // Update the modifierid.
+        $updatesql[] = 'modifierid = :modifierid';
+        $params['modifierid'] = (int)$USER->id;
+
+        // Update the time modified.
+        $updatesql[] = 'timemodified = :timemodified';
+        $params['timemodified'] = time();
+
+        // Build the SQL statement.
+        $updatesql = join(', ', $updatesql);
+        $sql = "UPDATE {user_enrolments}
+                   SET $updatesql
+                 WHERE id $ueidsql";
+
+        if ($DB->execute($sql, $params)) {
+            foreach ($users as $user) {
+                foreach ($user->enrolments as $enrolment) {
+                    $enrolment->courseid  = $enrolment->enrolmentinstance->courseid;
+                    $enrolment->enrol     = 'self';
+                    // Trigger event.
+                    $event = \core\event\user_enrolment_updated::create(
+                        array(
+                            'objectid' => $enrolment->id,
+                            'courseid' => $enrolment->courseid,
+                            'context' => context_course::instance($enrolment->courseid),
+                            'relateduserid' => $user->id,
+                            'other' => array('enrol' => 'self')
+                        )
+                    );
+                    $event->trigger();
+                }
+            }
+            // Delete cached course contacts for this course because they may be affected.
+            cache::make('core', 'coursecontacts')->delete($manager->get_context()->instanceid);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns a enrol_bulk_enrolment_operation extension form to be used
+     * in collecting required information for this operation to be processed.
+     *
+     * @param string|moodle_url|null $defaultaction
+     * @param mixed $defaultcustomdata
+     * @return enrol_self_editselectedusers_form
+     */
+    public function get_form($defaultaction = null, $defaultcustomdata = null) {
+        return new enrol_self_editselectedusers_form($defaultaction, $defaultcustomdata);
+    }
+}
index 5f4e5e1..9cfb17a 100644 (file)
@@ -28,6 +28,7 @@ $string['canntenrollate'] = 'You cannot enrol any more, since enrolment ended on
 $string['cohortnonmemberinfo'] = 'Only members of cohort \'{$a}\' can self-enrol.';
 $string['cohortonly'] = 'Only cohort members';
 $string['cohortonly_help'] = 'Self enrolment may be restricted to members of a specified cohort only. Note that changing this setting has no effect on existing enrolments.';
+$string['confirmbulkdeleteenrolment'] = 'Are you sure you want to delete these users enrolments?';
 $string['customwelcomemessage'] = 'Custom welcome message';
 $string['customwelcomemessage_help'] = 'A custom welcome message may be added as plain text or Moodle-auto format, including HTML tags and multi-lang tags.
 
@@ -39,6 +40,8 @@ The following placeholders may be included in the message:
 * User fullname {$a->fullname}';
 $string['defaultrole'] = 'Default role assignment';
 $string['defaultrole_desc'] = 'Select role which should be assigned to users during self enrolment';
+$string['deleteselectedusers'] = 'Delete selected user enrolments';
+$string['editselectedusers'] = 'Edit selected user enrolments';
 $string['enrolenddate'] = 'End date';
 $string['enrolenddate_help'] = 'If enabled, users can enrol themselves until this date only.';
 $string['enrolenddaterror'] = 'Enrolment end date cannot be earlier than start date';
@@ -107,6 +110,7 @@ $string['status_help'] = 'If enabled together with \'Allow new enrolments\' disa
 $string['unenrol'] = 'Unenrol user';
 $string['unenrolselfconfirm'] = 'Do you really want to unenrol yourself from course "{$a}"?';
 $string['unenroluser'] = 'Do you really want to unenrol "{$a->user}" from course "{$a->course}"?';
+$string['unenrolusers'] = 'Unenrol users';
 $string['usepasswordpolicy'] = 'Use password policy';
 $string['usepasswordpolicy_desc'] = 'Use standard password policy for enrolment keys.';
 $string['welcometocourse'] = 'Welcome to {$a}';
index 98e9583..e521c2e 100644 (file)
@@ -694,6 +694,25 @@ class enrol_self_plugin extends enrol_plugin {
         return $options;
     }
 
+    /**
+     * The self enrollment plugin has several bulk operations that can be performed.
+     * @param course_enrolment_manager $manager
+     * @return array
+     */
+    public function get_bulk_operations(course_enrolment_manager $manager) {
+        global $CFG;
+        require_once($CFG->dirroot.'/enrol/self/locallib.php');
+        $context = $manager->get_context();
+        $bulkoperations = array();
+        if (has_capability("enrol/self:manage", $context)) {
+            $bulkoperations['editselectedusers'] = new enrol_self_editselectedusers_operation($manager, $this);
+        }
+        if (has_capability("enrol/self:unenrol", $context)) {
+            $bulkoperations['deleteselectedusers'] = new enrol_self_deleteselectedusers_operation($manager, $this);
+        }
+        return $bulkoperations;
+    }
+
     /**
      * Add elements to the edit instance form.
      *
index dc17af1..850555e 100644 (file)
@@ -25,6 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 require_once("$CFG->libdir/formslib.php");
+require_once($CFG->dirroot . '/enrol/locallib.php');
 
 /**
  * Check if the given password match a group enrolment key in the specified course.
index f5f39d8..1cf7e30 100644 (file)
@@ -574,9 +574,11 @@ class core_files_renderer extends plugin_renderer_base {
         $rv = '
 <div tabindex="0" class="file-picker fp-generallayout" role="dialog" aria-live="assertive">
     <div class="fp-repo-area">
-        <ul class="fp-list">
-            <li class="fp-repo">
-                <a href="#"><img class="fp-repo-icon" alt=" " width="16" height="16" />&nbsp;<span class="fp-repo-name"></span></a>
+        <ul class="fp-list" role="tablist">
+            <li class="fp-repo" role="tab" aria-selected="false" tabindex="-1">
+                <a href="#" tabindex="-1"><img class="fp-repo-icon" alt=" " width="16" height="16" />&nbsp;
+                    <span class="fp-repo-name"></span>
+                </a>
             </li>
         </ul>
     </div>
index 755fa8a..6dabe7a 100644 (file)
@@ -270,7 +270,7 @@ abstract class grade_export {
         if ($grade_item->itemtype == 'mod') {
             $column->name = get_string('modulename', $grade_item->itemmodule).get_string('labelsep', 'langconfig').$grade_item->get_name();
         } else {
-            $column->name = $grade_item->get_name();
+            $column->name = $grade_item->get_name(true);
         }
 
         // We can't have feedback and display type at the same time.
index 8150e19..79c9e21 100644 (file)
@@ -60,8 +60,8 @@ require_login($course);
 $context = context_course::instance($course->id);
 require_capability('moodle/course:managegroups', $context);
 
-$PAGE->requires->js('/group/clientlib.js');
-$PAGE->requires->js('/group/module.js');
+$PAGE->requires->js('/group/clientlib.js', true);
+$PAGE->requires->js('/group/module.js', true);
 
 // Check for multiple/no group errors
 if (!$singlegroup) {
index 0a5501b..e7f8de7 100644 (file)
@@ -166,7 +166,7 @@ $CFG->tempdir              = $CFG->dataroot.'/temp';
 $CFG->cachedir             = $CFG->dataroot.'/cache';
 $CFG->localcachedir        = $CFG->dataroot.'/localcache';
 $CFG->admin                = $config->admin;
-$CFG->docroot              = 'http://docs.moodle.org';
+$CFG->docroot              = 'https://docs.moodle.org';
 $CFG->langotherroot        = $CFG->dataroot.'/lang';
 $CFG->langlocalroot        = $CFG->dataroot.'/lang';
 $CFG->directorypermissions = isset($distro->directorypermissions) ? $distro->directorypermissions : 00777; // let distros set dir permissions
@@ -483,7 +483,6 @@ if ($config->stage == INSTALL_DATABASETYPE) {
                        'pgsql'  => moodle_database::get_driver_instance('pgsql',  'native'),
                        'oci'    => moodle_database::get_driver_instance('oci',    'native'),
                        'sqlsrv' => moodle_database::get_driver_instance('sqlsrv', 'native'), // MS SQL*Server PHP driver
-                       'mssql'  => moodle_database::get_driver_instance('mssql',  'native'), // FreeTDS driver
                       );
 
     echo '<div class="userinput">';
index 914ebf9..31e6b21 100644 (file)
@@ -32,3 +32,4 @@ defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'Tungumál';
 $string['next'] = 'Næsta';
+$string['previous'] = 'Undanfarna';
index c428260..6c9b91d 100644 (file)
@@ -34,15 +34,18 @@ $string['admindirname'] = 'Adresár pre správu (admin)';
 $string['availablelangs'] = 'Dostupné jazykové balíčky';
 $string['chooselanguagehead'] = 'Vyberte jazyk';
 $string['chooselanguagesub'] = 'Zvoľte si jazyk pre inštaláciu. Tento jazyk bude tiež použitý ako východzí jazyk portálu, ale môže byť neskôr zmenený.';
-$string['clialreadyinstalled'] = 'Súbor config.php už existuje. Použite admin/cli/upgrade.php ak chcete aktualizovať váš portál.';
+$string['clialreadyconfigured'] = 'Konfiguračný súbor config.php už existuje. Spusťte admin/cli/install_database.php, ak chcete inštalovať Moodle pre tento portál.';
+$string['clialreadyinstalled'] = 'Súbor config.php už existuje. Použite admin/cli/install_database.php ak chcete aktualizovať váš portál.';
 $string['cliinstallheader'] = 'Moodle {$a} inštalačný program z príkazového riadku';
 $string['databasehost'] = 'Databázový server';
 $string['databasename'] = 'Názov databázy';
 $string['databasetypehead'] = 'Vyberte ovládač pre databázu';
 $string['dataroot'] = 'Adresár pre údaje';
+$string['datarootpermission'] = 'Prístupové práva k údajovému adresáru';
 $string['dbprefix'] = 'Predpona tabuliek';
 $string['dirroot'] = 'Adresár Moodle';
 $string['environmenthead'] = 'Kontrola programového prostredia...';
+$string['environmentsub2'] = 'Každé vydanie Moodle vyžaduje určitú minimálnu verziu PHP a niekoľko povinných rozšírení PHP. Plná kontrola prostredia sa vykonáva pred každou inštaláciou a aktualizáciou. Prosím, kontaktujte správcu servera, ak neviete, ako nainštalovať novú verziu, alebo povoliť rozšírenia PHP.';
 $string['errorsinenvironment'] = 'Kontrola programového prostredia zlyhala!';
 $string['installation'] = 'Inštalácia';
 $string['langdownloaderror'] = 'Bohužiaľ, jazyk "{$a}" sa nepodarilo nainštalovať. Inštalácia bude pokračovať v angličtine.';
@@ -62,18 +65,19 @@ $string['pathserrcreatedataroot'] = 'Inštalátor nemôže vytvoriť dátový ad
 $string['pathshead'] = 'Vytvoriť cesty';
 $string['pathsrodataroot'] = 'Kmeňový adresár nie je zapisovateľný';
 $string['pathsroparentdataroot'] = 'Nadriadený adresár ({$a->parent}) nie je zapisovateľný. Inštalátor nemôže vytvoriť dátový adresár ({$a->dataroot}). ';
-$string['pathssubdataroot'] = 'Potrebujete adresár, kam Moodle bude ukladať nahrané súbory. Adresár by mal povoliť čítanie a zapisovanie údajov používateľom web serveru, ale nesmie byť prístupný priamo cez web rozhranie. Pokiaľ ešte neexistuje, inštalácia sa pokúsi o jeho vytvorenie. ';
-$string['pathssubdirroot'] = 'Plná cesta adresára moodle inštalácie';
+$string['pathssubdataroot'] = '<p>Potrebujete adresár, kam Moodle bude ukladať nahrané súbory.</p>
+<p>Adresár by mal povoliť čítanie a zapisovanie údajov používateľom web serveru (webový server býva väčšinou spustený pod používateľom "www-data" alebo "apache"), ale nesmie byť prístupný priamo cez web rozhranie. Pokiaľ ešte neexistuje, inštalácia sa pokúsi o jeho vytvorenie. </p>';
+$string['pathssubdirroot'] = '<p>Absolútna cesta adresára Moodle inštalácie.</p>';
 $string['pathsunsecuredataroot'] = 'Umiestnenie dátového adresára nie je bezpečné';
 $string['pathswrongadmindir'] = 'Administrátorský adresár neexistuje';
 $string['phpextension'] = 'Rozšírenie PHP {$a}';
 $string['phpversion'] = 'Verzia PHP';
-$string['phpversionhelp'] = '<p>Moodle si vyžaduje verziu PHP aspoň  4.3.0 alebo 5.1.0 (5.0.x obsahuje veľa známych chýb).</p>
+$string['phpversionhelp'] = '<p>Moodle si vyžaduje verziu PHP aspoň  5.6.5 alebo 7.1 (7.0.x má určité obmedzenia jadra).</p>
 <p>Vy máte momentálne nainštalovanú túto verziu {$a}.</p>
-<p>Musíte aktualizovať PHP alebo sa presunúť na hostiteľský počítač s novšou verziou PHP!<br />(V prípade 5.0.X môžete tiež prejsť na verziu 4.4.x)</p>';
+<p>Musíte aktualizovať PHP alebo sa presunúť na hostiteľský počítač s novšou verziou PHP!</p>';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Podarilo so vám úspešne nainštalovať a spustiť balíček <strong>{$a->packname} {$a->packversion}</strong>. Gratulujeme!';
-$string['welcomep30'] = '<strong>{$a->installername}</strong> obsahuje aplikáciu k vytvoreniu prostredia, v ktorom bude prevádzkovaný váš <strong>Moodle</strong>. Menovite sa jedná o:';
+$string['welcomep30'] = 'Táto verzia <strong>{$a->installername}</strong> obsahuje aplikáciu k vytvoreniu prostredia, v ktorom bude prevádzkovaný váš <strong>Moodle</strong>. Menovite sa jedná o:';
 $string['welcomep40'] = 'Balíček tiež obsahuje <strong>Moodle vo verzii {$a->moodlerelease} ({$a->moodleversion})</strong>.';
 $string['welcomep50'] = 'Použitie všetkých aplikácií v tomto balíčku je viazané ich príslušnými licenciami. Kompletný balíček <strong>{$a->installername}</strong> je software s <a href="http://www.opensource.org/docs/definition_plain.html"> otvoreným kódom (open source)</a> a je šírený pod licenciou <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>.';
 $string['welcomep60'] = 'Nasledujúce stránky vás povedú v nekoľkých jednoduchých krokoch nastavením <strong>Moodle</strong> na vašom počítači. Môžete prijať východzie nastavenie, alebo si ich upraviť podľa svojich potrieb.';
index 6f164a7..b65be59 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+global $CFG;
+
+require_once("{$CFG->libdir}/filelib.php");
+require_once("{$CFG->dirroot}/iplookup/lib.php");
+
 
 /**
  * GeoIp data file parsing test.
  */
 class core_iplookup_geoip_testcase extends advanced_testcase {
-
     public function setUp() {
-        global $CFG;
-        require_once("$CFG->libdir/filelib.php");
-        require_once("$CFG->dirroot/iplookup/lib.php");
-
         if (!PHPUNIT_LONGTEST) {
-            // this may take a long time
+            // These tests are intensive and required downloads.
             $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
         }
 
         $this->resetAfterTest();
+    }
+
 
-        // let's store the file somewhere
+    /**
+     * Setup the GeoIP2File system.
+     */
+    public function setup_geoip2file() {
+        global $CFG;
+
+        // Store the file somewhere where it won't be wiped out..
         $gzfile = "$CFG->dataroot/phpunit/geoip/GeoLite2-City.mmdb.gz";
         check_dir_exists(dirname($gzfile));
         if (file_exists($gzfile) and (filemtime($gzfile) < time() - 60*60*24*30)) {
-            // delete file if older than 1 month
+            // Delete file if older than 1 month.
             unlink($gzfile);
         }
 
@@ -80,32 +88,42 @@ class core_iplookup_geoip_testcase extends advanced_testcase {
         $CFG->geoip2file = $geoipfile;
     }
 
-    public function test_ipv4() {
-        $result = iplookup_find_location('192.30.255.112');
-
-        $this->assertEquals('array', gettype($result));
-        $this->assertEquals('San Francisco', $result['city']);
-        $this->assertEquals(-122.3933, $result['longitude'], 'Coordinates are out of accepted tolerance', 0.01);
-        $this->assertEquals(37.7697, $result['latitude'], 'Coordinates are out of accepted tolerance', 0.01);
+    /**
+     * Test the format of data returned in the iplookup_find_location function.
+     *
+     * @dataProvider ip_provider
+     * @param   string  $ip The IP to test
+     */
+    public function test_ip($ip) {
+        $this->setup_geoip2file();
+
+        // Note: The results we get from the iplookup tests are beyond our control.
+        // We used to check a specific IP to a known location, but these have become less reliable and change too
+        // frequently to be used for testing.
+
+        $result = iplookup_find_location($ip);
+
+        $this->assertInternalType('array', $result);
+        $this->assertInternalType('float', $result['latitude']);
+        $this->assertInternalType('float', $result['longitude']);
+        $this->assertInternalType('string', $result['city']);
+        $this->assertInternalType('string', $result['country']);
+        $this->assertInternalType('array', $result['title']);
+        $this->assertInternalType('string', $result['title'][0]);
+        $this->assertInternalType('string', $result['title'][1]);
         $this->assertNull($result['error']);
-        $this->assertEquals('array', gettype($result['title']));
-        $this->assertEquals('San Francisco', $result['title'][0]);
-        $this->assertEquals('United States', $result['title'][1]);
     }
 
-    public function test_ipv6() {
-        // NOTE: these tests can be altered by the geoip dataset, there has been an attempt to get
-        // a 'reliable' result.
-
-        $result = iplookup_find_location('2607:f010:3fe:fff1::ff:fe00:25');
-
-        $this->assertEquals('array', gettype($result));
-        $this->assertEquals('Los Angeles', $result['city']);
-        $this->assertEquals(-118.2987, $result['longitude'], 'Coordinates are out of accepted tolerance', 0.01);
-        $this->assertEquals(33.7866, $result['latitude'], 'Coordinates are out of accepted tolerance', 0.01);
-        $this->assertNull($result['error']);
-        $this->assertEquals('array', gettype($result['title']));
-        $this->assertEquals('Los Angeles', $result['title'][0]);
-        $this->assertEquals('United States', $result['title'][1]);
+    /**
+     * Data provider for IP lookup test.
+     *
+     * @return array
+     */
+    public function ip_provider() {
+        return [
+            'IPv4: Sample suggested by maxmind themselves' => ['24.24.24.24'],
+            'IPv4: github.com' => ['192.30.255.112'],
+            'IPv6: UCLA' => ['2607:f010:3fe:fff1::ff:fe00:25'],
+        ];
     }
 }
index eebf74f..f8289e0 100644 (file)
@@ -49,6 +49,7 @@ $string['allowbeforeblock'] = 'Allowed list will be processed first';
 $string['allowbeforeblockdesc'] = 'By default, entries in the blocked IPs list are matched first. If this option is enabled, entries in the allowed IPs list are processed before the blocked list.';
 $string['allowblockstodock'] = 'Allow blocks to use the dock';
 $string['allowcategorythemes'] = 'Allow category themes';
+$string['allowcohortthemes'] = 'Allow cohort themes';
 $string['allowcoursethemes'] = 'Allow course themes';
 $string['allowediplist'] = 'Allowed IP list';
 $string['allowedemaildomains'] = 'Allowed email domains';
@@ -144,6 +145,7 @@ $string['configallcountrycodes'] = 'This is the list of countries that may be se
 $string['configallowassign'] = 'You can allow people who have the roles on the left side to assign some of the column roles to other people';
 $string['configallowblockstodock'] = 'If enabled and supported by the selected theme users can choose to move blocks to a special dock.';
 $string['configallowcategorythemes'] = 'If you enable this, then themes can be set at the category level. This will affect all child categories and courses unless they have specifically set their own theme. WARNING: Enabling category themes may affect performance.';
+$string['configallowcohortthemes'] = 'If you enable this, then themes can be set at the cohort level. This will affect all users with only one cohort or more than one but with the same theme.';
 $string['configallowcoursethemes'] = 'If you enable this, then courses will be allowed to set their own themes.  Course themes override all other theme choices (site, user, or session themes)';
 $string['configallowedemaildomains'] = 'List email domains that are allowed to be disclosed in the "From" section of outgoing email. The default of "Empty" will use the No-reply address for all outgoing email. The use of wildcards is allowed e.g. *.example.com will allow emails sent from any subdomain of example.com, but not example.com itself. This will require separate entry.';
 $string['configallowemailaddresses'] = 'To restrict new email addresses to particular domains, list them here separated by spaces. All other domains will be rejected. To allow subdomains, add the domain with a preceding \'.\'. To allow a root domain together with its subdomains, add the domain twice - once with a preceding \'.\' and once without e.g. .ourcollege.edu.au ourcollege.edu.au.';
index 49e7845..ee7503b 100644 (file)
@@ -58,6 +58,7 @@ $string['eventcohortmemberadded'] = 'User added to a cohort';
 $string['eventcohortmemberremoved'] = 'User removed from a cohort';
 $string['eventcohortupdated'] = 'Cohort updated';
 $string['external'] = 'External cohort';
+$string['invalidtheme'] = 'Cohort theme does not exist';
 $string['idnumber'] = 'Cohort ID';
 $string['memberscount'] = 'Cohort size';
 $string['name'] = 'Name';
index bb41d0a..d5f5cd3 100644 (file)
@@ -167,7 +167,6 @@ $string['memorylimithelp'] = '<p>The PHP memory limit for your server is current
     <p>However, on some servers this will prevent <b>all</b> PHP pages from working 
     (you will see errors when you look at pages) so you\'ll have to remove the .htaccess file.</p></li>
 </ol>';
-$string['mssqlextensionisnotpresentinphp'] = 'PHP has not been properly configured with the MSSQL extension so that it can communicate with SQL*Server.  Please check your php.ini file or recompile PHP.';
 $string['mysqliextensionisnotpresentinphp'] = 'PHP has not been properly configured with the MySQLi extension for it to communicate with MySQL. Please check your php.ini file or recompile PHP.';
 $string['nativemariadb'] = 'MariaDB (native/mariadb)';
 $string['nativemariadbhelp'] = '<p>The database is where most of the Moodle settings and data are stored and must be configured here.</p>
@@ -180,9 +179,6 @@ $string['nativemysqlihelp'] = '<p>The database is where most of the Moodle setti
 <p>The database name, username, and password are required fields; table prefix is optional.</p>
 <p>The database name may contain only alphanumeric characters, dollar ($) and underscore (_).</p>
 <p>If the database currently does not exist, and the user you specify has permission, Moodle will attempt to create a new database with the correct permissions and settings.</p>';
-$string['nativemssql'] = 'SQL*Server FreeTDS (native/mssql)';
-$string['nativemssqlhelp'] = 'Now you need to configure the database where most Moodle data will be stored.
-This database must already have been created and a username and password created to access it. Table prefix is mandatory.';
 $string['nativeoci'] = 'Oracle (native/oci)';
 $string['nativeocihelp'] = 'Now you need to configure the database where most Moodle data will be stored.
 This database must already have been created and a username and password created to access it. Table prefix is mandatory.';
@@ -254,3 +250,4 @@ $string['welcomep60'] = 'The following pages will lead you through some easy to
 $string['welcomep70'] = 'Click the "Next" button below to continue with the set up of <strong>Moodle</strong>.';
 $string['wwwroot'] = 'Web address';
 $string['wwwrooterror'] = 'The \'Web Address\' does not appear to be valid - this Moodle installation doesn\'t appear to be there. The value below has been reset.';
+// Deprecated since 3.4.
\ No newline at end of file
index 91eac47..a97ebbb 100644 (file)
@@ -45,6 +45,8 @@ $string['emailtagline'] = 'This is a copy of a message sent to you at "{$a->site
 $string['enabled'] = 'Enabled';
 $string['errorcallingprocessor'] = 'Error calling defined output';
 $string['errortranslatingdefault'] = 'Error translating default setting provided by plugin, using system defaults instead.';
+$string['eventnotificationviewed'] = 'Notification viewed';
+$string['eventnotificationsent'] = 'Notification sent';
 $string['eventmessagecontactadded'] = 'Message contact added';
 $string['eventmessagecontactblocked'] = 'Message contact blocked';
 $string['eventmessagecontactremoved'] = 'Message contact removed';
index 21b3c60..d9faa52 100644 (file)
@@ -1478,6 +1478,7 @@ $string['parentcategory'] = 'Parent category';
 $string['parentcoursenotfound'] = 'Parent course not found!';
 $string['parentfolder'] = 'Parent folder';
 $string['participants'] = 'Participants';
+$string['participantscount'] = 'Number of participants: {$a}';
 $string['participantslist'] = 'Participants list';
 $string['participationratio'] = 'Participation ratio';
 $string['participationreport'] = 'Participation report';
index 2129142..6eba816 100644 (file)
@@ -31,6 +31,10 @@ $string['authorname'] = 'Author name';
 $string['back'] = 'Back';
 $string['beadmin'] = 'You need to be an admin user to use this page.';
 $string['commenton'] = 'Comment on';
+$string['confirm_delete'] = 'Are you sure you want to delete the index for {$a}? Until the search area is indexed, users will not get search results from this area.';
+$string['confirm_indexall'] = 'Are you sure you want to update indexed contents now? If a large amount of content needs indexing, this can take a long time. For live servers, you should normally leave indexing to the \'Global search indexing\' scheduled task.';
+$string['confirm_reindexall'] = 'Are you sure you want to reindex all site contents now? If your site contains a large amount of content, this will take a long time, and users may not get full search results until it completes.';
+$string['confirm_deleteall'] = 'Are you sure you want to delete all indexed contents now? Until the site is indexed again, users will not get search results.';
 $string['createanindex'] = 'create an index';
 $string['createdon'] = 'Created on';
 $string['database'] = 'Database';
@@ -100,6 +104,7 @@ $string['search'] = 'Search';
 $string['search:message_received'] = 'Messages - received';
 $string['search:message_sent'] = 'Messages - sent';
 $string['search:mycourse'] = 'My courses';
+$string['search:section'] = 'Course sections';
 $string['search:user'] = 'Users';
 $string['searcharea'] = 'Search area';
 $string['searching'] = 'Searching in ...';
index 30f756a..b14741e 100644 (file)
@@ -2960,6 +2960,7 @@ class admin_setting_configselect extends admin_setting {
     public function __construct($name, $visiblename, $description, $defaultsetting, $choices) {
         // Look for optgroup and single options.
         if (is_array($choices)) {
+            $this->choices = [];
             foreach ($choices as $key => $val) {
                 if (is_array($val)) {
                     $this->optgroups[$key] = $val;
index 5666dc3..edc87d9 100644 (file)
Binary files a/lib/amd/build/auto_rows.min.js and b/lib/amd/build/auto_rows.min.js differ
index ab7a876..644d3a9 100644 (file)
@@ -49,9 +49,6 @@ define(['jquery'], function($) {
         var innerHeight = element.innerHeight();
         var padding = innerHeight - height;
 
-        // Set height to 1ox to force scroll height to calculate correctly.
-        element.height('1px');
-
         var scrollHeight = element[0].scrollHeight;
         var rows = (scrollHeight - padding) / (height / currentRows);
 
diff --git a/lib/antivirus/clamav/classes/privacy/provider.php b/lib/antivirus/clamav/classes/privacy/provider.php
new file mode 100644 (file)
index 0000000..61ccf03
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy Subsystem implementation for antivirus_clamav.
+ *
+ * @package    antivirus_clamav
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace antivirus_clamav\privacy;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Privacy Subsystem for antivirus_clamav implementing null_provider.
+ *
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class provider implements \core_privacy\local\metadata\null_provider {
+
+    /**
+     * Get the language string identifier with the component's language
+     * file to explain why this plugin stores no data.
+     *
+     * @return  string
+     */
+    public static function get_reason() : string {
+        return 'privacy:metadata';
+    }
+}
\ No newline at end of file
index 7fa0d6f..18a5b8f 100644 (file)
@@ -36,6 +36,7 @@ $string['pathtoclamdesc'] = 'If the running method is set to "command line", ent
 $string['pathtounixsocket'] = 'Unix domain socket';
 $string['pathtounixsocketdesc'] = 'If the running method is set to "Unix domain socket", enter the path to ClamAV Unix socket here. On Debian Linux this will be /var/run/clamav/clamd.ctl. Please make sure that clamav daemon has read access to uploaded files, the easiest way to ensure that is to add \'clamav\' user to your webserver group (\'www-data\' on Debian Linux).';
 $string['pluginname'] = 'ClamAV antivirus';
+$string['privacy:metadata'] = 'The ClamAV Antivirus plugin does not store any personal data.';
 $string['quarantinedir'] = 'Quarantine directory';
 $string['runningmethod'] = 'Running method';
 $string['runningmethoddesc'] = 'Method of running ClamAV. Command line is used by default, however on Unix systems better performance can be obtained by using system sockets.';
index ab03bb9..f3f4697 100644 (file)
@@ -81,7 +81,7 @@ class course_module_completion_updated extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/report/completion/index.php', array('course' => $this->courseid));
+        return new \moodle_url('/report/progress/index.php', array('course' => $this->courseid));
     }
 
     /**
index 7d5087e..b991853 100644 (file)
@@ -32,7 +32,6 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string $messagetable: the table we marked the message as deleted from (message/message_read).
  *      - int messageid: the id of the message.
  *      - int useridfrom: the id of the user who received the message.
  *      - int useridto: the id of the user who sent the message.
@@ -51,11 +50,11 @@ class message_deleted extends base {
      * @param int $userfromid the user who the message was from.
      * @param int $usertoid the user who the message was sent to.
      * @param int $userdeleted the user who deleted it.
-     * @param string $messagetable the table we are marking the message as deleted in.
      * @param int $messageid the id of the message that was deleted.
+     * @param int $muaid The id in the message_user_actions table
      * @return message_deleted
      */
-    public static function create_from_ids($userfromid, $usertoid, $userdeleted, $messagetable, $messageid) {
+    public static function create_from_ids($userfromid, $usertoid, $userdeleted, $messageid, $muaid) {
         // Check who was deleting the message.
         if ($userdeleted == $userfromid) {
             $relateduserid = $usertoid;
@@ -66,11 +65,11 @@ class message_deleted extends base {
         // We set the userid to the user who deleted the message, nothing to do
         // with whether or not they sent or received the message.
         $event = self::create(array(
+            'objectid' => $muaid,
             'userid' => $userdeleted,
             'context' => \context_system::instance(),
             'relateduserid' => $relateduserid,
             'other' => array(
-                'messagetable' => $messagetable,
                 'messageid' => $messageid,
                 'useridfrom' => $userfromid,
                 'useridto' => $usertoid
@@ -84,7 +83,8 @@ class message_deleted extends base {
      * Init method.
      */
     protected function init() {
-        $this->data['crud'] = 'u';
+        $this->data['objecttable'] = 'message_user_actions';
+        $this->data['crud'] = 'c';
         $this->data['edulevel'] = self::LEVEL_OTHER;
     }
 
@@ -126,10 +126,6 @@ class message_deleted extends base {
             throw new \coding_exception('The \'relateduserid\' must be set.');
         }
 
-        if (!isset($this->other['messagetable'])) {
-            throw new \coding_exception('The \'messagetable\' value must be set in other.');
-        }
-
         if (!isset($this->other['messageid'])) {
             throw new \coding_exception('The \'messageid\' value must be set in other.');
         }
@@ -143,11 +139,13 @@ class message_deleted extends base {
         }
     }
 
+    public static function get_objectid_mapping() {
+        return array('db' => 'message_user_actions', 'restore' => base::NOT_MAPPED);
+    }
+
     public static function get_other_mapping() {
         // Messages are not backed up, so no need to map them on restore.
         $othermapped = array();
-        // The messageid table varies so it cannot be mapped.
-        $othermapped['messageid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
         $othermapped['useridfrom'] = array('db' => 'user', 'restore' => base::NOT_MAPPED);
         $othermapped['useridto'] = array('db' => 'user', 'restore' => base::NOT_MAPPED);
         return $othermapped;
index e330919..9b3a8c5 100644 (file)
@@ -32,7 +32,6 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - int messageid: the id of the message.
  *      - int courseid: the id of the related course.
  * }
  *
@@ -69,14 +68,11 @@ class message_sent extends base {
         }
 
         $event = self::create(array(
+            'objectid' => $messageid,
             'userid' => $userfromid,
             'context' => \context_system::instance(),
             'relateduserid' => $usertoid,
             'other' => array(
-                // In earlier versions it can either be the id in the 'message_read' or 'message' table.
-                // Now it is always the id from 'message' table. Please note that the record is still moved
-                // to the 'message_read' table later when message marked as read.
-                'messageid' => $messageid,
                 'courseid' => $courseid
             )
         ));
@@ -88,6 +84,7 @@ class message_sent extends base {
      * Init method.
      */
     protected function init() {
+        $this->data['objecttable'] = 'messages';
         $this->data['crud'] = 'c';
         $this->data['edulevel'] = self::LEVEL_OTHER;
     }
@@ -133,8 +130,9 @@ class message_sent extends base {
         // The add_to_log function was only ever called when we sent a message from one user to another. We do not want
         // to return the legacy log data if we are sending a system message, so check that the userid is valid.
         if (\core_user::is_real_user($this->userid)) {
+            $messageid = $this->other['messageid'] ?? $this->objectid; // For BC we may have 'messageid' in other.
             return array(SITEID, 'message', 'write', 'index.php?user=' . $this->userid . '&id=' . $this->relateduserid .
-                '&history=1#m' . $this->other['messageid'], $this->userid);
+                '&history=1#m' . $messageid, $this->userid);
         }
 
         return null;
@@ -153,26 +151,18 @@ class message_sent extends base {
             throw new \coding_exception('The \'relateduserid\' must be set.');
         }
 
-        if (!isset($this->other['messageid'])) {
-            throw new \coding_exception('The \'messageid\' value must be set in other.');
-        }
-
         if (!isset($this->other['courseid'])) {
             throw new \coding_exception('The \'courseid\' value must be set in other.');
         }
     }
 
     public static function get_objectid_mapping() {
-        // Messages are not backed up, so no need to map them.
-        return false;
+        return array('db' => 'messages', 'restore' => base::NOT_MAPPED);
     }
 
     public static function get_other_mapping() {
-        // Messages are not backed up, so no need to map them on restore.
         $othermapped = array();
-        // The messages table could vary for older events - so cannot be mapped.
-        $othermapped['messageid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
-        $othermapped['courseid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
+        $othermapped['courseid'] = array('db' => 'course', 'restore' => base::NOT_MAPPED);
         return $othermapped;
     }
 }
index 8e12947..35ae1ff 100644 (file)
@@ -46,7 +46,7 @@ class message_viewed extends base {
      * Init method.
      */
     protected function init() {
-        $this->data['objecttable'] = 'message_read';
+        $this->data['objecttable'] = 'message_user_actions';
         $this->data['crud'] = 'c';
         $this->data['edulevel'] = self::LEVEL_OTHER;
     }
@@ -97,15 +97,13 @@ class message_viewed extends base {
     }
 
     public static function get_objectid_mapping() {
-        // Messages are not backed up, so no need to map them.
-        return array('db' => 'message_read', 'restore' => base::NOT_MAPPED);
+        return array('db' => 'message_user_actions', 'restore' => base::NOT_MAPPED);
     }
 
     public static function get_other_mapping() {
         // Messages are not backed up, so no need to map them on restore.
         $othermapped = array();
-        // The messages table could vary for older events - so cannot be mapped.
-        $othermapped['messageid'] = array('db' => base::NOT_MAPPED, 'restore' => base::NOT_MAPPED);
+        $othermapped['messageid'] = array('db' => 'messages', 'restore' => base::NOT_MAPPED);
         return $othermapped;
     }
 }
diff --git a/lib/classes/event/notification_sent.php b/lib/classes/event/notification_sent.php
new file mode 100644 (file)
index 0000000..1c2080c
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Notification sent event.
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Notification sent event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - int courseid: the id of the related course.
+ * }
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class notification_sent extends base {
+
+    /**
+     * Create event using ids.
+     *
+     * @param int $userfromid
+     * @param int $usertoid
+     * @param int $notificationid
+     * @param int $courseid course id the event is related with - SITEID if no relation exists.
+     * @return notification_sent
+     */
+    public static function create_from_ids($userfromid, $usertoid, $notificationid, $courseid) {
+        // We may be sending a notification from the 'noreply' address, which means we are not actually sending a
+        // notification from a valid user. In this case, we will set the userid to 0.
+        // Check if the userid is valid.
+        if (!\core_user::is_real_user($userfromid)) {
+            $userfromid = 0;
+        }
+
+        $event = self::create(
+            [
+                'objectid' => $notificationid,
+                'userid' => $userfromid,
+                'context' => \context_system::instance(),
+                'relateduserid' => $usertoid,
+                'other' => [
+                    'courseid' => $courseid
+                ]
+            ]
+        );
+
+        return $event;
+    }
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'notifications';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventnotificationsent', 'message');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/message/output/popup/notifications.php', array('notificationid' => $this->objectid));
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        // Check if we are sending from a valid user.
+        if (\core_user::is_real_user($this->userid)) {
+            return "The user with id '$this->userid' sent a notification to the user with id '$this->relateduserid'.";
+        }
+
+        return "A notification was sent by the system to the user with id '$this->relateduserid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        if (!isset($this->other['courseid'])) {
+            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        return array('db' => 'notifications', 'restore' => base::NOT_MAPPED);
+    }
+
+    public static function get_other_mapping() {
+        $othermapped = array();
+        $othermapped['courseid'] = array('db' => 'course', 'restore' => base::NOT_MAPPED);
+        return $othermapped;
+    }
+}
diff --git a/lib/classes/event/notification_viewed.php b/lib/classes/event/notification_viewed.php
new file mode 100644 (file)
index 0000000..f33c333
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Notification viewed event.
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Notification viewed event class.
+ *
+ * @package    core
+ * @copyright  2018 Mark Nelson <markn@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class notification_viewed extends base {
+
+    /**
+     * Create event using ids.
+     *
+     * @param int $userfromid
+     * @param int $usertoid
+     * @param int $notificationid
+     * @return notification_viewed
+     */
+    public static function create_from_ids($userfromid, $usertoid, $notificationid) {
+        // We may be sending a notification from the 'noreply' address, which means we are not actually sending a
+        // notification from a valid user. In this case, we will set the userid to 0.
+        // Check if the userid is valid.
+        if (!\core_user::is_real_user($userfromid)) {
+            $userfromid = 0;
+        }
+
+        // Get the context for the user who received the notification.
+        $context = \context_user::instance($usertoid, IGNORE_MISSING);
+        // If the user no longer exists the context value will be false, in this case use the system context.
+        if ($context === false) {
+            $context = \context_system::instance();
+        }
+
+        $event = self::create(
+            [
+                'objectid' => $notificationid,
+                'userid' => $usertoid, // Using the user who read the notification as they are the ones performing the action.
+                'context' => $context,
+                'relateduserid' => $userfromid
+            ]
+        );
+
+        return $event;
+    }
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'notifications';
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventnotificationviewed', 'message');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/message/output/popup/notifications.php', array('notificationid' => $this->objectid));
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' read a notification from the user with id '$this->relateduserid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
+
+    public static function get_objectid_mapping() {
+        return array('db' => 'notifications', 'restore' => base::NOT_MAPPED);
+    }
+}
index d08489d..c05e14d 100644 (file)
@@ -54,7 +54,7 @@ class manager {
      * @param \core\message\message $eventdata fully prepared event data for processors
      * @param \stdClass $savemessage the message saved in 'message' table
      * @param array $processorlist list of processors for target user
-     * @return int $messageid the id from 'message' or 'message_read' table (false is not returned)
+     * @return int $messageid the id from 'messages' (false is not returned)
      */
     public static function send_message($eventdata, \stdClass $savemessage, array $processorlist) {
         global $CFG;
@@ -77,27 +77,35 @@ class manager {
         require_once($CFG->dirroot.'/message/lib.php'); // This is most probably already included from messagelib.php file.
 
         if (empty($processorlist)) {
-            // Trigger event for sending a message - we need to do this before marking as read!
-            \core\event\message_sent::create_from_ids(
-                $eventdata->userfrom->id,
-                $eventdata->userto->id,
-                $savemessage->id,
-                $eventdata->courseid
+            // Trigger event for sending a message or notification - we need to do this before marking as read!
+            if ($eventdata->notification) {
+                \core\event\notification_sent::create_from_ids(
+                    $eventdata->userfrom->id,
+                    $eventdata->userto->id,
+                    $savemessage->id,
+                    $eventdata->courseid
+                )->trigger();
+            } else { // Must be a message.
+                \core\event\message_sent::create_from_ids(
+                    $eventdata->userfrom->id,
+                    $eventdata->userto->id,
+                    $savemessage->id,
+                    $eventdata->courseid
                 )->trigger();
+            }
 
-            if ($savemessage->notification or empty($CFG->messaging)) {
+            if ($eventdata->notification or empty($CFG->messaging)) {
                 // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
                 // The same goes if the messaging is completely disabled.
-                // We cannot insert directly to the message_read table because we want to get all events in proper order!
-                $messageid = message_mark_message_read($savemessage, time(), true);
-
-            } else {
-                // Just add it to the list of unread messages, there is no way it could be delivered to them,
-                // but they can read it via the messaging UI later.
-                $messageid = $savemessage->id;
+                if ($eventdata->notification) {
+                    $savemessage->timeread = null;
+                    \core_message\api::mark_notification_as_read($savemessage);
+                } else {
+                    \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
+                }
             }
 
-            return $messageid;
+            return $savemessage->id;
         }
 
         // Let the manager do the sending or buffering when db transaction in progress.
@@ -133,7 +141,6 @@ class manager {
             return $savemessage->id;
         }
 
-        $failed = false;
         foreach ($processorlist as $procname) {
             // Let new messaging class add custom content based on the processor.
             $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
@@ -142,40 +149,38 @@ class manager {
             $processor = \core_message\api::get_processed_processor_object($stdproc);
             if (!$processor->object->send_message($proceventdata)) {
                 debugging('Error calling message processor ' . $procname);
-                $failed = true;
-                // Previously the $messageid = false here was overridden
-                // by other processors and message_mark_message_read() below.
             }
         }
 
-        // Trigger event for sending a message - must be done before marking as read.
-        \core\event\message_sent::create_from_ids(
-            $eventdata->userfrom->id,
-            $eventdata->userto->id,
-            $savemessage->id,
-            $eventdata->courseid
+        // Trigger event for sending a message or notification - we need to do this before marking as read!
+        if ($eventdata->notification) {
+            \core\event\notification_sent::create_from_ids(
+                $eventdata->userfrom->id,
+                $eventdata->userto->id,
+                $savemessage->id,
+                $eventdata->courseid
             )->trigger();
+        } else { // Must be a message.
+            \core\event\message_sent::create_from_ids(
+                $eventdata->userfrom->id,
+                $eventdata->userto->id,
+                $savemessage->id,
+                $eventdata->courseid
+            )->trigger();
+        }
 
         if (empty($CFG->messaging)) {
-            // If messaging is disabled and they previously had forum notifications handled by the popup processor
-            // or any processor that puts a row in message_working then the notification will remain forever
-            // unread. To prevent this mark the message read if messaging is disabled.
-            $messageid = message_mark_message_read($savemessage, time());
-
-        } else if ($failed) {
-            // Something failed, better keep it as unread then.
-            $messageid = $savemessage->id;
-
-        } else if ($DB->count_records('message_working', array('unreadmessageid' => $savemessage->id)) == 0) {
-            // If there is no more processors that want to process this we can move message to message_read.
-            $messageid = message_mark_message_read($savemessage, time(), true);
-
-        } else {
-            // Some processor is still working on the data, let's keep it unread.
-            $messageid = $savemessage->id;
+            // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
+            // The same goes if the messaging is completely disabled.
+            if ($eventdata->notification) {
+                $savemessage->timeread = null;
+                \core_message\api::mark_notification_as_read($savemessage);
+            } else {
+                \core_message\api::mark_message_as_read($eventdata->userto->id, $savemessage);
+            }
         }
 
-        return $messageid;
+        return $savemessage->id;
     }
 
     /**
index 7af9689..302e536 100644 (file)
@@ -1884,7 +1884,7 @@ class core_plugin_manager {
             ),
 
             'search' => array(
-                'solr'
+                'simpledb', 'solr'
             ),
 
             'scormreport' => array(
@@ -1908,7 +1908,7 @@ class core_plugin_manager {
                 'dbtransfer', 'filetypes', 'generator', 'health', 'httpsreplace', 'innodb', 'installaddon',
                 'langimport', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound', 'mobile', 'multilangupgrade',
                 'monitor', 'oauth2', 'phpunit', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task', 'templatelibrary',
-                'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb'
+                'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb'
             ),
 
             'webservice' => array(
index 4debce3..6d1a2c4 100644 (file)
@@ -50,7 +50,7 @@ class messaging_cleanup_task extends scheduled_task {
         if (!empty($CFG->messagingdeletereadnotificationsdelay)) {
             $notificationdeletetime = $timenow - $CFG->messagingdeletereadnotificationsdelay;
             $params = array('notificationdeletetime' => $notificationdeletetime);
-            $DB->delete_records_select('message_read', 'notification=1 AND timeread<:notificationdeletetime', $params);
+            $DB->delete_records_select('notifications', 'timeread < :notificationdeletetime', $params);
         }
 
     }
index 721edfb..5be4bb1 100644 (file)
@@ -184,7 +184,7 @@ class tag_cron_task extends scheduled_task {
         $sql = "SELECT DISTINCT component, itemtype
                   FROM {tag_instance}
                  WHERE itemtype <> 'user' or component <> 'core'";
-        $tagareas = $DB->get_records_sql($sql);
+        $tagareas = $DB->get_recordset_sql($sql);
         foreach ($tagareas as $tagarea) {
             $sql = 'SELECT ti.id
                       FROM {tag_instance} ti
@@ -196,6 +196,7 @@ class tag_cron_task extends scheduled_task {
                 $tagarray[] = $tagid->id;
             }
         }
+        $tagareas->close();
 
         // Get instances for each of the ids to be deleted.
         if (count($tagarray) > 0) {
index 97df485..b9b9003 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20180222" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20180403" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <INDEX NAME="useridto_timeusertodeleted_notification" UNIQUE="false" FIELDS="useridto, timeusertodeleted, notification"/>
       </INDEXES>
     </TABLE>
+    <TABLE NAME="messages" COMMENT="Stores all messages">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="useridfrom" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="conversationid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="subject" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="fullmessage" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="fullmessageformat" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="fullmessagehtml" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="smallmessage" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="useridfrom" TYPE="foreign" FIELDS="useridfrom" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="conversationid" TYPE="foreign" FIELDS="conversationid" REFTABLE="message_conversations" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="conversationid_timecreated" UNIQUE="false" FIELDS="conversationid, timecreated"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="message_conversations" COMMENT="Stores all message conversations">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="convhash" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="convhash" UNIQUE="true" FIELDS="convhash"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="message_conversation_members" COMMENT="Stores all members in a conversations">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="conversationid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="conversationid" TYPE="foreign" FIELDS="conversationid" REFTABLE="message_conversations" REFFIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
+    <TABLE NAME="message_user_actions" COMMENT="Stores all per-user actions on individual messages">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="messageid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="action" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="messageid" TYPE="foreign" FIELDS="messageid" REFTABLE="messages" REFFIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="userid_messageid_action" UNIQUE="true" FIELDS="userid, messageid, action"/>
+      </INDEXES>
+    </TABLE>
+    <TABLE NAME="notifications" COMMENT="Stores all notifications">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="useridfrom" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="useridto" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="subject" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The message subject"/>
+        <FIELD NAME="fullmessage" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="fullmessageformat" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="fullmessagehtml" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="smallmessage" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="eventtype" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="contexturl" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="contexturlname" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="timeread" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="useridto" TYPE="foreign" FIELDS="useridto" REFTABLE="user" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
     <TABLE NAME="message_contacts" COMMENT="Maintains lists of relationships between users">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
         <FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false" COMMENT="Component (plugintype_pluignname) that manages the cohort, manual modifications are allowed only when set to NULL"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="theme" TYPE="char" LENGTH="50" NOTNULL="false" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="primary key of the table, please edit me"/>
       </KEYS>
     </TABLE>
-    <TABLE NAME="message_working" COMMENT="Lists all the messages and processors that need to be processed">
-      <FIELDS>
-        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" COMMENT="id of the table, please edit me"/>
-        <FIELD NAME="unreadmessageid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="message that still needs some processing (on message table)"/>
-        <FIELD NAME="processorid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The processor with processes the message"/>
-      </FIELDS>
-      <KEYS>
-        <KEY NAME="primary" TYPE="primary" FIELDS="id" COMMENT="primary key of the table, please edit me"/>
-      </KEYS>
-      <INDEXES>
-        <INDEX NAME="unreadmessageid_idx" UNIQUE="false" FIELDS="unreadmessageid" COMMENT="Index on unreadmessage id"/>
-      </INDEXES>
-    </TABLE>
     <TABLE NAME="files" COMMENT="description of files, content is stored in sha1 file pool">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
index 35ba7f9..55943d6 100644 (file)
@@ -990,6 +990,15 @@ $functions = array(
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
         'ajax' => true,
     ),
+    'core_message_mark_notification_read' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'mark_notification_read',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Mark a single notification as read, trigger notification_viewed event.',
+        'type' => 'write',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+        'ajax' => true,
+    ),
     'core_message_message_processor_config_form' => array(
         'classname' => 'core_message_external',
         'methodname' => 'message_processor_config_form',
index 78ebbee..edafff2 100644 (file)
@@ -2020,5 +2020,201 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2018022800.03);
     }
 
+    if ($oldversion < 2018032200.01) {
+        // Define table 'messages' to be created.
+        $table = new xmldb_table('messages');
+
+        // Adding fields to table 'messages'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('useridfrom', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('conversationid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('subject', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('fullmessage', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('fullmessageformat', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, 0);
+        $table->add_field('fullmessagehtml', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('smallmessage', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'messages'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('useridfrom', XMLDB_KEY_FOREIGN, array('useridfrom'), 'user', array('id'));
+        $table->add_key('conversationid', XMLDB_KEY_FOREIGN, array('conversationid'), 'message_conversations', array('id'));
+
+        // Conditionally launch create table for 'messages'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Define table 'message_conversations' to be created.
+        $table = new xmldb_table('message_conversations');
+
+        // Adding fields to table 'message_conversations'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'message_conversations'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Conditionally launch create table for 'message_conversations'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Define table 'message_conversation_members' to be created.
+        $table = new xmldb_table('message_conversation_members');
+
+        // Adding fields to table 'message_conversation_members'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('conversationid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'message_conversation_members'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('conversationid', XMLDB_KEY_FOREIGN, array('conversationid'), 'message_conversations', array('id'));
+        $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
+
+        // Conditionally launch create table for 'message_conversation_members'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Define table 'message_user_actions' to be created.
+        $table = new xmldb_table('message_user_actions');
+
+        // Adding fields to table 'message_user_actions'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('messageid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('action', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'message_user_actions'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
+        $table->add_key('messageid', XMLDB_KEY_FOREIGN, array('messageid'), 'messages', array('id'));
+
+        // Conditionally launch create table for 'message_user_actions'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Define table 'notifications' to be created.
+        $table = new xmldb_table('notifications');
+
+        // Adding fields to table 'notifications'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('useridfrom', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('useridto', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('subject', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('fullmessage', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('fullmessageformat', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, 0);
+        $table->add_field('fullmessagehtml', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('smallmessage', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('component', XMLDB_TYPE_CHAR, '100', null, null, null, null);
+        $table->add_field('eventtype', XMLDB_TYPE_CHAR, '100', null, null, null, null);
+        $table->add_field('contexturl', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('contexturlname', XMLDB_TYPE_TEXT, null, null, null, null, null);
+        $table->add_field('timeread', XMLDB_TYPE_INTEGER, '10', null, false, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'notifications'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $table->add_key('useridto', XMLDB_KEY_FOREIGN, array('useridto'), 'user', array('id'));
+
+        // Conditionally launch create table for 'notifications'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032200.01);
+    }
+
+    if ($oldversion < 2018032200.04) {
+        // Define table 'message_conversations' to be updated.
+        $table = new xmldb_table('message_conversations');
+        $field = new xmldb_field('convhash', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null, 'id');
+
+        // Conditionally launch add field 'convhash'.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Conditionally launch add index.
+        $index = new xmldb_index('convhash', XMLDB_INDEX_UNIQUE, array('convhash'));
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032200.04);
+    }
+
+    if ($oldversion < 2018032200.05) {
+        // Drop table that is no longer needed.
+        $table = new xmldb_table('message_working');
+        if ($dbman->table_exists($table)) {
+            $dbman->drop_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032200.05);
+    }
+
+    if ($oldversion < 2018032200.06) {
+        // Define table 'message_user_actions' to add an index to.
+        $table = new xmldb_table('message_user_actions');
+
+        // Conditionally launch add index.
+        $index = new xmldb_index('userid_messageid_action', XMLDB_INDEX_UNIQUE, array('userid, messageid, action'));
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032200.06);
+    }
+
+    if ($oldversion < 2018032200.07) {
+        // Define table 'messages' to add an index to.
+        $table = new xmldb_table('messages');
+
+        // Conditionally launch add index.
+        $index = new xmldb_index('conversationid_timecreated', XMLDB_INDEX_NOTUNIQUE, array('conversationid, timecreated'));
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032200.07);
+    }
+
+    if ($oldversion < 2018032700.00) {
+        // Update default search engine to search_simpledb if global search is disabled and there is no solr index defined.
+        if (empty($CFG->enableglobalsearch) && empty(get_config('search_solr', 'indexname'))) {
+            set_config('searchengine', 'simpledb');
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018032700.00);
+    }
+
+    if ($oldversion < 2018040500.01) {
+
+        // Define field indexpriority to be added to search_index_requests. Allow null initially.
+        $table = new xmldb_table('cohort');
+        $field = new xmldb_field('theme', XMLDB_TYPE_CHAR, '50',
+                null, null, null, null, 'timemodified');
+
+        // Conditionally add field.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2018040500.01);
+    }
+
     return true;
 }
index 132eb55..d479b71 100644 (file)
@@ -4709,29 +4709,10 @@ function message_contact_link($userid, $linktype='add', $return=false, $script=n
 }
 
 /**
- * Get the users recent event notifications
- *
  * @deprecated since Moodle 3.2
- * @param object $user the current user
- * @param int $limitfrom can be used for paging
- * @param int $limitto can be used for paging
- * @return array
  */
 function message_get_recent_notifications($user, $limitfrom=0, $limitto=100) {
-    debugging('message_get_recent_notifications() is deprecated and is no longer used.', DEBUG_DEVELOPER);
-
-    global $DB;
-
-    $userfields = user_picture::fields('u', array('lastaccess'));
-    $sql = "SELECT mr.id AS message_read_id, $userfields, mr.notification, mr.smallmessage, mr.fullmessage, mr.fullmessagehtml, mr.fullmessageformat, mr.timecreated as timecreated, mr.contexturl, mr.contexturlname
-              FROM {message_read} mr
-                   JOIN {user} u ON u.id=mr.useridfrom
-             WHERE mr.useridto = :userid1 AND u.deleted = '0' AND mr.notification = :notification
-             ORDER BY mr.timecreated DESC";
-    $params = array('userid1' => $user->id, 'notification' => 1);
-
-    $notifications =  $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
-    return $notifications;
+    throw new coding_exception('message_get_recent_notifications() can not be used any more.', DEBUG_DEVELOPER);
 }
 
 /**
@@ -4801,162 +4782,10 @@ function message_history_link($userid1, $userid2, $return=false, $keywords='', $
 }
 
 /**
- * Search a user's messages
- *
- * Returns a list of posts found using an array of search terms
- * eg   word  +word -word
- *
  * @deprecated since Moodle 3.2
- * @param array $searchterms an array of search terms (strings)
- * @param bool $fromme include messages from the user?
- * @param bool $tome include messages to the user?
- * @param mixed $courseid SITEID for admins searching all messages. Other behaviour not yet implemented
- * @param int $userid the user ID of the current user
- * @return mixed An array of messages or false if no matching messages were found
  */
 function message_search($searchterms, $fromme=true, $tome=true, $courseid='none', $userid=0) {
-    debugging('message_search() is deprecated and is no longer used.', DEBUG_DEVELOPER);
-
-    global $CFG, $USER, $DB;
-
-    // If user is searching all messages check they are allowed to before doing anything else.
-    if ($courseid == SITEID && !has_capability('moodle/site:readallmessages', context_system::instance())) {
-        print_error('accessdenied','admin');
-    }
-
-    // If no userid sent then assume current user.
-    if ($userid == 0) $userid = $USER->id;
-
-    // Some differences in SQL syntax.
-    if ($DB->sql_regex_supported()) {
-        $REGEXP    = $DB->sql_regex(true);
-        $NOTREGEXP = $DB->sql_regex(false);
-    }
-
-    $searchcond = array();
-    $params = array();
-    $i = 0;
-
-    // Preprocess search terms to check whether we have at least 1 eligible search term.
-    // If we do we can drop words around it like 'a'.
-    $dropshortwords = false;
-    foreach ($searchterms as $searchterm) {
-        if (strlen($searchterm) >= 2) {
-            $dropshortwords = true;
-        }
-    }
-
-    foreach ($searchterms as $searchterm) {
-        $i++;
-
-        $NOT = false; // Initially we aren't going to perform NOT LIKE searches, only MSSQL and Oracle.
-
-        if ($dropshortwords && strlen($searchterm) < 2) {
-            continue;
-        }
-        // Under Oracle and MSSQL, trim the + and - operators and perform simpler LIKE search.
-        if (!$DB->sql_regex_supported()) {
-            if (substr($searchterm, 0, 1) == '-') {
-                $NOT = true;
-            }
-            $searchterm = trim($searchterm, '+-');
-        }
-
-        if (substr($searchterm,0,1) == "+") {
-            $searchterm = substr($searchterm,1);
-            $searchterm = preg_quote($searchterm, '|');
-            $searchcond[] = "m.fullmessage $REGEXP :ss$i";
-            $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)";
-
-        } else if (substr($searchterm,0,1) == "-") {
-            $searchterm = substr($searchterm,1);
-            $searchterm = preg_quote($searchterm, '|');
-            $searchcond[] = "m.fullmessage $NOTREGEXP :ss$i";
-            $params['ss'.$i] = "(^|[^a-zA-Z0-9])$searchterm([^a-zA-Z0-9]|$)";
-
-        } else {
-            $searchcond[] = $DB->sql_like("m.fullmessage", ":ss$i", false, true, $NOT);
-            $params['ss'.$i] = "%$searchterm%";
-        }
-    }
-
-    if (empty($searchcond)) {
-        $searchcond = " ".$DB->sql_like('m.fullmessage', ':ss1', false);
-        $params['ss1'] = "%";
-    } else {
-        $searchcond = implode(" AND ", $searchcond);
-    }
-
-    // There are several possibilities
-    // 1. courseid = SITEID : The admin is searching messages by all users
-    // 2. courseid = ??     : A teacher is searching messages by users in
-    //                        one of their courses - currently disabled
-    // 3. courseid = none   : User is searching their own messages;
-    //    a.  Messages from user
-    //    b.  Messages to user
-    //    c.  Messages to and from user
-
-    if ($fromme && $tome) {
-        $searchcond .= " AND ((useridto = :useridto AND timeusertodeleted = 0) OR
-            (useridfrom = :useridfrom AND timeuserfromdeleted = 0))";
-        $params['useridto'] = $userid;
-        $params['useridfrom'] = $userid;
-    } else if ($fromme) {
-        $searchcond .= " AND (useridfrom = :useridfrom AND timeuserfromdeleted = 0)";
-        $params['useridfrom'] = $userid;
-    } else if ($tome) {
-        $searchcond .= " AND (useridto = :useridto AND timeusertodeleted = 0)";
-        $params['useridto'] = $userid;
-    }
-    if ($courseid == SITEID) { // Admin is searching all messages.
-        $m_read   = $DB->get_records_sql("SELECT m.id, m.useridto, m.useridfrom, m.smallmessage, m.fullmessage, m.timecreated
-                                            FROM {message_read} m
-                                           WHERE $searchcond", $params, 0, MESSAGE_SEARCH_MAX_RESULTS);
-        $m_unread = $DB->get_records_sql("SELECT m.id, m.useridto, m.useridfrom, m.smallmessage, m.fullmessage, m.timecreated
-                                            FROM {message} m
-                                           WHERE $searchcond", $params, 0, MESSAGE_SEARCH_MAX_RESULTS);
-
-    } else if ($courseid !== 'none') {
-        // This has not been implemented due to security concerns.
-        $m_read   = array();
-        $m_unread = array();
-
-    } else {
-
-        if ($fromme and $tome) {
-            $searchcond .= " AND (m.useridfrom=:userid1 OR m.useridto=:userid2)";
-            $params['userid1'] = $userid;
-            $params['userid2'] = $userid;
-
-        } else if ($fromme) {
-            $searchcond .= " AND m.useridfrom=:userid";
-            $params['userid'] = $userid;
-
-        } else if ($tome) {
-            $searchcond .= " AND m.useridto=:userid";
-            $params['userid'] = $userid;
-        }
-
-        $m_read   = $DB->get_records_sql("SELECT m.id, m.useridto, m.useridfrom, m.smallmessage, m.fullmessage, m.timecreated
-                                            FROM {message_read} m
-                                           WHERE $searchcond", $params, 0, MESSAGE_SEARCH_MAX_RESULTS);
-        $m_unread = $DB->get_records_sql("SELECT m.id, m.useridto, m.useridfrom, m.smallmessage, m.fullmessage, m.timecreated
-                                            FROM {message} m
-                                           WHERE $searchcond", $params, 0, MESSAGE_SEARCH_MAX_RESULTS);
-
-    }
-
-    /// The keys may be duplicated in $m_read and $m_unread so we can't
-    /// do a simple concatenation
-    $messages = array();
-    foreach ($m_read as $m) {
-        $messages[] = $m;
-    }
-    foreach ($m_unread as $m) {
-        $messages[] = $m;
-    }
-
-    return (empty($messages)) ? false : $messages;
+    throw new coding_exception('message_search() can not be used any more.', DEBUG_DEVELOPER);
 }
 
 /**
@@ -5070,63 +4899,10 @@ function message_get_fragment($message, $keywords) {
 }
 
 /**
- * Retrieve the messages between two users
- *
  * @deprecated since Moodle 3.2
- * @param object $user1 the current user
- * @param object $user2 the other user
- * @param int $limitnum the maximum number of messages to retrieve
- * @param bool $viewingnewmessages are we currently viewing new messages?
  */
 function message_get_history($user1, $user2, $limitnum=0, $viewingnewmessages=false) {
-    debugging('message_get_history() is deprecated and is no longer used.', DEBUG_DEVELOPER);
-
-    global $DB, $CFG;
-
-    $messages = array();
-
-    //we want messages sorted oldest to newest but if getting a subset of messages we need to sort
-    //desc to get the last $limitnum messages then flip the order in php
-    $sort = 'asc';
-    if ($limitnum>0) {
-        $sort = 'desc';
-    }
-
-    $notificationswhere = null;
-    //we have just moved new messages to read. If theyre here to see new messages dont hide notifications
-    if (!$viewingnewmessages && $CFG->messaginghidereadnotifications) {
-        $notificationswhere = 'AND notification=0';
-    }
-
-    //prevent notifications of your own actions appearing in your own message history
-    $ownnotificationwhere = ' AND NOT (useridfrom=? AND notification=1)';
-
-    $sql = "((useridto = ? AND useridfrom = ? AND timeusertodeleted = 0) OR
-        (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = 0))";
-    if ($messages_read = $DB->get_records_select('message_read', $sql . $notificationswhere . $ownnotificationwhere,
-        array($user1->id, $user2->id, $user2->id, $user1->id, $user1->id),
-        "timecreated $sort", '*', 0, $limitnum)) {
-        foreach ($messages_read as $message) {
-            $messages[] = $message;
-        }
-    }
-    if ($messages_new = $DB->get_records_select('message', $sql . $ownnotificationwhere,
-        array($user1->id, $user2->id, $user2->id, $user1->id, $user1->id),
-        "timecreated $sort", '*', 0, $limitnum)) {
-        foreach ($messages_new as $message) {
-            $messages[] = $message;
-        }
-    }
-
-    $result = core_collator::asort_objects_by_property($messages, 'timecreated', core_collator::SORT_NUMERIC);
-
-    //if we only want the last $limitnum messages
-    $messagecount = count($messages);
-    if ($limitnum > 0 && $messagecount > $limitnum) {
-        $messages = array_slice($messages, $messagecount - $limitnum, $limitnum, true);
-    }
-
-    return $messages;
+    throw new coding_exception('message_get_history() can not be used any more.', DEBUG_DEVELOPER);
 }
 
 /**
@@ -5197,9 +4973,9 @@ function message_get_contact_block_link($incontactlist, $isblocked, $contact, $s
  */
 function message_mark_messages_read($touserid, $fromuserid) {
     debugging('message_mark_messages_read() is deprecated and is no longer used, please use
-        \core_message\api::mark_all_read_for_user() instead.', DEBUG_DEVELOPER);
+        \core_message\api::mark_all_messages_as_read() instead.', DEBUG_DEVELOPER);
 
-    \core_message\api::mark_all_read_for_user($touserid, $fromuserid);
+    \core_message\api::mark_all_messages_as_read($touserid, $fromuserid);
 }
 
 /**
@@ -6035,144 +5811,11 @@ function prevent_form_autofill_password() {
 }
 
 /**
- * Get the users recent conversations meaning all the people they've recently
- * sent or received a message from plus the most recent message sent to or received from each other user
- *
  * @deprecated since Moodle 3.3 MDL-57370
- * @param object|int $userorid the current user or user id
- * @param int $limitfrom can be used for paging
- * @param int $limitto can be used for paging
- * @return array
  */
 function message_get_recent_conversations($userorid, $limitfrom = 0, $limitto = 100) {
-    global $DB;
-
-    debugging('message_get_recent_conversations() is deprecated. Please use \core_message\api::get_conversations() instead.', DEBUG_DEVELOPER);
-
-    if (is_object($userorid)) {
-        $user = $userorid;
-    } else {
-        $userid = $userorid;
-        $user = new stdClass();
-        $user->id = $userid;
-    }
-
-    $userfields = user_picture::fields('otheruser', array('lastaccess'));
-
-    // This query retrieves the most recent message received from or sent to
-    // seach other user.
-    //
-    // If two messages have the same timecreated, we take the one with the
-    // larger id.
-    //
-    // There is a separate query for read and unread messages as they are stored
-    // in different tables. They were originally retrieved in one query but it
-    // was so large that it was difficult to be confident in its correctness.
-    $uniquefield = $DB->sql_concat('message.useridfrom', "'-'", 'message.useridto');
-    $sql = "SELECT $uniquefield, $userfields,
-                   message.id as mid, message.notification, message.useridfrom, message.useridto,
-                   message.smallmessage, message.fullmessage, message.fullmessagehtml,
-                   message.fullmessageformat, message.timecreated,
-                   contact.id as contactlistid, contact.blocked
-              FROM {message_read} message
-              JOIN (
-                        SELECT MAX(id) AS messageid,
-                               matchedmessage.useridto,
-                               matchedmessage.useridfrom
-                         FROM {message_read} matchedmessage
-                   INNER JOIN (
-                               SELECT MAX(recentmessages.timecreated) timecreated,
-                                      recentmessages.useridfrom,
-                                      recentmessages.useridto
-                                 FROM {message_read} recentmessages
-                                WHERE (
-                                      (recentmessages.useridfrom = :userid1 AND recentmessages.timeuserfromdeleted = 0) OR
-                                      (recentmessages.useridto = :userid2   AND recentmessages.timeusertodeleted = 0)
-                                      )
-                             GROUP BY recentmessages.useridfrom, recentmessages.useridto
-                              ) recent ON matchedmessage.useridto     = recent.useridto
-                           AND matchedmessage.useridfrom   = recent.useridfrom
-                           AND matchedmessage.timecreated  = recent.timecreated
-                           WHERE (
-                                 (matchedmessage.useridfrom = :userid6 AND matchedmessage.timeuserfromdeleted = 0) OR
-                                 (matchedmessage.useridto = :userid7   AND matchedmessage.timeusertodeleted = 0)
-                                 )
-                      GROUP BY matchedmessage.useridto, matchedmessage.useridfrom
-                   ) messagesubset ON messagesubset.messageid = message.id
-              JOIN {user} otheruser ON (message.useridfrom = :userid4 AND message.useridto = otheruser.id)
-                OR (message.useridto   = :userid5 AND message.useridfrom   = otheruser.id)
-         LEFT JOIN {message_contacts} contact ON contact.userid  = :userid3 AND contact.contactid = otheruser.id
-             WHERE otheruser.deleted = 0 AND message.notification = 0
-          ORDER BY message.timecreated DESC";
-    $params = array(
-        'userid1' => $user->id,
-        'userid2' => $user->id,
-        'userid3' => $user->id,
-        'userid4' => $user->id,
-        'userid5' => $user->id,
-        'userid6' => $user->id,
-        'userid7' => $user->id
-    );
-    $read = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
-
-    // We want to get the messages that have not been read. These are stored in the 'message' table. It is the
-    // exact same query as the one above, except for the table we are querying. So, simply replace references to
-    // the 'message_read' table with the 'message' table.
-    $sql = str_replace('{message_read}', '{message}', $sql);
-    $unread = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
-
-    $unreadcountssql = 'SELECT useridfrom, count(*) as count
-                          FROM {message}
-                         WHERE useridto = :userid
-                           AND timeusertodeleted = 0
-                           AND notification = 0
-                      GROUP BY useridfrom';
-    $unreadcounts = $DB->get_records_sql($unreadcountssql, array('userid' => $user->id));
-
-    // Union the 2 result sets together looking for the message with the most
-    // recent timecreated for each other user.
-    // $conversation->id (the array key) is the other user's ID.
-    $conversations = array();
-    $conversation_arrays = array($unread, $read);
-    foreach ($conversation_arrays as $conversation_array) {
-        foreach ($conversation_array as $conversation) {
-            // Only consider it unread if $user has unread messages.
-            if (isset($unreadcounts[$conversation->useridfrom])) {
-                $conversation->isread = 0;
-                $conversation->unreadcount = $unreadcounts[$conversation->useridfrom]->count;
-            } else {
-                $conversation->isread = 1;
-            }
-
-            if (!isset($conversations[$conversation->id])) {
-                $conversations[$conversation->id] = $conversation;
-            } else {
-                $current = $conversations[$conversation->id];
-                // We need to maintain the isread and unreadcount values from existing
-                // parts of the conversation if we're replacing it.
-                $conversation->isread = ($conversation->isread && $current->isread);
-                if (isset($current->unreadcount) && !isset($conversation->unreadcount)) {
-                    $conversation->unreadcount = $current->unreadcount;
-                }
-
-                if ($current->timecreated < $conversation->timecreated) {
-                    $conversations[$conversation->id] = $conversation;
-                } else if ($current->timecreated == $conversation->timecreated) {
-                    if ($current->mid < $conversation->mid) {
-                        $conversations[$conversation->id] = $conversation;
-                    }
-                }
-            }
-        }
-    }
-
-    // Sort the conversations by $conversation->timecreated, newest to oldest
-    // There may be multiple conversations with the same timecreated
-    // The conversations array contains both read and unread messages (different tables) so sorting by ID won't work
-    $result = core_collator::asort_objects_by_property($conversations, 'timecreated', core_collator::SORT_NUMERIC);
-    $conversations = array_reverse($conversations);
-
-    return $conversations;
+    throw new coding_exception('message_get_recent_conversations() can not be used any more. ' .
+        'Please use \core_message\api::get_conversations() instead.', DEBUG_DEVELOPER);
 }
 
 /**
@@ -6577,3 +6220,185 @@ function question_is_only_toplevel_category_in_context($categoryid) {
 
     return question_is_only_child_of_top_category_in_context($categoryid);
 }
+
+/**
+ * Moves messages from a particular user from the message table (unread messages) to message_read
+ * This is typically only used when a user is deleted
+ *
+ * @param object $userid User id
+ * @return boolean success
+ * @deprecated since Moodle 3.5
+ */
+function message_move_userfrom_unread2read($userid) {
+    debugging('message_move_userfrom_unread2read() is deprecated and is no longer used.', DEBUG_DEVELOPER);
+
+    global $DB;
+
+    // Move all unread messages from message table to message_read.
+    if ($messages = $DB->get_records_select('message', 'useridfrom = ?', array($userid), 'timecreated')) {
+        foreach ($messages as $message) {
+            message_mark_message_read($message, 0); // Set timeread to 0 as the message was never read.
+        }
+    }
+    return true;
+}
+
+/**
+ * Retrieve users blocked by $user1
+ *
+ * @param object $user1 the user whose messages are being viewed
+ * @param object $user2 the user $user1 is talking to. If they are being blocked
+ *                      they will have a variable called 'isblocked' added to their user object
+ * @return array the users blocked by $user1
+ * @deprecated since Moodle 3.5
+ */
+function message_get_blocked_users($user1=null, $user2=null) {
+    debugging('message_get_blocked_users() is deprecated, please use \core_message\api::get_blocked_users() instead.',
+        DEBUG_DEVELOPER);
+
+    global $USER;
+
+    if (empty($user1)) {
+        $user1 = new stdClass();
+        $user1->id = $USER->id;
+    }
+
+    return \core_message\api::get_blocked_users($user1->id);
+}
+
+/**
+ * Retrieve $user1's contacts (online, offline and strangers)
+ *
+ * @param object $user1 the user whose messages are being viewed
+ * @param object $user2 the user $user1 is talking to. If they are a contact
+ *                      they will have a variable called 'iscontact' added to their user object
+ * @return array containing 3 arrays. array($onlinecontacts, $offlinecontacts, $strangers)
+ * @deprecated since Moodle 3.5
+ */
+function message_get_contacts($user1=null, $user2=null) {
+    debugging('message_get_contacts() is deprecated and is no longer used.', DEBUG_DEVELOPER);
+
+    global $DB, $CFG, $USER;
+
+    if (empty($user1)) {
+        $user1 = $USER;
+    }
+
+    if (!empty($user2)) {
+        $user2->iscontact = false;
+    }
+
+    $timetoshowusers = 300; // Seconds default.
+    if (isset($CFG->block_online_users_timetosee)) {
+        $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+    }
+
+    // Rime which a user is counting as being active since.
+    $timefrom = time() - $timetoshowusers;
+
+    // People in our contactlist who are online.
+    $onlinecontacts  = array();
+    // People in our contactlist who are offline.
+    $offlinecontacts = array();
+    // People who are not in our contactlist but have sent us a message.
+    $strangers       = array();
+
+    // Get all in our contact list who are not blocked in our and count messages we have waiting from each of them.
+    $rs = \core_message\api::get_contacts_with_unread_message_count($user1->id);
+    foreach ($rs as $rd) {
+        if ($rd->lastaccess >= $timefrom) {
+            // They have been active recently, so are counted online.
+            $onlinecontacts[] = $rd;
+
+        } else {
+            $offlinecontacts[] = $rd;
+        }
+
+        if (!empty($user2) && $user2->id == $rd->id) {
+            $user2->iscontact = true;
+        }
+    }
+
+    // Get messages from anyone who isn't in our contact list and count the number of messages we have from each of them.
+    $rs = \core_message\api::get_non_contacts_with_unread_message_count($user1->id);
+    // Add user id as array index, so supportuser and noreply user don't get duplicated (if they are real users).
+    foreach ($rs as $rd) {
+        $strangers[$rd->id] = $rd;
+    }
+
+    // Add noreply user and support user to the list, if they don't exist.
+    $supportuser = core_user::get_support_user();
+    if (!isset($strangers[$supportuser->id]) && !$supportuser->deleted) {
+        $supportuser->messagecount = message_count_unread_messages($USER, $supportuser);
+        if ($supportuser->messagecount > 0) {
+            $strangers[$supportuser->id] = $supportuser;
+        }
+    }
+
+    $noreplyuser = core_user::get_noreply_user();
+    if (!isset($strangers[$noreplyuser->id]) && !$noreplyuser->deleted) {
+        $noreplyuser->messagecount = message_count_unread_messages($USER, $noreplyuser);
+        if ($noreplyuser->messagecount > 0) {
+            $strangers[$noreplyuser->id] = $noreplyuser;
+        }
+    }
+
+    return array($onlinecontacts, $offlinecontacts, $strangers);
+}
+
+/**
+ * Mark a single message as read
+ *
+ * @param stdClass $message An object with an object property ie $message->id which is an id in the message table
+ * @param int $timeread the timestamp for when the message should be marked read. Usually time().
+ * @param bool $messageworkingempty Is the message_working table already confirmed empty for this message?
+ * @return int the ID of the message in the messags table
+ * @deprecated since Moodle 3.5
+ */
+function message_mark_message_read($message, $timeread, $messageworkingempty=false) {
+    debugging('message_mark_message_read() is deprecated, please use \core_message\api::mark_message_as_read()
+        or \core_message\api::mark_notification_as_read().', DEBUG_DEVELOPER);
+
+    if (!empty($message->notification)) {
+        \core_message\api::mark_notification_as_read($message, $timeread);
+    } else {
+        \core_message\api::mark_message_as_read($message->useridto, $message, $timeread);
+    }
+
+    return $message->id;
+}
+
+
+/**
+ * Checks if a user can delete a message.
+ *
+ * @param stdClass $message the message to delete
+ * @param string $userid the user id of who we want to delete the message for (this may be done by the admin
+ *  but will still seem as if it was by the user)
+ * @return bool Returns true if a user can delete the message, false otherwise.
+ * @deprecated since Moodle 3.5
+ */
+function message_can_delete_message($message, $userid) {
+    debugging('message_can_delete_message() is deprecated, please use \core_message\api::can_delete_message() instead.',
+        DEBUG_DEVELOPER);
+
+    return \core_message\api::can_delete_message($userid, $message->id);
+}
+
+/**
+ * Deletes a message.
+ *
+ * This function does not verify any permissions.
+ *
+ * @param stdClass $message the message to delete
+ * @param string $userid the user id of who we want to delete the message for (this may be done by the admin
+ *  but will still seem as if it was by the user)
+ * @return bool
+ * @deprecated since Moodle 3.5
+ */
+function message_delete_message($message, $userid) {
+    debugging('message_delete_message() is deprecated, please use \core_message\api::delete_message() instead.',
+        DEBUG_DEVELOPER);
+
+    return \core_message\api::delete_message($userid, $message->id);
+}
index 48debee..c333222 100644 (file)
@@ -107,4 +107,18 @@ class mariadb_native_moodle_database extends mysqli_native_moodle_database {
         }
         return true;
     }
+
+    /**
+     * Does this mariadb instance support fulltext indexes?
+     *
+     * @return bool
+     */
+    public function is_fulltext_search_supported() {
+        $info = $this->get_server_info();
+
+        if (version_compare($info['version'], '10.0.5', '>=')) {
+            return true;
+        }
+        return false;
+    }
 }
index a16b933..77d0b98 100644 (file)
@@ -2687,4 +2687,14 @@ abstract class moodle_database {
     public function perf_get_queries_time() {
         return $this->queriestime;
     }
+
+    /**
+     * Whether the database is able to support full-text search or not.
+     *
+     * @return bool
+     */
+    public function is_fulltext_search_supported() {
+        // No support unless specified.
+        return false;
+    }
 }
diff --git a/lib/dml/mssql_native_moodle_database.php b/lib/dml/mssql_native_moodle_database.php
deleted file mode 100644 (file)
index 2299472..0000000
+++ /dev/null
@@ -1,1468 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Native mssql class representing moodle database interface.
- *
- * @package    core_dml
- * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once(__DIR__.'/moodle_database.php');
-require_once(__DIR__.'/mssql_native_moodle_recordset.php');
-require_once(__DIR__.'/mssql_native_moodle_temptables.php');
-
-/**
- * Native mssql class representing moodle database interface.
- *
- * @package    core_dml
- * @copyright  2009 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class mssql_native_moodle_database extends moodle_database {
-
-    protected $mssql     = null;
-    protected $last_error_reporting; // To handle mssql driver default verbosity
-    protected $collation;  // current DB collation cache
-    /**
-     * Does the used db version support ANSI way of limiting (2012 and higher)
-     * @var bool
-     */
-    protected $supportsoffsetfetch;
-
-    /**
-     * Detects if all needed PHP stuff installed.
-     * Note: can be used before connect()
-     * @return mixed true if ok, string if something
-     */
-    public function driver_installed() {
-        if (!function_exists('mssql_connect')) {
-            return get_string('mssqlextensionisnotpresentinphp', 'install');
-        }
-        return true;
-    }
-
-    /**
-     * Returns database family type - describes SQL dialect
-     * Note: can be used before connect()
-     * @return string db family name (mysql, postgres, mssql, oracle, etc.)
-     */
-    public function get_dbfamily() {
-        return 'mssql';
-    }
-
-    /**
-     * Returns more specific database driver type
-     * Note: can be used before connect()
-     * @return string db type mysqli, pgsql, oci, mssql, sqlsrv
-     */
-    protected function get_dbtype() {
-        return 'mssql';
-    }
-
-    /**
-     * Returns general database library name
-     * Note: can be used before connect()
-     * @return string db type pdo, native
-     */
-    protected function get_dblibrary() {
-        return 'native';
-    }
-
-    /**
-     * Returns localised database type name
-     * Note: can be used before connect()
-     * @return string
-     */
-    public function get_name() {
-        return get_string('nativemssql', 'install');
-    }
-
-    /**
-     * Returns localised database configuration help.
-     * Note: can be used before connect()
-     * @return string
-     */
-    public function get_configuration_help() {
-        return get_string('nativemssqlhelp', 'install');
-    }
-
-    /**
-     * Diagnose database and tables, this function is used
-     * to verify database and driver settings, db engine types, etc.
-     *
-     * @return string null means everything ok, string means problem found.
-     */
-    public function diagnose() {
-        // Verify the database is running with READ_COMMITTED_SNAPSHOT enabled.
-        // (that's required to get snapshots/row versioning on READ_COMMITED mode).
-        $correctrcsmode = false;
-        $sql = "SELECT is_read_committed_snapshot_on
-                  FROM sys.databases
-                 WHERE name = '{$this->dbname}'";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-        if ($result) {
-            if ($row = mssql_fetch_assoc($result)) {
-                $correctrcsmode = (bool)reset($row);
-            }
-        }
-        $this->free_result($result);
-
-        if (!$correctrcsmode) {
-            return get_string('mssqlrcsmodemissing', 'error');
-        }
-
-        // Arrived here, all right.
-        return null;
-    }
-
-    /**
-     * Connect to db
-     * Must be called before other methods.
-     * @param string $dbhost The database host.
-     * @param string $dbuser The database username.
-     * @param string $dbpass The database username's password.
-     * @param string $dbname The name of the database being connected to.
-     * @param mixed $prefix string means moodle db prefix, false used for external databases where prefix not used
-     * @param array $dboptions driver specific options
-     * @return bool true
-     * @throws dml_connection_exception if error
-     */
-    public function connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, array $dboptions=null) {
-        if ($prefix == '' and !$this->external) {
-            //Enforce prefixes for everybody but mysql
-            throw new dml_exception('prefixcannotbeempty', $this->get_dbfamily());
-        }
-
-        $driverstatus = $this->driver_installed();
-
-        if ($driverstatus !== true) {
-            throw new dml_exception('dbdriverproblem', $driverstatus);
-        }
-
-        $this->store_settings($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
-
-        $dbhost = $this->dbhost;
-        // Zero shouldn't be used as a port number so doing a check with empty() should be fine.
-        if (!empty($dboptions['dbport'])) {
-            if (stristr(PHP_OS, 'win') && !stristr(PHP_OS, 'darwin')) {
-                $dbhost .= ','.$dboptions['dbport'];
-            } else {
-                $dbhost .= ':'.$dboptions['dbport'];
-            }
-        }
-        ob_start();
-        if (!empty($this->dboptions['dbpersist'])) { // persistent connection
-            $this->mssql = mssql_pconnect($dbhost, $this->dbuser, $this->dbpass, true);
-        } else {
-            $this->mssql = mssql_connect($dbhost, $this->dbuser, $this->dbpass, true);
-        }
-        $dberr = ob_get_contents();
-        ob_end_clean();
-
-        if ($this->mssql === false) {
-            $this->mssql = null;
-            throw new dml_connection_exception($dberr);
-        }
-
-        // Disable logging until we are fully setup.
-        $this->query_log_prevent();
-
-        // already connected, select database and set some env. variables
-        $this->query_start("--mssql_select_db", null, SQL_QUERY_AUX);
-        $result = mssql_select_db($this->dbname, $this->mssql);
-        $this->query_end($result);
-
-        // No need to set charset. It's UTF8, with transparent conversions
-        // back and forth performed both by FreeTDS or ODBTP
-
-        // Allow quoted identifiers
-        $sql = "SET QUOTED_IDENTIFIER ON";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-
-        // Force ANSI nulls so the NULL check was done by IS NULL and NOT IS NULL
-        // instead of equal(=) and distinct(<>) symbols
-        $sql = "SET ANSI_NULLS ON";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-
-        // Force ANSI warnings so arithmetic/string overflows will be
-        // returning error instead of transparently truncating data
-        $sql = "SET ANSI_WARNINGS ON";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        // Concatenating null with anything MUST return NULL
-        $sql = "SET CONCAT_NULL_YIELDS_NULL  ON";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-
-        // Set transactions isolation level to READ_COMMITTED
-        // prevents dirty reads when using transactions +
-        // is the default isolation level of MSSQL
-        // Requires database to run with READ_COMMITTED_SNAPSHOT ON
-        $sql = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";
-        $this->query_start($sql, NULL, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        $this->free_result($result);
-
-        $serverinfo = $this->get_server_info();
-        // Fetch/offset is supported staring from SQL Server 2012.
-        $this->supportsoffsetfetch = $serverinfo['version'] > '11';
-
-        // We can enable logging now.
-        $this->query_log_allow();
-
-        // Connection stabilised and configured, going to instantiate the temptables controller
-        $this->temptables = new mssql_native_moodle_temptables($this);
-
-        return true;
-    }
-
-    /**
-     * Close database connection and release all resources
-     * and memory (especially circular memory references).
-     * Do NOT use connect() again, create a new instance if needed.
-     */
-    public function dispose() {
-        parent::dispose(); // Call parent dispose to write/close session and other common stuff before closing connection
-        if ($this->mssql) {
-            mssql_close($this->mssql);
-            $this->mssql = null;
-        }
-    }
-
-    /**
-     * Called before each db query.
-     * @param string $sql
-     * @param array array of parameters
-     * @param int $type type of query
-     * @param mixed $extrainfo driver specific extra information
-     * @return void
-     */
-    protected function query_start($sql, array $params=null, $type, $extrainfo=null) {
-        parent::query_start($sql, $params, $type, $extrainfo);
-        // mssql driver tends to send debug to output, we do not need that ;-)
-        $this->last_error_reporting = error_reporting(0);
-    }
-
-    /**
-     * Called immediately after each db query.
-     * @param mixed db specific result
-     * @return void
-     */
-    protected function query_end($result) {
-        // reset original debug level
-        error_reporting($this->last_error_reporting);
-        parent::query_end($result);
-    }
-
-    /**
-     * Returns database server info array
-     * @return array Array containing 'description' and 'version' info
-     */
-    public function get_server_info() {
-        static $info;
-        if (!$info) {
-            $info = array();
-            $sql = 'sp_server_info 2';
-            $this->query_start($sql, null, SQL_QUERY_AUX);
-            $result = mssql_query($sql, $this->mssql);
-            $this->query_end($result);
-            $row = mssql_fetch_row($result);
-            $info['description'] = $row[2];
-            $this->free_result($result);
-
-            $sql = 'sp_server_info 500';
-            $this->query_start($sql, null, SQL_QUERY_AUX);
-            $result = mssql_query($sql, $this->mssql);
-            $this->query_end($result);
-            $row = mssql_fetch_row($result);
-            $info['version'] = $row[2];
-            $this->free_result($result);
-        }
-        return $info;
-    }
-
-    /**
-     * Converts short table name {tablename} to real table name
-     * supporting temp tables (#) if detected
-     *
-     * @param string sql
-     * @return string sql
-     */
-    protected function fix_table_names($sql) {
-        if (preg_match_all('/\{([a-z][a-z0-9_]*)\}/', $sql, $matches)) {
-            foreach($matches[0] as $key=>$match) {
-                $name = $matches[1][$key];
-                if ($this->temptables->is_temptable($name)) {
-                    $sql = str_replace($match, $this->temptables->get_correct_name($name), $sql);
-                } else {
-                    $sql = str_replace($match, $this->prefix.$name, $sql);
-                }
-            }
-        }
-        return $sql;
-    }
-
-    /**
-     * Returns supported query parameter types
-     * @return int bitmask of accepted SQL_PARAMS_*
-     */
-    protected function allowed_param_types() {
-        return SQL_PARAMS_QM; // Not really, but emulated, see emulate_bound_params()
-    }
-
-    /**
-     * Returns last error reported by database engine.
-     * @return string error message
-     */
-    public function get_last_error() {
-        return mssql_get_last_message();
-    }
-
-    /**
-     * Return tables in database WITHOUT current prefix
-     * @param bool $usecache if true, returns list of cached tables.
-     * @return array of table names in lowercase and without prefix
-     */
-    public function get_tables($usecache=true) {
-        if ($usecache and $this->tables !== null) {
-            return $this->tables;
-        }
-        $this->tables = array();
-        $sql = "SELECT table_name
-                  FROM INFORMATION_SCHEMA.TABLES
-                 WHERE table_name LIKE '$this->prefix%'
-                   AND table_type = 'BASE TABLE'";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        if ($result) {
-            while ($row = mssql_fetch_row($result)) {
-                $tablename = reset($row);
-                if ($this->prefix !== false && $this->prefix !== '') {
-                    if (strpos($tablename, $this->prefix) !== 0) {
-                        continue;
-                    }
-                    $tablename = substr($tablename, strlen($this->prefix));
-                }
-                $this->tables[$tablename] = $tablename;
-            }
-            $this->free_result($result);
-        }
-
-        // Add the currently available temptables
-        $this->tables = array_merge($this->tables, $this->temptables->get_temptables());
-        return $this->tables;
-    }
-
-    /**
-     * Return table indexes - everything lowercased.
-     * @param string $table The table we want to get indexes from.
-     * @return array An associative array of indexes containing 'unique' flag and 'columns' being indexed
-     */
-    public function get_indexes($table) {
-        $indexes = array();
-        $tablename = $this->prefix.$table;
-
-        // Indexes aren't covered by information_schema metatables, so we need to
-        // go to sys ones. Skipping primary key indexes on purpose.
-        $sql = "SELECT i.name AS index_name, i.is_unique, ic.index_column_id, c.name AS column_name
-                  FROM sys.indexes i
-                  JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
-                  JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
-                  JOIN sys.tables t ON i.object_id = t.object_id
-                 WHERE t.name = '$tablename'
-                   AND i.is_primary_key = 0
-              ORDER BY i.name, i.index_id, ic.index_column_id";
-
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        if ($result) {
-            $lastindex = '';
-            $unique = false;
-            $columns = array();
-            while ($row = mssql_fetch_assoc($result)) {
-                if ($lastindex and $lastindex != $row['index_name']) { // Save lastindex to $indexes and reset info
-                    $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns);
-                    $unique = false;
-                    $columns = array();
-                }
-                $lastindex = $row['index_name'];
-                $unique = empty($row['is_unique']) ? false : true;
-                $columns[] = $row['column_name'];
-            }
-            if ($lastindex ) { // Add the last one if exists
-                $indexes[$lastindex] = array('unique' => $unique, 'columns' => $columns);
-            }
-            $this->free_result($result);
-        }
-        return $indexes;
-    }
-
-    /**
-     * Returns datailed information about columns in table. This information is cached internally.
-     * @param string $table name
-     * @param bool $usecache
-     * @return array array of database_column_info objects indexed with column names
-     */
-    public function get_columns($table, $usecache=true) {
-
-        if ($usecache) {
-            if ($this->temptables->is_temptable($table)) {
-                if ($data = $this->get_temp_tables_cache()->get($table)) {
-                    return $data;
-                }
-            } else {
-                if ($data = $this->get_metacache()->get($table)) {
-                    return $data;
-                }
-            }
-        }
-
-        $structure = array();
-
-        if (!$this->temptables->is_temptable($table)) { // normal table, get metadata from own schema
-            $sql = "SELECT column_name AS name,
-                           data_type AS type,
-                           numeric_precision AS max_length,
-                           character_maximum_length AS char_max_length,
-                           numeric_scale AS scale,
-                           is_nullable AS is_nullable,
-                           columnproperty(object_id(quotename(table_schema) + '.' +
-                               quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
-                           column_default AS default_value
-                      FROM INFORMATION_SCHEMA.COLUMNS
-                     WHERE table_name = '{" . $table . "}'
-                  ORDER BY ordinal_position";
-        } else { // temp table, get metadata from tempdb schema
-            $sql = "SELECT column_name AS name,
-                           data_type AS type,
-                           numeric_precision AS max_length,
-                           character_maximum_length AS char_max_length,
-                           numeric_scale AS scale,
-                           is_nullable AS is_nullable,
-                           columnproperty(object_id(quotename(table_schema) + '.' +
-                               quotename(table_name)), column_name, 'IsIdentity') AS auto_increment,
-                           column_default AS default_value
-                      FROM tempdb.INFORMATION_SCHEMA.COLUMNS
-                      JOIN tempdb.sys.objects ON name = table_name
-                     WHERE object_id = object_id('tempdb..{" . $table . "}')
-                  ORDER BY ordinal_position";
-        }
-
-        list($sql, $params, $type) = $this->fix_sql_params($sql, null);
-
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
-
-        if (!$result) {
-            return array();
-        }
-
-        while ($rawcolumn = mssql_fetch_assoc($result)) {
-
-            $rawcolumn = (object)$rawcolumn;
-
-            $info = new stdClass();
-            $info->name = $rawcolumn->name;
-            $info->type = $rawcolumn->type;
-            $info->meta_type = $this->mssqltype2moodletype($info->type);
-
-            // Prepare auto_increment info
-            $info->auto_increment = $rawcolumn->auto_increment ? true : false;
-
-            // Define type for auto_increment columns
-            $info->meta_type = ($info->auto_increment && $info->meta_type == 'I') ? 'R' : $info->meta_type;
-
-            // id columns being auto_incremnt are PK by definition
-            $info->primary_key = ($info->name == 'id' && $info->meta_type == 'R' && $info->auto_increment);
-
-            if ($info->meta_type === 'C' and $rawcolumn->char_max_length == -1) {
-                // This is NVARCHAR(MAX), not a normal NVARCHAR.
-                $info->max_length = -1;
-                $info->meta_type = 'X';
-            } else {
-                // Put correct length for character and LOB types
-                $info->max_length = $info->meta_type == 'C' ? $rawcolumn->char_max_length : $rawcolumn->max_length;
-                $info->max_length = ($info->meta_type == 'X' || $info->meta_type ==