Merge branch 'MDL-55082-master' of git://github.com/merrill-oakland/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 11 Jul 2016 14:20:48 +0000 (15:20 +0100)
committerDan Poltawski <dan@moodle.com>
Mon, 11 Jul 2016 14:20:48 +0000 (15:20 +0100)
244 files changed:
.jshintrc
.travis.yml
Gruntfile.js
admin/cli/install.php
admin/cli/install_database.php
admin/cli/reset_password.php
admin/environment.xml
admin/index.php
admin/roles/ajax.php
admin/searchareas.php [new file with mode: 0644]
admin/settings/plugins.php
admin/tool/behat/cli/run.php
admin/tool/lp/classes/external/course_module_summary_exporter.php
admin/tool/lp/coursecompetencies.php
admin/tool/lp/lib.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/classes/external.php
admin/tool/mobile/db/services.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/version.php
admin/tool/monitor/classes/eventobservers.php
admin/tool/monitor/classes/subscription.php
admin/tool/monitor/classes/subscription_manager.php
admin/tool/monitor/classes/task/check_subscriptions.php [new file with mode: 0644]
admin/tool/monitor/db/install.xml
admin/tool/monitor/db/tasks.php
admin/tool/monitor/db/upgrade.php
admin/tool/monitor/lang/en/tool_monitor.php
admin/tool/monitor/tests/subscription_test.php [new file with mode: 0644]
admin/tool/monitor/tests/task_check_subscriptions_test.php [new file with mode: 0644]
admin/tool/monitor/version.php
admin/tool/xmldb/actions/edit_field/edit_field.class.php
admin/tool/xmldb/actions/edit_index/edit_index.class.php
admin/tool/xmldb/actions/edit_key/edit_key.class.php
admin/tool/xmldb/actions/edit_table/edit_table.class.php
auth/db/auth.php
competency/classes/api.php
competency/classes/course_competency_settings.php
competency/classes/evidence.php
competency/tests/api_test.php
composer.json
course/externallib.php
course/tests/externallib_test.php
course/upgrade.txt [new file with mode: 0644]
course/view.php
enrol/externallib.php
enrol/lti/classes/helper.php
enrol/tests/externallib_test.php
enrol/upgrade.txt
grade/amd/build/edittree_index.min.js [new file with mode: 0644]
grade/amd/src/edittree_index.js [new file with mode: 0644]
grade/edit/tree/functions.js [deleted file]
grade/edit/tree/index.php
grade/edit/tree/lib.php
grade/grading/form/guide/lang/en/gradingform_guide.php
grade/grading/form/guide/tests/behat/behat_gradingform_guide.php
grade/grading/form/guide/tests/behat/edit_guide.feature
grade/lib.php
install.php
install/lang/da/error.php
install/lang/da/moodle.php
install/lang/dz/admin.php
install/lang/dz/moodle.php
lang/en/admin.php
lang/en/backup.php
lang/en/badges.php
lang/en/question.php
lang/en/search.php
lib/adminlib.php
lib/amd/build/permissionmanager.min.js
lib/amd/src/permissionmanager.js
lib/badgeslib.php
lib/classes/grades_external.php
lib/classes/plugin_manager.php
lib/classes/update/code_manager.php
lib/classes/user.php
lib/db/services.php
lib/db/upgrade.php
lib/ddl/tests/ddl_test.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/pgsql_native_moodle_recordset.php
lib/dml/tests/dml_test.php
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/src/editor/js/notify.js
lib/externallib.php
lib/filelib.php
lib/moodlelib.php
lib/phpmailer/moodle_phpmailer.php
lib/setup.php
lib/tests/behat/behat_hooks.php
lib/tests/externallib_test.php
lib/tests/filelib_test.php
lib/tests/fixtures/update_validator/zips/multidir.zip [new file with mode: 0644]
lib/tests/moodlelib_test.php
lib/tests/text_test.php
lib/tests/update_code_manager_test.php
lib/tests/user_test.php
lib/upgrade.txt
lib/xmldb/xmldb_table.php
login/tests/behat/behat_login.php [new file with mode: 0644]
login/tests/behat/change_password.feature [new file with mode: 0644]
message/output/airnotifier/message_output_airnotifier.php
mod/assign/amd/build/grading_actions.min.js
mod/assign/amd/build/grading_events.min.js [new file with mode: 0644]
mod/assign/amd/build/grading_panel.min.js
mod/assign/amd/build/grading_review_panel.min.js
mod/assign/amd/src/grading_actions.js
mod/assign/amd/src/grading_events.js [moved from report/search/settings.php with 62% similarity]
mod/assign/amd/src/grading_panel.js
mod/assign/amd/src/grading_review_panel.js
mod/assign/db/services.php
mod/assign/externallib.php
mod/assign/feedback/editpdf/styles.css
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/pix/layout-default.png [new file with mode: 0644]
mod/assign/pix/layout-default.svg [new file with mode: 0644]
mod/assign/pix/layout-expand-left.png [new file with mode: 0644]
mod/assign/pix/layout-expand-left.svg [new file with mode: 0644]
mod/assign/pix/layout-expand-right.png [new file with mode: 0644]
mod/assign/pix/layout-expand-right.svg [new file with mode: 0644]
mod/assign/styles.css
mod/assign/submission/file/locallib.php
mod/assign/submission/file/tests/locallib_test.php [new file with mode: 0644]
mod/assign/submission/onlinetext/locallib.php
mod/assign/submission/onlinetext/tests/locallib_test.php [new file with mode: 0644]
mod/assign/submission_form.php
mod/assign/submissionplugin.php
mod/assign/templates/grading_actions.mustache
mod/assign/templates/grading_app.mustache
mod/assign/tests/externallib_test.php
mod/assign/tests/locallib_test.php
mod/assign/upgrade.txt
mod/assign/version.php
mod/book/classes/external.php
mod/book/tests/externallib_test.php
mod/book/tests/search_test.php
mod/chat/classes/external.php
mod/chat/gui_header_js/jsupdate.php
mod/chat/gui_header_js/jsupdated.php
mod/chat/tests/externallib_test.php
mod/choice/backup/moodle2/backup_choice_stepslib.php
mod/choice/classes/external.php
mod/choice/db/access.php
mod/choice/lang/en/choice.php
mod/choice/lib.php
mod/choice/mod_form.php
mod/choice/renderer.php
mod/choice/tests/behat/allow_preview.feature
mod/choice/tests/behat/publish_results.feature
mod/choice/version.php
mod/data/classes/external.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/mod_form.php
mod/data/tests/externallib_test.php
mod/feedback/lang/en/feedback.php
mod/feedback/mod_form.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/forum/tests/search_test.php
mod/glossary/classes/external.php
mod/glossary/lib.php
mod/glossary/tests/search_test.php
mod/imscp/classes/external.php
mod/lesson/mod_form.php
mod/lesson/pagetypes/numerical.php
mod/lti/classes/external.php
mod/lti/lang/en/lti.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/tests/behat/addtool.feature
mod/lti/tests/behat/toolconfigure.feature
mod/lti/tests/externallib_test.php
mod/lti/typessettings.php
mod/lti/view.php
mod/quiz/classes/external.php
mod/quiz/tests/external_test.php
mod/scorm/classes/external.php
mod/scorm/datamodels/scormlib.php
mod/scorm/lang/en/scorm.php
mod/scorm/mod_form.php
mod/scorm/tests/externallib_test.php
mod/survey/backup/moodle2/backup_survey_stepslib.php
mod/survey/classes/external.php
mod/survey/db/install.xml
mod/survey/db/upgrade.php
mod/survey/lang/en/survey.php
mod/survey/lib.php
mod/survey/mod_form.php
mod/survey/save.php
mod/survey/tests/behat/survey_completion.feature [new file with mode: 0644]
mod/survey/tests/externallib_test.php
mod/survey/version.php
mod/wiki/classes/external.php
mod/wiki/tests/externallib_test.php
mod/wiki/tests/search_test.php
mod/workshop/lang/en/workshop.php
mod/workshop/renderer.php
mod/workshop/submission.php
mod/workshop/view.php
my/lib.php
npm-shrinkwrap.json
package.json
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/multichoice/renderer.php
question/type/multichoice/tests/behat/preview.feature
report/search/classes/output/form.php [deleted file]
report/search/classes/output/renderer.php [deleted file]
report/search/index.php [deleted file]
report/search/lang/en/report_search.php [deleted file]
repository/filepicker.php
repository/filesystem/lib.php
repository/lib.php
repository/repository_ajax.php
repository/upgrade.txt
search/classes/area/base.php
search/classes/document.php
search/classes/manager.php
search/classes/output/form/search.php
search/tests/fixtures/mock_search_area.php
search/tests/fixtures/testable_core_search.php
search/tests/manager_test.php
theme/bootstrapbase/less/moodle/search.less
theme/bootstrapbase/style/moodle.css
theme/clean/classes/core_renderer.php
user/classes/search/user.php [new file with mode: 0644]
user/externallib.php
user/messageselect.php
user/tests/search_test.php [new file with mode: 0644]
version.php
webservice/lib.php
webservice/soap/locallib.php
webservice/tests/externallib_test.php
webservice/upgrade.txt
webservice/upload.php
webservice/xmlrpc/locallib.php
webservice/xmlrpc/tests/locallib_test.php [new file with mode: 0644]
webservice/xmlrpc/tests/xmlrpc_server_test.php [new file with mode: 0644]

index ee94a05..b93ac60 100644 (file)
--- a/.jshintrc
+++ b/.jshintrc
@@ -1,3 +1,6 @@
+// NOTE: We use eslint now. This file is used only by shifter. We keep the configuration
+// here because shifter uses jshint after modules have been concating. Eslint can't
+// currently do this.
 {
     "asi":          false,
     "bitwise":      true,
index d1438f3..5a32a60 100644 (file)
@@ -14,15 +14,13 @@ language: php
 php:
     # We only run the highest and lowest supported versions to reduce the load on travis-ci.org.
     - 7.0
-    # - 5.6
-    # - 5.5
-    - 5.4
+    - 5.6
 
 env:
     # Although we want to run these jobs and see failures as quickly as possible, we also want to get the slowest job to
     # start first so that the total run time is not too high.
     #
-    # We only run MySQL on PHP 5.6, so run that first.
+    # We only run MySQL on PHP 7.0, so run that first.
     # CI Tests should be second-highest in priority as these only take <= 60 seconds to run under normal circumstances.
     # Postgres is significantly is pretty reasonable in its run-time.
 
@@ -50,17 +48,13 @@ matrix:
     exclude:
         # MySQL - it's just too slow.
         # Exclude it on all versions except for 7.0
-        # - env: DB=mysqli   TASK=PHPUNIT
-        #   php: 5.6
-        #
-        # - env: DB=mysqli   TASK=PHPUNIT
-        #   php: 5.5
 
         - env: DB=mysqli   TASK=PHPUNIT
-          php: 5.4
+          php: 5.6
 
+       # One grunt execution is enough.
         - env: DB=none     TASK=GRUNT
-          php: 5.4
+          php: 5.6
 
         # Moodle 2.7 is not compatible with PHP 7 for the upgrade test.
         - env: DB=pgsql    TASK=UPGRADE
index b728341..0b52cc8 100644 (file)
@@ -101,10 +101,6 @@ module.exports = function(grunt) {
 
     // Project configuration.
     grunt.initConfig({
-        jshint: {
-            options: {jshintrc: '.jshintrc'},
-            amd: { src: amdSrc }
-        },
         eslint: {
             // Even though warnings dont stop the build we don't display warnings by default because
             // at this moment we've got too many core warnings.
@@ -286,7 +282,8 @@ module.exports = function(grunt) {
     var changedFiles = Object.create(null);
     var onChange = grunt.util._.debounce(function() {
           var files = Object.keys(changedFiles);
-          grunt.config('jshint.amd.src', files);
+          grunt.config('eslint.amd.src', files);
+          grunt.config('eslint.yui.src', files);
           grunt.config('uglify.amd.files', [{ expand: true, src: files, rename: uglifyRename }]);
           grunt.config('shifter.options.paths', files);
           changedFiles = Object.create(null);
@@ -299,7 +296,6 @@ module.exports = function(grunt) {
 
     // Register NPM tasks.
     grunt.loadNpmTasks('grunt-contrib-uglify');
-    grunt.loadNpmTasks('grunt-contrib-jshint');
     grunt.loadNpmTasks('grunt-contrib-less');
     grunt.loadNpmTasks('grunt-contrib-watch');
     grunt.loadNpmTasks('grunt-eslint');
@@ -308,7 +304,7 @@ module.exports = function(grunt) {
     grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter);
     grunt.registerTask('ignorefiles', 'Generate ignore files for linters', tasks.ignorefiles);
     grunt.registerTask('yui', ['eslint:yui', 'shifter']);
-    grunt.registerTask('amd', ['eslint:amd', 'jshint', 'uglify']);
+    grunt.registerTask('amd', ['eslint:amd', 'uglify']);
     grunt.registerTask('js', ['amd', 'yui']);
 
     // Register CSS taks.
index 9f3c0df..0b6c5ca 100644 (file)
@@ -147,10 +147,10 @@ define('PHPUNIT_TEST', false);
 define('IGNORE_COMPONENT_CACHE', true);
 
 // Check that PHP is of a sufficient version
-if (version_compare(phpversion(), "5.4.4") < 0) {
+if (version_compare(phpversion(), "5.6.5") < 0) {
     $phpversion = phpversion();
     // do NOT localise - lang strings would not work here and we CAN NOT move it after installib
-    fwrite(STDERR, "Moodle 2.7 or later requires at least PHP 5.4.4 (currently using version $phpversion).\n");
+    fwrite(STDERR, "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).\n");
     fwrite(STDERR, "Please upgrade your server software or install older Moodle version.\n");
     exit(1);
 }
index b66805e..f41263e 100644 (file)
@@ -63,10 +63,10 @@ Example:
 ";
 
 // Check that PHP is of a sufficient version
-if (version_compare(phpversion(), "5.4.4") < 0) {
+if (version_compare(phpversion(), "5.6.5") < 0) {
     $phpversion = phpversion();
     // do NOT localise - lang strings would not work here and we CAN NOT move it after installib
-    fwrite(STDERR, "Moodle 2.7 or later requires at least PHP 5.4.4 (currently using version $phpversion).\n");
+    fwrite(STDERR, "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).\n");
     fwrite(STDERR, "Please upgrade your server software or install older Moodle version.\n");
     exit(1);
 }
index 27f4ca9..a1535d1 100644 (file)
@@ -29,10 +29,23 @@ define('CLI_SCRIPT', true);
 require(__DIR__.'/../../config.php');
 require_once($CFG->libdir.'/clilib.php');      // cli only functions
 
+// Define the input options.
+$longparams = array(
+        'help' => false,
+        'username' => '',
+        'password' => '',
+        'ignore-password-policy' => false
+);
+
+$shortparams = array(
+        'h' => 'help',
+        'u' => 'username',
+        'p' => 'password',
+        'i' => 'ignore-password-policy'
+);
 
 // now get cli options
-list($options, $unrecognized) = cli_get_params(array('help'=>false),
-                                               array('h'=>'help'));
+list($options, $unrecognized) = cli_get_params($longparams, $shortparams);
 
 if ($unrecognized) {
     $unrecognized = implode("\n  ", $unrecognized);
@@ -47,29 +60,43 @@ There are no security checks here because anybody who is able to
 execute this file may execute any PHP too.
 
 Options:
--h, --help            Print out this help
+-h, --help                    Print out this help
+-u, --username=username       Specify username to change
+-p, --password=newpassword    Specify new password
+--ignore-password-policy      Ignore password policy when setting password
 
 Example:
 \$sudo -u www-data /usr/bin/php admin/cli/reset_password.php
+\$sudo -u www-data /usr/bin/php admin/cli/reset_password.php --username=rosaura --password=jiu3jiu --ignore-password-policy
 "; //TODO: localize - to be translated later when everything is finished
 
     echo $help;
     die;
 }
-cli_heading('Password reset'); // TODO: localize
-$prompt = "enter username (manual authentication only)"; // TODO: localize
-$username = cli_input($prompt);
+if ($options['username'] == '' ) {
+    cli_heading('Password reset'); // TODO: localize.
+    $prompt = "enter username (manual authentication only)"; // TODO: localize.
+    $username = cli_input($prompt);
+} else {
+    $username = $options['username'];
+}
 
 if (!$user = $DB->get_record('user', array('auth'=>'manual', 'username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
     cli_error("Can not find user '$username'");
 }
 
-$prompt = "Enter new password"; // TODO: localize
-$password = cli_input($prompt);
+if ($options['password'] == '' ) {
+    $prompt = "Enter new password"; // TODO: localize.
+    $password = cli_input($prompt);
+} else {
+    $password = $options['password'];
+}
 
 $errmsg = '';//prevent eclipse warning
-if (!check_password_policy($password, $errmsg)) {
-    cli_error($errmsg);
+if (!$options['ignore-password-policy'] ) {
+    if (!check_password_policy($password, $errmsg)) {
+        cli_error($errmsg);
+    }
 }
 
 $hashedpassword = hash_internal_user_password($password);
@@ -78,4 +105,4 @@ $DB->set_field('user', 'password', $hashedpassword, array('id'=>$user->id));
 
 echo "Password changed\n";
 
-exit(0); // 0 means success
\ No newline at end of file
+exit(0); // 0 means success.
index f281b45..7bef8fc 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="3.2" requires="2.7">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="5.5.31" />
+      <VENDOR name="mysql" version="5.5.31" />
+      <VENDOR name="postgres" version="9.1" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="10.2" />
+    </DATABASE>
+    <PHP version="5.6.5" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opensslrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlreader" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="intlrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbstorageengine" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="slashargumentswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unsupporteddbtablerowformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unoconvwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index 1000f93..382c252 100644 (file)
@@ -30,10 +30,10 @@ if (!file_exists('../config.php')) {
 }
 
 // Check that PHP is of a sufficient version as soon as possible
-if (version_compare(phpversion(), '5.4.4') < 0) {
+if (version_compare(phpversion(), '5.6.5') < 0) {
     $phpversion = phpversion();
     // do NOT localise - lang strings would not work here and we CAN NOT move it to later place
-    echo "Moodle 2.7 or later requires at least PHP 5.4.4 (currently using version $phpversion).<br />";
+    echo "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).<br />";
     echo "Please upgrade your server software or install older Moodle version.";
     die();
 }
index 717b8f2..2b66c9f 100644 (file)
@@ -36,6 +36,8 @@ require_login($course, false, $cm);
 require_capability('moodle/role:review', $context);
 require_sesskey();
 
+$OUTPUT->header();
+
 list($overridableroles, $overridecounts, $nameswithcounts) = get_overridable_roles($context,
         ROLENAME_BOTH, true);
 
diff --git a/admin/searchareas.php b/admin/searchareas.php
new file mode 100644 (file)
index 0000000..884d5b8
--- /dev/null
@@ -0,0 +1,180 @@
+<?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/>.
+
+/**
+ * Manage global search areas.
+ *
+ * @package   core_search
+ * @copyright 2016 Dan Poltawski <dan@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+require_once(__DIR__ . '/../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+
+admin_externalpage_setup('searchareas');
+
+$areaid = optional_param('areaid', null, PARAM_ALPHAEXT);
+$action = optional_param('action', null, PARAM_ALPHA);
+
+try {
+    $searchmanager = \core_search\manager::instance();
+} catch (core_search\engine_exception $searchmanagererror) {
+    // 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.
+        $area = \core_search\manager::get_search_area($areaid);
+        if ($area === false) {
+            throw new moodle_exception('invalidrequest');
+        }
+    }
+
+    // All actions but enable/disable need the search engine to be ready.
+    if ($action !== 'enable' && $action !== 'disable') {
+        if (!empty($searchmanagererror)) {
+            throw $searchmanagererror;
+        }
+    }
+
+    switch ($action) {
+        case 'enable':
+            $area->set_enabled(true);
+            echo $OUTPUT->notification(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);
+            break;
+        case 'delete':
+            $search = \core_search\manager::instance();
+            $search->delete_index($areaid);
+            echo $OUTPUT->notification(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);
+            break;
+        case 'reindexall':
+            $searchmanager->index(true);
+            echo $OUTPUT->notification(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);
+            break;
+        default:
+            throw new moodle_exception('invalidaction');
+            break;
+    }
+}
+
+$searchareas = \core_search\manager::get_search_areas_list();
+if (empty($searchmanagererror)) {
+    $areasconfig = $searchmanager->get_areas_config($searchareas);
+} else {
+    $areasconfig = false;
+}
+
+if (!empty($searchmanagererror)) {
+    $errorstr = get_string($searchmanagererror->errorcode, $searchmanagererror->module);
+    echo $OUTPUT->notification($errorstr, \core\output\notification::NOTIFY_ERROR);
+} else {
+    echo $OUTPUT->notification(get_string('indexinginfo', 'admin'), \core\output\notification::NOTIFY_INFO);
+}
+
+$table = new html_table();
+$table->id = 'core-search-areas';
+
+$table->head = array(get_string('searcharea', 'search'), get_string('enable'), get_string('newestdocindexed', 'admin'),
+    get_string('searchlastrun', 'admin'), get_string('searchindexactions', 'admin'));
+
+foreach ($searchareas as $area) {
+    $areaid = $area->get_area_id();
+    $columns = array(new html_table_cell($area->get_visible_name()));
+
+    if ($area->is_enabled()) {
+        $columns[] = $OUTPUT->action_icon(admin_searcharea_action_url('disable', $areaid),
+            new pix_icon('t/hide', get_string('disable'), 'moodle', array('title' => '', 'class' => 'iconsmall')),
+            null, array('title' => get_string('disable')));
+
+        if ($areasconfig) {
+            $columns[] = $areasconfig[$areaid]->lastindexrun;
+
+            if ($areasconfig[$areaid]->indexingstart) {
+                $timediff = $areasconfig[$areaid]->indexingend - $areasconfig[$areaid]->indexingstart;
+                $laststatus = $timediff . ' , ' .
+                    $areasconfig[$areaid]->docsprocessed . ' , ' .
+                    $areasconfig[$areaid]->recordsprocessed . ' , ' .
+                    $areasconfig[$areaid]->docsignored;
+            } else {
+                $laststatus = '';
+            }
+            $columns[] = $laststatus;
+            $columns[] = html_writer::link(admin_searcharea_action_url('delete', $areaid), 'Delete index');
+
+        } else {
+            $blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
+            $blankrow->colspan = 3;
+            $columns[] = $blankrow;
+        }
+
+    } else {
+        $columns[] = $OUTPUT->action_icon(admin_searcharea_action_url('enable', $areaid),
+            new pix_icon('t/show', get_string('enable'), 'moodle', array('title' => '', 'class' => 'iconsmall')),
+                null, array('title' => get_string('enable')));
+
+        $blankrow = new html_table_cell(get_string('searchareadisabled', 'admin'));
+        $blankrow->colspan = 3;
+        $columns[] = $blankrow;
+    }
+    $row = new html_table_row($columns);
+    $table->data[] = $row;
+}
+
+// Cross-search area tasks.
+$options = array();
+if (!empty($searchmanagererror)) {
+    $options['disabled'] = true;
+}
+echo $OUTPUT->box_start('search-areas-actions');
+echo $OUTPUT->single_button(admin_searcharea_action_url('indexall'), get_string('searchupdateindex', 'admin'), 'get', $options);
+echo $OUTPUT->single_button(admin_searcharea_action_url('reindexall'), get_string('searchreindexindex', 'admin'), 'get', $options);
+echo $OUTPUT->single_button(admin_searcharea_action_url('deleteall'), get_string('searchdeleteindex', 'admin'), 'get', $options);
+echo $OUTPUT->box_end();
+
+echo html_writer::table($table);
+echo $OUTPUT->footer();
+
+/**
+ * Helper for generating url for management actions.
+ *
+ * @param string $action
+ * @param string $areaid
+ * @return moodle_url
+ */
+function admin_searcharea_action_url($action, $areaid = false) {
+    $params = array('action' => $action, 'sesskey' => sesskey());
+    if ($areaid) {
+        $params['areaid'] = $areaid;
+    }
+    return new moodle_url('/admin/searchareas.php', $params);
+}
index 1beaca5..0711b23 100644 (file)
@@ -40,7 +40,9 @@ if ($hassiteconfig) {
         get_string('requiremodintro', 'admin'), get_string('requiremodintro_desc', 'admin'), 0));
     $ADMIN->add('modsettings', $temp);
 
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('mod') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('mod');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\mod $plugin */
         $plugin->load_settings($ADMIN, 'modsettings', $hassiteconfig);
     }
@@ -50,7 +52,9 @@ if ($hassiteconfig) {
     $temp = new admin_settingpage('manageformats', new lang_string('manageformats', 'core_admin'));
     $temp->add(new admin_setting_manageformats());
     $ADMIN->add('formatsettings', $temp);
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('format') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('format');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\format $plugin */
         $plugin->load_settings($ADMIN, 'formatsettings', $hassiteconfig);
     }
@@ -58,7 +62,9 @@ if ($hassiteconfig) {
     // blocks
     $ADMIN->add('modules', new admin_category('blocksettings', new lang_string('blocks')));
     $ADMIN->add('blocksettings', new admin_page_manageblocks());
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('block') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('block');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\block $plugin */
         $plugin->load_settings($ADMIN, 'blocksettings', $hassiteconfig);
     }
@@ -67,7 +73,9 @@ if ($hassiteconfig) {
     $ADMIN->add('modules', new admin_category('messageoutputs', new lang_string('messageoutputs', 'message')));
     $ADMIN->add('messageoutputs', new admin_page_managemessageoutputs());
     $ADMIN->add('messageoutputs', new admin_page_defaultmessageoutputs());
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('message') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('message');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\message $plugin */
         $plugin->load_settings($ADMIN, 'messageoutputs', $hassiteconfig);
     }
@@ -108,7 +116,9 @@ if ($hassiteconfig) {
     $temp = new admin_externalpage('authtestsettings', get_string('testsettings', 'core_auth'), new moodle_url("/auth/test_settings.php"), 'moodle/site:config', true);
     $ADMIN->add('authsettings', $temp);
 
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('auth') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('auth');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\auth $plugin */
         $plugin->load_settings($ADMIN, 'authsettings', $hassiteconfig);
     }
@@ -122,7 +132,9 @@ if ($hassiteconfig) {
     $temp = new admin_externalpage('enroltestsettings', get_string('testsettings', 'core_enrol'), new moodle_url("/enrol/test_settings.php"), 'moodle/site:config', true);
     $ADMIN->add('enrolments', $temp);
 
-    foreach(core_plugin_manager::instance()->get_plugins_of_type('enrol') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('enrol');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\enrol $plugin */
         $plugin->load_settings($ADMIN, 'enrolments', $hassiteconfig);
     }
@@ -133,7 +145,9 @@ if ($hassiteconfig) {
     $temp = new admin_settingpage('manageeditors', new lang_string('editorsettings', 'editor'));
     $temp->add(new admin_setting_manageeditors());
     $ADMIN->add('editorsettings', $temp);
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('editor') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('editor');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\editor $plugin */
         $plugin->load_settings($ADMIN, 'editorsettings', $hassiteconfig);
     }
@@ -143,7 +157,9 @@ if ($hassiteconfig) {
     $temp = new admin_settingpage('manageantiviruses', new lang_string('antivirussettings', 'antivirus'));
     $temp->add(new admin_setting_manageantiviruses());
     $ADMIN->add('antivirussettings', $temp);
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('antivirus') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('antivirus');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /* @var \core\plugininfo\antivirus $plugin */
         $plugin->load_settings($ADMIN, 'antivirussettings', $hassiteconfig);
     }
@@ -182,7 +198,9 @@ if ($hassiteconfig) {
     }
     $ADMIN->add('filtersettings', $temp);
 
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('filter') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('filter');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\filter $plugin */
         $plugin->load_settings($ADMIN, 'filtersettings', $hassiteconfig);
     }
@@ -283,7 +301,9 @@ if ($hassiteconfig) {
         new lang_string('createrepository', 'repository'), $url, 'moodle/site:config', true));
     $ADMIN->add('repositorysettings', new admin_externalpage('repositoryinstanceedit',
         new lang_string('editrepositoryinstance', 'repository'), $url, 'moodle/site:config', true));
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('repository') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('repository');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\repository $plugin */
         $plugin->load_settings($ADMIN, 'repositorysettings', $hassiteconfig);
     }
@@ -337,7 +357,9 @@ if ($hassiteconfig) {
                         'admin'), new lang_string('configenablewsdocumentation', 'admin', $wsdoclink), false));
     $ADMIN->add('webservicesettings', $temp);
     /// links to protocol pages
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('webservice') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('webservice');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\webservice $plugin */
         $plugin->load_settings($ADMIN, 'webservicesettings', $hassiteconfig);
     }
@@ -409,7 +431,9 @@ if ($hassiteconfig || has_capability('moodle/question:config', $systemcontext))
             get_string('responsehistory', 'question'), '', 0, $hiddenofvisible));
 
     // Settings for particular question types.
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('qtype') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('qtype');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\qtype $plugin */
         $plugin->load_settings($ADMIN, 'qtypesettings', $hassiteconfig);
     }
@@ -421,7 +445,9 @@ if ($hassiteconfig && !empty($CFG->enableplagiarism)) {
     $ADMIN->add('plagiarism', new admin_externalpage('manageplagiarismplugins', new lang_string('manageplagiarism', 'plagiarism'),
         $CFG->wwwroot . '/' . $CFG->admin . '/plagiarism.php'));
 
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('plagiarism') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('plagiarism');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\plagiarism $plugin */
         $plugin->load_settings($ADMIN, 'plagiarism', $hassiteconfig);
     }
@@ -445,6 +471,7 @@ if ($hassiteconfig) {
     }
     if (!empty($pages)) {
         $ADMIN->add('modules', new admin_category('coursereports', new lang_string('coursereports')));
+        core_collator::asort_objects_by_property($pages, 'visiblename');
         foreach ($pages as $page) {
             $ADMIN->add('coursereports', $page);
         }
@@ -468,6 +495,7 @@ foreach (core_component::get_plugin_list('report') as $report => $plugindir) {
 $ADMIN->add('modules', new admin_category('reportplugins', new lang_string('reports')));
 $ADMIN->add('reportplugins', new admin_externalpage('managereports', new lang_string('reportsmanage', 'admin'),
                                                     $CFG->wwwroot . '/' . $CFG->admin . '/reports.php'));
+core_collator::asort_objects_by_property($pages, 'visiblename');
 foreach ($pages as $page) {
     $ADMIN->add('reportplugins', $page);
 }
@@ -500,16 +528,11 @@ if ($hassiteconfig) {
     $temp->add(new admin_setting_configselect('searchengine',
                                 new lang_string('selectsearchengine', 'admin'), '', 'solr', $engines));
 
-    // Enable search areas.
-    $temp->add(new admin_setting_heading('searchareasheading', new lang_string('availablesearchareas', 'admin'), ''));
-    $searchareas = \core_search\manager::get_search_areas_list();
-    foreach ($searchareas as $areaid => $searcharea) {
-        list($componentname, $varname) = $searcharea->get_config_var_name();
-        $temp->add(new admin_setting_configcheckbox($componentname . '/' . $varname . '_enabled', $searcharea->get_visible_name(true),
-            '', 1, 1, 0));
-    }
     $ADMIN->add('searchplugins', $temp);
+    $ADMIN->add('searchplugins', new admin_externalpage('searchareas', new lang_string('searchareas', 'admin'),
+        new moodle_url('/admin/searchareas.php')));
 
+    core_collator::asort_objects_by_property($pages, 'visiblename');
     foreach ($pages as $page) {
         $ADMIN->add('searchplugins', $page);
     }
@@ -523,7 +546,9 @@ if ($hassiteconfig) {
 }
 
 // Now add various admin tools.
-foreach (core_plugin_manager::instance()->get_plugins_of_type('tool') as $plugin) {
+$plugins = core_plugin_manager::instance()->get_plugins_of_type('tool');
+core_collator::asort_objects_by_property($plugins, 'displayname');
+foreach ($plugins as $plugin) {
     /** @var \core\plugininfo\tool $plugin */
     $plugin->load_settings($ADMIN, null, $hassiteconfig);
 }
@@ -534,6 +559,7 @@ if ($hassiteconfig) {
     $ADMIN->add('cache', new admin_externalpage('cacheconfig', new lang_string('cacheconfig', 'cache'), $CFG->wwwroot .'/cache/admin.php'));
     $ADMIN->add('cache', new admin_externalpage('cachetestperformance', new lang_string('testperformance', 'cache'), $CFG->wwwroot . '/cache/testperformance.php'));
     $ADMIN->add('cache', new admin_category('cachestores', new lang_string('cachestores', 'cache')));
+    $ADMIN->locate('cachestores')->set_sorting(true);
     foreach (core_component::get_plugin_list('cachestore') as $plugin => $path) {
         $settingspath = $path.'/settings.php';
         if (file_exists($settingspath)) {
@@ -547,7 +573,9 @@ if ($hassiteconfig) {
 // Add Calendar type settings.
 if ($hassiteconfig) {
     $ADMIN->add('modules', new admin_category('calendartype', new lang_string('calendartypes', 'calendar')));
-    foreach (core_plugin_manager::instance()->get_plugins_of_type('calendartype') as $plugin) {
+    $plugins = core_plugin_manager::instance()->get_plugins_of_type('calendartype');
+    core_collator::asort_objects_by_property($plugins, 'displayname');
+    foreach ($plugins as $plugin) {
         /** @var \core\plugininfo\calendartype $plugin */
         $plugin->load_settings($ADMIN, 'calendartype', $hassiteconfig);
     }
@@ -562,7 +590,9 @@ if ($hassiteconfig) {
 
 // Extend settings for each local plugin. Note that their settings may be in any part of the
 // settings tree and may be visible not only for administrators.
-foreach (core_plugin_manager::instance()->get_plugins_of_type('local') as $plugin) {
+$plugins = core_plugin_manager::instance()->get_plugins_of_type('local');
+core_collator::asort_objects_by_property($plugins, 'displayname');
+foreach ($plugins as $plugin) {
     /** @var \core\plugininfo\local $plugin */
     $plugin->load_settings($ADMIN, null, $hassiteconfig);
 }
index 98fc305..f8a37e7 100644 (file)
@@ -101,6 +101,9 @@ if (empty($options['torun'])) {
 if (extension_loaded('pcntl')) {
     $disabled = explode(',', ini_get('disable_functions'));
     if (!in_array('pcntl_signal', $disabled)) {
+        // Handle interrupts on PHP7.
+        declare(ticks = 1);
+
         pcntl_signal(SIGTERM, "signal_handler");
         pcntl_signal(SIGINT, "signal_handler");
     }
index dfd24fd..e776a1a 100644 (file)
@@ -40,14 +40,18 @@ class course_module_summary_exporter extends \core_competency\external\exporter
     }
 
     protected function get_other_values(renderer_base $output) {
-        $context = $this->related['cm']->context;
+        $cm = $this->related['cm'];
+        $context = $cm->context;
 
-        return array(
-            'id' => $this->related['cm']->id,
-            'name' => external_format_string($this->related['cm']->name, $context->id),
-            'url' => $this->related['cm']->url->out(),
-            'iconurl' => $this->related['cm']->get_icon_url()->out()
+        $values = array(
+            'id' => $cm->id,
+            'name' => external_format_string($cm->name, $context->id),
+            'iconurl' => $cm->get_icon_url()->out()
         );
+        if ($cm->url) {
+            $values['url'] = $cm->url->out();
+        }
+        return $values;
     }
 
 
@@ -60,7 +64,8 @@ class course_module_summary_exporter extends \core_competency\external\exporter
                 'type' => PARAM_TEXT
             ),
             'url' => array(
-                'type' => PARAM_URL
+                'type' => PARAM_URL,
+                'optional' => true,
             ),
             'iconurl' => array(
                 'type' => PARAM_URL
index 4beab69..e700a17 100644 (file)
@@ -40,11 +40,11 @@ $url = new moodle_url('/admin/tool/lp/coursecompetencies.php', $urlparams);
 list($title, $subtitle) = \tool_lp\page_helper::setup_for_course($url, $course);
 
 $output = $PAGE->get_renderer('tool_lp');
+$page = new \tool_lp\output\course_competencies_page($course->id);
+
 echo $output->header();
 echo $output->heading($title);
 
-
-$page = new \tool_lp\output\course_competencies_page($course->id);
 echo $output->render($page);
 
 echo $output->footer();
index 870104e..62b6892 100644 (file)
@@ -36,6 +36,13 @@ function tool_lp_extend_navigation_course($navigation, $course, $coursecontext)
         return;
     }
 
+    // Check access to the course and competencies page.
+    $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
+    $context = context_course::instance($course->id);
+    if (!has_any_capability($capabilities, $context) || !can_access_course($course)) {
+        return;
+    }
+
     // Just a link to course competency.
     $title = get_string('competencies', 'core_competency');
     $path = new moodle_url("/admin/tool/lp/coursecompetencies.php", array('courseid' => $course->id));
index 1dbe965..c111010 100644 (file)
@@ -26,6 +26,7 @@ namespace tool_mobile;
 
 use core_component;
 use core_plugin_manager;
+use context_system;
 
 /**
  * API exposed by tool_mobile
@@ -82,4 +83,35 @@ class api {
         return $pluginsinfo;
     }
 
+    /**
+     * Returns a list of the site public settings, those not requiring authentication.
+     *
+     * @return array with the settings and warnings
+     */
+    public static function get_site_public_settings() {
+        global $CFG, $SITE, $PAGE;
+
+        $context = context_system::instance();
+        // We need this to make work the format text functions.
+        $PAGE->set_context($context);
+
+        $settings = array(
+            'wwwroot' => $CFG->wwwroot,
+            'httpswwwroot' => $CFG->httpswwwroot,
+            'sitename' => external_format_string($SITE->fullname, $context->id, true),
+            'guestlogin' => $CFG->guestloginbutton,
+            'rememberusername' => $CFG->rememberusername,
+            'authloginviaemail' => $CFG->authloginviaemail,
+            'registerauth' => $CFG->registerauth,
+            'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
+            'authinstructions' => format_text($CFG->auth_instructions),
+            'authnoneenabled' => (int) is_enabled_auth('none'),
+            'enablewebservices' => $CFG->enablewebservices,
+            'enablemobilewebservice' => $CFG->enablemobilewebservice,
+            'maintenanceenabled' => $CFG->maintenance_enabled,
+            'maintenancemessage' => format_text($CFG->maintenance_message),
+        );
+        return $settings;
+    }
+
 }
index ca5fb71..148f8a4 100644 (file)
@@ -95,4 +95,54 @@ class external extends external_api {
         );
     }
 
+    /**
+     * Returns description of get_site_public_settings() parameters.
+     *
+     * @return external_function_parameters
+     * @since  Moodle 3.2
+     */
+    public static function get_site_public_settings_parameters() {
+        return new external_function_parameters(array());
+    }
+
+    /**
+     * Returns a list of the site public settings, those not requiring authentication.
+     *
+     * @return array with the settings and warnings
+     * @since  Moodle 3.2
+     */
+    public static function get_site_public_settings() {
+        $result = api::get_site_public_settings();
+        $result['warnings'] = array();
+        return $result;
+    }
+
+    /**
+     * Returns description of get_site_public_settings() result value.
+     *
+     * @return external_description
+     * @since  Moodle 3.2
+     */
+    public static function get_site_public_settings_returns() {
+        return new external_single_structure(
+            array(
+                'wwwroot' => new external_value(PARAM_RAW, 'Site URL.'),
+                'httpswwwroot' => new external_value(PARAM_RAW, 'Site https URL (if httpslogin is enabled).'),
+                'sitename' => new external_value(PARAM_TEXT, 'Site name.'),
+                'guestlogin' => new external_value(PARAM_INT, 'Whether guest login is enabled.'),
+                'rememberusername' => new external_value(PARAM_INT, 'Values: 0 for No, 1 for Yes, 2 for optional.'),
+                'authloginviaemail' => new external_value(PARAM_INT, 'Whether log in via email is enabled.'),
+                'registerauth' => new external_value(PARAM_PLUGIN, 'Authentication method for user registration.'),
+                'forgottenpasswordurl' => new external_value(PARAM_URL, 'Forgotten password URL.'),
+                'authinstructions' => new external_value(PARAM_RAW, 'Authentication instructions.'),
+                'authnoneenabled' => new external_value(PARAM_INT, 'Whether auth none is enabled.'),
+                'enablewebservices' => new external_value(PARAM_INT, 'Whether Web Services are enabled.'),
+                'enablemobilewebservice' => new external_value(PARAM_INT, 'Whether the Mobile service is enabled.'),
+                'maintenanceenabled' => new external_value(PARAM_INT, 'Whether site maintenance is enabled.'),
+                'maintenancemessage' => new external_value(PARAM_RAW, 'Maintenance message.'),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
+
 }
index f7ddd74..20c6c84 100644 (file)
@@ -31,6 +31,16 @@ $functions = array(
         'description' => 'Returns a list of Moodle plugins supporting the mobile app.',
         'type'        => 'read',
         'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
+    ),
+
+    'tool_mobile_get_site_public_settings' => array(
+        'classname'   => 'tool_mobile\external',
+        'methodname'  => 'get_site_public_settings',
+        'description' => 'Returns a list of the site public settings, those not requiring authentication.',
+        'type'        => 'read',
+        'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+        'ajax'          => true,
+        'loginrequired' => false,
     )
 
 );
index 166fe5d..0ee8fc3 100644 (file)
@@ -50,7 +50,49 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $result = external::get_plugins_supporting_mobile();
         $result = external_api::clean_returnvalue(external::get_plugins_supporting_mobile_returns(), $result);
         $this->assertCount(0, $result['warnings']);
-        $this->assertCount(0, $result['plugins']);
+        $this->assertArrayHasKey('plugins', $result);
+        $this->assertTrue(is_array($result['plugins']));
+    }
+
+    public function test_get_site_public_settings() {
+        global $CFG, $SITE;
+
+        $this->resetAfterTest(true);
+        $result = external::get_site_public_settings();
+        $result = external_api::clean_returnvalue(external::get_site_public_settings_returns(), $result);
+
+        // Test default values.
+        $context = context_system::instance();
+        $expected = array(
+            'wwwroot' => $CFG->wwwroot,
+            'httpswwwroot' => $CFG->httpswwwroot,
+            'sitename' => external_format_string($SITE->fullname, $context->id, true),
+            'guestlogin' => $CFG->guestloginbutton,
+            'rememberusername' => $CFG->rememberusername,
+            'authloginviaemail' => $CFG->authloginviaemail,
+            'registerauth' => $CFG->registerauth,
+            'forgottenpasswordurl' => $CFG->forgottenpasswordurl,
+            'authinstructions' => format_text($CFG->auth_instructions),
+            'authnoneenabled' => (int) is_enabled_auth('none'),
+            'enablewebservices' => $CFG->enablewebservices,
+            'enablemobilewebservice' => $CFG->enablemobilewebservice,
+            'maintenanceenabled' => $CFG->maintenance_enabled,
+            'maintenancemessage' => format_text($CFG->maintenance_message),
+            'warnings' => array()
+        );
+        $this->assertEquals($expected, $result);
+
+        // Change a value.
+        set_config('registerauth', 'email');
+        $authinstructions = 'Something with <b>html tags</b>';
+        set_config('auth_instructions', $authinstructions);
+
+        $expected['registerauth'] = 'email';
+        $expected['authinstructions'] = format_text($authinstructions);
+
+        $result = external::get_site_public_settings();
+        $result = external_api::clean_returnvalue(external::get_site_public_settings_returns(), $result);
+        $this->assertEquals($expected, $result);
     }
 
 }
index 0efc6e1..0c57e23 100644 (file)
@@ -23,6 +23,6 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2016052300; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2016052301; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2016051900; // Requires this Moodle version.
 $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
index c11665e..793b11f 100644 (file)
@@ -140,6 +140,10 @@ class eventobservers {
             $subscriptions = subscription_manager::get_subscriptions_by_event($eventobj);
             $idstosend = array();
             foreach ($subscriptions as $subscription) {
+                // Only proceed to fire events and notifications if the subscription is active.
+                if (!subscription_manager::subscription_is_active($subscription)) {
+                    continue;
+                }
                 $starttime = $now - $subscription->timewindow;
                 $starttime = ($starttime > $subscription->lastnotificationsent) ? $starttime : $subscription->lastnotificationsent;
                 if ($subscription->courseid == 0) {
index 6e25f6e..98518e9 100644 (file)
@@ -55,17 +55,26 @@ class subscription {
      * Magic get method.
      *
      * @param string $prop property to get.
-     *
      * @return mixed
      * @throws \coding_exception
      */
     public function __get($prop) {
-        if (property_exists($this->subscription, $prop)) {
+        if (isset($this->subscription->$prop)) {
             return $this->subscription->$prop;
         }
         throw new \coding_exception('Property "' . $prop . '" doesn\'t exist');
     }
 
+    /**
+     * Magic isset method.
+     *
+     * @param string $prop the property to get.
+     * @return bool true if the property is set, false otherwise.
+     */
+    public function __isset($prop) {
+        return property_exists($this->subscription, $prop);
+    }
+
     /**
      * Get a human readable name for instances associated with this subscription.
      *
index 421984c..c4382e7 100644 (file)
@@ -35,6 +35,10 @@ defined('MOODLE_INTERNAL') || die();
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class subscription_manager {
+
+    /** @const Period of time, in days, after which an inactive subscription will be removed completely.*/
+    const INACTIVE_SUBSCRIPTION_LIFESPAN_IN_DAYS = 30;
+
     /**
      * Subscribe a user to a given rule.
      *
@@ -456,4 +460,78 @@ class subscription_manager {
 
         return false;
     }
+
+    /**
+     * Activates a group of subscriptions based on an input array of ids.
+     *
+     * @since 3.2.0
+     * @param array $ids of subscription ids.
+     * @return bool true if the operation was successful, false otherwise.
+     */
+    public static function activate_subscriptions(array $ids) {
+        global $DB;
+        if (!empty($ids)) {
+            list($sql, $params) = $DB->get_in_or_equal($ids);
+            $success = $DB->set_field_select('tool_monitor_subscriptions', 'inactivedate', '0', 'id ' . $sql, $params);
+            return $success;
+        }
+        return false;
+    }
+
+    /**
+     * Deactivates a group of subscriptions based on an input array of ids.
+     *
+     * @since 3.2.0
+     * @param array $ids of subscription ids.
+     * @return bool true if the operation was successful, false otherwise.
+     */
+    public static function deactivate_subscriptions(array $ids) {
+        global $DB;
+        if (!empty($ids)) {
+            $inactivedate = time();
+            list($sql, $params) = $DB->get_in_or_equal($ids);
+            $success = $DB->set_field_select('tool_monitor_subscriptions', 'inactivedate', $inactivedate, 'id ' . $sql,
+                                             $params);
+            return $success;
+        }
+        return false;
+    }
+
+    /**
+     * Deletes subscriptions which have been inactive for a period of time.
+     *
+     * @since 3.2.0
+     * @param int $userid if provided, only this user's stale subscriptions will be deleted.
+     * @return bool true if the operation was successful, false otherwise.
+     */
+    public static function delete_stale_subscriptions($userid = 0) {
+        global $DB;
+        // Get the expiry duration, in days.
+        $cutofftime = strtotime("-" . self::INACTIVE_SUBSCRIPTION_LIFESPAN_IN_DAYS . " days", time());
+
+        if (!empty($userid)) {
+            // Remove any stale subscriptions for the desired user only.
+            $success = $DB->delete_records_select('tool_monitor_subscriptions',
+                                                  'userid = ? AND inactivedate < ? AND inactivedate <> 0',
+                                                  array($userid, $cutofftime));
+
+        } else {
+            // Remove all stale subscriptions.
+            $success = $DB->delete_records_select('tool_monitor_subscriptions',
+                                                  'inactivedate < ? AND inactivedate <> 0',
+                                                  array($cutofftime));
+        }
+        return $success;
+    }
+
+    /**
+     * Check whether a subscription is active.
+     *
+     * @since 3.2.0
+     * @param \tool_monitor\subscription $subscription instance.
+     * @return bool true if the subscription is active, false otherwise.
+     */
+    public static function subscription_is_active(subscription $subscription) {
+        return empty($subscription->inactivedate);
+    }
 }
diff --git a/admin/tool/monitor/classes/task/check_subscriptions.php b/admin/tool/monitor/classes/task/check_subscriptions.php
new file mode 100644 (file)
index 0000000..8262f10
--- /dev/null
@@ -0,0 +1,274 @@
+<?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/>.
+namespace tool_monitor\task;
+use tool_monitor\subscription;
+use tool_monitor\subscription_manager;
+
+/**
+ * Simple task class responsible for activating, deactivating and removing subscriptions.
+ *
+ * Activation/deactivation is managed by looking at the same access rules used to determine whether a user can
+ * subscribe to the rule in the first place.
+ *
+ * Removal occurs when a subscription has been inactive for a period of time exceeding the lifespan, as set by
+ * subscription_manager::get_inactive_subscription_lifespan().
+ *
+ * I.e.
+ *  - Activation:   If a user can subscribe currently, then an existing subscription should be made active.
+ *  - Deactivation: If a user cannot subscribe currently, then an existing subscription should be made inactive.
+ *  - Removal:      If a user has a subscription that has been inactive for longer than the prescribed period, then
+ *                  delete the subscription entirely.
+ *
+ * @since      3.2.0
+ * @package    tool_monitor
+ * @copyright  2016 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class check_subscriptions extends \core\task\scheduled_task {
+
+    /** @var array 1d static cache, indexed by userid, storing whether or not the user has been fully set up.*/
+    protected $userssetupcache = array();
+
+    /** @var array 2d static cache, indexed by courseid and userid, storing whether a user can access the course with
+     *  the 'tool/monitor:subscribe' capability.
+     */
+    protected $courseaccesscache = array();
+
+    /**
+     * Get a descriptive name for this task.
+     *
+     * @since 3.2.0
+     * @return string name of the task.
+     */
+    public function get_name() {
+        return get_string('taskchecksubscriptions', 'tool_monitor');
+    }
+
+    /**
+     * Checks all course-level rule subscriptions and activates/deactivates based on current course access.
+     *
+     * The ordering of checks within the task is important for optimisation purposes. The aim is to be able to make a decision
+     * about whether to activate/deactivate each subscription without making unnecessary checks. The ordering roughly follows the
+     * context model, starting with system and user checks and moving down to course and course-module only when necessary.
+     *
+     * For example, if the user is suspended, then any active subscription is made inactive right away. I.e. there is no need to
+     * check site-level, course-level or course-module-level permissions. Likewise, if a subscriptions is site-level, there is no
+     * need to check course-level and course-module-level permissions.
+     *
+     * The task performs the following checks, in this order:
+     * 1. Check for a suspended user, breaking if suspended.
+     * 2. Check for an incomplete (not set up) user, breaking if not fully set up.
+     * 3. Check for the required capability in the relevant context, breaking if the capability is not found.
+     * 4. Check whether the subscription is site-context, breaking if true.
+     * 5. Check whether the user has course access, breaking only if the subscription is not also course-module-level.
+     * 6. Check whether the user has course-module access.
+     *
+     * @since 3.2.0
+     */
+    public function execute() {
+        global $DB;
+
+        if (!get_config('tool_monitor', 'enablemonitor')) {
+            return; // The tool is disabled. Nothing to do.
+        }
+
+        $toactivate   = array(); // Store the ids of subscriptions to be activated upon completion.
+        $todeactivate = array(); // Store the ids of subscriptions to be deactivated upon completion.
+
+        // Resultset rows are ordered by userid and courseid to work nicely with get_fast_modinfo() caching.
+        $sql = "SELECT u.id AS userid, u.firstname AS userfirstname, u.lastname AS userlastname, u.suspended AS usersuspended,
+                       u.email AS useremail, c.visible as coursevisible, c.cacherev as coursecacherev, s.courseid AS subcourseid,
+                       s.userid AS subuserid, s.cmid AS subcmid, s.inactivedate AS subinactivedate, s.id AS subid
+                  FROM {user} u
+                  JOIN {tool_monitor_subscriptions} s ON (s.userid = u.id)
+             LEFT JOIN {course} c ON (c.id = s.courseid)
+                 WHERE u.id = s.userid
+              ORDER BY s.userid, s.courseid";
+        $rs = $DB->get_recordset_sql($sql);
+
+        foreach ($rs as $row) {
+            // Create skeleton records from the result. This should be enough to use in subsequent access calls and avoids DB hits.
+            $sub = $this->get_subscription_from_rowdata($row);
+            $sub = new subscription($sub);
+            if (!isset($user) || $user->id != $sub->userid) {
+                $user= $this->get_user_from_rowdata($row);
+            }
+            if ((!isset($course) || $course->id != $sub->courseid) && !empty($sub->courseid)) {
+                $course = $this->get_course_from_rowdata($row);
+            }
+
+            // The user is suspended at site level, so deactivate any active subscriptions.
+            if ($user->suspended) {
+                if (subscription_manager::subscription_is_active($sub)) {
+                    $todeactivate[] = $sub->id;
+                }
+                continue;
+            }
+
+            // Is the user fully set up? As per require_login on the subscriptions page.
+            if (!$this->is_user_setup($user)) {
+                if (subscription_manager::subscription_is_active($sub)) {
+                    $todeactivate[] = $sub->id;
+                }
+                continue;
+            }
+
+            // Determine the context, based on the subscription course id.
+            $sitelevelsubscription = false;
+            if (empty($sub->courseid)) {
+                $context = \context_system::instance();
+                $sitelevelsubscription = true;
+            } else {
+                $context = \context_course::instance($sub->courseid);
+            }
+
+            // Check capability in the context.
+            if (!has_capability('tool/monitor:subscribe', $context, $user)) {
+                if (subscription_manager::subscription_is_active($sub)) {
+                    $todeactivate[] = $sub->id;
+                }
+                continue;
+            }
+
+            // If the subscription is site-level, then we've run all the checks required to make an access decision.
+            if ($sitelevelsubscription) {
+                if (!subscription_manager::subscription_is_active($sub)) {
+                    $toactivate[] = $sub->id;
+                }
+                continue;
+            }
+
+            // Check course access.
+            if (!$this->user_can_access_course($user, $course, 'tool/monitor:subscribe')) {
+                if (subscription_manager::subscription_is_active($sub)) {
+                    $todeactivate[] = $sub->id;
+                }
+                continue;
+            }
+
+            // If the subscription has no course module relationship.
+            if (empty($sub->cmid)) {
+                if (!subscription_manager::subscription_is_active($sub)) {
+                    $toactivate[] = $sub->id;
+                }
+                continue;
+            }
+
+            // Otherwise, check the course module info. We use the same checks as on the subscription page.
+            $modinfo = get_fast_modinfo($course, $sub->userid);
+            $cm = $modinfo->get_cm($sub->cmid);
+            if (!$cm || !$cm->uservisible || !$cm->available) {
+                if (subscription_manager::subscription_is_active($sub)) {
+                    $todeactivate[] = $sub->id;
+                }
+                continue;
+            }
+
+            // The course module is available and visible, so make a decision.
+            if (!subscription_manager::subscription_is_active($sub)) {
+                $toactivate[] = $sub->id;
+            }
+        }
+        $rs->close();
+
+        // Activate/deactivate/delete relevant subscriptions.
+        subscription_manager::activate_subscriptions($toactivate);
+        subscription_manager::deactivate_subscriptions($todeactivate);
+        subscription_manager::delete_stale_subscriptions();
+    }
+
+    /**
+     * Determines whether a user is fully set up, using cached results where possible.
+     *
+     * @since 3.2.0
+     * @param \stdClass $user the user record.
+     * @return bool true if the user is fully set up, false otherwise.
+     */
+    protected function is_user_setup($user) {
+        if (!isset($this->userssetupcache[$user->id])) {
+            $this->userssetupcache[$user->id] = !user_not_fully_set_up($user);
+        }
+        return $this->userssetupcache[$user->id];
+    }
+
+    /**
+     * Determines a user's access to a course with a given capability, using cached results where possible.
+     *
+     * @since 3.2.0
+     * @param \stdClass $user the user record.
+     * @param \stdClass $course the course record.
+     * @param string $capability the capability to check.
+     * @return bool true if the user can access the course with the specified capability, false otherwise.
+     */
+    protected function user_can_access_course($user, $course, $capability) {
+        if (!isset($this->courseaccesscache[$course->id][$user->id][$capability])) {
+            $this->courseaccesscache[$course->id][$user->id][$capability] = can_access_course($course, $user, $capability, true);
+        }
+        return $this->courseaccesscache[$course->id][$user->id][$capability];
+    }
+
+    /**
+     * Returns a partial subscription record, created from properties of the supplied recordset row object.
+     * Intended to return a minimal record for specific use within this class and in subsequent access control calls only.
+     *
+     * @since 3.2.0
+     * @param \stdClass $rowdata the row object.
+     * @return \stdClass a partial subscription record.
+     */
+    protected function get_subscription_from_rowdata($rowdata) {
+        $sub = new \stdClass();
+        $sub->id = $rowdata->subid;
+        $sub->userid = $rowdata->subuserid;
+        $sub->courseid = $rowdata->subcourseid;
+        $sub->cmid = $rowdata->subcmid;
+        $sub->inactivedate = $rowdata->subinactivedate;
+        return $sub;
+    }
+
+    /**
+     * Returns a partial course record, created from properties of the supplied recordset row object.
+     * Intended to return a minimal record for specific use within this class and in subsequent access control calls only.
+     *
+     * @since 3.2.0
+     * @param \stdClass $rowdata the row object.
+     * @return \stdClass a partial course record.
+     */
+    protected function get_course_from_rowdata($rowdata) {
+        $course = new \stdClass();
+        $course->id = $rowdata->subcourseid;
+        $course->visible = $rowdata->coursevisible;
+        $course->cacherev = $rowdata->coursecacherev;
+        return $course;
+    }
+
+    /**
+     * Returns a partial user record, created from properties of the supplied recordset row object.
+     * Intended to return a minimal record for specific use within this class and in subsequent access control calls only.
+     *
+     * @since 3.2.0
+     * @param \stdClass $rowdata the row object.
+     * @return \stdClass a partial user record.
+     */
+    protected function get_user_from_rowdata($rowdata) {
+        $user = new \stdClass();
+        $user->id = $rowdata->userid;
+        $user->firstname = $rowdata->userfirstname;
+        $user->lastname = $rowdata->userlastname;
+        $user->email = $rowdata->useremail;
+        $user->suspended = $rowdata->usersuspended;
+        return $user;
+    }
+}
index 7729199..9a3f1a1 100644 (file)
@@ -38,6 +38,7 @@
         <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="User id of the subscriber"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Timestamp of when this subscription was created"/>
         <FIELD NAME="lastnotificationsent" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Timestamp of the time when a notification was last sent for this subscription."/>
+        <FIELD NAME="inactivedate" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
index e70c344..20f324d 100644 (file)
@@ -32,5 +32,14 @@ $tasks = array(
         'day' => '*',
         'dayofweek' => '*',
         'month' => '*'
+    ),
+    array(
+        'classname' => 'tool_monitor\task\check_subscriptions',
+        'blocking' => 0,
+        'minute' => 'R',
+        'hour' => 'R',
+        'day' => '*',
+        'dayofweek' => '*',
+        'month' => '*'
     )
 );
index 9f8e53b..aaea29a 100644 (file)
@@ -62,5 +62,20 @@ function xmldb_tool_monitor_upgrade($oldversion) {
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2016052305) {
+
+        // Define field inactivedate to be added to tool_monitor_subscriptions.
+        $table = new xmldb_table('tool_monitor_subscriptions');
+        $field = new xmldb_field('inactivedate', XMLDB_TYPE_INTEGER, '10', null, true, null, 0, 'lastnotificationsent');
+
+        // Conditionally launch add field inactivedate.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Monitor savepoint reached.
+        upgrade_plugin_savepoint(true, 2016052305, 'tool', 'monitor');
+    }
+
     return true;
 }
index c0fbf7f..fb47717 100644 (file)
@@ -99,4 +99,5 @@ $string['subhelp'] = 'Subscription details';
 $string['subhelp_help'] = 'This subscription listens for when the event \'{$a->eventname}\' has been triggered in \'{$a->moduleinstance}\' {$a->frequency} time(s) in {$a->minutes} minute(s).';
 $string['subscribeto'] = 'Subscribe to rule "{$a}"';
 $string['taskcleanevents'] = 'Removes any unnecessary event monitor events';
+$string['taskchecksubscriptions'] = 'Activate/deactivate invalid rule subscriptions';
 $string['unsubscribe'] = 'Unsubscribe';
diff --git a/admin/tool/monitor/tests/subscription_test.php b/admin/tool/monitor/tests/subscription_test.php
new file mode 100644 (file)
index 0000000..3548c7f
--- /dev/null
@@ -0,0 +1,65 @@
+<?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/>.
+defined('MOODLE_INTERNAL') || exit();
+
+/**
+ * Unit tests for the subscription class.
+ * @since 3.2.0
+ *
+ * @package    tool_monitor
+ * @category   test
+ * @copyright  2016 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_monitor_subscription_testcase extends advanced_testcase {
+
+    /**
+     * @var \tool_monitor\subscription $subscription object.
+     */
+    private $subscription;
+
+    /**
+     * Test set up.
+     */
+    public function setUp() {
+        $this->resetAfterTest(true);
+
+        // Create the mock subscription.
+        $sub = new stdClass();
+        $sub->id = 100;
+        $sub->name = 'My test rule';
+        $sub->courseid = 20;
+        $this->subscription = $this->getMock('\tool_monitor\subscription',null, array($sub));
+    }
+
+    /**
+     * Test for the magic __isset method.
+     */
+    public function test_magic_isset() {
+        $this->assertEquals(true, isset($this->subscription->name));
+        $this->assertEquals(true, isset($this->subscription->courseid));
+        $this->assertEquals(false, isset($this->subscription->ruleid));
+    }
+
+    /**
+     * Test for the magic __get method.
+     */
+    public function test_magic_get() {
+        $this->assertEquals(20, $this->subscription->courseid);
+        $this->setExpectedException('coding_exception');
+        $this->subscription->ruleid;
+    }
+}
diff --git a/admin/tool/monitor/tests/task_check_subscriptions_test.php b/admin/tool/monitor/tests/task_check_subscriptions_test.php
new file mode 100644 (file)
index 0000000..b526784
--- /dev/null
@@ -0,0 +1,364 @@
+<?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/>.
+defined('MOODLE_INTERNAL') || exit();
+
+/**
+ * Unit tests for the tool_monitor clean events task.
+ * @since 3.2.0
+ *
+ * @package    tool_monitor
+ * @category   test
+ * @copyright  2016 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_monitor_task_check_subscriptions_testcase extends advanced_testcase {
+
+    private $course;
+    private $user;
+    private $rule;
+    private $subscription;
+    private $teacherrole;
+    private $studentrole;
+
+    /**
+     * Test set up.
+     */
+    public function setUp() {
+        global $DB;
+        set_config('enablemonitor', 1, 'tool_monitor');
+        $this->resetAfterTest(true);
+
+        // All tests defined herein need a user, course, rule and subscription, so set these up.
+        $this->user = $this->getDataGenerator()->create_user();
+        $this->course = $this->getDataGenerator()->create_course();
+
+        $rule = new stdClass();
+        $rule->userid = 2; // Rule created by admin.
+        $rule->courseid = $this->course->id;
+        $rule->plugin = 'mod_book';
+        $rule->eventname = '\mod_book\event\course_module_viewed';
+        $rule->timewindow = 500;
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+        $this->rule = $monitorgenerator->create_rule($rule);
+
+        $sub = new stdClass();
+        $sub->courseid = $this->course->id;
+        $sub->userid = $this->user->id;
+        $sub->ruleid = $this->rule->id;
+        $this->subscription = $monitorgenerator->create_subscription($sub);
+
+        // Also set up a student and a teacher role for use in some tests.
+        $this->teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+        $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
+    }
+
+    /**
+     * Reloads the subscription object from the DB.
+     *
+     * @return void.
+     */
+    private function reload_subscription() {
+        global $DB;
+        $sub = $DB->get_record('tool_monitor_subscriptions', array('id' => $this->subscription->id));
+        $this->subscription = new \tool_monitor\subscription($sub);
+    }
+
+    /**
+     * Test to confirm the task is named correctly.
+     */
+    public function test_task_name() {
+        $task = new \tool_monitor\task\check_subscriptions();
+        $this->assertEquals(get_string('taskchecksubscriptions', 'tool_monitor'), $task->get_name());
+    }
+
+    /**
+     * Test to confirm that site level subscriptions are activated and deactivated according to system capabilities.
+     */
+    public function test_site_level_subscription() {
+        // Create a site level subscription.
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+        $sub = new stdClass();
+        $sub->userid = $this->user->id;
+        $sub->ruleid = $this->rule->id;
+        $this->subscription = $monitorgenerator->create_subscription($sub);
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should be inactive as the user doesn't have the capability. Pass in the id only to refetch the data.
+        $this->reload_subscription();
+        $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Now, assign the user as a teacher role at system context.
+        $this->getDataGenerator()->role_assign($this->teacherrole->id, $this->user->id, context_system::instance());
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should be active now. Pass in the id only to refetch the data.
+        $this->reload_subscription();
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+    }
+
+    /**
+     * Test to confirm that if the module is disabled, no changes are made to active subscriptions.
+     */
+    public function test_module_disabled() {
+        set_config('enablemonitor', 0, 'tool_monitor');
+
+        // Subscription should be active to start with.
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Run the task. Note, we never enrolled the user.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should still be active. Pass in the id only to refetch the data.
+        $this->reload_subscription();
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+    }
+
+    /**
+     * Test to confirm an active, valid subscription stays active once the scheduled task is run.
+     */
+    public function test_active_unaffected() {
+        // Enrol the user as a teacher. This role should have the required capability.
+        $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->teacherrole->id);
+
+        // Subscription should be active to start with.
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should still be active. Pass in the id only to refetch the data.
+        $this->reload_subscription();
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+    }
+
+    /**
+     * Test to confirm that a subscription for a user without an enrolment to the course is made inactive.
+     */
+    public function test_course_enrolment() {
+        // Subscription should be active until deactivated by the scheduled task. Remember, by default the test setup
+        // doesn't enrol the user, so the first run of the task should deactivate it.
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should NOT be active. Pass in the id only to refetch the data.
+        $this->reload_subscription();
+        $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Enrol the user.
+        $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->teacherrole->id);
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // Subscription should now be active again.
+        $this->reload_subscription();
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+    }
+
+    /**
+     * Test to confirm that subscriptions for enrolled users without the required capability are made inactive.
+     */
+    public function test_enrolled_user_with_no_capability() {
+        // Enrol the user. By default, students won't have the required capability.
+        $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->studentrole->id);
+
+        // The subscription should be active to start with. Pass in the id only to refetch the data.
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should NOT be active. Pass in the id only to refetch the data.
+        $this->reload_subscription();
+        $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+    }
+
+    /**
+     * Test to confirm that subscriptions for users who fail can_access_course(), are deactivated.
+     */
+    public function test_can_access_course() {
+        // Enrol the user as a teacher. This role should have the required capability.
+        $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->teacherrole->id);
+
+        // Strip the ability to see hidden courses, so we'll fail the check_subscriptions->user_can_access_course call.
+        $context = \context_course::instance($this->course->id);
+        assign_capability('moodle/course:viewhiddencourses', CAP_PROHIBIT, $this->teacherrole->id, $context);
+
+        // Subscription should be active to start with.
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Hide the course.
+        course_change_visibility($this->course->id, false);
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should be inactive. Pass in the id only to refetch the data.
+        $this->reload_subscription();
+        $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+    }
+
+    /**
+     * Test to confirm that subscriptions for enrolled users who don't have CM access, are deactivated.
+     */
+    public function test_cm_access() {
+        // Enrol the user as a student but grant to ability to subscribe. Students cannot view hidden activities.
+        $context = \context_course::instance($this->course->id);
+        assign_capability('tool/monitor:subscribe', CAP_ALLOW, $this->studentrole->id, $context);
+        $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->studentrole->id);
+
+        // Generate a course module.
+        $book = $this->getDataGenerator()->create_module('book', array('course' => $this->course->id));
+
+        // And add a subscription to it.
+        $sub = new stdClass();
+        $sub->courseid = $this->course->id;
+        $sub->userid = $this->user->id;
+        $sub->ruleid = $this->rule->id;
+        $sub->cmid = $book->cmid;
+        $monitorgenerator = $this->getDataGenerator()->get_plugin_generator('tool_monitor');
+        $this->subscription = $monitorgenerator->create_subscription($sub);
+
+        // The subscription should be active to start with. Pass in the id only to refetch the data.
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should still be active. Pass in the id only to refetch the data.
+        $this->reload_subscription();
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Make the course module invisible, which should in turn make the subscription inactive.
+        set_coursemodule_visible($book->cmid, false);
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should NOT be active. Pass in the id only to refetch the data.
+        $this->reload_subscription();
+        $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Make the course module visible again.
+        set_coursemodule_visible($book->cmid, true);
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should be active. Pass in the id only to refetch the data.
+        $this->reload_subscription();
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+    }
+
+    /**
+     * Test to confirm that long term inactive subscriptions are removed entirely.
+     */
+    public function test_stale_subscription_removal() {
+        global $DB;
+        // Manually set the inactivedate to 1 day older than the limit allowed.
+        $daysold = 1 + \tool_monitor\subscription_manager::INACTIVE_SUBSCRIPTION_LIFESPAN_IN_DAYS;
+
+        $inactivedate = strtotime("-$daysold days", time());
+        $DB->set_field('tool_monitor_subscriptions', 'inactivedate', $inactivedate, array('id' => $this->subscription->id));
+
+        // Subscription should be inactive to start with.
+        $this->reload_subscription();
+        $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // Subscription should now not exist at all.
+        $this->assertEquals(false, $DB->record_exists('tool_monitor_subscriptions', array('id' => $this->subscription->id)));
+    }
+
+    /**
+     * Test to confirm that subscriptions for a partially set up user are deactivated.
+     */
+    public function test_user_not_fully_set_up() {
+        global $DB;
+
+        // Enrol the user as a teacher.
+        $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->teacherrole->id);
+
+        // The subscription should be active to start.
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Unset the user's email address, so we fail the check_subscriptions->is_user_setup() call.
+        $DB->set_field('user', 'email', '', array('id' => $this->user->id));
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should now be inactive.
+        $this->reload_subscription();
+        $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+    }
+
+    /**
+     * Test to confirm that a suspended user's subscriptions are deactivated properly.
+     */
+    public function test_suspended_user() {
+        global $DB;
+
+        // Enrol the user as a teacher. This role should have the required capability.
+        $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id, $this->teacherrole->id);
+
+        // Subscription should be active to start with.
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Suspend the user.
+        $DB->set_field('user', 'suspended', '1', array('id' => $this->user->id));
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should now be inactive.
+        $this->reload_subscription();
+        $this->assertEquals(false, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+
+        // Unsuspend the user.
+        $DB->set_field('user', 'suspended', '0', array('id' => $this->user->id));
+
+        // Run the task.
+        $task = new \tool_monitor\task\check_subscriptions();
+        $task->execute();
+
+        // The subscription should now be active again.
+        $this->reload_subscription();
+        $this->assertEquals(true, \tool_monitor\subscription_manager::subscription_is_active($this->subscription));
+    }
+}
index c078165..9e5a5a3 100644 (file)
@@ -26,6 +26,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2016052300;     // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2016052305;     // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2016051900;     // Requires this Moodle version.
 $plugin->component = 'tool_monitor'; // Full name of the plugin (used for diagnostics).
index 5900d32..99d0ce5 100644 (file)
@@ -122,7 +122,7 @@ class edit_field extends XMLDBAction {
             $o.= '      <input type="hidden" name ="name" value="' .  s($field->getName()) .'" />';
             $o.= '      <tr valign="top"><td>Name:</td><td colspan="2">' . s($field->getName()) . '</td></tr>';
         } else {
-            $o.= '      <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="30" maxlength="30" id="name" value="' . s($field->getName()) . '" /></td></tr>';
+            $o.= '      <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="'.xmldb_field::NAME_MAX_LENGTH.'" maxlength="'.xmldb_field::NAME_MAX_LENGTH.'" id="name" value="' . s($field->getName()) . '" /></td></tr>';
         }
         // XMLDB field comment
         $o.= '      <tr valign="top"><td><label for="comment" accesskey="c">Comment:</label></td><td colspan="2"><textarea name="comment" rows="3" cols="80" id="comment">' . s($field->getComment()) . '</textarea></td></tr>';
index 7725c9e..a72b1c5 100644 (file)
@@ -113,7 +113,7 @@ class edit_index extends XMLDBAction {
         if ($structure->getIndexUses($table->getName(), $index->getName())) {
             $disabled = ' disabled="disabled " ';
         }
-        $o.= '      <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="30" id="name"' . $disabled . ' value="' . s($index->getName()) . '" /></td></tr>';
+        $o.= '      <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="'.xmldb_field::NAME_MAX_LENGTH.'" id="name"' . $disabled . ' value="' . s($index->getName()) . '" /></td></tr>';
         // XMLDB key comment
         $o.= '      <tr valign="top"><td><label for="comment" accesskey="c">Comment:</label></td><td colspan="2"><textarea name="comment" rows="3" cols="80" id="comment">' . s($index->getComment()) . '</textarea></td></tr>';
         // xmldb_index Type
index c1d1c3b..6e32acf 100644 (file)
@@ -113,7 +113,7 @@ class edit_key extends XMLDBAction {
         if ($structure->getKeyUses($table->getName(), $key->getName())) {
             $disabled = ' disabled="disabled " ';
         }
-        $o.= '      <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="30" id="name"' . $disabled . ' value="' . s($key->getName()) . '" /></td></tr>';
+        $o.= '      <tr valign="top"><td><label for="name" accesskey="n">Name:</label></td><td colspan="2"><input name="name" type="text" size="'.xmldb_field::NAME_MAX_LENGTH.'" id="name"' . $disabled . ' value="' . s($key->getName()) . '" /></td></tr>';
         // XMLDB key comment
         $o.= '      <tr valign="top"><td><label for="comment" accesskey="c">Comment:</label></td><td colspan="2"><textarea name="comment" rows="3" cols="80" id="comment">' . s($key->getComment()) . '</textarea></td></tr>';
         // xmldb_key Type
index 4199a6c..5c2e7b9 100644 (file)
@@ -129,7 +129,7 @@ class edit_table extends XMLDBAction {
         if ($structure->getTableUses($table->getName())) {
             $o.= '      <tr valign="top"><td>Name:</td><td><input type="hidden" name ="name" value="' . s($table->getName()) . '" />' . s($table->getName()) .'</td></tr>';
         } else {
-            $o.= '      <tr valign="top"><td><label for="name" accesskey="p">Name:</label></td><td><input name="name" type="text" size="28" maxlength="28" id="name" value="' . s($table->getName()) . '" /></td></tr>';
+            $o.= '      <tr valign="top"><td><label for="name" accesskey="p">Name:</label></td><td><input name="name" type="text" size="'.xmldb_table::NAME_MAX_LENGTH.'" maxlength="'.xmldb_table::NAME_MAX_LENGTH.'" id="name" value="' . s($table->getName()) . '" /></td></tr>';
         }
         $o.= '      <tr valign="top"><td><label for="comment" accesskey="c">Comment:</label></td><td><textarea name="comment" rows="3" cols="80" id="comment">' . s($table->getComment()) . '</textarea></td></tr>';
         $o.= '      <tr valign="top"><td>&nbsp;</td><td><input type="submit" value="' .$this->str['change'] . '" /></td></tr>';
index ebebaa1..b1fa091 100644 (file)
@@ -302,20 +302,27 @@ class auth_plugin_db extends auth_plugin_base {
 
             // Find obsolete users.
             if (count($userlist)) {
-                list($notin_sql, $params) = $DB->get_in_or_equal($userlist, SQL_PARAMS_NAMED, 'u', false);
-                $params['authtype'] = $this->authtype;
-                $sql = "SELECT u.*
+                $remove_users = array();
+                // All the drivers can cope with chunks of 10,000. See line 4491 of lib/dml/tests/dml_est.php
+                $userlistchunks = array_chunk($userlist , 10000);
+                foreach($userlistchunks as $userlistchunk) {
+                    list($notin_sql, $params) = $DB->get_in_or_equal($userlistchunk, SQL_PARAMS_NAMED, 'u', false);
+                    $params['authtype'] = $this->authtype;
+                    $sql = "SELECT u.id, u.username
                           FROM {user} u
                          WHERE u.auth=:authtype AND u.deleted=0 AND u.mnethostid=:mnethostid $suspendselect AND u.username $notin_sql";
+                    $params['mnethostid'] = $CFG->mnet_localhost_id;
+                    $remove_users = $remove_users + $DB->get_records_sql($sql, $params);
+                }
             } else {
-                $sql = "SELECT u.*
+                $sql = "SELECT u.id, u.username
                           FROM {user} u
                          WHERE u.auth=:authtype AND u.deleted=0 AND u.mnethostid=:mnethostid $suspendselect";
                 $params = array();
                 $params['authtype'] = $this->authtype;
+                $params['mnethostid'] = $CFG->mnet_localhost_id;
+                $remove_users = $DB->get_records_sql($sql, $params);
             }
-            $params['mnethostid'] = $CFG->mnet_localhost_id;
-            $remove_users = $DB->get_records_sql($sql, $params);
 
             if (!empty($remove_users)) {
                 $trace->output(get_string('auth_dbuserstoremove','auth_db', count($remove_users)));
@@ -358,12 +365,20 @@ class auth_plugin_db extends auth_plugin_base {
 
             // Only go ahead if we actually have fields to update locally.
             if (!empty($updatekeys)) {
-                list($in_sql, $params) = $DB->get_in_or_equal($userlist, SQL_PARAMS_NAMED, 'u', true);
-                $params['authtype'] = $this->authtype;
-                $sql = "SELECT u.id, u.username
+                $update_users = array();
+                // All the drivers can cope with chunks of 10,000. See line 4491 of lib/dml/tests/dml_est.php
+                $userlistchunks = array_chunk($userlist , 10000);
+                foreach($userlistchunks as $userlistchunk) {
+                    list($in_sql, $params) = $DB->get_in_or_equal($userlistchunk, SQL_PARAMS_NAMED, 'u', true);
+                    $params['authtype'] = $this->authtype;
+                    $params['mnethostid'] = $CFG->mnet_localhost_id;
+                    $sql = "SELECT u.id, u.username
                           FROM {user} u
-                         WHERE u.auth=:authtype AND u.deleted=0 AND u.username {$in_sql}";
-                if ($update_users = $DB->get_records_sql($sql, $params)) {
+                         WHERE u.auth = :authtype AND u.deleted = 0 AND u.mnethostid = :mnethostid AND u.username {$in_sql}";
+                    $update_users = $update_users + $DB->get_records_sql($sql, $params);
+                }
+
+                if ($update_users) {
                     $trace->output("User entries to update: ".count($update_users));
 
                     foreach ($update_users as $user) {
index b11daa4..043111e 100644 (file)
@@ -4166,11 +4166,8 @@ class api {
             return array();
         }
 
-        $params = array(
-            'usercompetencyid' => $usercompetency->get_id(),
-            'contextid' => context_course::instance($courseid)->id
-        );
-        return evidence::get_records($params, $sort, $order, $skip, $limit);
+        $context = context_course::instance($courseid);
+        return evidence::get_records_for_usercompetency($usercompetency->get_id(), $context, $sort, $order, $skip, $limit);
     }
 
     /**
index 79a8cc5..1e12a36 100644 (file)
@@ -88,7 +88,7 @@ class course_competency_settings extends persistent {
     public static function can_read($courseid) {
         $context = context_course::instance($courseid);
 
-        $capabilities = array('moodle/competency:coursecompetencyview');
+        $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
 
         return has_any_capability($capabilities, $context);
     }
index ed3445d..40e1448 100644 (file)
@@ -284,4 +284,52 @@ class evidence extends persistent {
         return has_capability('moodle/competency:evidencedelete', context_user::instance($userid));
     }
 
+    /**
+     * Load a list of records in a context for a user competency.
+     *
+     * @param int $usercompetencyid The id of the user competency.
+     * @param context $context Context to filter the evidence list.
+     * @param string $sort The field from the evidence table to sort on.
+     * @param string $order The sort direction
+     * @param int $skip Limitstart.
+     * @param int $limit Number of rows to return.
+     *
+     * @return \core_competency\persistent[]
+     */
+    public static function get_records_for_usercompetency($usercompetencyid,
+                                                          \context $context,
+                                                          $sort = '',
+                                                          $order = 'ASC',
+                                                          $skip = 0,
+                                                          $limit = 0) {
+        global $DB;
+
+        $params = array(
+            'usercompid' => $usercompetencyid,
+            'path' => $context->path . '/%',
+            'contextid' => $context->id
+        );
+
+        if (!empty($sort)) {
+            $sort = ' ORDER BY e.' . $sort . ' ' . $order . ', e.id ASC';
+        } else {
+            $sort = ' ORDER BY e.id ASC';
+        }
+
+        $sql = 'SELECT e.*
+                  FROM {' . static::TABLE . '} e
+                  JOIN {context} c ON c.id = e.contextid
+                 WHERE (c.path LIKE :path OR c.id = :contextid)
+                   AND e.usercompetencyid = :usercompid
+                 ' . $sort;
+        $records = $DB->get_records_sql($sql, $params, $skip, $limit);
+        $instances = array();
+
+        foreach ($records as $record) {
+            $newrecord = new static(0, $record);
+            array_push($instances, $newrecord);
+        }
+        return $instances;
+    }
+
 }
index 9e5c4d3..cc96253 100644 (file)
@@ -2579,6 +2579,47 @@ class core_competency_api_testcase extends advanced_testcase {
         $this->assertEquals(null, $ev4->get_actionuserid());
     }
 
+    public function test_list_evidence_in_course() {
+        global $SITE;
+
+        $this->resetAfterTest(true);
+        $dg = $this->getDataGenerator();
+        $lpg = $dg->get_plugin_generator('core_competency');
+        $u1 = $dg->create_user();
+        $course = $dg->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $this->setAdminUser();
+        $f = $lpg->create_framework();
+        $c = $lpg->create_competency(array('competencyframeworkid' => $f->get_id()));
+        $c2 = $lpg->create_competency(array('competencyframeworkid' => $f->get_id()));
+        $cc = api::add_competency_to_course($course->id, $c->get_id());
+        $cc2 = api::add_competency_to_course($course->id, $c2->get_id());
+
+        $pagegenerator = $this->getDataGenerator()->get_plugin_generator('mod_page');
+        $page = $pagegenerator->create_instance(array('course' => $course->id));
+
+        $cm = get_coursemodule_from_instance('page', $page->id);
+        $cmcontext = context_module::instance($cm->id);
+        // Add the competency to the course module.
+        $ccm = api::add_competency_to_course_module($cm, $c->get_id());
+
+        // Now add the evidence to the course.
+        $evidence1 = api::add_evidence($u1->id, $c->get_id(), $coursecontext->id, \core_competency\evidence::ACTION_LOG,
+            'invaliddata', 'error');
+
+        $result = api::list_evidence_in_course($u1->id, $course->id, $c->get_id());
+        $this->assertEquals($result[0]->get_id(), $evidence1->get_id());
+
+        // Now add the evidence to the course module.
+        $evidence2 = api::add_evidence($u1->id, $c->get_id(), $cmcontext->id, \core_competency\evidence::ACTION_LOG,
+            'invaliddata', 'error');
+
+        $result = api::list_evidence_in_course($u1->id, $course->id, $c->get_id(), 'timecreated', 'ASC');
+        $this->assertEquals($evidence1->get_id(), $result[0]->get_id());
+        $this->assertEquals($evidence2->get_id(), $result[1]->get_id());
+    }
+
     public function test_list_course_modules_using_competency() {
         global $SITE;
 
index ee734d8..73e59f1 100644 (file)
@@ -1,4 +1,9 @@
 {
+    "name": "moodle/moodle",
+    "license": "GPL-3.0",
+    "description": "Moodle - the world's open source learning platform",
+    "type": "project",
+    "homepage": "https://moodle.org",
     "require-dev": {
         "phpunit/phpunit": "4.8.*",
         "phpunit/dbUnit": "1.4.*",
index b348aa1..3458723 100644 (file)
@@ -195,9 +195,12 @@ class core_course_external extends external_api {
                 $sectionvalues['id'] = $section->id;
                 $sectionvalues['name'] = get_section_name($course, $section);
                 $sectionvalues['visible'] = $section->visible;
+
+                $options = (object) array('noclean' => true);
                 list($sectionvalues['summary'], $sectionvalues['summaryformat']) =
                         external_format_text($section->summary, $section->summaryformat,
-                                $context->id, 'course', 'section', $section->id);
+                                $context->id, 'course', 'section', $section->id, $options);
+                $sectionvalues['section'] = $section->section;
                 $sectioncontents = array();
 
                 //for each module of the section
@@ -325,6 +328,7 @@ class core_course_external extends external_api {
                     'visible' => new external_value(PARAM_INT, 'is the section visible', VALUE_OPTIONAL),
                     'summary' => new external_value(PARAM_RAW, 'Section description'),
                     'summaryformat' => new external_format_value('summary'),
+                    'section' => new external_value(PARAM_INT, 'Section number inside the course', VALUE_OPTIONAL),
                     'modules' => new external_multiple_structure(
                             new external_single_structure(
                                 array(
@@ -429,8 +433,8 @@ class core_course_external extends external_api {
 
             $courseinfo = array();
             $courseinfo['id'] = $course->id;
-            $courseinfo['fullname'] = $course->fullname;
-            $courseinfo['shortname'] = $course->shortname;
+            $courseinfo['fullname'] = external_format_string($course->fullname, $context->id);
+            $courseinfo['shortname'] = external_format_string($course->shortname, $context->id);
             $courseinfo['displayname'] = external_format_string(get_course_display_name_for_list($course), $context->id);
             $courseinfo['categoryid'] = $course->category;
             list($courseinfo['summary'], $courseinfo['summaryformat']) =
@@ -1548,7 +1552,7 @@ class core_course_external extends external_api {
                     }
 
                     if (isset($value)) {
-                        $conditions[$key] = $crit['value'];
+                        $conditions[$key] = $value;
                         $wheres[] = $key . " = :" . $key;
                     }
                 }
index 9ad5d73..71372e7 100644 (file)
@@ -234,6 +234,16 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         $this->assertEquals(1, count($categories));
 
+        // Same query, but forcing a parameters clean.
+        $categories = core_course_external::get_categories(array(
+            array('key' => 'id', 'value' => "$category1->id"),
+            array('key' => 'idnumber', 'value' => $category1->idnumber),
+            array('key' => 'name', 'value' => $category1->name . "<br/>"),
+            array('key' => 'visible', 'value' => '1')), 0);
+        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
+
+        $this->assertEquals(1, count($categories));
+
         // Retrieve categories from parent.
         $categories = core_course_external::get_categories(array(
             array('key' => 'parent', 'value' => $category3->id)), 1);
@@ -521,7 +531,9 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         $generatedcourses = array();
         $coursedata['idnumber'] = 'idnumbercourse1';
-        $coursedata['fullname'] = 'Course 1 for PHPunit test';
+        // Adding tags here to check that format_string is applied.
+        $coursedata['fullname'] = '<b>Course 1 for PHPunit test</b>';
+        $coursedata['shortname'] = '<b>Course 1 for PHPunit test</b>';
         $coursedata['summary'] = 'Course 1 description';
         $coursedata['summaryformat'] = FORMAT_MOODLE;
         $course1  = self::getDataGenerator()->create_course($coursedata);
@@ -551,14 +563,16 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(2, count($courses));
 
         foreach ($courses as $course) {
+            $coursecontext = context_course::instance($course['id']);
             $dbcourse = $generatedcourses[$course['id']];
             $this->assertEquals($course['idnumber'], $dbcourse->idnumber);
-            $this->assertEquals($course['fullname'], $dbcourse->fullname);
-            $this->assertEquals($course['displayname'], get_course_display_name_for_list($dbcourse));
+            $this->assertEquals($course['fullname'], external_format_string($dbcourse->fullname, $coursecontext->id));
+            $this->assertEquals($course['displayname'], external_format_string(get_course_display_name_for_list($dbcourse),
+                $coursecontext->id));
             // Summary was converted to the HTML format.
             $this->assertEquals($course['summary'], format_text($dbcourse->summary, FORMAT_MOODLE, array('para' => false)));
             $this->assertEquals($course['summaryformat'], FORMAT_HTML);
-            $this->assertEquals($course['shortname'], $dbcourse->shortname);
+            $this->assertEquals($course['shortname'], external_format_string($dbcourse->shortname, $coursecontext->id));
             $this->assertEquals($course['categoryid'], $dbcourse->category);
             $this->assertEquals($course['format'], $dbcourse->format);
             $this->assertEquals($course['showgrades'], $dbcourse->showgrades);
@@ -686,6 +700,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
      * @return array A list with the course object and course modules objects
      */
     private function prepare_get_course_contents_test() {
+        global $DB;
         $course  = self::getDataGenerator()->create_course();
         $forumdescription = 'This is the forum description';
         $forum = $this->getDataGenerator()->create_module('forum',
@@ -710,6 +725,10 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
         $this->assignUserCapability('moodle/course:update', $context->id, $roleid);
 
+        $conditions = array('course' => $course->id, 'section' => 2);
+        $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
+        rebuild_course_cache($course->id, true);
+
         return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm);
     }
 
@@ -749,10 +768,14 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
             }
         }
         $this->assertEquals(2, $testexecuted);
+        $this->assertEquals(0, $firstsection['section']);
 
         // Check that the only return section has the 5 created modules.
         $this->assertCount(4, $firstsection['modules']);
         $this->assertCount(1, $lastsection['modules']);
+        $this->assertEquals(2, $lastsection['section']);
+        $this->assertContains('<iframe', $lastsection['summary']);
+        $this->assertContains('</iframe>', $lastsection['summary']);
 
         try {
             $sections = core_course_external::get_course_contents($course->id,
diff --git a/course/upgrade.txt b/course/upgrade.txt
new file mode 100644 (file)
index 0000000..8e73dd9
--- /dev/null
@@ -0,0 +1,7 @@
+This files describes API changes in /course/*,
+information provided here is intended especially for developers.
+
+=== 3.2 ===
+
+ * External function core_course_external::get_course_contents now returns the section's number in the course (new section field).
+
index 2bd50f0..6bb4865 100644 (file)
@@ -7,7 +7,7 @@
     require_once($CFG->libdir.'/completionlib.php');
 
     $id          = optional_param('id', 0, PARAM_INT);
-    $name        = optional_param('name', '', PARAM_RAW);
+    $name        = optional_param('name', '', PARAM_TEXT);
     $edit        = optional_param('edit', -1, PARAM_BOOL);
     $hide        = optional_param('hide', 0, PARAM_INT);
     $show        = optional_param('show', 0, PARAM_INT);
index 8db19d9..711cc69 100644 (file)
@@ -300,7 +300,7 @@ class core_enrol_external extends external_api {
         $params = self::validate_parameters(self::get_users_courses_parameters(), array('userid'=>$userid));
 
         $courses = enrol_get_users_courses($params['userid'], true, 'id, shortname, fullname, idnumber, visible,
-                   summary, summaryformat, format, showgrades, lang, enablecompletion');
+                   summary, summaryformat, format, showgrades, lang, enablecompletion, category');
         $result = array();
 
         foreach ($courses as $course) {
@@ -323,12 +323,24 @@ class core_enrol_external extends external_api {
 
             list($course->summary, $course->summaryformat) =
                 external_format_text($course->summary, $course->summaryformat, $context->id, 'course', 'summary', null);
-
-            $result[] = array('id' => $course->id, 'shortname' => $course->shortname, 'fullname' => $course->fullname,
-                'idnumber' => $course->idnumber, 'visible' => $course->visible, 'enrolledusercount' => $enrolledusercount,
-                'summary' => $course->summary, 'summaryformat' => $course->summaryformat, 'format' => $course->format,
-                'showgrades' => $course->showgrades, 'lang' => $course->lang, 'enablecompletion' => $course->enablecompletion
-                );
+            $course->fullname = external_format_string($course->fullname, $context->id);
+            $course->shortname = external_format_string($course->shortname, $context->id);
+
+            $result[] = array(
+                'id' => $course->id,
+                'shortname' => $course->shortname,
+                'fullname' => $course->fullname,
+                'idnumber' => $course->idnumber,
+                'visible' => $course->visible,
+                'enrolledusercount' => $enrolledusercount,
+                'summary' => $course->summary,
+                'summaryformat' => $course->summaryformat,
+                'format' => $course->format,
+                'showgrades' => $course->showgrades,
+                'lang' => $course->lang,
+                'enablecompletion' => $course->enablecompletion,
+                'category' => $course->category
+            );
         }
 
         return $result;
@@ -355,7 +367,8 @@ class core_enrol_external extends external_api {
                     'showgrades' => new external_value(PARAM_BOOL, 'true if grades are shown, otherwise false', VALUE_OPTIONAL),
                     'lang'      => new external_value(PARAM_LANG, 'forced course language', VALUE_OPTIONAL),
                     'enablecompletion' => new external_value(PARAM_BOOL, 'true if completion is enabled, otherwise false',
-                                                                VALUE_OPTIONAL)
+                                                                VALUE_OPTIONAL),
+                    'category' => new external_value(PARAM_INT, 'course category id', VALUE_OPTIONAL),
                 )
             )
         );
index d2d6336..83a9b13 100644 (file)
@@ -202,8 +202,10 @@ class helper {
             'connecttimeout' => 5
         );
 
-        if (!$iconfiles = $fs->create_file_from_url($filerecord, $url, $urlparams)) {
-            return self::PROFILE_IMAGE_UPDATE_FAILED;
+        try {
+            $fs->create_file_from_url($filerecord, $url, $urlparams);
+        } catch (\file_exception $e) {
+            return get_string($e->errorcode, $e->module, $e->a);
         }
 
         $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false);
index 21ad04d..a7d35ad 100644 (file)
@@ -363,6 +363,8 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
         $this->resetAfterTest(true);
 
         $coursedata1 = array(
+            'fullname'         => '<b>Course 1</b>',                // Adding tags here to check that external_format_string works.
+            'shortname'         => '<b>Course 1</b>',               // Adding tags here to check that external_format_string works.
             'summary'          => 'Lightwork Course 1 description',
             'summaryformat'    => FORMAT_MOODLE,
             'lang'             => 'en',
@@ -401,6 +403,8 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
 
         // Check there are no differences between $course1 properties and course values returned by the webservice
         // only for those fields listed in the $coursedata1 array.
+        $course1->fullname = external_format_string($course1->fullname, $contexts[$course1->id]->id);
+        $course1->shortname = external_format_string($course1->shortname, $contexts[$course1->id]->id);
         foreach ($enrolledincourses as $courseenrol) {
             if ($courseenrol['id'] == $course1->id) {
                 foreach ($coursedata1 as $fieldname => $value) {
index ca0586b..4dc90ca 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /enrol/* - plugins,
 information provided here is intended especially for developers.
 
+=== 3.2 ===
+
+* External function core_enrol_external::get_users_courses now return the category id as an additional optional field.
+
 === 3.1 ===
 
 * core_enrol_external::get_enrolled_users now supports two additional parameters for ordering: sortby and sortdirection.
diff --git a/grade/amd/build/edittree_index.min.js b/grade/amd/build/edittree_index.min.js
new file mode 100644 (file)
index 0000000..b960b82
Binary files /dev/null and b/grade/amd/build/edittree_index.min.js differ
diff --git a/grade/amd/src/edittree_index.js b/grade/amd/src/edittree_index.js
new file mode 100644 (file)
index 0000000..9316f84
--- /dev/null
@@ -0,0 +1,127 @@
+// 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/>.
+
+/**
+ * Enhance the gradebook tree setup with various facilities.
+ *
+ * @module     core_grades/edittree_index
+ * @package    core_grades
+ * @copyright  2016 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+], function($) {
+    /**
+     * Enhance the edittree functionality.
+     *
+     * @method edittree
+     */
+    var edittree = function() {
+        // Watch items and toggle the move menu accordingly.
+        $('body').on('change', '.itemselect.ignoredirty', edittree.checkMoveMenuState);
+
+        // Watch for the 'All' and 'None' links.
+        $('body').on('click', '[data-action="grade_edittree-index-bulkselect"]', edittree.toggleAllSelectItems);
+
+        // Watch for the weight override checkboxes.
+        $('body').on('change', '.weightoverride', edittree.toggleWeightInput);
+
+        // Watch changes to the bulk move menu and submit.
+        $('#menumoveafter').on('change', function() {
+            var form = $(this).closest('form'),
+                bulkmove = form.find('#bulkmoveinput');
+
+            bulkmove.val(1);
+            form.submit();
+        });
+
+        // CHeck the initial state of the move menu.
+        edittree.checkMoveMenuState();
+    };
+
+    /**
+     * Toggle the weight input field based on its checkbox.
+     *
+     * @method toggleWeightInput
+     * @param {EventFacade} e
+     * @private
+     */
+    edittree.toggleWeightInput = function(e) {
+        e.preventDefault();
+        var node = $(this),
+            row = node.closest('tr');
+
+        $('input[name="weight_' + row.data('itemid') + '"]').prop('disabled', !node.prop('checked'));
+    };
+
+    /**
+     * Toggle all select boxes on or off.
+     *
+     * @method toggleAllSelectItems
+     * @param {EventFacade} e
+     * @private
+     */
+    edittree.toggleAllSelectItems = function(e) {
+        e.preventDefault();
+
+        var node = $(this),
+            row = node.closest('tr');
+        $('.' + row.data('category') + ' .itemselect').prop('checked', node.data('checked'));
+
+        edittree.checkMoveMenuState();
+    };
+
+    /**
+     * Get the move menu.
+     *
+     * @method getMoveMenu
+     * @private
+     * @return {jQuery}
+     */
+    edittree.getMoveMenu = function() {
+        return $('#menumoveafter');
+    };
+
+    /**
+     * Check whether any checkboxes are ticked.
+     *
+     * @method checkMoveMenuState
+     * @private
+     * @return {Boolean}
+     */
+    edittree.checkMoveMenuState = function() {
+        var menu = edittree.getMoveMenu();
+        if (!menu.length) {
+            return false;
+        }
+
+        var selected;
+        $('.itemselect').each(function() {
+            selected = $(this).prop('checked');
+
+            // Return early if any are checked.
+            return !selected;
+        });
+
+        menu.prop('disabled', !selected);
+
+        return selected;
+    };
+
+    return /** @alias module:core_grades/edittree_index */ {
+        enhance: edittree
+    };
+});
diff --git a/grade/edit/tree/functions.js b/grade/edit/tree/functions.js
deleted file mode 100644 (file)
index 2da2f01..0000000
+++ /dev/null
@@ -1,259 +0,0 @@
-/**
- * Toggles the selection checkboxes of all grade items children of the given eid (a category id)
- */
-function togglecheckboxes(event, args) {
-YUI().use('yui2-dom', 'yui2-element', function (Y) {
-
-    var rows = Y.YUI2.util.Dom.getElementsByClassName(args.eid);
-
-    for (var i = 0; i < rows.length; i++) {
-        var element = new Y.YUI2.util.Element(rows[i]);
-        var checkboxes = element.getElementsByClassName('itemselect');
-        if (checkboxes[0]) {
-            checkboxes[0].checked=args.check;
-        }
-    }
-
-    toggleCategorySelector();
-
-});
-}
-
-function toggle_advanced_columns() {
-YUI().use('yui2-dom', function (Y) {
-
-    var advEls = Y.YUI2.util.Dom.getElementsByClassName("advanced");
-    var shownAdvEls = Y.YUI2.util.Dom.getElementsByClassName("advancedshown");
-
-    for (var i = 0; i < advEls.length; i++) {
-        Y.YUI2.util.Dom.replaceClass(advEls[i], "advanced", "advancedshown");
-    }
-
-    for (var i = 0; i < shownAdvEls.length; i++) {
-        Y.YUI2.util.Dom.replaceClass(shownAdvEls[i], "advancedshown", "advanced");
-    }
-
-});
-}
-
-/**
- * Check if any of the grade item checkboxes is ticked. If yes, enable the dropdown. Otherwise, disable it
- */
-function toggleCategorySelector() {
-YUI().use('yui2-dom', function (Y) {
-
-    var menumoveafter = document.getElementById('menumoveafter');
-    if (!menumoveafter) {
-        return;
-    }
-
-    var itemboxes = Y.YUI2.util.Dom.getElementsByClassName('itemselect');
-    for (var i = 0; i < itemboxes.length; i++) {
-        if (itemboxes[i].checked) {
-            menumoveafter.disabled = false;
-            return true;
-        }
-    }
-    menumoveafter.disabled = 'disabled';
-
-});
-}
-
-function submit_bulk_move(e, args) {
-    document.getElementById('bulkmoveinput').value = 1;
-    document.getElementById('gradetreeform').submit();
-}
-
-function update_category_aggregation(e, args) {
-    var selectmenu = e.target;
-    window.location = 'index.php?id='+args.courseid+'&category='+args.category+'&aggregationtype='+selectmenu.get('value')+'&sesskey='+args.sesskey;
-}
-
-/**
- * The weight override checkboxes toggle the disabled status of their associated weight fields.
- */
-YUI().use('node', 'delegate', function(Y) {
-    Y.on('domready', function() {
-        Y.delegate('click', function(e) {
-            var t = e.currentTarget,
-                itemid = t.get('id').split('_')[1];
-            Y.one('input[name=weight_' + itemid + ']').set('disabled', t.get('checked') ? false : true);
-        }, Y.config.doc.body, 'input.weightoverride');
-    });
-});
-
-/* TODO: finish and rewrite for YUI3...
-Y.YUI2.namespace('grade_edit_tree');
-
-(function() {
-    var Dom = Y.YUI2.util.Dom;
-    var DDM = Y.YUI2.util.DragDropMgr;
-    var Event = Y.YUI2.util.Event;
-    var gretree = Y.YUI2.grade_edit_tree;
-
-    gretree.DDApp = {
-
-        init: function() {
-
-            var edit_tree_table = Dom.get('grade_edit_tree_table');
-            var i;
-            var item_rows = edit_tree_table.getElementsByClassName('item', 'tr');
-            var category_rows = edit_tree_table.getElementsByClassName('category', 'tr');
-
-            new Y.YUI2.util.DDTarget('grade_edit_tree_table');
-
-            for (i = 0; i < item_rows.length; i++) {
-                if (!Dom.hasClass(item_rows[i],'categoryitem')) {
-                    new gretree.DDList(item_rows[i]);
-                }
-            }
-
-            for (i = 0; i < category_rows.length; i++) {
-                if (!Dom.hasClass(category_rows[i],'coursecategory')) {
-                    // Find the cell that spans rows for this category
-                    var rowspancell = category_rows[i].getElementsByClassName('name', 'td');
-                    var rowspan = parseInt(rowspancell[0].previousSibling.rowSpan) + 1;
-                    var rows = Array(rowspan);
-                    var lastRow = category_rows[i];
-
-                    for (var j = 0; j < rowspan; j++) {
-                        rows[j] = lastRow;
-                        lastRow = lastRow.nextSibling;
-                    }
-
-                    new gretree.DDList(rows);
-                }
-            }
-
-            Y.YUI2.util.Event.on("showButton", "click", this.showOrder);
-            Y.YUI2.util.Event.on("switchButton", "click", this.switchStyles);
-        },
-
-        showOrder: function() {
-            var parseTable = function(table, title) {
-                var items = table.getElementsByTagName('tr');
-                var out = title + ": ";
-
-                for (i = 0; i < items.length; i++) {
-                    out += items[i].id + ' ';
-                }
-                return out;
-            };
-
-            var table = Dom.get('grade_edit_tree_table');
-            alert(parseTable(table, "Grade edit tree table"));
-        },
-
-        switchStyles: function() {
-            Dom.get('grade_edit_tree_table').className = 'draglist_alt';
-        }
-    };
-
-    gretree.DDList = function(id, sGroup, config) {
-
-        gretree.DDList.superclass.constructor.call(this, id, sGroup, config);
-        this.logger =  this.logger || Y.YUI2;
-        var el = this.getDragEl();
-        Dom.setStyle(el, 'opacity', 0.67);
-
-        this.goingUp = false;
-        this.lastY = 0;
-    };
-
-    Y.YUI2.extend(gretree.DDList, Y.YUI2.util.DDProxy, {
-
-        startDrag: function(x, y) {
-            this.logger.log(this.id + ' startDrag');
-
-            // Make the proxy look like the source element
-            var dragEl = this.getDragEl();
-            var clickEl = this.getEl();
-
-            Dom.setStyle(clickEl, 'visibility', 'hidden');
-
-            dragEl.innerHTML = clickEl.innerHTML;
-
-            Dom.setStyle(dragEl, 'color', Dom.getStyle(clickEl, 'color'));
-            Dom.setStyle(dragEl, 'backgroundColor', Dom.getStyle(clickEl, 'backgroundColor'));
-            Dom.setStyle(dragEl, 'border', '2px solid gray');
-        },
-
-        endDrag: function(e) {
-            this.logger.log(this.id + ' endDrag');
-            var srcEl = this.getEl();
-            var proxy = this.getDragEl();
-
-            // Show the proxy element and adnimate it to the src element's location
-            Dom.setStyle(proxy, 'visibility', '');
-            var a = new Y.YUI2.util.Motion(proxy, { points: { to: Dom.getXY(srcEl) } }, 0.2, Y.YUI2.util.Easing.easeOut);
-            var proxyid = proxy.id;
-            var thisid = this.id;
-
-            // Hide the proxy and show the source element when finished with the animation
-            a.onComplete.subscribe(function() {
-                Dom.setStyle(proxyid, 'visibility', 'hidden');
-                Dom.setStyle(thisid, 'visibility', '');
-            });
-
-            a.animate();
-        },
-
-        onDragDrop: function(e, id) {
-            this.logger.log(this.id + ' dragDrop');
-
-            // If there is one drop interaction, the tr was dropped either on the table, or it was dropped on the current location of the source element
-
-            if (DDM.interactionInfo.drop.length === 1) {
-                // The position of the cursor at the time of the drop (Y.YUI2.util.Point)
-                var pt = DDM.interactionInfo.point;
-
-                // The region occupied by the source element at the time of the drop
-                var region = DDM.interactionInfo.sourceRegion;
-
-                // Check to see if we are over the source element's location. We will append to the bottom of the list once we are sure it was a drop in the negative space
-                if (!region.intersect(pt)) {
-                    var destEl = Dom.get(id);
-                    var destDD = DDM.getDDById(id);
-                    destEl.appendChild(this.getEl());
-                    destDD.isEmpty = false;
-                    DDM.refreshCache();
-                }
-            }
-        },
-
-        onDrag: function(e) {
-
-            // Keep track of the direction of the drag for use during onDragOver
-            var y = Event.getPageY(e);
-
-            if (y < this.lastY) {
-                this.goingUp = true;
-            } else if (y > this.lastY) {
-                this.goingUp = false;
-            }
-
-            this.lastY = y;
-        },
-
-        onDragOver: function(e, id) {
-            var srcEl = this.getEl();
-            var destEl = Dom.get(id);
-
-            // We are only concerned with tr items, we ignore the dragover notifications for the table
-            if (destEl.nodeName.toLowerCase() == 'tr') {
-                var orig_p = srcEl.parentNode;
-                var p = destEl.parentNode;
-
-                if (this.goingup) {
-                    p.insertBefore(srcEl, destEl); // insert above
-                } else {
-                    p.insertBefore(srcEl, destEl.nextSibling); // insert below
-                }
-
-                DDM.refreshCache();
-            }
-        }
-    });
-    // Y.YUI2.util.Event.onDOMReady(gretree.DDApp.init, gretree.DDApp, true); // Uncomment this line when dragdrop is fully implemented
-})();
-*/
\ No newline at end of file
index 8384259..0257798 100644 (file)
@@ -45,8 +45,7 @@ require_login($course);
 $context = context_course::instance($course->id);
 require_capability('moodle/grade:manage', $context);
 
-// todo $PAGE->requires->js_module() should be used here instead
-$PAGE->requires->js('/grade/edit/tree/functions.js');
+$PAGE->requires->js_call_amd('grades/edittree_index', 'enhance');
 
 /// return tracking object
 $gpr = new grade_plugin_return(array('type'=>'edit', 'plugin'=>'tree', 'courseid'=>$courseid));
@@ -266,7 +265,6 @@ if (!$moving && count($grade_edit_tree->categories) > 1) {
     $attributes = array('id'=>'menumoveafter', 'class' => 'ignoredirty singleselect');
     echo html_writer::label(get_string('moveselectedto', 'grades'), 'menumoveafter');
     echo html_writer::select($grade_edit_tree->categories, 'moveafter', '', array(''=>'choosedots'), $attributes);
-    $OUTPUT->add_action_handler(new component_action('change', 'submit_bulk_move'), 'menumoveafter');
     echo '<div id="noscriptgradetreeform" class="hiddenifjs">
             <input type="submit" value="'.get_string('go').'" />
           </div>';
index bf4a4a6..8b9e101 100644 (file)
@@ -295,6 +295,8 @@ class grade_edit_tree {
             $row = new html_table_row();
             $row->id = 'grade-item-' . $eid;
             $row->attributes['class'] = $courseclass . ' category ' . $dimmed;
+            $row->attributes['data-category'] = $eid;
+            $row->attributes['data-itemid'] = $category->get_grade_item()->id;
             foreach ($rowclasses as $class) {
                 $row->attributes['class'] .= ' ' . $class;
             }
@@ -309,9 +311,14 @@ class grade_edit_tree {
 
             foreach ($this->columns as $column) {
                 if (!($this->moving && $column->hide_when_moving)) {
-                    $row->cells[] = $column->get_category_cell($category, $levelclass, array('id' => $id,
-                        'name' => $object->name, 'level' => $level, 'actions' => $actions,
-                        'moveaction' => $moveaction, 'eid' => $eid));
+                    $row->cells[] = $column->get_category_cell($category, $levelclass, [
+                        'id' => $id,
+                        'name' => $object->name,
+                        'level' => $level,
+                        'actions' => $actions,
+                        'moveaction' => $moveaction,
+                        'eid' => $eid,
+                    ]);
                 }
             }
 
@@ -344,6 +351,7 @@ class grade_edit_tree {
             $gradeitemrow = new html_table_row();
             $gradeitemrow->id = 'grade-item-' . $eid;
             $gradeitemrow->attributes['class'] = $categoryitemclass . ' item ' . $dimmed;
+            $gradeitemrow->attributes['data-itemid'] = $object->id;
             foreach ($rowclasses as $class) {
                 $gradeitemrow->attributes['class'] .= ' ' . $class;
             }
@@ -392,20 +400,35 @@ class grade_edit_tree {
         $str = '';
 
         if ($aggcoef == 'aggregationcoefweight' || $aggcoef == 'aggregationcoef' || $aggcoef == 'aggregationcoefextraweight') {
-            return '<label class="accesshide" for="weight_'.$item->id.'">'.
-                get_string('extracreditvalue', 'grades', $itemname).'</label>'.
-                '<input type="text" size="6" id="weight_'.$item->id.'" name="weight_'.$item->id.'"
-                value="'.grade_edit_tree::format_number($item->aggregationcoef).'" />';
+            return '<label class="accesshide" for="weight_'.$item->id.'">' .
+                get_string('extracreditvalue', 'grades', $itemname).'</label>' .
+                html_writer::empty_tag('input', [
+                    'type'          => 'text',
+                    'size'          => 6,
+                    'id'            => 'weight_' . $item->id,
+                    'name'          => 'weight_' . $item->id,
+                    'value'         => self::format_number($item->aggregationcoef),
+                ]);
+
         } else if ($aggcoef == 'aggregationcoefextraweightsum') {
 
             $checkboxname = 'weightoverride_' . $item->id;
             $checkboxlbl = html_writer::tag('label', get_string('overrideweightofa', 'grades', $itemname),
                 array('for' => $checkboxname, 'class' => 'accesshide'));
-            $checkbox = html_writer::empty_tag('input', array('name' => $checkboxname,
-                'type' => 'hidden', 'value' => 0));
-            $checkbox .= html_writer::empty_tag('input', array('name' => $checkboxname,
-                'type' => 'checkbox', 'value' => 1, 'id' => $checkboxname, 'class' => 'weightoverride',
-                'checked' => ($item->weightoverride ? 'checked' : null)));
+            $checkbox = html_writer::empty_tag('input', [
+                'name'          => $checkboxname,
+                'type'          => 'hidden',
+                'value'         => 0,
+            ]);
+
+            $checkbox .= html_writer::empty_tag('input', [
+                'name' => $checkboxname,
+                'type' => 'checkbox',
+                'value' => 1,
+                'id' => $checkboxname,
+                'class' => 'weightoverride',
+                'checked' => ($item->weightoverride ? 'checked' : null),
+            ]);
 
             $name = 'weight_' . $item->id;
             $hiddenlabel = html_writer::tag(
@@ -854,15 +877,20 @@ class grade_edit_tree_column_select extends grade_edit_tree_column {
     }
 
     public function get_category_cell($category, $levelclass, $params) {
-        global $OUTPUT;
         if (empty($params['eid'])) {
             throw new Exception('Array key (eid) missing from 3rd param of grade_edit_tree_column_select::get_category_cell($category, $levelclass, $params)');
         }
-        $selectall  = new action_link(new moodle_url('#'), get_string('all'), new component_action('click', 'togglecheckboxes', array('eid' => $params['eid'], 'check' => true)));
-        $selectnone = new action_link(new moodle_url('#'), get_string('none'), new component_action('click', 'togglecheckboxes', array('eid' => $params['eid'], 'check' => false)));
+        $selectall = html_writer::link('#', get_string('all'), [
+            'data-action' => 'grade_edittree-index-bulkselect',
+            'data-checked' => true,
+        ]);
+        $selectnone = html_writer::link('#', get_string('none'), [
+            'data-action' => 'grade_edittree-index-bulkselect',
+            'data-checked' => false,
+        ]);
 
         $categorycell = parent::get_category_cell($category, $levelclass, $params);
-        $categorycell->text = $OUTPUT->render($selectall) . ' / ' . $OUTPUT->render($selectnone);
+        $categorycell->text = $selectall . ' / ' . $selectnone;
         return $categorycell;
     }
 
@@ -876,7 +904,7 @@ class grade_edit_tree_column_select extends grade_edit_tree_column {
             $itemcell->text = '<label class="accesshide" for="select_'.$params['eid'].'">'.
                 get_string('select', 'grades', $item->itemname).'</label>
                 <input class="itemselect ignoredirty" type="checkbox" name="select_'.$params['eid'].'" id="select_'.$params['eid'].
-                '" onchange="toggleCategorySelector();"/>'; // TODO: convert to YUI handler
+                '"/>';
         }
         return $itemcell;
     }
index 71e2c5d..ed2ae2d 100644 (file)
@@ -70,7 +70,7 @@ $string['guidestatus'] = 'Current marking guide status';
 $string['hidemarkerdesc'] = 'Hide marker criterion descriptions';
 $string['hidestudentdesc'] = 'Hide student criterion descriptions';
 $string['insertcomment'] = 'Insert frequently used comment';
-$string['maxscore'] = 'Maximum mark';
+$string['maxscore'] = 'Maximum score';
 $string['name'] = 'Name';
 $string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.';
 $string['pluginname'] = 'Marking guide';
index f368c14..7c90524 100644 (file)
@@ -57,7 +57,7 @@ class behat_gradingform_guide extends behat_base {
      * @param TableNode $guide
      */
     public function i_define_the_following_marking_guide(TableNode $guide) {
-        $steptableinfo = '| Criterion name | Description for students | Description for markers | Maximum mark |';
+        $steptableinfo = '| Criterion name | Description for students | Description for markers | Maximum score |';
 
         if ($criteria = $guide->getHash()) {
             $addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_guide'));
@@ -92,7 +92,7 @@ class behat_gradingform_guide extends behat_base {
                 $this->set_guide_field_value($criterionroot . '[descriptionmarkers]', $criterion['Description for markers']);
 
                 // Set the field value for the Max score field.
-                $this->set_guide_field_value($criterionroot . '[maxscore]', $criterion['Maximum mark']);
+                $this->set_guide_field_value($criterionroot . '[maxscore]', $criterion['Maximum score']);
             }
         }
     }
index abe994a..f043693 100644 (file)
@@ -29,10 +29,10 @@ Feature: Marking guides can be created and edited
       | Name        | Assignment 1 marking guide     |
       | Description | Marking guide test description |
     And I define the following marking guide:
-      | Criterion name    | Description for students         | Description for markers         | Maximum mark |
-      | Guide criterion A | Guide A description for students | Guide A description for markers | 30           |
-      | Guide criterion B | Guide B description for students | Guide B description for markers | 30           |
-      | Guide criterion C | Guide C description for students | Guide C description for markers | 40           |
+      | Criterion name    | Description for students         | Description for markers         | Maximum score |
+      | Guide criterion A | Guide A description for students | Guide A description for markers | 30            |
+      | Guide criterion B | Guide B description for students | Guide B description for markers | 30            |
+      | Guide criterion C | Guide C description for students | Guide C description for markers | 40            |
     And I define the following frequently used comments:
       | Comment 1 |
       | Comment 2 |
index 1c0da1e..2983874 100644 (file)
@@ -1039,7 +1039,8 @@ function print_grade_page_head($courseid, $active_type, $active_plugin=null,
             if (isset($user)) {
                 $output = $OUTPUT->context_header(
                         array(
-                            'heading' => fullname($user),
+                            'heading' => html_writer::link(new moodle_url('/user/view.php', array('id' => $user->id,
+                                'course' => $courseid)), fullname($user)),
                             'user' => $user,
                             'usercontext' => context_user::instance($user->id)
                         ), 2
index 6a7209d..4bdcf41 100644 (file)
@@ -62,10 +62,10 @@ date_default_timezone_set(@date_default_timezone_get());
 @ini_set('display_errors', '1');
 
 // Check that PHP is of a sufficient version.
-if (version_compare(phpversion(), '5.4.4') < 0) {
+if (version_compare(phpversion(), '5.6.5') < 0) {
     $phpversion = phpversion();
     // do NOT localise - lang strings would not work here and we CAN not move it after installib
-    echo "Moodle 2.7 or later requires at least PHP 5.4.4 (currently using version $phpversion).<br />";
+    echo "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).<br />";
     echo "Please upgrade your server software or install older Moodle version.";
     die;
 }
index caa1978..ec7a094 100644 (file)
@@ -30,6 +30,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['cannotcreatedboninstall'] = '<p>Kan ikke oprette databasen.</p> <p>Den specificerede database eksisterer ikke og brugeren har ikke tilladelse til at oprette den.</p> <p>Administrator bør verificere databasekonfigurationen.</p>';
 $string['cannotcreatelangdir'] = 'Kan ikke oprette sprogmappe';
 $string['cannotcreatetempdir'] = 'Kan ikke oprette temp-mappe';
 $string['cannotdownloadcomponents'] = 'Kan ikke downloade komponenter';
index 92cfce4..e390683 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'Sprog';
+$string['moodlelogo'] = 'Moodlelogo';
 $string['next'] = 'Næste';
 $string['previous'] = 'Forrige';
 $string['reload'] = 'Genindlæs';
index 7c94740..78f7d06 100644 (file)
@@ -40,3 +40,4 @@ $string['cliunknowoption'] = 'ངོས་འཛིན་འབད་མ་ཚ
 $string['cliyesnoprompt'] = 'y ཟེར་འབྲི་བ་ཅིན་ཨིནཟེརཝ་དང་ n ཟེར་འབྲི་བ་ཅིན་མིན་)';
 $string['environmentrequireinstall'] = 'གཞི་བཙུགས་འབད་དི་ལྕོགས་ཅན་བཟོ་དགོ།';
 $string['environmentrequireversion'] = 'ཐོན་རིམ་  {$a->needed}དགོས་མཁོ་ཡོདཔ་ལས་ ཁྱོད་ཀྱི་ {$a->current}གཡོག་བཀོལ་བའི་བསྒང་ཡོད།';
+$string['upgradekeyset'] = 'ལྡེ་མིག་ཡར་བསྐྱེད་གཏང་། (གཞི་སྒྲིག་མི་འགྱོ་ནི་གི་དོན་ལུ་སྟོངམ་སྦེ་བཞག)';
index 4a638f4..44dcdbc 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'སྐད་ཡིག།';
+$string['moodlelogo'] = 'Moodle ལས་རྟགས།';
 $string['next'] = 'ཤུལ་མམ';
 $string['previous'] = 'ཧེ་མམ';
 $string['reload'] = 'ཡང་བསྐྱར་མངོན་གསལ་འབད';
index e4398be..eafa560 100644 (file)
@@ -65,7 +65,7 @@ $string['authpreventaccountcreation_help'] = 'When a user authenticates, an acco
 $string['authsettings'] = 'Manage authentication';
 $string['autolang'] = 'Language autodetect';
 $string['autologinguests'] = 'Auto-login guests';
-$string['availablesearchareas'] = 'Available areas for search';
+$string['searchareas'] = 'Search areas';
 $string['availableto'] = 'Available to';
 $string['availablelicenses'] = 'Available licences';
 $string['backgroundcolour'] = 'Transparent colour';
@@ -586,6 +586,7 @@ $string['ignore'] = 'Ignore';
 $string['includemoduleuserdata'] = 'Include module user data';
 $string['incompatibleblocks'] = 'Incompatible blocks';
 $string['indexdata'] = 'Index data';
+$string['indexinginfo'] = 'The recommended way to index your site\'s contents is using "Global search indexing" scheduled task which runs automatically by Cron.';
 $string['installhijacked'] = 'Installation must be finished from the original IP address, sorry.';
 $string['installsessionerror'] = 'Can not initialise PHP session, please verify that your browser accepts cookies.';
 $string['intlrecommended'] = 'Intl extension is used to improve internationalization support, such as locale aware sorting.';
@@ -750,6 +751,7 @@ $string['navshowmycoursecategories_help'] = 'If enabled courses in the users my
 $string['navsortmycoursessort'] = 'Sort my courses';
 $string['navsortmycoursessort_help'] = 'This determines whether courses are listed under My courses according to the sort order (i.e. the order set in Site administration > Courses > Manage courses and categories) or alphabetically by course setting.';
 $string['neverdeleteruns'] = 'Never delete runs';
+$string['newestdocindexed'] = 'Newest document indexed';
 $string['nobookmarksforuser'] = 'You do not have any bookmarks.';
 $string['nodatabase'] = 'No database';
 $string['nohttpsformobilewarning'] = 'It is recommended to enable HTTPS with a valid certificate. The Moodle app will always try to use a secured connection first.';
@@ -937,10 +939,22 @@ $string['rssglobaldisabled'] = 'Disabled at server level';
 $string['save'] = 'Save';
 $string['savechanges'] = 'Save changes';
 $string['search'] = 'Search';
+$string['searchalldeleted'] = 'All indexed contents have been deleted';
+$string['searchareaenabled'] = 'Search area enabled';
+$string['searchareadisabled'] = 'Search area disabled';
+$string['searchdeleteindex'] = 'Delete all indexed contents';
 $string['searchengine'] = 'Search engine';
+$string['searchindexactions'] = 'Index actions';
+$string['searchindexdeleted'] = 'Index deleted';
+$string['searchindexupdated'] = 'Search engine contents have been updated';
 $string['searchinsettings'] = 'Search in settings';
+$string['searchlastrun'] = 'Last run (time, # docs, # records, # ignores)';
+$string['searchnotavailable'] = 'Search is not available';
+$string['searchreindexed'] = 'All site\'s contents have been reindexed';
+$string['searchreindexindex'] = 'Reindex all site contents';
 $string['searchresults'] = 'Search results';
 $string['searchsetupinfo'] = 'Search setup';
+$string['searchupdateindex'] = 'Update indexed contents';
 $string['sectionerror'] = 'Section error!';
 $string['secureforms'] = 'Use additional form security';
 $string['security'] = 'Security';
index a63c3e6..3f75c53 100644 (file)
@@ -76,11 +76,11 @@ $string['backuptypesection'] = 'Section';
 $string['backupversion'] = 'Backup version';
 $string['cannotfindassignablerole'] = 'The {$a} role in the backup file cannot be mapped to any of the roles that you are allowed to assign.';
 $string['choosefilefromcoursebackup'] = 'Course backup area';
-$string['choosefilefromcoursebackup_help'] = 'When backup courses using default settings, backup files will be stored here';
+$string['choosefilefromcoursebackup_help'] = 'Course backups made using default settings are stored here.';
 $string['choosefilefromuserbackup'] = 'User private backup area';
-$string['choosefilefromuserbackup_help'] = 'When backup courses with "Anonymize user information" option ticked, backup files will be stored here';
+$string['choosefilefromuserbackup_help'] = 'Backup files with anonymized user information are stored here.';
 $string['choosefilefromactivitybackup'] = 'Activity backup area';
-$string['choosefilefromactivitybackup_help'] = 'When backup activities using default settings, backup files will be stored here';
+$string['choosefilefromactivitybackup_help'] = 'Activity backups made using default settings are stored here.';
 $string['choosefilefromautomatedbackup'] = 'Automated backups';
 $string['choosefilefromautomatedbackup_help'] = 'Contains automatically generated backups.';
 $string['configgeneralactivities'] = 'Sets the default for including activities in a backup.';
@@ -130,9 +130,9 @@ $string['filealiasesrestorefailures_help'] = 'Aliases are symbolic links to othe
 
 More details and the actual reason of the failure can be found in the restore log file.';
 $string['filealiasesrestorefailures_link'] = 'restore/filealiases';
-$string['filereferencesincluded'] = 'File references to external contents included in backup package, they won\'t work on other sites.';
-$string['filereferencessamesite'] = 'Backup is from the same site, file references can be restored';
-$string['filereferencesnotsamesite'] = 'Backup is from other site, file references cannot be restored';
+$string['filereferencesincluded'] = 'File references to external contents are included in the backup file. These won\'t work if the backup is restored on a different site.';
+$string['filereferencessamesite'] = 'The backup file is from this site, and so file references can be restored.';
+$string['filereferencesnotsamesite'] = 'The backup file is from a different site, and so file references cannot be restored.';
 $string['generalactivities'] = 'Include activities and resources';
 $string['generalanonymize'] = 'Anonymise information';
 $string['generalbackdefaults'] = 'General backup defaults';
@@ -180,7 +180,7 @@ $string['lockedbyconfig'] = 'This setting has been locked by the default backup
 $string['lockedbyhierarchy'] = 'Locked by dependencies';
 $string['loglifetime'] = 'Keep logs for';
 $string['managefiles'] = 'Manage backup files';
-$string['missingfilesinpool'] = 'Some files could not be saved during the backup, it won\'t be possible to restore them.';
+$string['missingfilesinpool'] = 'Some files could not be saved during the backup, and so it will not be possible to restore them.';
 $string['moodleversion'] = 'Moodle version';
 $string['moreresults'] = 'There are too many results, enter a more specific search.';
 $string['nomatchingcourses'] = 'There are no courses to display';
index abf487a..1077607 100644 (file)
@@ -87,7 +87,7 @@ $string['backpackdetails'] = 'Backpack settings';
 $string['backpackemail'] = 'Email address';
 $string['backpackemail_help'] = 'The email address associated with your backpack. While you are connected, any badges earned on this site will be associated with this email address.';
 $string['personaconnection'] = 'Sign in with your email';
-$string['personaconnection_help'] = 'Persona is a system for identifying yourself across the web, using an email address that you own. The Open Badges backpack uses Persona as a login system, so to be able to connect to a backpack you with need a Persona account.
+$string['personaconnection_help'] = 'Persona is a system for identifying yourself across the web, using an email address that you own. The Open Badges backpack uses Persona as a login system, so to be able to connect to a backpack you will need a Persona account.
 
 For more information about Persona visit <a href="https://login.persona.org/about">https://login.persona.org/about</a>.';
 $string['backpackimport'] = 'Badge import settings';
index d245288..b3c377d 100644 (file)
@@ -65,7 +65,7 @@ $string['categorycurrent'] = 'Current category';
 $string['categorycurrentuse'] = 'Use this category';
 $string['categorydoesnotexist'] = 'This category does not exist';
 $string['categoryinfo'] = 'Category info';
-$string['categorymove'] = 'The category \'{$a->name}\' contains {$a->count} questions (some of them may be old, hidden, questions, or Random questions that are still in use in some existing quizzes). Please choose another category to move them to.';
+$string['categorymove'] = 'The category \'{$a->name}\' contains {$a->count} questions (some of which may be hidden questions or random questions that are still in use in a quiz). Please choose another category to move them to.';
 $string['categorymoveto'] = 'Save in category';
 $string['categorynamecantbeblank'] = 'The category name cannot be blank.';
 $string['clickflag'] = 'Flag question';
index c7219ec..bfe296c 100644 (file)
@@ -81,6 +81,7 @@ $string['runindexertest'] = 'Run indexer test';
 $string['score'] = 'Score';
 $string['search'] = 'Search';
 $string['search:mycourse'] = 'My courses';
+$string['search:user'] = 'Users';
 $string['searcharea'] = 'Search area';
 $string['searching'] = 'Searching in ...';
 $string['searchnotpermitted'] = 'You are not allowed to do a search';
index c8f24f6..75b9789 100644 (file)
@@ -9715,7 +9715,7 @@ class admin_setting_searchsetupinfo extends admin_setting {
 
         // Available areas.
         $row = array();
-        $url = new moodle_url('/admin/settings.php?section=manageglobalsearch#admin-searchengine');
+        $url = new moodle_url('/admin/searchareas.php');
         $row[0] = '2. ' . html_writer::tag('a', get_string('enablesearchareas', 'admin'),
                         array('href' => $url));
 
@@ -9750,7 +9750,7 @@ class admin_setting_searchsetupinfo extends admin_setting {
 
         // Indexed data.
         $row = array();
-        $url = new moodle_url('/report/search/index.php#searchindexform');
+        $url = new moodle_url('/admin/searchareas.php');
         $row[0] = '4. ' . html_writer::tag('a', get_string('indexdata', 'admin'), array('href' => $url));
         if ($anyindexed) {
             $status = html_writer::tag('span', get_string('yes'), array('class' => 'statusok'));
index d479d28..4d5abac 100644 (file)
Binary files a/lib/amd/build/permissionmanager.min.js and b/lib/amd/build/permissionmanager.min.js differ
index cb77bca..3cb6034 100644 (file)
@@ -54,7 +54,8 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates'], functio
             sesskey: config.sesskey
         };
 
-        $.post(adminurl + 'roles/ajax.php', params)
+        // Need to tell jQuery to expect JSON as the content type may not be correct (MDL-55041).
+        $.post(adminurl + 'roles/ajax.php', params, null, 'json')
             .done(function(data) {
               try {
                   overideableroles = data;
@@ -88,7 +89,7 @@ define(['jquery', 'core/config', 'core/notification', 'core/templates'], functio
             action: action,
             capability: row.data('name')
         };
-        $.post(adminurl + 'roles/ajax.php', params)
+        $.post(adminurl + 'roles/ajax.php', params, null, 'json')
         .done(function(data) {
             var action = data;
             try {
index 4db12f8..9103fcd 100644 (file)
@@ -1089,7 +1089,7 @@ function get_backpack_settings($userid, $refresh = false) {
                 $badges = $backpack->get_badges($collection->collectionid);
                 if (isset($badges->badges)) {
                     $out->badges = array_merge($out->badges, $badges->badges);
-                    $out->totalbadges += count($out->badges);
+                    $out->totalbadges += count($badges->badges);
                 } else {
                     $out->badges = array_merge($out->badges, array());
                 }
index 565f268..f2a0e6d 100644 (file)
@@ -38,6 +38,8 @@ class core_grades_external extends external_api {
      *
      * @return external_function_parameters
      * @since Moodle 2.7
+     * @deprecated Moodle 3.2 MDL-51373 - Please do not call this function any more.
+     * @see gradereport_user_external::get_grades_table for a similar function
      */
     public static function get_grades_parameters() {
         return new external_function_parameters(
@@ -65,6 +67,8 @@ class core_grades_external extends external_api {
      * @param  array  $userids      Array of user ids
      * @return array                Array of grades
      * @since Moodle 2.7
+     * @deprecated Moodle 3.2 MDL-51373 - Please do not call this function any more.
+     * @see gradereport_user_external::get_grades_table for a similar function
      */
     public static function get_grades($courseid, $component = null, $activityid = null, $userids = array()) {
         global $CFG, $USER, $DB;
@@ -293,6 +297,8 @@ class core_grades_external extends external_api {
      * @param  int $iteminstance    Item instance
      * @param  int $itemnumber      Item number
      * @return grade_item           A grade_item instance
+     * @deprecated Moodle 3.2 MDL-51373 - Please do not call this function any more.
+     * @see gradereport_user_external::get_grades_table for a similar function
      */
     private static function get_grade_item($courseid, $itemtype, $itemmodule = null, $iteminstance = null, $itemnumber = null) {
         $gradeiteminstance = null;
@@ -311,6 +317,8 @@ class core_grades_external extends external_api {
      *
      * @return external_description
      * @since Moodle 2.7
+     * @deprecated Moodle 3.2 MDL-51373 - Please do not call this function any more.
+     * @see gradereport_user_external::get_grades_table for a similar function
      */
     public static function get_grades_returns() {
         return new external_single_structure(
@@ -405,6 +413,15 @@ class core_grades_external extends external_api {
 
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function get_grades_is_deprecated() {
+        return true;
+    }
+
     /**
      * Returns description of method parameters
      *
index 6a978eb..0b983f3 100644 (file)
@@ -1660,6 +1660,7 @@ class core_plugin_manager {
         $plugins = array(
             'qformat' => array('blackboard', 'learnwise'),
             'enrol' => array('authorize'),
+            'report' => array('search'),
             'tinymce' => array('dragmath'),
             'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport'),
             'theme' => array('mymobile', 'afterburner', 'anomaly', 'arialist', 'binarius', 'boxxie', 'brick', 'formal_white',
@@ -1870,7 +1871,7 @@ class core_plugin_manager {
 
             'report' => array(
                 'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist',
-                'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'search',
+                'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances',
                 'security', 'stats', 'performance', 'usersessions'
             ),
 
index 12719cb..9eac43b 100644 (file)
@@ -26,6 +26,7 @@ namespace core\update;
 
 use core_component;
 use coding_exception;
+use moodle_exception;
 use SplFileInfo;
 use RecursiveDirectoryIterator;
 use RecursiveIteratorIterator;
@@ -159,15 +160,18 @@ class code_manager {
      */
     public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
 
+        // Extract the package into a temporary location.
         $fp = get_file_packer('application/zip');
-        $files = $fp->extract_to_pathname($zipfilepath, $targetdir);
+        $tempdir = make_request_directory();
+        $files = $fp->extract_to_pathname($zipfilepath, $tempdir);
 
         if (!$files) {
             return array();
         }
 
+        // If requested, rename the root directory of the plugin.
         if (!empty($rootdir)) {
-            $files = $this->rename_extracted_rootdir($targetdir, $rootdir, $files);
+            $files = $this->rename_extracted_rootdir($tempdir, $rootdir, $files);
         }
 
         // Sometimes zip may not contain all parent directories, add them to make it consistent.
@@ -187,6 +191,9 @@ class code_manager {
             }
         }
 
+        // Move the extracted files into the target location.
+        $this->move_extracted_plugin_files($tempdir, $targetdir, $files);
+
         // Set the permissions of extracted subdirs and files.
         $this->set_plugin_files_permissions($targetdir, $files);
 
@@ -443,12 +450,10 @@ class code_manager {
     /**
      * Renames the root directory of the extracted ZIP package.
      *
-     * This method does not validate the presence of the single root directory
-     * (it is the validator's duty). It just searches for the first directory
-     * under the given location and renames it.
-     *
-     * The method will not rename the root if the requested location already
-     * exists.
+     * This internal helper method assumes that the plugin ZIP package has been
+     * extracted into a temporary empty directory so the plugin folder is the
+     * only folder there. The ZIP package is supposed to be validated so that
+     * it contains just a single root folder.
      *
      * @param string $dirname fullpath location of the extracted ZIP package
      * @param string $rootdir the requested name of the root directory
@@ -473,8 +478,11 @@ class code_manager {
                 continue;
             }
             if (is_dir($dirname.'/'.$item)) {
+                if ($found !== null and $found !== $item) {
+                    // Multiple directories found.
+                    throw new moodle_exception('unexpected_archive_structure', 'core_plugin');
+                }
                 $found = $item;
-                break;
             }
         }
 
@@ -520,4 +528,34 @@ class code_manager {
             }
         }
     }
+
+    /**
+     * Moves the extracted contents of the plugin ZIP into the target location.
+     *
+     * @param string $sourcedir full path to the directory the ZIP file was extracted to
+     * @param mixed $targetdir full path to the directory where the files should be moved to
+     * @param array $files list of extracted files
+     */
+    protected function move_extracted_plugin_files($sourcedir, $targetdir, array $files) {
+        global $CFG;
+
+        foreach ($files as $file => $status) {
+            if ($status !== true) {
+                throw new moodle_exception('corrupted_archive_structure', 'core_plugin', '', $file, $status);
+            }
+
+            $source = $sourcedir.'/'.$file;
+            $target = $targetdir.'/'.$file;
+
+            if (is_dir($source)) {
+                continue;
+
+            } else {
+                if (!is_dir(dirname($target))) {
+                    mkdir(dirname($target), $CFG->directorypermissions, true);
+                }
+                rename($source, $target);
+            }
+        }
+    }
 }
index 5a94658..6a10819 100644 (file)
@@ -152,14 +152,15 @@ class core_user {
         // If noreply user is set then use it, else create one.
         if (!empty($CFG->noreplyuserid)) {
             self::$noreplyuser = self::get_user($CFG->noreplyuserid);
+            self::$noreplyuser->emailstop = 1; // Force msg stop for this user.
+            return self::$noreplyuser;
+        } else {
+            // Do not cache the dummy user record to avoid language internationalization issues.
+            $noreplyuser = self::get_dummy_user_record();
+            $noreplyuser->maildisplay = '1'; // Show to all.
+            $noreplyuser->emailstop = 1;
+            return $noreplyuser;
         }
-
-        if (empty(self::$noreplyuser)) {
-            self::$noreplyuser = self::get_dummy_user_record();
-            self::$noreplyuser->maildisplay = '1'; // Show to all.
-        }
-        self::$noreplyuser->emailstop = 1; // Force msg stop for this user.
-        return self::$noreplyuser;
     }
 
     /**
@@ -182,18 +183,19 @@ class core_user {
         // If custom support user is set then use it, else if supportemail is set then use it, else use noreply.
         if (!empty($CFG->supportuserid)) {
             self::$supportuser = self::get_user($CFG->supportuserid, '*', MUST_EXIST);
-        }
-
-        // Try sending it to support email if support user is not set.
-        if (empty(self::$supportuser) && !empty($CFG->supportemail)) {
-            self::$supportuser = self::get_dummy_user_record();
-            self::$supportuser->id = self::SUPPORT_USER;
-            self::$supportuser->email = $CFG->supportemail;
+        } else if (empty(self::$supportuser) && !empty($CFG->supportemail)) {
+            // Try sending it to support email if support user is not set.
+            $supportuser = self::get_dummy_user_record();
+            $supportuser->id = self::SUPPORT_USER;
+            $supportuser->email = $CFG->supportemail;
             if ($CFG->supportname) {
-                self::$supportuser->firstname = $CFG->supportname;
+                $supportuser->firstname = $CFG->supportname;
             }
-            self::$supportuser->username = 'support';
-            self::$supportuser->maildisplay = '1'; // Show to all.
+            $supportuser->username = 'support';
+            $supportuser->maildisplay = '1'; // Show to all.
+            // Unset emailstop to make sure support message is sent.
+            $supportuser->emailstop = 0;
+            return $supportuser;
         }
 
         // Send support msg to admin user if nothing is set above.
index 6428bec..3148646 100644 (file)
@@ -214,7 +214,8 @@ $functions = array(
         'classpath' => 'course/externallib.php',
         'description' => 'Return category details',
         'type' => 'read',
-        'capabilities' => 'moodle/category:viewhiddencategories'
+        'capabilities' => 'moodle/category:viewhiddencategories',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_course_get_contents' => array(
         'classname' => 'core_course_external',
@@ -392,7 +393,8 @@ $functions = array(
     'core_grades_get_grades' => array(
         'classname' => 'core_grades_external',
         'methodname' => 'get_grades',
-        'description' => 'Returns student course total grade and grades for activities.
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                                     Returns student course total grade and grades for activities.
                                      This function does not return category or manual items.
                                      This function is suitable for managers or teachers not students.',
         'type' => 'read',
@@ -403,7 +405,6 @@ $functions = array(
         'methodname' => 'update_grades',
         'description' => 'Update a grade item and associated student grades.',
         'type' => 'write',
-        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_grading_get_definitions' => array(
         'classname' => 'core_grading_external',
index ee468b6..629ab19 100644 (file)
@@ -2072,5 +2072,16 @@ function xmldb_main_upgrade($oldversion) {
     // Moodle v3.1.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2016070700.01) {
+
+        // If someone is emotionally attached to it let's leave the config (basically the version) there.
+        if (!file_exists($CFG->dirroot . '/report/search/classes/output/form.php')) {
+            unset_all_config_for_plugin('report_search');
+        }
+
+        // Savepoint reached.
+        upgrade_main_savepoint(true, 2016070700.01);
+    }
+
     return true;
 }
index e18732b..0338bad 100644 (file)
@@ -199,6 +199,7 @@ class core_ddl_testcase extends database_driver_testcase {
      * Test behaviour of create_table()
      */
     public function test_create_table() {
+
         $DB = $this->tdb; // Do not use global $DB!
         $dbman = $this->tdb->get_manager();
 
@@ -289,8 +290,9 @@ class core_ddl_testcase extends database_driver_testcase {
             $this->assertInstanceOf('ddl_exception', $e);
         }
 
-        // Long table name names - the largest allowed.
-        $table = new xmldb_table('test_table0123456789_____xyz');
+        // Long table name names - the largest allowed by the configuration which exclude the prefix to ensure it's created.
+        $tablechars = str_repeat('a', xmldb_table::NAME_MAX_LENGTH);
+        $table = new xmldb_table($tablechars);
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
         $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
         $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
@@ -302,8 +304,9 @@ class core_ddl_testcase extends database_driver_testcase {
         $this->assertTrue($dbman->table_exists($table));
         $dbman->drop_table($table);
 
-        // Table name is too long.
-        $table = new xmldb_table('test_table0123456789_____xyz9');
+        // Table name is too long, ignoring any prefix size set.
+        $tablechars = str_repeat('a', xmldb_table::NAME_MAX_LENGTH + 1);
+        $table = new xmldb_table($tablechars);
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
         $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
         $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
@@ -337,7 +340,7 @@ class core_ddl_testcase extends database_driver_testcase {
         // Weird column names - the largest allowed.
         $table = new xmldb_table('test_table3');
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('abcdef____0123456789_______xyz', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
+        $table->add_field(str_repeat('b', xmldb_field::NAME_MAX_LENGTH), XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
         $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
         $table->setComment("This is a test'n drop table. You can drop it safely");
 
@@ -347,10 +350,10 @@ class core_ddl_testcase extends database_driver_testcase {
         $this->assertTrue($dbman->table_exists($table));
         $dbman->drop_table($table);
 
-        // Too long field name - max 30.
+        // Too long field name.
         $table = new xmldb_table('test_table4');
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
-        $table->add_field('abcdeabcdeabcdeabcdeabcdeabcdez', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
+        $table->add_field(str_repeat('a', xmldb_field::NAME_MAX_LENGTH + 1), XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '2');
         $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
         $table->setComment("This is a test'n drop table. You can drop it safely");
 
index efc5043..381e889 100644 (file)
@@ -39,7 +39,6 @@ class pgsql_native_moodle_database extends moodle_database {
 
     /** @var resource $pgsql database resource */
     protected $pgsql     = null;
-    protected $bytea_oid = null;
 
     protected $last_error_reporting; // To handle pgsql driver default verbosity
 
@@ -154,6 +153,15 @@ class pgsql_native_moodle_database extends moodle_database {
             $connection = "host='$this->dbhost' $port user='$this->dbuser' password='$pass' dbname='$this->dbname'";
         }
 
+        // ALTER USER and ALTER DATABASE are overridden by these settings.
+        $options = array('--client_encoding=utf8', '--standard_conforming_strings=on');
+        // Select schema if specified, otherwise the first one wins.
+        if (!empty($this->dboptions['dbschema'])) {
+            $options[] = "-c search_path=" . addcslashes($this->dboptions['dbschema'], "'\\");
+        }
+
+        $connection .= " options='".implode(' ', $options)."'";
+
         ob_start();
         if (empty($this->dboptions['dbpersist'])) {
             $this->pgsql = pg_connect($connection, PGSQL_CONNECT_FORCE_NEW);
@@ -170,34 +178,6 @@ class pgsql_native_moodle_database extends moodle_database {
             throw new dml_connection_exception($dberr);
         }
 
-        $this->query_start("--pg_set_client_encoding()", null, SQL_QUERY_AUX);
-        pg_set_client_encoding($this->pgsql, 'utf8');
-        $this->query_end(true);
-
-        $sql = '';
-        // Only for 9.0 and upwards, set bytea encoding to old format.
-        if ($this->is_min_version('9.0')) {
-            $sql = "SET bytea_output = 'escape'; ";
-        }
-
-        // Select schema if specified, otherwise the first one wins.
-        if (!empty($this->dboptions['dbschema'])) {
-            $sql .= "SET search_path = '".$this->dboptions['dbschema']."'; ";
-        }
-
-        // Find out the bytea oid.
-        $sql .= "SELECT oid FROM pg_type WHERE typname = 'bytea'";
-        $this->query_start($sql, null, SQL_QUERY_AUX);
-        $result = pg_query($this->pgsql, $sql);
-        $this->query_end($result);
-
-        $this->bytea_oid = pg_fetch_result($result, 0, 0);
-        pg_free_result($result);
-        if ($this->bytea_oid === false) {
-            $this->pgsql = null;
-            throw new dml_connection_exception('Can not read bytea type.');
-        }
-
         // Connection stabilised and configured, going to instantiate the temptables controller
         $this->temptables = new pgsql_native_moodle_temptables($this);
 
@@ -273,18 +253,6 @@ class pgsql_native_moodle_database extends moodle_database {
         return array('description'=>$info['server'], 'version'=>$info['server']);
     }
 
-    /**
-     * Returns if the RDBMS server fulfills the required version
-     *
-     * @param string $version version to check against
-     * @return bool returns if the version is fulfilled (true) or no (false)
-     */
-    private function is_min_version($version) {
-        $server = $this->get_server_info();
-        $server = $server['version'];
-        return version_compare($server, $version, '>=');
-    }
-
     /**
      * Returns supported query parameter types
      * @return int bitmask of accepted SQL_PARAMS_*
@@ -623,9 +591,11 @@ class pgsql_native_moodle_database extends moodle_database {
         if (is_bool($value)) { // Always, convert boolean to int
             $value = (int)$value;
 
-        } else if ($column->meta_type === 'B') { // BLOB detected, we return 'blob' array instead of raw value to allow
-            if (!is_null($value)) {             // binding/executing code later to know about its nature
-                $value = array('blob' => $value);
+        } else if ($column->meta_type === 'B') {
+            if (!is_null($value)) {
+                // standard_conforming_strings must be enabled, otherwise pg_escape_bytea() will double escape
+                // \ and produce data errors.  This is set on the connection.
+                $value = pg_escape_bytea($this->pgsql, $value);
             }
 
         } else if ($value === '') {
@@ -756,7 +726,7 @@ class pgsql_native_moodle_database extends moodle_database {
     }
 
     protected function create_recordset($result) {
-        return new pgsql_native_moodle_recordset($result, $this->bytea_oid);
+        return new pgsql_native_moodle_recordset($result);
     }
 
     /**
@@ -794,11 +764,11 @@ class pgsql_native_moodle_database extends moodle_database {
         $this->query_end($result);
 
         // find out if there are any blobs
-        $numrows = pg_num_fields($result);
+        $numfields = pg_num_fields($result);
         $blobs = array();
-        for($i=0; $i<$numrows; $i++) {
-            $type_oid = pg_field_type_oid($result, $i);
-            if ($type_oid == $this->bytea_oid) {
+        for ($i = 0; $i < $numfields; $i++) {
+            $type = pg_field_type($result, $i);
+            if ($type == 'bytea') {
                 $blobs[] = pg_field_name($result, $i);
             }
         }
@@ -812,8 +782,7 @@ class pgsql_native_moodle_database extends moodle_database {
                 $id = reset($row);
                 if ($blobs) {
                     foreach ($blobs as $blob) {
-                        // note: in PostgreSQL 9.0 the returned blobs are hexencoded by default - see http://www.postgresql.org/docs/9.0/static/runtime-config-client.html#GUC-BYTEA-OUTPUT
-                        $row[$blob] = $row[$blob] !== null ? pg_unescape_bytea($row[$blob]) : null;
+                        $row[$blob] = ($row[$blob] !== null ? pg_unescape_bytea($row[$blob]) : null);
                     }
                 }
                 if (isset($return[$id])) {
@@ -843,6 +812,13 @@ class pgsql_native_moodle_database extends moodle_database {
         $this->query_end($result);
 
         $return = pg_fetch_all_columns($result, 0);
+
+        if (pg_field_type($result, 0) == 'bytea') {
+            foreach ($return as $key => $value) {
+                $return[$key] = ($value === null ? $value : pg_unescape_bytea($value));
+            }
+        }
+
         pg_free_result($result);
 
         return $return;
@@ -931,7 +907,6 @@ class pgsql_native_moodle_database extends moodle_database {
         }
 
         $cleaned = array();
-        $blobs   = array();
 
         foreach ($dataobject as $field=>$value) {
             if ($field === 'id') {
@@ -941,33 +916,10 @@ class pgsql_native_moodle_database extends moodle_database {
                 continue;
             }
             $column = $columns[$field];
-            $normalised_value = $this->normalise_value($column, $value);
-            if (is_array($normalised_value) && array_key_exists('blob', $normalised_value)) {
-                $cleaned[$field] = '@#BLOB#@';
-                $blobs[$field] = $normalised_value['blob'];
-            } else {
-                $cleaned[$field] = $normalised_value;
-            }
-        }
-
-        if (empty($blobs)) {
-            return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
+            $cleaned[$field] = $this->normalise_value($column, $value);
         }
 
-        $id = $this->insert_record_raw($table, $cleaned, true, $bulk);
-
-        foreach ($blobs as $key=>$value) {
-            $value = pg_escape_bytea($this->pgsql, $value);
-            $sql = "UPDATE {$this->prefix}$table SET $key = '$value'::bytea WHERE id = $id";
-            $this->query_start($sql, NULL, SQL_QUERY_UPDATE);
-            $result = pg_query($this->pgsql, $sql);
-            $this->query_end($result);
-            if ($result !== false) {
-                pg_free_result($result);
-            }
-        }
-
-        return ($returnid ? $id : true);
+        return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
 
     }
 
@@ -1002,14 +954,6 @@ class pgsql_native_moodle_database extends moodle_database {
 
         $columns = $this->get_columns($table, true);
 
-        // Make sure there are no nasty blobs!
-        foreach ($columns as $column) {
-            if ($column->binary) {
-                parent::insert_records($table, $dataobjects);
-                return;
-            }
-        }
-
         $fields = null;
         $count = 0;
         $chunk = array();
@@ -1042,7 +986,7 @@ class pgsql_native_moodle_database extends moodle_database {
     }
 
     /**
-     * Insert records in chunks, no binary support, strict param types...
+     * Insert records in chunks, strict param types...
      *
      * Note: can be used only from insert_records().
      *
@@ -1087,39 +1031,17 @@ class pgsql_native_moodle_database extends moodle_database {
 
         $columns = $this->get_columns($table);
         $cleaned = array();
-        $blobs   = array();
 
         foreach ($dataobject as $field=>$value) {
             $this->detect_objects($value);
             if (!isset($columns[$field])) {
                 continue;
             }
-            if ($columns[$field]->meta_type === 'B') {
-                if (!is_null($value)) {
-                    $cleaned[$field] = '@#BLOB#@';
-                    $blobs[$field] = $value;
-                    continue;
-                }
-            }
-
-            $cleaned[$field] = $value;
-        }
-
-        $this->insert_record_raw($table, $cleaned, false, true, true);
-        $id = $dataobject['id'];
-
-        foreach ($blobs as $key=>$value) {
-            $value = pg_escape_bytea($this->pgsql, $value);
-            $sql = "UPDATE {$this->prefix}$table SET $key = '$value'::bytea WHERE id = $id";
-            $this->query_start($sql, NULL, SQL_QUERY_UPDATE);
-            $result = pg_query($this->pgsql, $sql);
-            $this->query_end($result);
-            if ($result !== false) {
-                pg_free_result($result);
-            }
+            $column = $columns[$field];
+            $cleaned[$field] = $this->normalise_value($column, $value);
         }
 
-        return true;
+        return $this->insert_record_raw($table, $cleaned, false, true, true);
     }
 
     /**
@@ -1182,40 +1104,17 @@ class pgsql_native_moodle_database extends moodle_database {
 
         $columns = $this->get_columns($table);
         $cleaned = array();
-        $blobs   = array();
 
         foreach ($dataobject as $field=>$value) {
             if (!isset($columns[$field])) {
                 continue;
             }
             $column = $columns[$field];
-            $normalised_value = $this->normalise_value($column, $value);
-            if (is_array($normalised_value) && array_key_exists('blob', $normalised_value)) {
-                $cleaned[$field] = '@#BLOB#@';
-                $blobs[$field] = $normalised_value['blob'];
-            } else {
-                $cleaned[$field] = $normalised_value;
-            }
+            $cleaned[$field] = $this->normalise_value($column, $value);
         }
 
         $this->update_record_raw($table, $cleaned, $bulk);
 
-        if (empty($blobs)) {
-            return true;
-        }
-
-        $id = (int)$dataobject['id'];
-
-        foreach ($blobs as $key=>$value) {
-            $value = pg_escape_bytea($this->pgsql, $value);
-            $sql = "UPDATE {$this->prefix}$table SET $key = '$value'::bytea WHERE id = $id";
-            $this->query_start($sql, NULL, SQL_QUERY_UPDATE);
-            $result = pg_query($this->pgsql, $sql);
-            $this->query_end($result);
-
-            pg_free_result($result);
-        }
-
         return true;
     }
 
@@ -1245,24 +1144,10 @@ class pgsql_native_moodle_database extends moodle_database {
         $columns = $this->get_columns($table);
         $column = $columns[$newfield];
 
-        $normalised_value = $this->normalise_value($column, $newvalue);
-        if (is_array($normalised_value) && array_key_exists('blob', $normalised_value)) {
-            // Update BYTEA and return
-            $normalised_value = pg_escape_bytea($this->pgsql, $normalised_value['blob']);
-            $sql = "UPDATE {$this->prefix}$table SET $newfield = '$normalised_value'::bytea $select";
-            $this->query_start($sql, NULL, SQL_QUERY_UPDATE);
-            $result = pg_query_params($this->pgsql, $sql, $params);
-            $this->query_end($result);
-            pg_free_result($result);
-            return true;
-        }
+        $normalisedvalue = $this->normalise_value($column, $newvalue);
 
-        if (is_null($normalised_value)) {
-            $newfield = "$newfield = NULL";
-        } else {
-            $newfield = "$newfield = \$".$i;
-            $params[] = $normalised_value;
-        }
+        $newfield = "$newfield = \$" . $i;
+        $params[] = $normalisedvalue;
         $sql = "UPDATE {$this->prefix}$table SET $newfield $select";
 
         $this->query_start($sql, $params, SQL_QUERY_UPDATE);
@@ -1275,7 +1160,7 @@ class pgsql_native_moodle_database extends moodle_database {
     }
 
     /**
-     * Delete one or more records from a table which match a particular WHERE clause.
+     * Delete one or more records from a table which match a particular WHERE clause, lobs not supported.
      *
      * @param string $table The database table to be checked against.
      * @param string $select A fragment of SQL to be used in a where clause in the SQL call (used to define the selection criteria).
@@ -1315,11 +1200,6 @@ class pgsql_native_moodle_database extends moodle_database {
         if (strpos($param, '%') !== false) {
             debugging('Potential SQL injection detected, sql_like() expects bound parameters (? or :named)');
         }
-        if ($escapechar === '\\') {
-            // Prevents problems with C-style escapes of enclosing '\',
-            // E'... bellow prevents compatibility warnings.
-            $escapechar = '\\\\';
-        }
 
         // postgresql does not support accent insensitive text comparisons, sorry
         if ($casesensitive) {
@@ -1327,7 +1207,7 @@ class pgsql_native_moodle_database extends moodle_database {
         } else {
             $LIKE = $notlike ? 'NOT ILIKE' : 'ILIKE';
         }
-        return "$fieldname $LIKE $param ESCAPE E'$escapechar'";
+        return "$fieldname $LIKE $param ESCAPE '$escapechar'";
     }
 
     public function sql_bitxor($int1, $int2) {
index fd1f905..dc99f5b 100644 (file)
@@ -38,18 +38,21 @@ class pgsql_native_moodle_recordset extends moodle_recordset {
     protected $result;
     /** @var current row as array.*/
     protected $current;
-    protected $bytea_oid;
     protected $blobs = array();
 
-    public function __construct($result, $bytea_oid) {
-        $this->result    = $result;
-        $this->bytea_oid = $bytea_oid;
-
-        // find out if there are any blobs
-        $numrows = pg_num_fields($result);
-        for($i=0; $i<$numrows; $i++) {
-            $type_oid = pg_field_type_oid($result, $i);
-            if ($type_oid == $this->bytea_oid) {
+    /**
+     * Build a new recordset to iterate over.
+     *
+     * @param resource $result A pg_query() result object to create a recordset from.
+     */
+    public function __construct($result) {
+        $this->result = $result;
+
+        // Find out if there are any blobs.
+        $numfields = pg_num_fields($result);
+        for ($i = 0; $i < $numfields; $i++) {
+            $type = pg_field_type($result, $i);
+            if ($type == 'bytea') {
                 $this->blobs[] = pg_field_name($result, $i);
             }
         }
index 7df8222..7851e2e 100644 (file)
@@ -1919,13 +1919,16 @@ class core_dml_testcase extends database_driver_testcase {
 
         $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
         $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('onebinary', XMLDB_TYPE_BINARY, 'big', null, null, null);
         $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
         $dbman->create_table($table);
 
-        $DB->insert_record($tablename, array('course' => 1));
-        $DB->insert_record($tablename, array('course' => 3));
-        $DB->insert_record($tablename, array('course' => 2));
-        $DB->insert_record($tablename, array('course' => 6));
+        $binarydata = '\\'.chr(241);
+
+        $DB->insert_record($tablename, array('course' => 1, 'onebinary' => $binarydata));
+        $DB->insert_record($tablename, array('course' => 3, 'onebinary' => $binarydata));
+        $DB->insert_record($tablename, array('course' => 2, 'onebinary' => $binarydata));
+        $DB->insert_record($tablename, array('course' => 6, 'onebinary' => $binarydata));
 
         $fieldset = $DB->get_fieldset_sql("SELECT * FROM {{$tablename}} WHERE course > ?", array(1));
         $this->assertInternalType('array', $fieldset);
@@ -1934,6 +1937,14 @@ class core_dml_testcase extends database_driver_testcase {
         $this->assertEquals(2, $fieldset[0]);
         $this->assertEquals(3, $fieldset[1]);
         $this->assertEquals(4, $fieldset[2]);
+
+        $fieldset = $DB->get_fieldset_sql("SELECT onebinary FROM {{$tablename}} WHERE course > ?", array(1));
+        $this->assertInternalType('array', $fieldset);
+
+        $this->assertCount(3, $fieldset);
+        $this->assertEquals($binarydata, $fieldset[0]);
+        $this->assertEquals($binarydata, $fieldset[1]);
+        $this->assertEquals($binarydata, $fieldset[2]);
     }
 
     public function test_insert_record_raw() {
@@ -3016,6 +3027,10 @@ class core_dml_testcase extends database_driver_testcase {
         $this->assertEquals($clob, $DB->get_field($tablename, 'onetext', array('id' => 1)), 'Test CLOB set_field (full contents output disabled)');
         $this->assertEquals($blob, $DB->get_field($tablename, 'onebinary', array('id' => 1)), 'Test BLOB set_field (full contents output disabled)');
 
+        // Empty data in binary columns works.
+        $DB->set_field_select($tablename, 'onebinary', '', 'id = ?', array(1));
+        $this->assertEquals('', $DB->get_field($tablename, 'onebinary', array('id' => 1)), 'Blobs need to accept empty values.');
+
         // And "small" LOBs too, just in case.
         $newclob = substr($clob, 0, 500);
         $newblob = substr($blob, 0, 250);
index fbcfd8b..d10c55a 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index c8557d0..ebf6605 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index d2b7ea5..41bd2cd 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index f9881c1..dc209f0 100644 (file)
@@ -127,7 +127,9 @@ EditorNotify.prototype = {
         this.hideTimer = Y.later(intTimeout, this, function() {
             Y.log('Hide Atto notification.', 'debug', LOGNAME_NOTIFY);
             this.hideTimer = null;
-            this.messageOverlay.hide(true);
+            if (this.messageOverlay.inDoc()) {
+                this.messageOverlay.hide(true);
+            }
         });
 
         return this;
index befa4e6..26eac65 100644 (file)
@@ -904,6 +904,7 @@ function external_format_string($str, $contextid, $striplinks = true, $options =
  *                      returned. Default false.
  *      allowid     :   If true then id attributes will not be removed, even when using htmlpurifier. Default (different from
  *                      format_text) true. Default changed id attributes are commonly needed.
+ *      blanktarget :   If true all <a> tags will have target="_blank" added unless target is explicitly specified.
  * </pre>
  *
  * @param string $text The content that may contain ULRs in need of rewriting.
@@ -1128,4 +1129,70 @@ class external_util {
         return array($courses, $warnings);
     }
 
+    /**
+     * Returns all area files (optionally limited by itemid).
+     *
+     * @param int $contextid context ID
+     * @param string $component component
+     * @param string $filearea file area
+     * @param int $itemid item ID or all files if not specified
+     * @param bool $useitemidinurl wether to use the item id in the file URL (modules intro don't use it)
+     * @return array of files, compatible with the external_files structure.
+     * @since Moodle 3.2
+     */
+    public static function get_area_files($contextid, $component, $filearea, $itemid = false, $useitemidinurl = true) {
+        $files = array();
+        $fs = get_file_storage();
+
+        if ($areafiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'itemid, filepath, filename', false)) {
+            foreach ($areafiles as $areafile) {
+                $file = array();
+                $file['filename'] = $areafile->get_filename();
+                $file['filepath'] = $areafile->get_filepath();
+                $file['mimetype'] = $areafile->get_mimetype();
+                $file['filesize'] = $areafile->get_filesize();
+                $file['timemodified'] = $areafile->get_timemodified();
+                $fileitemid = $useitemidinurl ? $areafile->get_itemid() : null;
+                $file['fileurl'] = moodle_url::make_webservice_pluginfile_url($contextid, $component, $filearea,
+                                    $fileitemid, $areafile->get_filepath(), $areafile->get_filename())->out(false);
+                $files[] = $file;
+            }
+        }
+        return $files;
+    }
+}
+
+/**
+ * External structure representing a set of files.
+ *
+ * @package    core_webservice
+ * @copyright  2016 Juan Leyva
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.2
+ */
+class external_files extends external_multiple_structure {
+
+    /**
+     * Constructor
+     * @param string $desc Description for the multiple structure.
+     * @param int $required The type of value (VALUE_REQUIRED OR VALUE_OPTIONAL).
+     */
+    public function __construct($desc = 'List of files.', $required = VALUE_REQUIRED) {
+
+        parent::__construct(
+            new external_single_structure(
+                array(
+                    'filename' => new external_value(PARAM_FILE, 'File name.', VALUE_OPTIONAL),
+                    'filepath' => new external_value(PARAM_PATH, 'File path.', VALUE_OPTIONAL),
+                    'filesize' => new external_value(PARAM_INT, 'File size.', VALUE_OPTIONAL),
+                    'fileurl' => new external_value(PARAM_URL, 'Downloadable file url.', VALUE_OPTIONAL),
+                    'timemodified' => new external_value(PARAM_INT, 'Time modified.', VALUE_OPTIONAL),
+                    'mimetype' => new external_value(PARAM_RAW, 'File mime type.', VALUE_OPTIONAL),
+                ),
+                'File.'
+            ),
+            $desc,
+            $required
+        );
+    }
 }
index c26865d..63aed7b 100644 (file)
@@ -2160,7 +2160,6 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring
             $options->nocache = true; // temporary workaround for MDL-5136
             $text = $pathisstring ? $path : implode('', file($path));
 
-            $text = file_modify_html_header($text);
             $output = format_text($text, FORMAT_HTML, $options, $COURSE->id);
 
             readstring_accel($output, $mimetype, false);
@@ -2344,7 +2343,6 @@ function send_stored_file($stored_file, $lifetime=null, $filter=0, $forcedownloa
             $options->noclean = true;
             $options->nocache = true; // temporary workaround for MDL-5136
             $text = $stored_file->get_content();
-            $text = file_modify_html_header($text);
             $output = format_text($text, FORMAT_HTML, $options, $COURSE->id);
 
             readstring_accel($output, $mimetype, false);
@@ -2608,57 +2606,6 @@ function byteserving_send_file($handle, $mimetype, $ranges, $filesize) {
     }
 }
 
-/**
- * add includes (js and css) into uploaded files
- * before returning them, useful for themes and utf.js includes
- *
- * @global stdClass $CFG
- * @param string $text text to search and replace
- * @return string text with added head includes
- * @todo MDL-21120
- */
-function file_modify_html_header($text) {
-    // first look for <head> tag
-    global $CFG;
-
-    $stylesheetshtml = '';
-/*
-    foreach ($CFG->stylesheets as $stylesheet) {
-        //TODO: MDL-21120
-        $stylesheetshtml .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'" />'."\n";
-    }
-*/
-    // TODO The code below is actually a waste of CPU. When MDL-29738 will be implemented it should be re-evaluated too.
-
-    preg_match('/\<head\>|\<HEAD\>/', $text, $matches);
-    if ($matches) {
-        $replacement = '<head>'.$stylesheetshtml;
-        $text = preg_replace('/\<head\>|\<HEAD\>/', $replacement, $text, 1);
-        return $text;
-    }
-
-    // if not, look for <html> tag, and stick <head> right after
-    preg_match('/\<html\>|\<HTML\>/', $text, $matches);
-    if ($matches) {
-        // replace <html> tag with <html><head>includes</head>
-        $replacement = '<html>'."\n".'<head>'.$stylesheetshtml.'</head>';
-        $text = preg_replace('/\<html\>|\<HTML\>/', $replacement, $text, 1);
-        return $text;
-    }
-
-    // if not, look for <body> tag, and stick <head> before body
-    preg_match('/\<body\>|\<BODY\>/', $text, $matches);
-    if ($matches) {
-        $replacement = '<head>'.$stylesheetshtml.'</head>'."\n".'<body>';
-        $text = preg_replace('/\<body\>|\<BODY\>/', $replacement, $text, 1);
-        return $text;
-    }
-
-    // if not, just stick a <head> tag at the beginning
-    $text = '<head>'.$stylesheetshtml.'</head>'."\n".$text;
-    return $text;
-}
-
 /**
  * Tells whether the filename is executable.
  *
@@ -2688,6 +2635,145 @@ function file_is_executable($filename) {
     }
 }
 
+/**
+ * Overwrite an existing file in a draft area.
+ *
+ * @param  stored_file $newfile      the new file with the new content and meta-data
+ * @param  stored_file $existingfile the file that will be overwritten
+ * @throws moodle_exception
+ * @since Moodle 3.2
+ */
+function file_overwrite_existing_draftfile(stored_file $newfile, stored_file $existingfile) {
+    if ($existingfile->get_component() != 'user' or $existingfile->get_filearea() != 'draft') {
+        throw new coding_exception('The file to overwrite is not in a draft area.');
+    }
+
+    $fs = get_file_storage();
+    // Remember original file source field.
+    $source = @unserialize($existingfile->get_source());
+    // Remember the original sortorder.
+    $sortorder = $existingfile->get_sortorder();
+    if ($newfile->is_external_file()) {
+        // New file is a reference. Check that existing file does not have any other files referencing to it
+        if (isset($source->original) && $fs->search_references_count($source->original)) {
+            throw new moodle_exception('errordoublereference', 'repository');
+        }
+    }
+
+    // Delete existing file to release filename.
+    $newfilerecord = array(
+        'contextid' => $existingfile->get_contextid(),
+        'component' => 'user',
+        'filearea' => 'draft',
+        'itemid' => $existingfile->get_itemid(),
+        'timemodified' => time()
+    );
+    $existingfile->delete();
+
+    // Create new file.
+    $newfile = $fs->create_file_from_storedfile($newfilerecord, $newfile);
+    // Preserve original file location (stored in source field) for handling references.
+    if (isset($source->original)) {
+        if (!($newfilesource = @unserialize($newfile->get_source()))) {
+            $newfilesource = new stdClass();
+        }
+        $newfilesource->original = $source->original;
+        $newfile->set_source(serialize($newfilesource));
+    }
+    $newfile->set_sortorder($sortorder);
+}
+
+/**
+ * Add files from a draft area into a final area.
+ *
+ * Most of the time you do not want to use this. It is intended to be used
+ * by asynchronous services which cannot direcly manipulate a final
+ * area through a draft area. Instead they add files to a new draft
+ * area and merge that new draft into the final area when ready.
+ *
+ * @param int $draftitemid the id of the draft area to use.
+ * @param int $contextid this parameter and the next two identify the file area to save to.
+ * @param string $component component name
+ * @param string $filearea indentifies the file area
+ * @param int $itemid identifies the item id or false for all items in the file area
+ * @param array $options area options (subdirs=false, maxfiles=-1, maxbytes=0, areamaxbytes=FILE_AREA_MAX_BYTES_UNLIMITED)
+ * @see file_save_draft_area_files
+ * @since Moodle 3.2
+ */
+function file_merge_files_from_draft_area_into_filearea($draftitemid, $contextid, $component, $filearea, $itemid,
+                                                        array $options = null) {
+    // We use 0 here so file_prepare_draft_area creates a new one, finaldraftid will be updated with the new draft id.
+    $finaldraftid = 0;
+    file_prepare_draft_area($finaldraftid, $contextid, $component, $filearea, $itemid, $options);
+    file_merge_draft_area_into_draft_area($draftitemid, $finaldraftid);
+    file_save_draft_area_files($finaldraftid, $contextid, $component, $filearea, $itemid, $options);
+}
+
+/**
+ * Merge files from two draftarea areas.
+ *
+ * This does not handle conflict resolution, files in the destination area which appear
+ * to be more recent will be kept disregarding the intended ones.
+ *
+ * @param int $getfromdraftid the id of the draft area where are the files to merge.
+ * @param int $mergeintodraftid the id of the draft area where new files will be merged.
+ * @throws coding_exception
+ * @since Moodle 3.2
+ */
+function file_merge_draft_area_into_draft_area($getfromdraftid, $mergeintodraftid) {
+    global $USER;
+
+    $fs = get_file_storage();
+    $contextid = context_user::instance($USER->id)->id;
+
+    if (!$filestomerge = $fs->get_area_files($contextid, 'user', 'draft', $getfromdraftid)) {
+        throw new coding_exception('Nothing to merge or area does not belong to current user');
+    }
+
+    $currentfiles = $fs->get_area_files($contextid, 'user', 'draft', $mergeintodraftid);
+
+    // Get hashes of the files to merge.
+    $newhashes = array();
+    foreach ($filestomerge as $filetomerge) {
+        $filepath = $filetomerge->get_filepath();
+        $filename = $filetomerge->get_filename();
+
+        $newhash = $fs->get_pathname_hash($contextid, 'user', 'draft', $mergeintodraftid, $filepath, $filename);
+        $newhashes[$newhash] = $filetomerge;
+    }
+
+    // Calculate wich files must be added.
+    foreach ($currentfiles as $file) {
+        $filehash = $file->get_pathnamehash();
+        // One file to be merged already exists.
+        if (isset($newhashes[$filehash])) {
+            $updatedfile = $newhashes[$filehash];
+
+            // Avoid race conditions.
+            if ($file->get_timemodified() > $updatedfile->get_timemodified()) {
+                // The existing file is more recent, do not copy the suposedly "new" one.
+                unset($newhashes[$filehash]);
+                continue;
+            }
+            // Update existing file (not only content, meta-data too).
+            file_overwrite_existing_draftfile($updatedfile, $file);
+            unset($newhashes[$filehash]);
+        }
+    }
+
+    foreach ($newhashes as $newfile) {
+        $newfilerecord = array(
+            'contextid' => $contextid,
+            'component' => 'user',
+            'filearea' => 'draft',
+            'itemid' => $mergeintodraftid,
+            'timemodified' => time()
+        );
+
+        $fs->create_file_from_storedfile($newfilerecord, $newfile);
+    }
+}
+
 /**
  * RESTful cURL class
  *
index 33569f6..4a15f63 100644 (file)
@@ -4302,7 +4302,7 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
  * @return stdClass A {@link $USER} object - BC only, do not use
  */
 function complete_user_login($user) {
-    global $CFG, $USER;
+    global $CFG, $USER, $SESSION;
 
     \core\session\manager::login_user($user);
 
@@ -4345,6 +4345,7 @@ function complete_user_login($user) {
             if ($changeurl = $userauth->change_password_url()) {
                 redirect($changeurl);
             } else {
+                $SESSION->wantsurl = core_login_get_return_url();
                 redirect($CFG->httpswwwroot.'/login/change_password.php');
             }
         } else {
@@ -6256,65 +6257,54 @@ function valid_uploaded_file($newfile) {
 /**
  * Returns the maximum size for uploading files.
  *
- * There are eight possible upload limits:
- * 1. No limit, if the upload isn't using a post request and the user has permission to ignore limits.
- * 2. in Apache using LimitRequestBody (no way of checking or changing this)
- * 3. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
- * 4. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
- * 5. in php.ini for 'post_max_size' (can not be changed inside PHP)
- * 6. by the Moodle admin in $CFG->maxbytes
- * 7. by the teacher in the current course $course->maxbytes
- * 8. by the teacher for the current module, eg $assignment->maxbytes
+ * There are seven possible upload limits:
+ * 1. in Apache using LimitRequestBody (no way of checking or changing this)
+ * 2. in php.ini for 'upload_max_filesize' (can not be changed inside PHP)
+ * 3. in .htaccess for 'upload_max_filesize' (can not be changed inside PHP)
+ * 4. in php.ini for 'post_max_size' (can not be changed inside PHP)
+ * 5. by the Moodle admin in $CFG->maxbytes
+ * 6. by the teacher in the current course $course->maxbytes
+ * 7. by the teacher for the current module, eg $assignment->maxbytes
  *
  * These last two are passed to this function as arguments (in bytes).
  * Anything defined as 0 is ignored.
  * The smallest of all the non-zero numbers is returned.
  *
- * The php.ini settings are only used if $usespost is true. This allows repositories that do not use post requests, such as
- * repository_filesystem, to copy in files that are larger than post_max_size if the user has permission.
+ * @todo Finish documenting this function
  *
  * @param int $sitebytes Set maximum size
  * @param int $coursebytes Current course $course->maxbytes (in bytes)
  * @param int $modulebytes Current module ->maxbytes (in bytes)
- * @param bool $usespost Does the upload we're getting the max size for use a post request?
+ * @param bool $unused This parameter has been deprecated and is not used any more.
  * @return int The maximum size for uploading files.
  */
-function get_max_upload_file_size($sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $usespost = true) {
-    $sizes = array();
-
-    if ($usespost) {
-        if (!$filesize = ini_get('upload_max_filesize')) {
-            $filesize = '5M';
-        }
-        $sizes[] = get_real_size($filesize);
+function get_max_upload_file_size($sitebytes=0, $coursebytes=0, $modulebytes=0, $unused = false) {
 
-        if ($postsize = ini_get('post_max_size')) {
-            $sizes[] = get_real_size($postsize);
-        }
+    if (! $filesize = ini_get('upload_max_filesize')) {
+        $filesize = '5M';
+    }
+    $minimumsize = get_real_size($filesize);
 
-        if ($sitebytes > 0) {
-            $sizes[] = $sitebytes;
-        }
-    } else {
-        if ($sitebytes != 0) {
-            // It's for possible that $sitebytes == USER_CAN_IGNORE_FILE_SIZE_LIMITS (-1).
-            $sizes[] = $sitebytes;
+    if ($postsize = ini_get('post_max_size')) {
+        $postsize = get_real_size($postsize);
+        if ($postsize < $minimumsize) {
+            $minimumsize = $postsize;
         }
     }
 
-    if ($coursebytes > 0) {
-        $sizes[] = $coursebytes;
+    if (($sitebytes > 0) and ($sitebytes < $minimumsize)) {
+        $minimumsize = $sitebytes;
     }
 
-    if ($modulebytes > 0) {
-        $sizes[] = $modulebytes;
+    if (($coursebytes > 0) and ($coursebytes < $minimumsize)) {
+        $minimumsize = $coursebytes;
     }
 
-    if (empty($sizes)) {
-        throw new coding_exception('You must specify at least one filesize limit.');
+    if (($modulebytes > 0) and ($modulebytes < $minimumsize)) {
+        $minimumsize = $modulebytes;
     }
 
-    return min($sizes);
+    return $minimumsize;
 }
 
 /**
@@ -6327,11 +6317,11 @@ function get_max_upload_file_size($sitebytes = 0, $coursebytes = 0, $modulebytes
  * @param int $coursebytes Current course $course->maxbytes (in bytes)
  * @param int $modulebytes Current module ->maxbytes (in bytes)
  * @param stdClass $user The user
- * @param bool $usespost Does the upload we're getting the max size for use a post request?
+ * @param bool $unused This parameter has been deprecated and is not used any more.
  * @return int The maximum size for uploading files.
  */
 function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes = 0, $modulebytes = 0, $user = null,
-        $usespost = true) {
+        $unused = false) {
     global $USER;
 
     if (empty($user)) {
@@ -6339,10 +6329,10 @@ function get_user_max_upload_file_size($context, $sitebytes = 0, $coursebytes =
     }
 
     if (has_capability('moodle/course:ignorefilesizelimits', $context, $user)) {
-        return get_max_upload_file_size(USER_CAN_IGNORE_FILE_SIZE_LIMITS, 0, 0, $usespost);
+        return USER_CAN_IGNORE_FILE_SIZE_LIMITS;
     }
 
-    return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes, $usespost);
+    return get_max_upload_file_size($sitebytes, $coursebytes, $modulebytes);
 }
 
 /**
index b3955ae..a25b992 100644 (file)
@@ -91,11 +91,15 @@ class moodle_phpmailer extends PHPMailer {
     public function encodeHeader($str, $position = 'text') {
         $encoded = core_text::encode_mimeheader($str, $this->CharSet);
         if ($encoded !== false) {
-            $encoded = str_replace("\n", $this->LE, $encoded);
             if ($position === 'phrase') {
-                return ("\"$encoded\"");
+                // Escape special symbols in each line in the encoded string, join back together and enclose in quotes.
+                $chunks = preg_split("/\\n/", $encoded);
+                $chunks = array_map(function($chunk) {
+                    return addcslashes($chunk, "\0..\37\177\\\"");
+                }, $chunks);
+                return '"' . join($this->LE, $chunks) . '"';
             }
-            return $encoded;
+            return str_replace("\n", $this->LE, $encoded);
         }
 
         return parent::encodeHeader($str, $position);
index b9f51c1..2972b6b 100644 (file)
@@ -337,10 +337,10 @@ if (file_exists("$CFG->dataroot/climaintenance.html")) {
 
 if (CLI_SCRIPT) {
     // sometimes people use different PHP binary for web and CLI, make 100% sure they have the supported PHP version
-    if (version_compare(phpversion(), '5.4.4') < 0) {
+    if (version_compare(phpversion(), '5.6.5') < 0) {
         $phpversion = phpversion();
         // do NOT localise - lang strings would not work here and we CAN NOT move it to later place
-        echo "Moodle 2.7 or later requires at least PHP 5.4.4 (currently using version $phpversion).\n";
+        echo "Moodle 3.2 or later requires at least PHP 5.6.5 (currently using version $phpversion).\n";
         echo "Some servers may have multiple PHP versions installed, are you using the correct executable?\n";
         exit(1);
     }
index 0b95413..dfaf9f6 100644 (file)
@@ -96,16 +96,31 @@ class behat_hooks extends behat_base {
      */
     protected static $timings = array();
 
+    /**
+     * Hook to capture BeforeSuite event so as to give access to moodle codebase.
+     * This will try and catch any exception and exists if anything fails.
+     *
+     * @param BeforeSuiteScope $scope scope passed by event fired before suite.
+     * @BeforeSuite
+     */
+    public static function before_suite_hook(BeforeSuiteScope $scope) {
+        try {
+            self::before_suite($scope);
+        } catch (behat_stop_exception $e) {
+            echo $e->getMessage() . PHP_EOL;
+            exit(1);
+        }
+    }
+
     /**
      * Gives access to moodle codebase, ensures all is ready and sets up the test lock.
      *
      * Includes config.php to use moodle codebase with $CFG->behat_*
      * instead of $CFG->prefix and $CFG->dataroot, called once per suite.
      *
-     * @param SuiteEvent $event event before suite.
+     * @param BeforeSuiteScope $scope scope passed by event fired before suite.
      * @static
-     * @throws Exception
-     * @BeforeSuite
+     * @throws behat_stop_exception
      */
     public static function before_suite(BeforeSuiteScope $scope) {
         global $CFG;
@@ -132,7 +147,8 @@ class behat_hooks extends behat_base {
         // before each scenario (accidental user deletes) in the BeforeScenario hook.
 
         if (!behat_util::is_test_mode_enabled()) {
-            throw new Exception('Behat only can run if test mode is enabled. More info in ' . behat_command::DOCS_URL . '#Running_tests');
+            throw new behat_stop_exception('Behat only can run if test mode is enabled. More info in ' .
+                behat_command::DOCS_URL . '#Running_tests');
         }
 
         // Reset all data, before checking for check_server_status.
@@ -145,7 +161,7 @@ class behat_hooks extends behat_base {
         // Prevents using outdated data, upgrade script would start and tests would fail.
         if (!behat_util::is_test_data_updated()) {
             $commandpath = 'php admin/tool/behat/cli/init.php';
-            throw new Exception("Your behat test site is outdated, please run\n\n    " .
+            throw new behat_stop_exception("Your behat test site is outdated, please run\n\n    " .
                     $commandpath . "\n\nfrom your moodle dirroot to drop and install the behat test site again.");
         }
         // Avoid parallel tests execution, it continues when the previous lock is released.
@@ -158,35 +174,43 @@ class behat_hooks extends behat_base {
         }
 
         if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) {
-            throw new Exception('You set $CFG->behat_faildump_path to a non-writable directory');
+            throw new behat_stop_exception('You set $CFG->behat_faildump_path to a non-writable directory');
+        }
+
+        // Handle interrupts on PHP7.
+        if (extension_loaded('pcntl')) {
+            $disabled = explode(',', ini_get('disable_functions'));
+            if (!in_array('pcntl_signal', $disabled)) {
+                declare(ticks = 1);
+            }
         }
     }
 
     /**
      * Gives access to moodle codebase, to keep track of feature start time.
      *
-     * @param FeatureEvent $event event fired before feature.
+     * @param BeforeFeatureScope $scope scope passed by event fired before feature.
      * @BeforeFeature
      */
-    public static function before_feature(BeforeFeatureScope $event) {
+    public static function before_feature(BeforeFeatureScope $scope) {
         if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
             return;
         }
-        $file = $event->getFeature()->getFile();
+        $file = $scope->getFeature()->getFile();
         self::$timings[$file] = microtime(true);
     }
 
     /**
      * Gives access to moodle codebase, to keep track of feature end time.
      *
-     * @param FeatureEvent $event event fired after feature.
+     * @param AfterFeatureScope $scope scope passed by event fired after feature.
      * @AfterFeature
      */
-    public static function after_feature(AfterFeatureScope $event) {
+    public static function after_feature(AfterFeatureScope $scope) {
         if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
             return;
         }
-        $file = $event->getFeature()->getFile();
+        $file = $scope->getFeature()->getFile();
         self::$timings[$file] = microtime(true) - self::$timings[$file];
         // Probably didn't actually run this, don't output it.
         if (self::$timings[$file] < 1) {
@@ -197,10 +221,10 @@ class behat_hooks extends behat_base {
     /**
      * Gives access to moodle codebase, to keep track of suite timings.
      *
-     * @param SuiteEvent $event event fired after suite.
+     * @param AfterSuiteScope $scope scope passed by event fired after suite.
      * @AfterSuite
      */
-    public static function after_suite(AfterSuiteScope $event) {
+    public static function after_suite(AfterSuiteScope $scope) {
         if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
             return;
         }
@@ -218,12 +242,26 @@ class behat_hooks extends behat_base {
     }
 
     /**
-     * Resets the test environment.
+     * Hook to capture before scenario event to get scope.
      *
-     * @param OutlineExampleEvent|ScenarioEvent $event event fired before scenario.
-     * @throws coding_exception If here we are not using the test database it should be because of a coding error
+     * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
      * @BeforeScenario
      */
+    public function before_scenario_hook(BeforeScenarioScope $scope) {
+        try {
+            $this->before_scenario($scope);
+        } catch (behat_stop_exception $e) {
+            echo $e->getMessage() . PHP_EOL;
+            exit(1);
+        }
+    }
+
+    /**
+     * Resets the test environment.
+     *
+     * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
+     * @throws behat_stop_exception If here we are not using the test database it should be because of a coding error
+     */
     public function before_scenario(BeforeScenarioScope $scope) {
         global $DB, $SESSION, $CFG;
 
@@ -233,7 +271,7 @@ class behat_hooks extends behat_base {
                php_sapi_name() != 'cli' ||
                !behat_util::is_test_mode_enabled() ||
                !behat_util::is_test_site()) {
-            throw new coding_exception('Behat only can modify the test database and the test dataroot!');
+            throw new behat_stop_exception('Behat only can modify the test database and the test dataroot!');
         }
 
         $moreinfo = 'More info in ' . behat_command::DOCS_URL . '#Running_tests';
@@ -243,12 +281,12 @@ class behat_hooks extends behat_base {
         } catch (CurlExec $e) {
             // Exception thrown by WebDriver, so only @javascript tests will be caugth; in
             // behat_util::check_server_status() we already checked that the server is running.
-            $this->stop_execution($driverexceptionmsg);
+            throw new behat_stop_exception($driverexceptionmsg);
         } catch (DriverException $e) {
-            $this->stop_execution($driverexceptionmsg);
+            throw new behat_stop_exception($driverexceptionmsg);
         } catch (UnknownError $e) {
             // Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
-            $this->stop_execution($e->getMessage());
+            throw new behat_stop_exception($e->getMessage());
         }
 
         // We need the Mink session to do it and we do it only before the first scenario.
@@ -283,13 +321,13 @@ class behat_hooks extends behat_base {
             // Let's be conservative as we never know when new upstream issues will affect us.
             $session->visit($this->locate_path('/'));
         } catch (UnknownError $e) {
-            $this->stop_execution($e->getMessage());
+            throw new behat_stop_exception($e->getMessage());
         }
 
 
         // Checking that the root path is a Moodle test site.
         if (self::is_first_scenario()) {
-            $notestsiteexception = new Exception('The base URL (' . $CFG->wwwroot . ') is not a behat test site, ' .
+            $notestsiteexception = new behat_stop_exception('The base URL (' . $CFG->wwwroot . ') is not a behat test site, ' .
                 'ensure you started the built-in web server in the correct directory or your web server is correctly started and set up');
             $this->find("xpath", "//head/child::title[normalize-space(.)='" . behat_util::BEHATSITENAME . "']", $notestsiteexception);
 
@@ -308,6 +346,7 @@ class behat_hooks extends behat_base {
      * default would be at framework level, which will stop the execution of
      * the run.
      *
+     * @param BeforeStepScope $scope scope passed by event fired before step.
      * @BeforeStep
      */
     public function before_step_javascript(BeforeStepScope $scope) {
@@ -335,6 +374,7 @@ class behat_hooks extends behat_base {
      * default would be at framework level, which will stop the execution of
      * the run.
      *
+     * @param AfterStepScope $scope scope passed by event fired after step..
      * @AfterStep
      */
     public function after_step_javascript(AfterStepScope $scope) {
@@ -391,10 +431,10 @@ class behat_hooks extends behat_base {
      * This is needed to close all extra browser windows and starting
      * one browser window.
      *
-     * @param AfterScenarioScope $event event fired after scenario.
+     * @param AfterScenarioScope $scope scope passed by event fired after scenario.
      * @AfterScenario @_switch_window
      */
-    public function after_scenario_switchwindow(AfterScenarioScope $event) {
+    public function after_scenario_switchwindow(AfterScenarioScope $scope) {
         for ($count = 0; $count < self::EXTENDED_TIMEOUT; $count++) {
             try {
                 $this->getSession()->restart();
@@ -421,7 +461,7 @@ class behat_hooks extends behat_base {
      * Take screenshot when a step fails.
      *
      * @throws Exception
-     * @param AfterStepScope $scope
+     * @param AfterStepScope $scope scope passed by event after step.
      */
     protected function take_screenshot(AfterStepScope $scope) {
         // Goutte can't save screenshots.
@@ -448,7 +488,7 @@ class behat_hooks extends behat_base {
      * Take a dump of the page content when a step fails.
      *
      * @throws Exception
-     * @param AfterStepScope $scope
+     * @param AfterStepScope $scope scope passed by event after step.
      */
     protected function take_contentdump(AfterStepScope $scope) {
         list ($dir, $filename) = $this->get_faildump_filename($scope, 'html');
@@ -468,7 +508,7 @@ class behat_hooks extends behat_base {
      *
      * This is used for content such as the DOM, and screenshots.
      *
-     * @param AfterStepScope $scope
+     * @param AfterStepScope $scope scope passed by event after step.
      * @param String $filetype The file suffix to use. Limited to 4 chars.
      */
     protected function get_faildump_filename(AfterStepScope $scope, $filetype) {
@@ -528,18 +568,15 @@ class behat_hooks extends behat_base {
     protected static function is_first_scenario() {
         return !(self::$initprocessesfinished);
     }
-
-    /**
-     * Stops execution because of some exception.
-     *
-     * @param string $exception
-     * @return void
-     */
-    protected function stop_execution($exception) {
-        $text = get_string('unknownexceptioninfo', 'tool_behat');
-        echo $text . PHP_EOL . $exception . PHP_EOL;
-        exit(1);
-    }
-
 }
 
+/**
+ * Behat stop exception
+ *
+ * This exception is thrown from before suite or scenario if any setup problem found.
+ *
+ * @package    core_test
+ * @copyright  2016 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_stop_exception extends \Exception{}
\ No newline at end of file
index 39796cc..cda9bd3 100644 (file)
@@ -30,6 +30,19 @@ require_once($CFG->libdir . '/externallib.php');
 
 
 class core_externallib_testcase extends advanced_testcase {
+    protected $DB;
+
+    public function setUp() {
+        $this->DB = null;
+    }
+
+    public function tearDown() {
+        global $DB;
+        if ($this->DB !== null) {
+            $DB = $this->DB;
+        }
+    }
+
     public function test_validate_params() {
         $params = array('text'=>'aaa', 'someid'=>'6');
         $description = new external_function_parameters(array('someid' => new external_value(PARAM_INT, 'Some int value'),
@@ -459,6 +472,73 @@ class core_externallib_testcase extends advanced_testcase {
         $this->assertSame($beforecourse, $COURSE);
     }
 
+    /**
+     * Text external_util::get_area_files
+     */
+    public function test_external_util_get_area_files() {
+        global $CFG, $DB;
+
+        $this->DB = $DB;
+        $DB = $this->getMockBuilder('moodle_database')->getMock();
+
+        $content = base64_encode("Let us create a nice simple file.");
+        $timemodified = 102030405;
+        $itemid = 42;
+        $filesize = strlen($content);
+
+        $DB->method('get_records_sql')->willReturn([
+            (object) [
+                'filename'      => 'example.txt',
+                'filepath'      => '/',
+                'mimetype'      => 'text/plain',
+                'filesize'      => $filesize,
+                'timemodified'  => $timemodified,
+                'itemid'        => $itemid,
+                'pathnamehash'  => sha1('/example.txt'),
+            ],
+        ]);
+
+        $component = 'mod_foo';
+        $filearea = 'area';
+        $context = 12345;
+
+        $expectedfiles[] = array(
+            'filename' => 'example.txt',
+            'filepath' => '/',
+            'fileurl' => "{$CFG->wwwroot}/webservice/pluginfile.php/{$context}/{$component}/{$filearea}/{$itemid}/example.txt",
+            'timemodified' => $timemodified,
+            'filesize' => $filesize,
+            'mimetype' => 'text/plain',
+        );
+        // Get all the files for the area.
+        $files = external_util::get_area_files($context, $component, $filearea, false);
+        $this->assertEquals($expectedfiles, $files);
+
+        // Get just the file indicated by $itemid.
+        $files = external_util::get_area_files($context, $component, $filearea, $itemid);
+        $this->assertEquals($expectedfiles, $files);
+
+    }
+
+    /**
+     * Text external files structure.
+     */
+    public function test_external_files() {
+
+        $description = new external_files();
+
+        // First check that the expected default values and keys are returned.
+        $expectedkeys = array_flip(array('filename', 'filepath', 'filesize', 'fileurl', 'timemodified', 'mimetype'));
+        $returnedkeys = array_flip(array_keys($description->content->keys));
+        $this->assertEquals($expectedkeys, $returnedkeys);
+        $this->assertEquals('List of files.', $description->desc);
+        $this->assertEquals(VALUE_REQUIRED, $description->required);
+        foreach ($description->content->keys as $key) {
+            $this->assertEquals(VALUE_OPTIONAL, $key->required);
+        }
+
+    }
+
 }
 
 /*
index 78854dd..a4a1faf 100644 (file)
@@ -971,6 +971,221 @@ EOF;
         // Compare the final text is the same that the original.
         $this->assertEquals($originaltext, $finaltext);
     }
+
+    /**
+     * Helpter function to create draft files
+     *
+     * @param  array  $filedata data for the file record (to not use defaults)
+     * @return stored_file the stored file instance
+     */
+    public static function create_draft_file($filedata = array()) {
+        global $USER;
+
+        self::setAdminUser();
+        $fs = get_file_storage();
+
+        $filerecord = array(
+            'component' => 'user',
+            'filearea'  => 'draft',
+            'itemid'    => isset($filedata['itemid']) ? $filedata['itemid'] : file_get_unused_draft_itemid(),
+            'author'    => isset($filedata['author']) ? $filedata['author'] : fullname($USER),
+            'filepath'  => isset($filedata['filepath']) ? $filedata['filepath'] : '/',
+            'filename'  => isset($filedata['filename']) ? $filedata['filename'] : 'file.txt',
+        );
+
+        if (isset($filedata['contextid'])) {
+            $filerecord['contextid'] = $filedata['contextid'];
+        } else {
+            $usercontext = context_user::instance($USER->id);
+            $filerecord['contextid'] = $usercontext->id;
+        }
+        $source = isset($filedata['source']) ? $filedata['source'] : serialize((object)array('source' => 'From string'));
+        $content = isset($filedata['content']) ? $filedata['content'] : 'some content here';
+
+        $file = $fs->create_file_from_string($filerecord, $content);
+        $file->set_source($source);
+
+        return $file;
+    }
+
+    /**
+     * Test file_merge_files_from_draft_area_into_filearea
+     */
+    public function test_file_merge_files_from_draft_area_into_filearea() {
+        global $USER, $CFG;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $fs = get_file_storage();
+        $usercontext = context_user::instance($USER->id);
+
+        // Create a draft file.
+        $filename = 'data.txt';
+        $filerecord = array(
+            'filename'  => $filename,
+        );
+        $file = self::create_draft_file($filerecord);
+
+        $maxbytes = $CFG->userquota;
+        $maxareabytes = $CFG->userquota;
+        $options = array('subdirs' => 1,
+                         'maxbytes' => $maxbytes,
+                         'maxfiles' => -1,
+                         'areamaxbytes' => $maxareabytes);
+
+        // Add new file.
+        file_merge_files_from_draft_area_into_filearea($file->get_itemid(), $usercontext->id, 'user', 'private', 0, $options);
+
+        $files = $fs->get_area_files($usercontext->id, 'user', 'private', 0);
+        // Directory and file.
+        $this->assertCount(2, $files);
+        $found = false;
+        foreach ($files as $file) {
+            if (!$file->is_directory()) {
+                $found = true;
+                $this->assertEquals($filename, $file->get_filename());
+                $this->assertEquals('some content here', $file->get_content());
+            }
+        }
+        $this->assertTrue($found);
+
+        // Add two more files.
+        $filerecord = array(
+            'itemid'  => $file->get_itemid(),
+            'filename'  => 'second.txt',
+        );
+        self::create_draft_file($filerecord);
+        $filerecord = array(
+            'itemid'  => $file->get_itemid(),
+            'filename'  => 'third.txt',
+        );
+        $file = self::create_draft_file($filerecord);
+
+        file_merge_files_from_draft_area_into_filearea($file->get_itemid(), $usercontext->id, 'user', 'private', 0, $options);
+
+        $files = $fs->get_area_files($usercontext->id, 'user', 'private', 0);
+        $this->assertCount(4, $files);
+
+        // Update contents of&nb