Merge branch 'MDL-67691' of https://github.com/stronk7/moodle
authorAdrian Greeve <abgreeve@gmail.com>
Mon, 27 Jan 2020 23:52:21 +0000 (07:52 +0800)
committerAdrian Greeve <abgreeve@gmail.com>
Mon, 27 Jan 2020 23:52:21 +0000 (07:52 +0800)
251 files changed:
admin/cli/cron.php
admin/renderer.php
admin/settings/courses.php
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/behat/tests/behat/get_and_set_fields.feature
admin/tool/task/cli/adhoc_task.php
admin/tool/uploadcourse/classes/course.php
admin/tool/uploadcourse/lang/en/tool_uploadcourse.php
admin/tool/uploadcourse/tests/course_test.php
admin/tool/uploaduser/tests/behat/upload_users.feature
auth/ldap/auth.php
auth/ldap/tests/plugin_test.php
calendar/classes/local/event/forms/managesubscriptions.php
completion/criteria/completion_criteria_activity.php
course/lib.php
course/management.php
enrol/ldap/lib.php
enrol/ldap/tests/ldap_test.php
enrol/self/lib.php
enrol/self/tests/self_test.php
filter/activitynames/tests/filter_test.php
filter/tex/filter.php
group/autogroup.php
group/classes/output/user_groups_editable.php
group/externallib.php
group/index.php
group/lib.php
group/overview.php
group/tests/behat/auto_creation.feature
group/tests/behat/create_groups.feature
h5p/classes/core.php
h5p/classes/framework.php
h5p/tests/framework_test.php
h5p/tests/generator/lib.php
h5p/tests/h5p_core_test.php
install/lang/ca/error.php
install/lang/el/install.php
install/lang/my/langconfig.php
lang/en/admin.php
lib/adodb/adodb-active-record.inc.php
lib/adodb/adodb-active-recordx.inc.php
lib/adodb/adodb-csvlib.inc.php
lib/adodb/adodb-datadict.inc.php
lib/adodb/adodb-error.inc.php
lib/adodb/adodb-errorhandler.inc.php
lib/adodb/adodb-errorpear.inc.php
lib/adodb/adodb-exceptions.inc.php
lib/adodb/adodb-iterator.inc.php
lib/adodb/adodb-lib.inc.php
lib/adodb/adodb-memcache.lib.inc.php
lib/adodb/adodb-pager.inc.php
lib/adodb/adodb-pear.inc.php
lib/adodb/adodb-perf.inc.php
lib/adodb/adodb-php4.inc.php
lib/adodb/adodb-time.inc.php
lib/adodb/adodb.inc.php
lib/adodb/datadict/datadict-access.inc.php
lib/adodb/datadict/datadict-db2.inc.php
lib/adodb/datadict/datadict-firebird.inc.php
lib/adodb/datadict/datadict-generic.inc.php
lib/adodb/datadict/datadict-ibase.inc.php
lib/adodb/datadict/datadict-informix.inc.php
lib/adodb/datadict/datadict-mssql.inc.php
lib/adodb/datadict/datadict-mssqlnative.inc.php
lib/adodb/datadict/datadict-mysql.inc.php
lib/adodb/datadict/datadict-oci8.inc.php
lib/adodb/datadict/datadict-postgres.inc.php
lib/adodb/datadict/datadict-sapdb.inc.php
lib/adodb/datadict/datadict-sqlite.inc.php
lib/adodb/datadict/datadict-sybase.inc.php
lib/adodb/drivers/adodb-access.inc.php
lib/adodb/drivers/adodb-ado.inc.php
lib/adodb/drivers/adodb-ado5.inc.php
lib/adodb/drivers/adodb-ado_access.inc.php
lib/adodb/drivers/adodb-ado_mssql.inc.php
lib/adodb/drivers/adodb-borland_ibase.inc.php
lib/adodb/drivers/adodb-csv.inc.php
lib/adodb/drivers/adodb-db2.inc.php
lib/adodb/drivers/adodb-db2oci.inc.php
lib/adodb/drivers/adodb-db2ora.inc.php
lib/adodb/drivers/adodb-fbsql.inc.php
lib/adodb/drivers/adodb-firebird.inc.php
lib/adodb/drivers/adodb-ibase.inc.php
lib/adodb/drivers/adodb-informix.inc.php
lib/adodb/drivers/adodb-informix72.inc.php
lib/adodb/drivers/adodb-ldap.inc.php
lib/adodb/drivers/adodb-mssql.inc.php
lib/adodb/drivers/adodb-mssqlnative.inc.php
lib/adodb/drivers/adodb-mssqlpo.inc.php
lib/adodb/drivers/adodb-mysql.inc.php
lib/adodb/drivers/adodb-mysqli.inc.php
lib/adodb/drivers/adodb-mysqlpo.inc.php
lib/adodb/drivers/adodb-mysqlt.inc.php
lib/adodb/drivers/adodb-netezza.inc.php
lib/adodb/drivers/adodb-oci8.inc.php
lib/adodb/drivers/adodb-oci805.inc.php
lib/adodb/drivers/adodb-oci8po.inc.php
lib/adodb/drivers/adodb-oci8quercus.inc.php
lib/adodb/drivers/adodb-odbc.inc.php
lib/adodb/drivers/adodb-odbc_db2.inc.php
lib/adodb/drivers/adodb-odbc_mssql.inc.php
lib/adodb/drivers/adodb-odbc_oracle.inc.php
lib/adodb/drivers/adodb-odbtp.inc.php
lib/adodb/drivers/adodb-odbtp_unicode.inc.php
lib/adodb/drivers/adodb-oracle.inc.php
lib/adodb/drivers/adodb-pdo.inc.php
lib/adodb/drivers/adodb-pdo_mssql.inc.php
lib/adodb/drivers/adodb-pdo_mysql.inc.php
lib/adodb/drivers/adodb-pdo_oci.inc.php
lib/adodb/drivers/adodb-pdo_pgsql.inc.php
lib/adodb/drivers/adodb-pdo_sqlite.inc.php
lib/adodb/drivers/adodb-postgres.inc.php
lib/adodb/drivers/adodb-postgres64.inc.php
lib/adodb/drivers/adodb-postgres7.inc.php
lib/adodb/drivers/adodb-postgres8.inc.php
lib/adodb/drivers/adodb-postgres9.inc.php
lib/adodb/drivers/adodb-proxy.inc.php
lib/adodb/drivers/adodb-sapdb.inc.php
lib/adodb/drivers/adodb-sqlanywhere.inc.php
lib/adodb/drivers/adodb-sqlite.inc.php
lib/adodb/drivers/adodb-sqlite3.inc.php
lib/adodb/drivers/adodb-sqlitepo.inc.php
lib/adodb/drivers/adodb-sybase.inc.php
lib/adodb/drivers/adodb-sybase_ase.inc.php
lib/adodb/drivers/adodb-vfp.inc.php
lib/adodb/perf/perf-db2.inc.php
lib/adodb/perf/perf-informix.inc.php
lib/adodb/perf/perf-mssql.inc.php
lib/adodb/perf/perf-mssqlnative.inc.php
lib/adodb/perf/perf-mysql.inc.php
lib/adodb/perf/perf-oci8.inc.php
lib/adodb/perf/perf-postgres.inc.php
lib/adodb/pivottable.inc.php
lib/adodb/readme_moodle.txt
lib/adodb/rsfilter.inc.php
lib/adodb/toexport.inc.php
lib/adodb/tohtml.inc.php
lib/behat/behat_base.php
lib/classes/local/cli/shutdown.php [new file with mode: 0644]
lib/classes/shutdown_manager.php
lib/classes/task/h5p_get_content_types_task.php
lib/cronlib.php
lib/db/upgrade.php
lib/dml/pgsql_native_moodle_database.php
lib/editor/atto/plugins/html/thirdpartylibs.xml
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-debug.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-min.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror.js
lib/editor/atto/plugins/html/yui/src/codemirror/js/codemirror.js
lib/editor/atto/plugins/html/yui/src/codemirror/js/css.js
lib/editor/atto/plugins/html/yui/src/codemirror/js/htmlmixed.js
lib/editor/atto/plugins/html/yui/src/codemirror/js/javascript.js
lib/editor/atto/plugins/html/yui/src/codemirror/js/xml.js
lib/editor/atto/plugins/html/yui/src/codemirror/readme_moodle.txt
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/clean.js
lib/filterlib.php
lib/google/readme_moodle.txt
lib/google/src/Google/Http/REST.php
lib/htmlpurifier/HTMLPurifier.php
lib/htmlpurifier/HTMLPurifier/AttrDef/HTML/Bool.php
lib/htmlpurifier/HTMLPurifier/AttrDef/URI/Host.php
lib/htmlpurifier/HTMLPurifier/CSSDefinition.php
lib/htmlpurifier/HTMLPurifier/Config.php
lib/htmlpurifier/HTMLPurifier/ConfigSchema.php
lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema.ser
lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt
lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.AllowParseManyTags.txt [new file with mode: 0644]
lib/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt
lib/htmlpurifier/HTMLPurifier/EntityParser.php
lib/htmlpurifier/HTMLPurifier/HTMLModule.php
lib/htmlpurifier/HTMLPurifier/HTMLModule/SafeScripting.php
lib/htmlpurifier/HTMLPurifier/Language/messages/en-x-test.php
lib/htmlpurifier/HTMLPurifier/Language/messages/en-x-testmini.php
lib/htmlpurifier/HTMLPurifier/Lexer/DOMLex.php
lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php
lib/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php
lib/htmlpurifier/HTMLPurifier/VarParser.php
lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php
lib/htmlpurifier/readme_moodle.txt
lib/listlib.php
lib/minify/matthiasmullie-minify/src/CSS.php
lib/minify/readme_moodle.txt
lib/moodlelib.php
lib/questionlib.php
lib/setup.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_navigation.php
lib/tests/fixtures/testable_core_h5p.php [new file with mode: 0644]
lib/tests/h5p_get_content_types_task_test.php
lib/tests/other/pdflibtestpage.php
lib/thirdpartylibs.xml
message/lib.php
message/output/popup/amd/build/notification_popover_controller.min.js
message/output/popup/amd/build/notification_popover_controller.min.js.map
message/output/popup/amd/src/notification_popover_controller.js
message/output/popup/lib.php
message/output/popup/mark_notification_read.php
message/templates/message_drawer_view_overview_header.mustache
message/templates/message_drawer_view_settings_body_content.mustache
message/templates/message_drawer_view_settings_body_content_notification_preferences.mustache
message/templates/message_popover.mustache
message/tests/behat/message_drawer_manage_contacts.feature
mod/assign/locallib.php
mod/forum/amd/build/discussion_list.min.js
mod/forum/amd/build/discussion_list.min.js.map
mod/forum/amd/src/discussion_list.js
mod/forum/classes/local/managers/capability.php
mod/forum/lib.php
mod/forum/report/summary/tests/behat/summary_filter_groups.feature [new file with mode: 0644]
mod/forum/report/summary/tests/behat/summary_filter_no_groups.feature [new file with mode: 0644]
mod/forum/templates/discussion_list.mustache
mod/forum/templates/inpage_reply_v2.mustache
mod/forum/templates/setting_switch.mustache
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/tests/behat/discussion_subscriptions.feature
mod/forum/tests/behat/forum_subscriptions_default.feature
mod/forum/tests/behat/recent_activity.feature [new file with mode: 0644]
mod/forum/tests/managers_capability_test.php
mod/glossary/lib.php
mod/glossary/tests/behat/entries_always_editable.feature
mod/lti/db/access.php
mod/lti/lang/en/lti.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/mod_form.js
mod/lti/mod_form.php
mod/lti/version.php
mod/quiz/styles.css
mod/scorm/mod_form.php
mod/wiki/parser/parser.php
question/editlib.php
question/engine/renderer.php
question/format/examview/format.php
question/format/examview/tests/examviewformat_test.php
question/tests/behat/delete_question_activities.feature [new file with mode: 0644]
report/outline/tests/behat/user.feature
theme/boost/amd/build/loader.min.js
theme/boost/amd/build/loader.min.js.map
theme/boost/amd/src/loader.js
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/debug.scss
theme/boost/scss/moodle/popover-region.scss
theme/boost/scss/moodle/question.scss
theme/boost/scss/preset/default.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/classes/output/user_roles_editable.php
version.php

index d7d1646..fe72683 100644 (file)
@@ -74,4 +74,6 @@ if ($options['stop']) {
     die;
 }
 
+\core\local\cli\shutdown::script_supports_graceful_exit();
+
 cron_run();
index 4dbe8c0..4ab7754 100644 (file)
@@ -1290,7 +1290,7 @@ class core_admin_renderer extends plugin_renderer_base {
         if ($unavailable or $unknown) {
             $out .= $this->output->heading(get_string('misdepsunavail', 'core_plugin'));
             if ($unknown) {
-                $out .= $this->output->notification(get_string('misdepsunknownlist', 'core_plugin', implode($unknown, ', ')));
+                $out .= $this->output->notification(get_string('misdepsunknownlist', 'core_plugin', implode(', ', $unknown)));
             }
             if ($unavailable) {
                 $unavailablelist = array();
@@ -1305,7 +1305,7 @@ class core_admin_renderer extends plugin_renderer_base {
                     $unavailablelist[] = $unavailablelistitem;
                 }
                 $out .= $this->output->notification(get_string('misdepsunavaillist', 'core_plugin',
-                    implode($unavailablelist, ', ')));
+                    implode(', ', $unavailablelist)));
             }
             $out .= $this->output->container_start('plugins-check-dependencies-actions');
             $out .= ' '.html_writer::link(new moodle_url('/admin/tool/installaddon/'),
@@ -1425,7 +1425,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 html_writer::div($plugin->name, 'name').' '.html_writer::div($plugin->component, 'component'),
                 $plugin->version->release,
                 $plugin->version->version,
-                implode($supportedmoodles, ' '),
+                implode(' ', $supportedmoodles),
                 $info
             );
         }
@@ -2037,7 +2037,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 if (empty($CFG->docroot) or $environment_result->plugin) {
                     $report = get_string($stringtouse, 'admin', $rec);
                 } else {
-                    $report = $this->doc_link(join($linkparts, '/'), get_string($stringtouse, 'admin', $rec), true);
+                    $report = $this->doc_link(join('/', $linkparts), get_string($stringtouse, 'admin', $rec), true);
                 }
                 // Enclose report text in div so feedback text will be displayed underneath it.
                 $report = html_writer::div($report);
index 5989158..ad5ede6 100644 (file)
@@ -276,6 +276,8 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $maxkeptoptions = array(
         0 => new lang_string('all'), 1 => '1',
         2 => '2',
+        3 => '3',
+        4 => '4',
         5 => '5',
         10 => '10',
         20 => '20',
index f078fab..19644be 100644 (file)
@@ -259,9 +259,9 @@ Feature: Set up contextual data for tests
     Then the "groups" select box should contain "Group 1 (1)"
     And the "groups" select box should contain "Group 2 (1)"
     And I set the field "groups" to "Group 1 (1)"
-    And the "members" select box should contain "Student 1"
+    And the "members" select box should contain "Student 1 (student1@example.com)"
     And I set the field "groups" to "Group 2 (1)"
-    And the "members" select box should contain "Student 2"
+    And the "members" select box should contain "Student 2 (student2@example.com)"
 
   Scenario: Add cohorts and cohort members with data generator
     Given the following "categories" exist:
index 9ade5e0..3f1d88e 100644 (file)
@@ -147,13 +147,13 @@ Feature: Verify that all form fields values can be get and set
     And I navigate to "Users > Groups" in current page administration
     # Select (multi-select & AJAX) - Checking "I set the field" and "select box should contain".
     And I set the field "groups" to "Group 2"
-    And the "members" select box should contain "Student 2"
-    And the "members" select box should contain "Student 3"
-    And the "members" select box should not contain "Student 1"
+    And the "members" select box should contain "Student 2 (s2@example.com)"
+    And the "members" select box should contain "Student 3 (s3@example.com)"
+    And the "members" select box should not contain "Student 1 (s1@example.com)"
     And I set the field "groups" to "Group 1"
-    And the "members" select box should contain "Student 1"
-    And the "members" select box should contain "Student 2"
-    And the "members" select box should not contain "Student 3"
+    And the "members" select box should contain "Student 1 (s1@example.com)"
+    And the "members" select box should contain "Student 2 (s2@example.com)"
+    And the "members" select box should not contain "Student 3 (s3@example.com)"
     # Checkbox (AJAX) - Checking "I set the field" and "I set the following fields to these values".
     And I am on "Course 1" course homepage
     And I add a "Lesson" to section "1"
index 755026b..1a04ba8 100644 (file)
@@ -115,5 +115,7 @@ cron_setup_user();
 $humantimenow = date('r', time());
 $keepalive = (int)$options['keep-alive'];
 
+\core\local\cli\shutdown::script_supports_graceful_exit();
+
 mtrace("Server Time: {$humantimenow}\n");
 cron_run_adhoc_tasks(time(), $keepalive, $checklimits);
index 6f2cffa..8ae7a8d 100644 (file)
@@ -412,6 +412,12 @@ class tool_uploadcourse_course {
                 $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
                 return false;
             }
+
+            // Ensure we don't overflow the maximum length of the shortname field.
+            if (core_text::strlen($this->shortname) > 255) {
+                $this->error('invalidshortnametoolong', new lang_string('invalidshortnametoolong', 'tool_uploadcourse', 255));
+                return false;
+            }
         }
 
         $exists = $this->exists();
@@ -479,6 +485,12 @@ class tool_uploadcourse_course {
             return false;
         }
 
+        // Ensure we don't overflow the maximum length of the fullname field.
+        if (!empty($coursedata['fullname']) && core_text::strlen($coursedata['fullname']) > 254) {
+            $this->error('invalidfullnametoolong', new lang_string('invalidfullnametoolong', 'tool_uploadcourse', 254));
+            return false;
+        }
+
         // If the course does not exist, or will be forced created.
         if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
 
index 7f3e9d7..9a0e2c6 100644 (file)
@@ -93,6 +93,8 @@ $string['invalideupdatemode'] = 'Invalid update mode selected';
 $string['invalidvisibilitymode'] = 'Invalid visible mode';
 $string['invalidroles'] = 'Invalid role names: {$a}';
 $string['invalidshortname'] = 'Invalid shortname';
+$string['invalidfullnametoolong'] = 'The fullname field is limited to {$a} characters';
+$string['invalidshortnametoolong'] = 'The shortname field is limited to {$a} characters';
 $string['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}';
 $string['missingshortnamenotemplate'] = 'Missing shortname and shortname template not set';
 $string['mode'] = 'Upload mode';
index c0d29e0..0add18e 100644 (file)
@@ -82,6 +82,37 @@ class tool_uploadcourse_course_testcase extends advanced_testcase {
         $this->assertArrayHasKey('invalidshortname', $co->get_errors());
     }
 
+    public function test_invalid_shortname_too_long() {
+        $this->resetAfterTest();
+
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+
+        $upload = new tool_uploadcourse_course($mode, $updatemode, [
+            'category' => 1,
+            'fullname' => 'New course',
+            'shortname' => str_repeat('X', 2000),
+        ]);
+
+        $this->assertFalse($upload->prepare());
+        $this->assertArrayHasKey('invalidshortnametoolong', $upload->get_errors());
+    }
+
+    public function test_invalid_fullname_too_long() {
+        $this->resetAfterTest();
+
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+
+        $upload = new tool_uploadcourse_course($mode, $updatemode, [
+            'category' => 1,
+            'fullname' => str_repeat('X', 2000),
+        ]);
+
+        $this->assertFalse($upload->prepare());
+        $this->assertArrayHasKey('invalidfullnametoolong', $upload->get_errors());
+    }
+
     public function test_invalid_visibility() {
         $this->resetAfterTest(true);
         $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
index a92b633..fc55002 100644 (file)
@@ -36,7 +36,7 @@ Feature: Upload users
     And I am on "Maths" course homepage
     And I navigate to "Users > Groups" in current page administration
     And I set the field "groups" to "Section 1 (1)"
-    And the "members" select box should contain "Tom Jones"
+    And the "members" select box should contain "Tom Jones (jonest@example.com)"
 
   @javascript
   Scenario: Upload users enrolling them on courses and groups applying defaults
index 1052f3a..f03372f 100644 (file)
@@ -691,6 +691,7 @@ class auth_plugin_ldap extends auth_plugin_base {
         ////
         // prepare some data we'll need
         $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')';
+        $servercontrols = array();
 
         $contexts = explode(';', $this->config->contexts);
 
@@ -708,24 +709,58 @@ class auth_plugin_ldap extends auth_plugin_base {
 
             do {
                 if ($ldappagedresults) {
-                    ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldapcookie);
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        // Before 7.3, use this function that was deprecated in PHP 7.4.
+                        ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldapcookie);
+                    } else {
+                        // PHP 7.3 and up, use server controls.
+                        $servercontrols = array(array(
+                            'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array(
+                                'size' => $this->config->pagesize, 'cookie' => $ldapcookie)));
+                    }
                 }
                 if ($this->config->search_sub) {
                     // Use ldap_search to find first user from subtree.
-                    $ldapresult = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute));
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        $ldapresult = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute));
+                    } else {
+                        $ldapresult = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute),
+                            0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
+                    }
                 } else {
                     // Search only in this context.
-                    $ldapresult = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute));
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        $ldapresult = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute));
+                    } else {
+                        $ldapresult = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute),
+                            0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
+                    }
                 }
                 if (!$ldapresult) {
                     continue;
                 }
                 if ($ldappagedresults) {
-                    $pagedresp = ldap_control_paged_result_response($ldapconnection, $ldapresult, $ldapcookie);
-                    // Function ldap_control_paged_result_response() does not overwrite $ldapcookie if it fails, by
-                    // setting this to null we avoid an infinite loop.
-                    if ($pagedresp === false) {
-                        $ldapcookie = null;
+                    // Get next server cookie to know if we'll need to continue searching.
+                    $ldapcookie = '';
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        // Before 7.3, use this function that was deprecated in PHP 7.4.
+                        $pagedresp = ldap_control_paged_result_response($ldapconnection, $ldapresult, $ldapcookie);
+                        // Function ldap_control_paged_result_response() does not overwrite $ldapcookie if it fails, by
+                        // setting this to null we avoid an infinite loop.
+                        if ($pagedresp === false) {
+                            $ldapcookie = null;
+                        }
+                    } else {
+                        // Get next cookie from controls.
+                        ldap_parse_result($ldapconnection, $ldapresult, $errcode, $matcheddn,
+                            $errmsg, $referrals, $controls);
+                        if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
+                            $ldapcookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
+                        }
                     }
                 }
                 if ($entry = @ldap_first_entry($ldapconnection, $ldapresult)) {
@@ -1504,6 +1539,7 @@ class auth_plugin_ldap extends auth_plugin_base {
         if ($filter == '*') {
            $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')';
         }
+        $servercontrols = array();
 
         $contexts = explode(';', $this->config->contexts);
         if (!empty($this->config->create_context)) {
@@ -1520,20 +1556,54 @@ class auth_plugin_ldap extends auth_plugin_base {
 
             do {
                 if ($ldap_pagedresults) {
-                    ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldap_cookie);
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        // Before 7.3, use this function that was deprecated in PHP 7.4.
+                        ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldap_cookie);
+                    } else {
+                        // PHP 7.3 and up, use server controls.
+                        $servercontrols = array(array(
+                            'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array(
+                                'size' => $this->config->pagesize, 'cookie' => $ldap_cookie)));
+                    }
                 }
                 if ($this->config->search_sub) {
                     // Use ldap_search to find first user from subtree.
-                    $ldap_result = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute));
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        $ldap_result = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute));
+                    } else {
+                        $ldap_result = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute),
+                            0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
+                    }
                 } else {
                     // Search only in this context.
-                    $ldap_result = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute));
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        $ldap_result = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute));
+                    } else {
+                        $ldap_result = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute),
+                            0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
+                    }
                 }
                 if(!$ldap_result) {
                     continue;
                 }
                 if ($ldap_pagedresults) {
-                    ldap_control_paged_result_response($ldapconnection, $ldap_result, $ldap_cookie);
+                    // Get next server cookie to know if we'll need to continue searching.
+                    $ldap_cookie = '';
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        // Before 7.3, use this function that was deprecated in PHP 7.4.
+                        ldap_control_paged_result_response($ldapconnection, $ldap_result, $ldap_cookie);
+                    } else {
+                        // Get next cookie from controls.
+                        ldap_parse_result($ldapconnection, $ldap_result, $errcode, $matcheddn,
+                            $errmsg, $referrals, $controls);
+                        if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
+                            $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
+                        }
+                    }
                 }
                 $users = ldap_get_entries_moodle($ldapconnection, $ldap_result);
                 // Add found users to list.
index f746583..17ee9dd 100644 (file)
@@ -36,7 +36,36 @@ defined('MOODLE_INTERNAL') || die();
 
 class auth_ldap_plugin_testcase extends advanced_testcase {
 
-    public function test_auth_ldap() {
+    /**
+     * Data provider for auth_ldap tests
+     *
+     * Used to ensure that all the paged stuff works properly, irrespectively
+     * of the pagesize configured (that implies all the chunking and paging
+     * built in the plugis is doing its work consistently). Both searching and
+     * not searching within subcontexts.
+     *
+     * @return array[]
+     */
+    public function auth_ldap_provider() {
+        $pagesizes = [1, 3, 5, 1000];
+        $subcontexts = [0, 1];
+        $combinations = [];
+        foreach ($pagesizes as $pagesize) {
+            foreach ($subcontexts as $subcontext) {
+                $combinations["pagesize {$pagesize}, subcontexts {$subcontext}"] = [$pagesize, $subcontext];
+            }
+        }
+        return $combinations;
+    }
+
+    /**
+     * General auth_ldap testcase
+     *
+     * @dataProvider auth_ldap_provider
+     * @param int $pagesize Value to be configured in settings controlling page size.
+     * @param int $subcontext Value to be configured in settings controlling searching in subcontexts.
+     */
+    public function test_auth_ldap(int $pagesize, int $subcontext) {
         global $CFG, $DB;
 
         if (!extension_loaded('ldap')) {
@@ -100,12 +129,12 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         set_config('start_tls', 0, 'auth_ldap');
         set_config('ldap_version', 3, 'auth_ldap');
         set_config('ldapencoding', 'utf-8', 'auth_ldap');
-        set_config('pagesize', '2', 'auth_ldap');
+        set_config('pagesize', $pagesize, 'auth_ldap');
         set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth_ldap');
         set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth_ldap');
         set_config('user_type', 'rfc2307', 'auth_ldap');
         set_config('contexts', 'ou=users,'.$topdn, 'auth_ldap');
-        set_config('search_sub', 0, 'auth_ldap');
+        set_config('search_sub', $subcontext, 'auth_ldap');
         set_config('opt_deref', LDAP_DEREF_NEVER, 'auth_ldap');
         set_config('user_attribute', 'cn', 'auth_ldap');
         set_config('memberattribute', 'memberuid', 'auth_ldap');
index 6063f40..ef8d4b7 100644 (file)
@@ -78,9 +78,9 @@ class managesubscriptions extends \moodleform {
         $mform->addElement('filepicker', 'importfile', get_string('importfromfile', 'calendar'), null, array('accepted_types' => '.ics'));
 
         // Disable appropriate elements depending on import from value.
-        $mform->disabledIf('pollinterval', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_FILE);
-        $mform->disabledIf('url',  'importfrom', 'eq', CALENDAR_IMPORT_FROM_FILE);
-        $mform->disabledIf('importfile', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_URL);
+        $mform->hideIf('pollinterval', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_FILE);
+        $mform->hideIf('url',  'importfrom', 'eq', CALENDAR_IMPORT_FROM_FILE);
+        $mform->hideIf('importfile', 'importfrom', 'eq', CALENDAR_IMPORT_FROM_URL);
 
         // Add the select elements for the available event types.
         $this->add_event_type_elements($mform, $eventtypes);
index 592c84e..3e43ef1 100644 (file)
@@ -288,7 +288,7 @@ class completion_criteria_activity extends completion_criteria {
             }
         }
 
-        $details['requirement'] = implode($details['requirement'], ', ');
+        $details['requirement'] = implode(', ', $details['requirement']);
 
         $details['status'] = '';
 
index 9eca03f..1c6dd3a 100644 (file)
@@ -1151,7 +1151,9 @@ function course_delete_module($cmid, $async = false) {
     }
 
     // Delete activity context questions and question categories.
-    question_delete_activity($cm);
+    $showinfo = !defined('AJAX_SCRIPT') || AJAX_SCRIPT == '0';
+
+    question_delete_activity($cm, $showinfo);
 
     // Call the delete_instance function, if it returns false throw an exception.
     if (!$deleteinstancefunction($cm->instance)) {
index 492a9f1..ad74a3e 100644 (file)
@@ -316,7 +316,8 @@ if ($action !== false && confirm_sesskey()) {
                         $notificationsfail[] = get_string('movecategoryownparent', 'error', $cattomove->get_formatted_name());
                         continue;
                     }
-                    if (strpos($movetocat->path, $cattomove->path) === 0) {
+                    // Don't allow user to move selected category into one of it's own sub-categories.
+                    if (strpos($movetocat->path, $cattomove->path . '/') === 0) {
                         $notificationsfail[] = get_string('movecategoryparentconflict', 'error', $cattomove->get_formatted_name());
                         continue;
                     }
index 4d37457..8c4672d 100644 (file)
@@ -379,6 +379,7 @@ class enrol_ldap_plugin extends enrol_plugin {
             }
 
             $ldap_cookie = '';
+            $servercontrols = array();
             foreach ($ldap_contexts as $ldap_context) {
                 $ldap_context = trim($ldap_context);
                 if (empty($ldap_context)) {
@@ -388,28 +389,60 @@ class enrol_ldap_plugin extends enrol_plugin {
                 $flat_records = array();
                 do {
                     if ($ldap_pagedresults) {
-                        ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
+                        // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                        if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                            // Before 7.3, use this function that was deprecated in PHP 7.4.
+                            ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
+                        } else {
+                            // PHP 7.3 and up, use server controls.
+                            $servercontrols = array(array(
+                                'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array(
+                                    'size' => $this->config->pagesize, 'cookie' => $ldap_cookie)));
+                        }
                     }
 
                     if ($this->config->course_search_sub) {
                         // Use ldap_search to find first user from subtree
-                        $ldap_result = @ldap_search($this->ldapconnection,
-                                                    $ldap_context,
-                                                    $ldap_search_pattern,
-                                                    $ldap_fields_wanted);
+                        // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                        if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                            $ldap_result = @ldap_search($this->ldapconnection, $ldap_context,
+                                $ldap_search_pattern, $ldap_fields_wanted);
+                        } else {
+                            $ldap_result = @ldap_search($this->ldapconnection, $ldap_context,
+                                $ldap_search_pattern, $ldap_fields_wanted,
+                                0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
+                        }
                     } else {
                         // Search only in this context
-                        $ldap_result = @ldap_list($this->ldapconnection,
-                                                  $ldap_context,
-                                                  $ldap_search_pattern,
-                                                  $ldap_fields_wanted);
+                        // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                        if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                            $ldap_result = @ldap_list($this->ldapconnection, $ldap_context,
+                                $ldap_search_pattern, $ldap_fields_wanted);
+                        } else {
+                            $ldap_result = @ldap_list($this->ldapconnection, $ldap_context,
+                                $ldap_search_pattern, $ldap_fields_wanted,
+                                0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
+                        }
                     }
                     if (!$ldap_result) {
                         continue; // Next
                     }
 
                     if ($ldap_pagedresults) {
-                        ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
+                        // Get next server cookie to know if we'll need to continue searching.
+                        $ldap_cookie = '';
+                        // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                        if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                            // Before 7.3, use this function that was deprecated in PHP 7.4.
+                            ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
+                        } else {
+                            // Get next cookie from controls.
+                            ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn,
+                                $errmsg, $referrals, $controls);
+                            if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
+                                $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
+                            }
+                        }
                     }
 
                     // Check and push results
@@ -769,24 +802,44 @@ class enrol_ldap_plugin extends enrol_plugin {
             }
 
             $ldap_cookie = '';
+            $servercontrols = array();
             $flat_records = array();
             do {
                 if ($ldap_pagedresults) {
-                    ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        // Before 7.3, use this function that was deprecated in PHP 7.4.
+                        ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
+                    } else {
+                        // PHP 7.3 and up, use server controls.
+                        $servercontrols = array(array(
+                            'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array(
+                                'size' => $this->config->pagesize, 'cookie' => $ldap_cookie)));
+                    }
                 }
 
                 if ($this->get_config('course_search_sub')) {
                     // Use ldap_search to find first user from subtree
-                    $ldap_result = @ldap_search($this->ldapconnection,
-                                                $context,
-                                                $ldap_search_pattern,
-                                                $ldap_fields_wanted);
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        $ldap_result = @ldap_search($this->ldapconnection, $context,
+                            $ldap_search_pattern, $ldap_fields_wanted);
+                    } else {
+                        $ldap_result = @ldap_search($this->ldapconnection, $context,
+                            $ldap_search_pattern, $ldap_fields_wanted,
+                            0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
+                    }
                 } else {
                     // Search only in this context
-                    $ldap_result = @ldap_list($this->ldapconnection,
-                                              $context,
-                                              $ldap_search_pattern,
-                                              $ldap_fields_wanted);
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        $ldap_result = @ldap_list($this->ldapconnection, $context,
+                            $ldap_search_pattern, $ldap_fields_wanted);
+                    } else {
+                        $ldap_result = @ldap_list($this->ldapconnection, $context,
+                            $ldap_search_pattern, $ldap_fields_wanted,
+                            0, -1, -1, LDAP_DEREF_NEVER, $servercontrols);
+                    }
                 }
 
                 if (!$ldap_result) {
@@ -794,7 +847,20 @@ class enrol_ldap_plugin extends enrol_plugin {
                 }
 
                 if ($ldap_pagedresults) {
-                    ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
+                    // Get next server cookie to know if we'll need to continue searching.
+                    $ldap_cookie = '';
+                    // TODO: Remove the old branch of code once PHP 7.3.0 becomes required (Moodle 4.1).
+                    if (version_compare(PHP_VERSION, '7.3.0', '<')) {
+                        // Before 7.3, use this function that was deprecated in PHP 7.4.
+                        ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
+                    } else {
+                        // Get next cookie from controls.
+                        ldap_parse_result($this->ldapconnection, $ldap_result, $errcode, $matcheddn,
+                            $errmsg, $referrals, $controls);
+                        if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
+                            $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
+                        }
+                    }
                 }
 
                 // Check and push results. ldap_get_entries() already
index 9ac6277..9893146 100644 (file)
@@ -39,7 +39,36 @@ global $CFG;
 
 class enrol_ldap_testcase extends advanced_testcase {
 
-    public function test_enrol_ldap() {
+    /**
+     * Data provider for enrol_ldap tests
+     *
+     * Used to ensure that all the paged stuff works properly, irrespectively
+     * of the pagesize configured (that implies all the chunking and paging
+     * built in the plugis is doing its work consistently). Both searching and
+     * not searching within subcontexts.
+     *
+     * @return array[]
+     */
+    public function enrol_ldap_provider() {
+        $pagesizes = [1, 3, 5, 1000];
+        $subcontexts = [0, 1];
+        $combinations = [];
+        foreach ($pagesizes as $pagesize) {
+            foreach ($subcontexts as $subcontext) {
+                $combinations["pagesize {$pagesize}, subcontexts {$subcontext}"] = [$pagesize, $subcontext];
+            }
+        }
+        return $combinations;
+    }
+
+    /**
+     * General enrol_ldap testcase
+     *
+     * @dataProvider enrol_ldap_provider
+     * @param int $pagesize Value to be configured in settings controlling page size.
+     * @param int $subcontext Value to be configured in settings controlling searching in subcontexts.
+     */
+    public function test_enrol_ldap(int $pagesize, int $subcontext) {
         global $CFG, $DB;
 
         if (!extension_loaded('ldap')) {
@@ -83,10 +112,10 @@ class enrol_ldap_testcase extends advanced_testcase {
         $enrol->set_config('start_tls', 0);
         $enrol->set_config('ldap_version', 3);
         $enrol->set_config('ldapencoding', 'utf-8');
-        $enrol->set_config('pagesize', '2');
+        $enrol->set_config('pagesize', $pagesize);
         $enrol->set_config('bind_dn', TEST_ENROL_LDAP_BIND_DN);
         $enrol->set_config('bind_pw', TEST_ENROL_LDAP_BIND_PW);
-        $enrol->set_config('course_search_sub', 0);
+        $enrol->set_config('course_search_sub', $subcontext);
         $enrol->set_config('memberattribute_isdn', 0);
         $enrol->set_config('user_contexts', '');
         $enrol->set_config('user_search_sub', 0);
index f3bc7b5..65b1440 100644 (file)
@@ -450,8 +450,9 @@ class enrol_self_plugin extends enrol_plugin {
             $userid = $instance->userid;
             unset($instance->userid);
             $this->unenrol_user($instance, $userid);
-            $days = $instance->customint2 / 60*60*24;
-            $trace->output("unenrolling user $userid from course $instance->courseid as they have did not log in for at least $days days", 1);
+            $days = $instance->customint2 / DAYSECS;
+            $trace->output("unenrolling user $userid from course $instance->courseid " .
+                "as they did not log in for at least $days days", 1);
         }
         $rs->close();
 
@@ -467,8 +468,9 @@ class enrol_self_plugin extends enrol_plugin {
             $userid = $instance->userid;
             unset($instance->userid);
             $this->unenrol_user($instance, $userid);
-                $days = $instance->customint2 / 60*60*24;
-            $trace->output("unenrolling user $userid from course $instance->courseid as they have did not access course for at least $days days", 1);
+            $days = $instance->customint2 / DAYSECS;
+            $trace->output("unenrolling user $userid from course $instance->courseid " .
+                "as they did not access the course for at least $days days", 1);
         }
         $rs->close();
 
index 7c701ed..e939eaf 100644 (file)
@@ -61,7 +61,7 @@ class enrol_self_testcase extends advanced_testcase {
 
         $now = time();
 
-        $trace = new null_progress_trace();
+        $trace = new progress_trace_buffer(new text_progress_trace(), false);
 
         // Prepare some data.
 
@@ -133,18 +133,32 @@ class enrol_self_testcase extends advanced_testcase {
         // Execute sync - this is the same thing used from cron.
 
         $selfplugin->sync($trace, $course2->id);
+        $output = $trace->get_buffer();
+        $trace->reset_buffer();
         $this->assertEquals(10, $DB->count_records('user_enrolments'));
-
+        $this->assertStringContainsString('No expired enrol_self enrolments detected', $output);
         $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user1->id)));
         $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user2->id)));
         $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user1->id)));
         $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user3->id)));
+
         $selfplugin->sync($trace, null);
+        $output = $trace->get_buffer();
+        $trace->reset_buffer();
         $this->assertEquals(6, $DB->count_records('user_enrolments'));
         $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user1->id)));
         $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user2->id)));
         $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user1->id)));
         $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user3->id)));
+        $this->assertStringContainsString('unenrolling user ' . $user1->id . ' from course ' . $course1->id .
+            ' as they did not log in for at least 14 days', $output);
+        $this->assertStringContainsString('unenrolling user ' . $user1->id . ' from course ' . $course3->id .
+            ' as they did not log in for at least 50 days', $output);
+        $this->assertStringContainsString('unenrolling user ' . $user2->id . ' from course ' . $course1->id .
+            ' as they did not access the course for at least 14 days', $output);
+        $this->assertStringContainsString('unenrolling user ' . $user3->id . ' from course ' . $course3->id .
+            ' as they did not access the course for at least 50 days', $output);
+        $this->assertStringNotContainsString('unenrolling user ' . $user4->id, $output);
 
         $this->assertEquals(6, $DB->count_records('role_assignments'));
         $this->assertEquals(4, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
index 403170e..b21c924 100644 (file)
@@ -37,7 +37,6 @@ require_once($CFG->dirroot . '/filter/activitynames/filter.php'); // Include the
 class filter_activitynames_filter_testcase extends advanced_testcase {
 
     public function test_links() {
-        global $CFG;
         $this->resetAfterTest(true);
 
         // Create a test course.
@@ -59,8 +58,8 @@ class filter_activitynames_filter_testcase extends advanced_testcase {
         preg_match_all('~<a class="autolink" title="([^"]*)" href="[^"]*/mod/page/view.php\?id=([0-9]+)">([^<]*)</a>~',
                 $filtered, $matches);
 
-        // There should be 3 links links.
-        $this->assertEquals(2, count($matches[1]));
+        // There should be 2 links links.
+        $this->assertCount(2, $matches[1]);
 
         // Check text of title attribute.
         $this->assertEquals($page1->name, $matches[1][0]);
@@ -74,4 +73,33 @@ class filter_activitynames_filter_testcase extends advanced_testcase {
         $this->assertEquals($page1->name, $matches[3][0]);
         $this->assertEquals($page2->name, $matches[3][1]);
     }
+
+    public function test_links_activity_named_hyphen() {
+        $this->resetAfterTest(true);
+
+        // Create a test course.
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+
+        // Work around an issue with the activity names filter which maintains a static cache
+        // of activities for current course ID. We can re-build the cache by switching user.
+        $this->setUser($this->getDataGenerator()->create_user());
+
+        // Create a page activity named '-' (single hyphen).
+        $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'name' => '-']);
+
+        $html = '<p>Please read the - page.</p>';
+        $filtered = format_text($html, FORMAT_HTML, array('context' => $context));
+
+        // Find the page link in the filtered html.
+        preg_match_all('~<a class="autolink" title="([^"]*)" href="[^"]*/mod/page/view.php\?id=([0-9]+)">([^<]*)</a>~',
+            $filtered, $matches);
+
+        // We should have exactly one match.
+        $this->assertCount(1, $matches[1]);
+
+        $this->assertEquals($page->name, $matches[1][0]);
+        $this->assertEquals($page->cmid, $matches[2][0]);
+        $this->assertEquals($page->name, $matches[3][0]);
+    }
 }
index 924837f..395638d 100644 (file)
@@ -160,7 +160,7 @@ class filter_tex extends moodle_text_filter {
             '\\\\\((.+?)\\\\\)',
             '\\[tex\\](.+?)\\[\/tex\\]'
         );
-        $megarule = '/' . implode($rules, '|') . '/is';
+        $megarule = '/' . implode('|', $rules) . '/is';
         preg_match_all($megarule, $text, $matches);
         for ($i=0; $i<count($matches[0]); $i++) {
             $texexp = '';
index b66ca45..cc2aeeb 100644 (file)
@@ -99,8 +99,9 @@ if ($editform->is_cancelled()) {
     // Display only active users if the option was selected or they do not have the capability to view suspended users.
     $onlyactive = !empty($data->includeonlyactiveenrol) || !has_capability('moodle/course:viewsuspendedusers', $context);
 
+    $extrafields = get_extra_user_fields($context);
     $users = groups_get_potential_members($data->courseid, $data->roleid, $source, $orderby, !empty($data->notingroup),
-        $onlyactive);
+        $onlyactive, $extrafields);
     $usercnt = count($users);
 
     if ($data->allocateby == 'random') {
@@ -171,7 +172,7 @@ if ($editform->is_cancelled()) {
             $table->width = '90%';
         }
         $table->data  = array();
-
+        $viewfullnames = has_capability('moodle/site:viewfullnames', $context);
         foreach ($groups as $group) {
             $line = array();
             if (groups_get_group_by_name($courseid, $group['name'])) {
@@ -183,7 +184,16 @@ if ($editform->is_cancelled()) {
             if ($data->allocateby != 'no') {
                 $unames = array();
                 foreach ($group['members'] as $user) {
-                    $unames[] = fullname($user, true);
+                    $fullname = fullname($user, $viewfullnames);
+                    if ($extrafields) {
+                        $extrafieldsdisplay = [];
+                        foreach ($extrafields as $field) {
+                            $extrafieldsdisplay[] = s($user->{$field});
+                        }
+                        $fullname .= ' (' . implode(', ', $extrafieldsdisplay) . ')';
+                    }
+
+                    $unames[] = $fullname;
                 }
                 $line[] = implode(', ', $unames);
                 $line[] = count($group['members']);
index 9595a03..13dba2a 100644 (file)
@@ -77,8 +77,10 @@ class user_groups_editable extends \core\output\inplace_editable {
         foreach ($coursegroups as $group) {
             $options[$group->id] = format_string($group->name, true, ['context' => $this->context]);
         }
-        $this->edithint = get_string('editusersgroupsa', 'group', fullname($user));
-        $this->editlabel = get_string('editusersgroupsa', 'group', fullname($user));
+
+        $fullname = fullname($user, has_capability('moodle/site:viewfullnames', $this->context));
+        $this->edithint = get_string('editusersgroupsa', 'group', $fullname);
+        $this->editlabel = get_string('editusersgroupsa', 'group', $fullname);
 
         $attributes = ['multiple' => true];
         $this->set_type_autocomplete($options, $attributes);
@@ -98,7 +100,7 @@ class user_groups_editable extends \core\output\inplace_editable {
         }
 
         if (!empty($listofgroups)) {
-            $this->displayvalue = implode($listofgroups, ', ');
+            $this->displayvalue = implode(', ', $listofgroups);
         } else {
             $this->displayvalue = get_string('groupsnone');
         }
index bebede4..341e727 100644 (file)
@@ -556,7 +556,8 @@ class core_group_external extends external_api {
             require_capability('moodle/course:managegroups', $context);
 
             if (!groups_remove_member_allowed($group, $user)) {
-                throw new moodle_exception('errorremovenotpermitted', 'group', '', fullname($user));
+                $fullname = fullname($user, has_capability('moodle/site:viewfullnames', $context));
+                throw new moodle_exception('errorremovenotpermitted', 'group', '', $fullname);
             }
             groups_remove_member($group, $user);
         }
index 79c9e21..e2aa22b 100644 (file)
@@ -80,7 +80,13 @@ switch ($action) {
 
     case 'ajax_getmembersingroup':
         $roles = array();
-        if ($groupmemberroles = groups_get_members_by_role($groupids[0], $courseid, 'u.id, ' . get_all_user_name_fields(true, 'u'))) {
+
+        $extrafields = get_extra_user_fields($context);
+        if ($groupmemberroles = groups_get_members_by_role($groupids[0], $courseid,
+                'u.id, ' . user_picture::fields('u', $extrafields))) {
+
+            $viewfullnames = has_capability('moodle/site:viewfullnames', $context);
+
             foreach($groupmemberroles as $roleid=>$roledata) {
                 $shortroledata = new stdClass();
                 $shortroledata->name = $roledata->name;
@@ -88,7 +94,15 @@ switch ($action) {
                 foreach($roledata->users as $member) {
                     $shortmember = new stdClass();
                     $shortmember->id = $member->id;
-                    $shortmember->name = fullname($member, true);
+                    $shortmember->name = fullname($member, $viewfullnames);
+                    if ($extrafields) {
+                        $extrafieldsdisplay = [];
+                        foreach ($extrafields as $field) {
+                            $extrafieldsdisplay[] = s($member->{$field});
+                        }
+                        $shortmember->name .= ' (' . implode(', ', $extrafieldsdisplay) . ')';
+                    }
+
                     $shortroledata->users[] = $shortmember;
                 }
                 $roles[] = $shortroledata;
@@ -188,15 +202,27 @@ if ($groups) {
 // Get list of group members to render if there is a single selected group.
 $members = array();
 if ($singlegroup) {
-    $usernamefields = get_all_user_name_fields(true, 'u');
-    if ($groupmemberroles = groups_get_members_by_role(reset($groupids), $courseid, 'u.id, ' . $usernamefields)) {
+    $extrafields = get_extra_user_fields($context);
+    if ($groupmemberroles = groups_get_members_by_role(reset($groupids), $courseid,
+            'u.id, ' . user_picture::fields('u', $extrafields))) {
+
+        $viewfullnames = has_capability('moodle/site:viewfullnames', $context);
+
         foreach ($groupmemberroles as $roleid => $roledata) {
             $users = array();
             foreach ($roledata->users as $member) {
-                $users[] = (object)[
-                    'value' => $member->id,
-                    'text' => fullname($member, true)
-                ];
+                $shortmember = new stdClass();
+                $shortmember->value = $member->id;
+                $shortmember->text = fullname($member, $viewfullnames);
+                if ($extrafields) {
+                    $extrafieldsdisplay = [];
+                    foreach ($extrafields as $field) {
+                        $extrafieldsdisplay[] = s($member->{$field});
+                    }
+                    $shortmember->text .= ' (' . implode(', ', $extrafieldsdisplay) . ')';
+                }
+
+                $users[] = $shortmember;
             }
             $members[] = (object)[
                 'role' => s($roledata->name),
index 42e7fbe..9833016 100644 (file)
@@ -785,11 +785,12 @@ function groups_get_possible_roles($context) {
  * @param string $orderby The column to sort users by
  * @param int $notingroup restrict to users not in existing groups
  * @param bool $onlyactiveenrolments restrict to users who have an active enrolment in the course
+ * @param array $extrafields Extra user fields to return
  * @return array An array of the users
  */
 function groups_get_potential_members($courseid, $roleid = null, $source = null,
                                       $orderby = 'lastname ASC, firstname ASC',
-                                      $notingroup = null, $onlyactiveenrolments = false) {
+                                      $notingroup = null, $onlyactiveenrolments = false, $extrafields = []) {
     global $DB;
 
     $context = context_course::instance($courseid);
@@ -847,7 +848,7 @@ function groups_get_potential_members($courseid, $roleid = null, $source = null,
         }
     }
 
-    $allusernamefields = get_all_user_name_fields(true, 'u');
+    $allusernamefields = user_picture::fields('u', $extrafields);
     $sql = "SELECT DISTINCT u.id, u.username, $allusernamefields, u.idnumber
               FROM {user} u
               JOIN ($esql) e ON e.id = u.id
index 785cc63..5c23992 100644 (file)
@@ -110,7 +110,9 @@ if ($groupingid) {
 
 list($sort, $sortparams) = users_order_by_sql('u');
 
-$allnames = get_all_user_name_fields(true, 'u');
+$extrafields = get_extra_user_fields($context);
+$allnames = 'u.id, ' . user_picture::fields('u', $extrafields);
+
 $sql = "SELECT g.id AS groupid, gg.groupingid, u.id AS userid, $allnames, u.idnumber, u.username
           FROM {groups} g
                LEFT JOIN {groupings_groups} gg ON g.id = gg.groupid
@@ -121,8 +123,9 @@ $sql = "SELECT g.id AS groupid, gg.groupingid, u.id AS userid, $allnames, u.idnu
 
 $rs = $DB->get_recordset_sql($sql, array_merge($params, $sortparams));
 foreach ($rs as $row) {
-    $user = new stdClass();
-    $user = username_load_fields_from_object($user, $row, null, array('id' => 'userid', 'username', 'idnumber'));
+    $user = username_load_fields_from_object((object) [], $row, null,
+        array_merge(['id' => 'userid', 'username', 'idnumber'], $extrafields));
+
     if (!$row->groupingid) {
         $row->groupingid = OVERVIEW_GROUPING_GROUP_NO_GROUPING;
     }
@@ -248,9 +251,20 @@ foreach ($members as $gpgid=>$groupdata) {
             $line[] = html_writer::tag('span', $name, array('class' => 'group_hoverdescription', 'data-groupid' => $gpid));
             $hoverevents[$gpid] = get_string('descriptiona', null, $jsdescription);
         }
+        $viewfullnames = has_capability('moodle/site:viewfullnames', $context);
         $fullnames = array();
         foreach ($users as $user) {
-            $fullnames[] = '<a href="'.$CFG->wwwroot.'/user/view.php?id='.$user->id.'&amp;course='.$course->id.'">'.fullname($user, true).'</a>';
+            $displayname = fullname($user, $viewfullnames);
+            if ($extrafields) {
+                $extrafieldsdisplay = [];
+                foreach ($extrafields as $field) {
+                    $extrafieldsdisplay[] = s($user->{$field});
+                }
+                $displayname .= ' (' . implode(', ', $extrafieldsdisplay) . ')';
+            }
+
+            $fullnames[] = html_writer::link(new moodle_url('/user/view.php', ['id' => $user->id, 'course' => $course->id]),
+                $displayname);
         }
         $line[] = implode(', ', $fullnames);
         $line[] = count($users);
index 54b4763..9d8c4de 100644 (file)
@@ -80,15 +80,13 @@ Feature: Automatic creation of groups
       | Group/member count | 4 |
       | Grouping of auto-created groups | New grouping |
       | Grouping name | Grouping name |
+      | Allocate members | Alphabetically by last name, first name |
     And I press "Preview"
-    Then I should see "Group members"
-    And I should see "User count"
-    And I should see "Group A" in the ".generaltable" "css_element"
-    And I should see "Group B" in the ".generaltable" "css_element"
-    And I should see "Group C" in the ".generaltable" "css_element"
-    And I should see "4" in the "Group A" "table_row"
-    And I should see "4" in the "Group B" "table_row"
-    And I should see "2" in the "Group C" "table_row"
+    Then the following should exist in the "generaltable" table:
+      | Groups (3)   | Group members                    | User count (10) |
+      | Group A      | Student 1 (student1@example.com) | 4               |
+      | Group B      | Student 5 (student5@example.com) | 4               |
+      | Group C      | Student 9 (student9@example.com) | 2               |
     And I set the field "Prevent last small group" to "1"
     And I press "Preview"
     And I should see "Group A" in the ".generaltable" "css_element"
@@ -163,7 +161,7 @@ Feature: Automatic creation of groups
     And I set the field "Auto create based on" to "Members per group"
     When I set the field "Group/member count" to "11"
     And I press "Preview"
-    Then I should see "Suspended student 11"
+    Then I should see "Suspended student 11 (suspendedstudent11@example.com)"
 
   Scenario: Do not display 'Include only active enrolments' if user does not have the 'moodle/course:viewsuspendedusers' capability
     Given I log out
index 8cfa041..ebf2832 100644 (file)
@@ -39,13 +39,15 @@ Feature: Organize students into groups
     And I add "Student 2 (student2@example.com)" user to "Group 2" group members
     And I add "Student 3 (student3@example.com)" user to "Group 2" group members
     Then I set the field "groups" to "Group 1 (2)"
-    And the "members" select box should contain "Student 0"
-    And the "members" select box should contain "Student 1"
-    And the "members" select box should not contain "Student 2"
+    And the "members" select box should contain "Student 0 (student0@example.com)"
+    And the "members" select box should contain "Student 1 (student1@example.com)"
+    And the "members" select box should not contain "Student 2 (student2@example.com)"
+    And the "members" select box should not contain "Student 3 (student3@example.com)"
     And I set the field "groups" to "Group 2 (2)"
-    And the "members" select box should contain "Student 2"
-    And the "members" select box should contain "Student 3"
-    And the "members" select box should not contain "Student 0"
+    And the "members" select box should contain "Student 2 (student2@example.com)"
+    And the "members" select box should contain "Student 3 (student3@example.com)"
+    And the "members" select box should not contain "Student 0 (student0@example.com)"
+    And the "members" select box should not contain "Student 1 (student1@example.com)"
     And I navigate to course participants
     And I open the autocomplete suggestions list
     And I click on "Group: Group 1" item in the autocomplete list
@@ -59,6 +61,36 @@ Feature: Organize students into groups
     And I should see "Student 3"
     And I should not see "Student 0"
 
+  @javascript
+  Scenario: Assign students to groups with site user identity configured
+    Given the following "courses" exist:
+      | fullname | shortname | groupmode |
+      | Course 1 | C1        | 1         |
+    And the following "users" exist:
+      | username | firstname | lastname | email               | country |
+      | teacher  | Teacher   | 1        | teacher@example.com | GB      |
+      | student  | Student   | 1        | student@example.com | DE      |
+    And the following "course enrolments" exist:
+      | user    | course | role           |
+      | teacher | C1     | editingteacher |
+      | student | C1     | student        |
+    And the following config values are set as admin:
+      | showuseridentity | email,country |
+    And I log in as "teacher"
+    And I am on "Course 1" course homepage
+    And I navigate to "Users > Groups" in current page administration
+    And I press "Create group"
+    And I set the following fields to these values:
+      | Group name | Group 1 |
+    And I press "Save changes"
+    When I add "Student 1 (student@example.com, DE)" user to "Group 1" group members
+    And I set the field "groups" to "Group 1 (1)"
+    Then the "members" select box should contain "Student 1 (student@example.com\, DE)"
+    # Non-AJAX version of the groups page.
+    And I press "Add/remove users"
+    And I press "Back to groups"
+    And the "members" select box should contain "Student 1 (student@example.com\, DE)"
+
   Scenario: Create groups and groupings without the 'moodle/course:changeidnumber' capability
     Given the following "courses" exist:
       | fullname | shortname | category | groupmode |
index fb4eb27..660396d 100644 (file)
@@ -27,6 +27,7 @@ namespace core_h5p;
 defined('MOODLE_INTERNAL') || die();
 
 require_once("$CFG->libdir/filelib.php");
+
 use H5PCore;
 use H5PFrameworkInterface;
 use stdClass;
@@ -170,6 +171,9 @@ class core extends \H5PCore {
 
         $typesinstalled = [];
 
+        $factory = new factory();
+        $framework = $factory->get_framework();
+
         foreach ($contenttypes->contentTypes as $type) {
             // Don't fetch content types that require a higher H5P core API version.
             if (!$this->is_required_core_api($type->coreApiVersionNeeded)) {
@@ -183,9 +187,6 @@ class core extends \H5PCore {
                 'patchVersion' => $type->version->patch
             ];
 
-            $factory = new \core_h5p\factory();
-            $framework = $factory->get_framework();
-
             $shoulddownload = true;
             if ($framework->getLibraryId($type->id, $type->version->major, $type->version->minor)) {
                 if (!$framework->isPatchedLibrary($library)) {
@@ -216,7 +217,7 @@ class core extends \H5PCore {
      * @return int|null Returns the id of the content type library installed, null otherwise.
      */
     public function fetch_content_type(array $library): ?int {
-        $factory = new \core_h5p\factory();
+        $factory = new factory();
 
         // Get a temp path to download the content type.
         $temppath = make_request_directory();
@@ -247,7 +248,9 @@ class core extends \H5PCore {
         $file->delete();
 
         $librarykey = static::libraryToString($library);
-        return $factory->get_storage()->h5pC->librariesJsonData[$librarykey]["libraryId"];
+        $libraryid = $factory->get_storage()->h5pC->librariesJsonData[$librarykey]["libraryId"];
+
+        return $libraryid;
     }
 
     /**
index 416f358..4818d86 100644 (file)
@@ -100,7 +100,7 @@ class framework implements \H5PFrameworkInterface {
         $response = download_file_content($url, null, $data, true, 300, 20,
                 false, $stream);
 
-        if (empty($response->error)) {
+        if (empty($response->error) && ($response->status != '404')) {
             return $response->results;
         } else {
             $this->setErrorMessage($response->error, 'failed-fetching-external-data');
index 4042be1..099844b 100644 (file)
@@ -81,8 +81,9 @@ class framework_testcase extends \advanced_testcase {
 
         $this->resetAfterTest();
 
+        $library = 'H5P.Accordion';
         // Provide a valid URL to an external H5P content.
-        $url = "https://h5p.org/sites/default/files/h5p/exports/arithmetic-quiz-22-57860.h5p";
+        $url = $this->getExternalTestFileUrl('/'.$library.'.h5p');
 
         // Test fetching an external H5P content without defining a path to where the file should be stored.
         $data = $this->framework->fetchExternalData($url, null, true);
@@ -112,8 +113,9 @@ class framework_testcase extends \advanced_testcase {
 
         $this->resetAfterTest();
 
+        $library = 'H5P.Accordion';
         // Provide a valid URL to an external H5P content.
-        $url = "https://h5p.org/sites/default/files/h5p/exports/arithmetic-quiz-22-57860.h5p";
+        $url = $this->getExternalTestFileUrl('/'.$library.'.h5p');
 
         $h5pfolderpath = $CFG->tempdir . uniqid('/h5p-');
 
@@ -145,7 +147,7 @@ class framework_testcase extends \advanced_testcase {
         $this->resetAfterTest();
 
         // Provide an URL to an external file that is not an H5P content file.
-        $url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf";
+        $url = $this->getExternalTestFileUrl('/h5pcontenttypes.json');
 
         $data = $this->framework->fetchExternalData($url, null, true);
 
@@ -156,7 +158,7 @@ class framework_testcase extends \advanced_testcase {
         // The uploaded file should exist on the filesystem with it's original extension.
         // NOTE: The file would be later validated by the H5P Validator.
         $h5pfolderpath = $this->framework->getUploadedH5pFolderPath();
-        $this->assertTrue(file_exists($h5pfolderpath . '.pdf'));
+        $this->assertTrue(file_exists($h5pfolderpath . '.json'));
     }
 
     /**
index e9981a9..5f4654e 100644 (file)
@@ -23,7 +23,8 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-use core_h5p\factory;
+use core_h5p\autoloader;
+use core_h5p\core;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -343,30 +344,28 @@ class core_h5p_generator extends \component_generator_base {
     }
 
     /**
-     * Create content type records in the h5p_libraries database table.
+     * Create H5P content type records in the h5p_libraries database table.
      *
-     * @param int $pending Number of content types not installed
+     * @param array $typestonotinstall H5P content types that should not be installed
+     * @param core $core h5p_test_core instance required to use the exttests URL
      * @return array Data of the content types not installed.
      */
-    public function create_content_types(int $pending): array {
+    public function create_content_types(array $typestonotinstall, core $core): array {
         global $DB;
 
-        $factory = new factory();
-        $core = $factory->get_core();
+        autoloader::register();
 
         // Get info of latest content types versions.
         $contenttypes = $core->get_latest_content_types()->contentTypes;
 
-        $size = count($contenttypes) - $pending;
-
-        // Avoid to install 2 content types.
-        $chunks = array_chunk($contenttypes, $size);
-
-        $contenttypes = $chunks[0];
-        $pendingtypes = $chunks[1];
+        $installedtypes = 0;
 
         // Fake installation of all other H5P content types.
         foreach ($contenttypes as $contenttype) {
+            // Don't install pending content types.
+            if (in_array($contenttype->id, $typestonotinstall)) {
+                continue;
+            }
             $library = [
                 'machinename' => $contenttype->id,
                 'majorversion' => $contenttype->version->major,
@@ -377,8 +376,9 @@ class core_h5p_generator extends \component_generator_base {
                 'coreminor' => $contenttype->coreApiVersionNeeded->minor
             ];
             $DB->insert_record('h5p_libraries', (object) $library);
+            $installedtypes++;
         }
 
-        return [$contenttypes, $pendingtypes];
+        return [$installedtypes, count($typestonotinstall)];
     }
 }
index 9bca6c6..27d8882 100644 (file)
@@ -23,9 +23,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-namespace core_h5p\local\tests;
-
-use core_h5p\factory;
+namespace core_h5p;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -41,10 +39,16 @@ defined('MOODLE_INTERNAL') || die();
 class h5p_core_test extends \advanced_testcase {
 
     protected function setup() {
+        global $CFG;
         parent::setUp();
 
-        $factory = new factory();
+        autoloader::register();
+
+        require_once($CFG->libdir . '/tests/fixtures/testable_core_h5p.php');
+
+        $factory = new h5p_test_factory();
         $this->core = $factory->get_core();
+        $this->core->set_endpoint($this->getExternalTestFileUrl(''));
     }
 
     /**
@@ -54,12 +58,12 @@ class h5p_core_test extends \advanced_testcase {
     public function test_fetch_content_type(): void {
         global $DB;
 
-        $this->resetAfterTest(true);
-
         if (!PHPUNIT_LONGTEST) {
             $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
         }
 
+        $this->resetAfterTest(true);
+
         // Get info of latest content types versions.
         $contenttypes = $this->core->get_latest_content_types()->contentTypes;
         // We are installing the first content type.
@@ -95,27 +99,27 @@ class h5p_core_test extends \advanced_testcase {
     public function test_fetch_latest_content_types(): void {
         global $DB;
 
-        $this->resetAfterTest(true);
-
         if (!PHPUNIT_LONGTEST) {
             $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
         }
 
+        $this->resetAfterTest(true);
+
         $contentfiles = $DB->count_records('h5p_libraries');
 
         // Initially there are no h5p records in database.
         $this->assertEquals(0, $contentfiles);
 
+        $contenttypespending = ['H5P.Accordion'];
+
         // Fetch generator.
         $generator = \testing_util::get_data_generator();
         $h5pgenerator = $generator->get_plugin_generator('core_h5p');
 
         // Get info of latest content types versions.
-        [$contenttypes, $contenttoinstall] = $h5pgenerator->create_content_types(1);
+        [$installedtypes, $typesnotinstalled] = $h5pgenerator->create_content_types($contenttypespending, $this->core);
         // Number of H5P content types.
-        $numcontenttypes = count($contenttypes) + count($contenttoinstall);
-
-        $contenttoinstall = $contenttoinstall[0];
+        $numcontenttypes = $installedtypes + $typesnotinstalled;
 
         // Content type libraries has runnable set to 1.
         $conditions = ['runnable' => 1];
@@ -123,7 +127,7 @@ class h5p_core_test extends \advanced_testcase {
 
         // There is a record for each installed content type, except the one that was hold for later.
         $this->assertEquals($numcontenttypes - 1, count($contentfiles));
-        $this->assertArrayNotHasKey($contenttoinstall->id, $contentfiles);
+        $this->assertArrayNotHasKey($contenttypespending[0], $contentfiles);
 
         $result = $this->core->fetch_latest_content_types();
 
@@ -131,9 +135,9 @@ class h5p_core_test extends \advanced_testcase {
 
         // There is a new record for the new installed content type.
         $this->assertCount($numcontenttypes, $contentfiles);
-        $this->assertArrayHasKey($contenttoinstall->id, $contentfiles);
+        $this->assertArrayHasKey($contenttypespending[0], $contentfiles);
         $this->assertCount(1, $result->typesinstalled);
-        $this->assertStringStartsWith($contenttoinstall->id, $result->typesinstalled[0]['name']);
+        $this->assertStringStartsWith($contenttypespending[0], $result->typesinstalled[0]['name']);
 
         // New execution doesn't install any content type.
         $result = $this->core->fetch_latest_content_types();
index 626222c..e0a9177 100644 (file)
@@ -46,7 +46,7 @@ $string['downloadedfilecheckfailed'] = 'Ha fallat la comprovació del fitxer bai
 $string['invalidmd5'] = 'L\'md5 no és vàlid. Torneu a provar-ho';
 $string['missingrequiredfield'] = 'Falta algun camp necessari';
 $string['remotedownloaderror'] = '<p>No s\'ha pogut baixar el component al vostre servidor. Verifiqueu els paràmetres del servidor intermediari. Es recomana vivament l\'extensió cURL de PHP.</p>
-<p>Haureu de baixar manualment el fitxer <a href="{$a->url}">{$a->url}</a>, copiar-lo a la ubicació Â«{$a->dest}» del vostre servidor i descomprimir-lo allí.</p>';
+<p>Haureu de baixar manualment el fitxer <a href="{$a->url}">{$a->url}</a>, copiar-lo a la ubicació Â«{$a->dest}» del vostre servidor i descomprimir-lo allà.</p>';
 $string['wrongdestpath'] = 'El camí de destinació és erroni';
 $string['wrongsourcebase'] = 'L\'adreça (URL) base de la font és errònia';
 $string['wrongzipfilename'] = 'El nom del fitxer ZIP és erroni';
index 4226002..aa518e4 100644 (file)
@@ -70,12 +70,12 @@ $string['pathsroparentdataroot'] = 'Ο γονικός φάκελος ({$a->paren
 $string['pathssubadmindir'] = 'Κάποιοι λίγοι κεντρικοί υπολογιστές ιστού χρησιμοποιούν το /admin ως ειδική διεύθυνση URL για την πρόσβαση σε κάποιο πίνακα ελέγχου ή κάτι τέτοιο. Δυστυχώς αυτό έρχεται σε αντίθεση με την τυπική τοποθεσία των σελίδων διαχείρισης (admin) του Moodle. Αυτό μπορεί να διορθωθεί με την μετονομασία του admin φακέλου στην εγκατάστασή σας, και βάζοντας αυτό το καινούργιο όνομα εδώ. Για παράδειγμα: <em>moodleadmin</em>. Αυτό θα διορθώσει όλους τους συνδέσμους με το admin στην διεύθυνσή τους σε όλη την εγκατάσταση του Moodle σας.';
 $string['pathssubdataroot'] = '<p>Ένας φάκελος όπου το Moodle θα αποθηκεύει όλα τα ανεβασμένα από τους χρήστες αρχεία.</p> <p>Αυτος ο φάκελος θα πρέπει να είναι αναγνώσιμος ΚΑΙ ΕΓΓΡΑΨΙΜΟΣ από τον χρήστη του εξυπηρετητή ιστού (συνήθως «nobody» ή «apache»).</p> <p>Δεν πρέπει να είναι προσβάσιμος κατευθείαν από τον ιστό.</p> <p>Αν ο φάκελος δεν υπάρχει, η διαδικασία εγκατάστασης θα προσπαθήσει να τον δημιουργήσει.</p>';
 $string['pathssubdirroot'] = '<p>Η πλήρης διαδρομή του φακέλου που περιέχει τα αρχεία κώδικα του Moodle.</p>';
-$string['pathssubwwwroot'] = '<p>Η πλήρης διεύθυνση (ιστού) από την οποία θα γίνεται η πρόσβαση στο Moodle, δηλαδή η διεύθυνση που οι χρήστες θα εισάγουν στην γραμμή διεύθυνσης του περιηγητή, για να έχουν πρόσβαση στου Moodle.</p>
+$string['pathssubwwwroot'] = '<p>Η πλήρης διεύθυνση από την οποία θα γίνεται η πρόσβαση στο Moodle, δηλαδή η διεύθυνση που οι χρήστες θα εισάγουν στην γραμμή διεύθυνσης του περιηγητή, για να έχουν πρόσβαση στου Moodle.</p>
 <p>Δεν είναι δυνατόν να έχετε πρόβαση στο Moodle χρησιμοποιώντας πολλαπλές διευθύνσεις. Εάν ο ιστότοπος θα είναι προσβάσιμος μέσω πολλαπλών διευθύνσεων τότε επιλέξτε την ευκολότερη και εγκαταστήστε μια μόνιμη ανακατεύθυνση για καθεμία από τις άλλες διευθύνσεις.</p>
 <p>Εάν ο ιστότοπός σας είναι προσβάσιμος τόσο από το Διαδίκτυο όσο και από ένα εσωτερικό δίκτυο (που συχνά λέγεται intranet) τότε χρησιμοποιήστε εδώ την δημόσια διεύθυνση.</p>
 <p>Αν η τρέχουσα διεύθυνση δεν είναι σωστή, παρακαλούμε αλλάξτε την URL διεύθυνση στην γραμμή διευθύνσεων του περιηγητή σας και επανεκκινήστε την εγκατάσταση.</p>';
 $string['pathsunsecuredataroot'] = 'Η τοποθεσία του Φάκελου Δεδομένων δεν είναι ασφαλής';
-$string['pathswrongadmindir'] = 'Ο Φάκελος Admin δεν υπάρχει';
+$string['pathswrongadmindir'] = 'Ο φάκελος Admin δεν υπάρχει';
 $string['phpextension'] = 'Επέκταση {$a} της PHP';
 $string['phpversion'] = 'Έκδοση της PHP';
 $string['phpversionhelp'] = 'p>Το Moodle απαιτεί η έκδοση της PHP να είναι τουλάχιστον 5.6.5 ή 7.1 (η 7.0.x έχει κάποιους περιορισμούς στη μηχανή).</p>
index 768a9c9..af3818c 100644 (file)
@@ -30,4 +30,4 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['thislanguage'] = 'myanma bhasa';
+$string['thislanguage'] = 'ဗမာစာ';
index d78836a..ef9c319 100644 (file)
@@ -120,6 +120,8 @@ $string['cfgwwwrootwarning'] = '$CFG->wwwroot is defined incorrectly in the conf
 $string['cleanup'] = 'Cleanup';
 $string['clianswerno'] = 'n';
 $string['cliansweryes'] = 'y';
+$string['cliexitgraceful'] = 'Exiting gracefully, please wait ...';
+$string['cliexitnow'] = 'Exiting right NOW';
 $string['cliincorrectvalueerror'] = 'Error, incorrect value "{$a->value}" for "{$a->option}"';
 $string['cliincorrectvalueretry'] = 'Incorrect value, please retry';
 $string['clistatusdisabled'] = 'Status: disabled';
index e003f8f..304ff06 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /*
 
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Latest version is available at http://adodb.org/
index 735702b..7598f1d 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /*
 
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Latest version is available at http://adodb.org/
index 8db3984..f141258 100644 (file)
@@ -8,7 +8,7 @@ $ADODB_INCLUDED_CSV = 1;
 
 /*
 
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 2a9a104..f8a5b7e 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index fb4a588..dbec57e 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * @version   v5.20.15  24-Nov-2019
+ * @version   v5.20.16  12-Jan-2020
  * @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  * @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
  * Released under both BSD license and Lesser GPL library license.
index a1206c6..a66f848 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * @version   v5.20.15  24-Nov-2019
+ * @version   v5.20.16  12-Jan-2020
  * @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  * @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
  * Released under both BSD license and Lesser GPL library license.
index d0fbd9e..f59f51d 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * @version   v5.20.15  24-Nov-2019
+ * @version   v5.20.16  12-Jan-2020
  * @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  * @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
  * Released under both BSD license and Lesser GPL library license.
index fda6543..77d16f2 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * @version   v5.20.15  24-Nov-2019
+ * @version   v5.20.16  12-Jan-2020
  * @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  * @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
  * Released under both BSD license and Lesser GPL library license.
index 5614134..f8c4fb0 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /*
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index cb0aebe..b9cfd3f 100644 (file)
@@ -6,7 +6,7 @@ global $ADODB_INCLUDED_LIB;
 $ADODB_INCLUDED_LIB = 1;
 
 /*
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index d21a1d4..394ce08 100644 (file)
@@ -11,7 +11,7 @@ if (empty($ADODB_INCLUDED_CSV)) include_once(ADODB_DIR.'/adodb-csvlib.inc.php');
 
 /*
 
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index ceb6963..5d0ca6d 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /*
-       @version   v5.20.15  24-Nov-2019
+       @version   v5.20.16  12-Jan-2020
        @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
        @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
          Released under both BSD license and Lesser GPL library license.
index 32a299b..d1037c0 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * @version   v5.20.15  24-Nov-2019
+ * @version   v5.20.16  12-Jan-2020
  * @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  * @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
  * Released under both BSD license and Lesser GPL library license.
index 1810d1f..05a413b 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index acf4135..69f8149 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /*
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 8b3b551..af8db3e 100644 (file)
@@ -4,7 +4,7 @@ ADOdb Date Library, part of the ADOdb abstraction library
 
 Latest version is available at http://adodb.org/
 
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
 
index 8ca4f58..74c52e0 100644 (file)
@@ -14,7 +14,7 @@
 /**
        \mainpage
 
-       @version   v5.20.15  24-Nov-2019
+       @version   v5.20.16  12-Jan-2020
        @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
        @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
 
@@ -224,7 +224,7 @@ if (!defined('_ADODB_LAYER')) {
                /**
                 * ADODB version as a string.
                 */
-               $ADODB_vers = 'v5.20.15  24-Nov-2019';
+               $ADODB_vers = 'v5.20.16  12-Jan-2020';
 
                /**
                 * Determines whether recordset->RecordCount() is used.
index 38282e6..6f68eb5 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 8222aa9..82f2f2b 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index a931cf7..77a954e 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index b08bd95..7247544 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 24af41c..d1aa15b 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index a756e11..9fb481e 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 9ddc645..27b58c6 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index b466cb7..be6a9a1 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 22329aa..e4f6e66 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 7b6bc92..b09fed5 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index d7add54..7641e33 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 973f9ed..6b6b15c 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 5de43e5..c67ee3a 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 3f42387..3623dc0 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index e74f217..730b874 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 584f1ed..3363876 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 583d887..0144327 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index f8641c8..58dd0c8 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
 Released under both BSD license and Lesser GPL library license.
index 689f4d7..d2ae2a4 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 0b87706..45fdcbf 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index b272744..371a5c7 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 9e8209f..20a1ecf 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
 
index 201865f..c5fc168 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 387221d..2dc330c 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index af7a9e1..51b3214 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
- @version   v5.20.15  24-Nov-2019
+ @version   v5.20.16  12-Jan-2020
  @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
  Released under both BSD license and Lesser GPL library license.
index fb286f5..06eabe5 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index fbf989f..95c1e7a 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 5f4f374..15e9dda 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
-* @version   v5.20.15  24-Nov-2019
+* @version   v5.20.16  12-Jan-2020
 * @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 * @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
 * Released under both BSD license and Lesser GPL library license.
index 4899cb4..fb15409 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim. All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 53b0b45..6749144 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
    Released under both BSD license and Lesser GPL library license.
index 6630fb7..6f88939 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index d6ec3a2..72f1d7e 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
@@ -1074,7 +1074,7 @@ class ADORecordset_mssqlnative extends ADORecordSet {
                is running. All associated result memory for the specified result identifier will automatically be freed.       */
        function _close()
        {
-               if(is_object($this->_queryID)) {
+               if(is_resource($this->_queryID)) {
                        $rez = sqlsrv_free_stmt($this->_queryID);
                        $this->_queryID = false;
                        return $rez;
index 5186696..8dd0e36 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
-* @version   v5.20.15  24-Nov-2019
+* @version   v5.20.16  12-Jan-2020
 * @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 * @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
 * Released under both BSD license and Lesser GPL library license.
index 4e30510..98bd31a 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index b96fde3..6313861 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 5e15811..14e0c5f 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 2e9852b..925e1aa 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index c75de50..f63cc13 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
 
index c840d25..5b4ff62 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /*
 
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim. All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
 
index c401f87..bda486c 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * @version   v5.20.15  24-Nov-2019
+ * @version   v5.20.16  12-Jan-2020
  * @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  * @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
  * Released under both BSD license and Lesser GPL library license.
index f3ec9fd..bbd6d26 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim. All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
@@ -120,7 +120,7 @@ class ADODB_oci8po extends ADODB_oci8 {
                        /*
                        * find the next character of the string
                        */
-                       $c = $sql[$i];
+            $c = $sql[$i];
 
                        if ($c == "'" && !$inString && $escaped==0)
                                /*
index 9a576bb..183ae17 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim. All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index c563cf9..6161e94 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 3b52702..aeae3fb 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 1184f12..520d1c0 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index e0e785a..0efa79b 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 391f9a7..116eb89 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 12687cf..9df0397 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-       @version   v5.20.15  24-Nov-2019
+       @version   v5.20.16  12-Jan-2020
        @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
        @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 6663f01..9f0919b 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index c0a2159..80f0992 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
-       @version   v5.20.15  24-Nov-2019
+       @version   v5.20.16  12-Jan-2020
        @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
        @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
 
index 4c6f2f9..162cd59 100644 (file)
@@ -2,7 +2,7 @@
 
 
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 5270e74..eea92ae 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index c014ecf..4584f3f 100644 (file)
@@ -2,7 +2,7 @@
 
 
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 0496989..b5cd221 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 24eacdb..0694fb3 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /*
- @version   v5.20.15  24-Nov-2019
+ @version   v5.20.16  12-Jan-2020
  @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index be4a9cf..61e3987 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
- @version   v5.20.15  24-Nov-2019
+ @version   v5.20.16  12-Jan-2020
  @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 4343555..b78c5c1 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
- @version   v5.20.15  24-Nov-2019
+ @version   v5.20.16  12-Jan-2020
  @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 6907534..e5aaaa7 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
- @version   v5.20.15  24-Nov-2019
+ @version   v5.20.16  12-Jan-2020
  @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 7ffff8a..4e3ce8d 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
- @version   v5.20.15  24-Nov-2019
+ @version   v5.20.16  12-Jan-2020
  @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 536dd3c..a4ab85c 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
- @version   v5.20.15  24-Nov-2019
+ @version   v5.20.16  12-Jan-2020
  @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index afa921a..0d19912 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index d621cc2..f2a1b81 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 156129f..77b5a17 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013  John Lim (jlim#natsoft.com).  All rights
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
 reserved.
index f151a5b..2f52fe0 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 60b133b..d9c7c51 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 08daaf3..8b3ec9e 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 38c940a..0b7d470 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim. All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 78b7bed..4e09f60 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 0b278aa..fe12f7f 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 01f8e1e..ddb8901 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 894cd05..5c0fa28 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 9886037..c1c87f9 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 58fb31f..55db774 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index d56d8ad..1009b37 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 7f09b0b..4c6a39f 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 20b31a8..b350f01 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /*
-@version   v5.20.15  24-Nov-2019
+@version   v5.20.16  12-Jan-2020
 @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
 @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index 75ec106..261b30f 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * @version   v5.20.15  24-Nov-2019
+ * @version   v5.20.16  12-Jan-2020
  * @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  * @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
  * Released under both BSD license and Lesser GPL library license.
index c8ba7b3..e0d1318 100644 (file)
@@ -1,4 +1,4 @@
-Description of ADODB V5.20.15 library import into Moodle
+Description of ADODB V5.20.16 library import into Moodle
 
 This library will be probably removed in Moodle 2.1,
 it is now used only in enrol and auth db plugins.
@@ -24,5 +24,3 @@ Added:
 
 Our changes:
  * MDL-67034 Fixes to make the library php74 compliant.
-
-skodak, iarenaza, moodler, stronk7, abgreeve, lameze, ankitagarwal, marinaglancy, rezaie9
index ed79446..2682122 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * @version   v5.20.15  24-Nov-2019
+ * @version   v5.20.16  12-Jan-2020
  * @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  * @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
  * Released under both BSD license and Lesser GPL library license.
index e111e64..f248be3 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * @version   v5.20.15  24-Nov-2019
+ * @version   v5.20.16  12-Jan-2020
  * @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
  * @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
  * Released under both BSD license and Lesser GPL library license.
index 13bfc54..bc69a44 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /*
-  @version   v5.20.15  24-Nov-2019
+  @version   v5.20.16  12-Jan-2020
   @copyright (c) 2000-2013 John Lim (jlim#natsoft.com). All rights reserved.
   @copyright (c) 2014      Damien Regad, Mark Newnham and the ADOdb community
   Released under both BSD license and Lesser GPL library license.
index a8aa67d..729ac1e 100644 (file)
@@ -338,20 +338,11 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         if (!$timeout) {
             $timeout = self::get_timeout();
         }
-        if ($microsleep) {
-            // Will sleep 1/10th of a second by default for self::get_timeout() seconds.
-            $loops = $timeout * 10;
-        } else {
-            // Will sleep for self::get_timeout() seconds.
-            $loops = $timeout;
-        }
 
-        // DOM will never change on non-javascript case; do not wait or try again.
-        if (!$this->running_javascript()) {
-            $loops = 1;
-        }
+        $start = microtime(true);
+        $end = $start + $timeout;
 
-        for ($i = 0; $i < $loops; $i++) {
+        do {
             // We catch the exception thrown by the step definition to execute it again.
             try {
                 // We don't check with !== because most of the time closures will return
@@ -367,14 +358,13 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
                 }
             }
 
-            if ($this->running_javascript()) {
-                if ($microsleep) {
-                    usleep(100000);
-                } else {
-                    sleep(1);
-                }
+            if (!$this->running_javascript()) {
+                break;
             }
-        }
+
+            usleep(100000);
+
+        } while (microtime(true) < $end);
 
         // Using coding_exception as is a development issue if no exception has been provided.
         if (!$exception) {
diff --git a/lib/classes/local/cli/shutdown.php b/lib/classes/local/cli/shutdown.php
new file mode 100644 (file)
index 0000000..93f295c
--- /dev/null
@@ -0,0 +1,81 @@
+<?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/>.
+
+/**
+ * CLI script shutdown helper class.
+ *
+ * @package    core
+ * @copyright  2019 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\local\cli;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * CLI script shutdown helper class.
+ *
+ * @package    core
+ * @copyright  2019 Brendan Heywood <brendan@catalyst-au.net>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class shutdown {
+
+    /** @var bool Should we exit gracefully at the next opportunity? */
+    protected static $cligracefulexit = false;
+
+    /**
+     * Declares that this CLI script can gracefully handle signals
+     *
+     * @return void
+     */
+    public static function script_supports_graceful_exit(): void {
+        \core_shutdown_manager::register_signal_handler('\core\local\cli\shutdown::signal_handler');
+    }
+
+    /**
+     * Should we gracefully exit?
+     *
+     * @return bool true if we should gracefully exit
+     */
+    public static function should_gracefully_exit(): bool {
+        return self::$cligracefulexit;
+    }
+
+    /**
+     * Handle the signal
+     *
+     * The first signal flags a graceful exit. If a second signal is received
+     * then it immediately exits.
+     *
+     * @param int $signo The signal number
+     * @return bool true if we should exit
+     */
+    public static function signal_handler(int $signo): bool {
+
+        if (self::$cligracefulexit) {
+            cli_heading(get_string('cliexitnow', 'admin'));
+            return true;
+        }
+
+        cli_heading(get_string('cliexitgraceful', 'admin'));
+        self::$cligracefulexit = true;
+        return false;
+    }
+
+}
+
index 9475c92..101b4a1 100644 (file)
@@ -33,7 +33,9 @@ defined('MOODLE_INTERNAL') || die();
  */
 class core_shutdown_manager {
     /** @var array list of custom callbacks */
-    protected static $callbacks = array();
+    protected static $callbacks = [];
+    /** @var array list of custom signal callbacks */
+    protected static $signalcallbacks = [];
     /** @var bool is this manager already registered? */
     protected static $registered = false;
 
@@ -66,7 +68,7 @@ class core_shutdown_manager {
      *
      * @param   int     $signo The signal being handled
      */
-    public static function signal_handler($signo) {
+    public static function signal_handler(int $signo) {
         // Note: There is no need to manually call the shutdown handler.
         // The fact that we are calling exit() in this script means that the standard shutdown handling is performed
         // anyway.
@@ -92,7 +94,41 @@ class core_shutdown_manager {
                 $exitcode = 1;
         }
 
-        exit ($exitcode);
+        // Normally we should exit unless a callback tells us to wait.
+        $shouldexit = true;
+        foreach (self::$signalcallbacks as $data) {
+            list($callback, $params) = $data;
+            try {
+                array_unshift($params, $signo);
+                $shouldexit = call_user_func_array($callback, $params) && $shouldexit;
+            } catch (Throwable $e) {
+                // @codingStandardsIgnoreStart
+                error_log('Exception ignored in signal function ' . get_callable_name($callback) . ': ' . $e->getMessage());
+                // @codingStandardsIgnoreEnd
+            }
+        }
+
+        if ($shouldexit) {
+            exit ($exitcode);
+        }
+    }
+
+    /**
+     * Register custom signal handler function.
+     *
+     * If a handler returns false the signal will be ignored.
+     *
+     * @param callable $callback
+     * @param array $params
+     * @return void
+     */
+    public static function register_signal_handler($callback, array $params = null): void {
+        if (!is_callable($callback)) {
+            // @codingStandardsIgnoreStart
+            error_log('Invalid custom signal function detected ' . var_export($callback, true));
+            // @codingStandardsIgnoreEnd
+        }
+        self::$signalcallbacks[] = [$callback, $params ?? []];
     }
 
     /**
@@ -100,9 +136,15 @@ class core_shutdown_manager {
      *
      * @param callable $callback
      * @param array $params
+     * @return void
      */
-    public static function register_function($callback, array $params = null) {
-        self::$callbacks[] = array($callback, $params);
+    public static function register_function($callback, array $params = null): void {
+        if (!is_callable($callback)) {
+            // @codingStandardsIgnoreStart
+            error_log('Invalid custom shutdown function detected '.var_export($callback, true));
+            // @codingStandardsIgnoreEnd
+        }
+        self::$callbacks[] = [$callback, $params ?? []];
     }
 
     /**
@@ -115,20 +157,11 @@ class core_shutdown_manager {
         foreach (self::$callbacks as $data) {
             list($callback, $params) = $data;
             try {
-                if (!is_callable($callback)) {
-                    error_log('Invalid custom shutdown function detected '.var_export($callback, true));
-                    continue;
-                }
-                if ($params === null) {
-                    call_user_func($callback);
-                } else {
-                    call_user_func_array($callback, $params);
-                }
-            } catch (Exception $e) {
-                error_log('Exception ignored in shutdown function '.get_callable_name($callback).': '.$e->getMessage());
+                call_user_func_array($callback, $params);
             } catch (Throwable $e) {
-                // Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
+                // @codingStandardsIgnoreStart
                 error_log('Exception ignored in shutdown function '.get_callable_name($callback).': '.$e->getMessage());
+                // @codingStandardsIgnoreEnd
             }
         }
 
index e41d402..b3e70c4 100644 (file)
@@ -44,13 +44,23 @@ class h5p_get_content_types_task extends scheduled_task {
         return get_string('h5pgetcontenttypestask', 'admin');
     }
 
+    /**
+     * Get an \core_h5p\core instance.
+     *
+     * @return \core_h5p\core
+     */
+    public function get_core() {
+        $factory = new factory();
+        $core = $factory->get_core();
+        return $core;
+    }
+
     /**
      * Execute the task.
      */
     public function execute() {
 
-        $factory = new factory();
-        $core = $factory->get_core();
+        $core = $this->get_core();
 
         $result = $core->fetch_latest_content_types();
 
index 0adbcc9..f15fae8 100644 (file)
@@ -114,7 +114,8 @@ function cron_run_scheduled_tasks(int $timenow) {
 
     // Run all scheduled tasks.
     try {
-        while (!\core\task\manager::static_caches_cleared_since($timenow) &&
+        while (!\core\local\cli\shutdown::should_gracefully_exit() &&
+                !\core\task\manager::static_caches_cleared_since($timenow) &&
                 $task = \core\task\manager::get_next_scheduled_task($timenow)) {
             cron_run_inner_scheduled_task($task);
             unset($task);
@@ -167,7 +168,8 @@ function cron_run_adhoc_tasks(int $timenow, $keepalive = 0, $checklimits = true)
     $taskcount = 0;
 
     // Run all adhoc tasks.
-    while (!\core\task\manager::static_caches_cleared_since($timenow)) {
+    while (!\core\local\cli\shutdown::should_gracefully_exit() &&
+            !\core\task\manager::static_caches_cleared_since($timenow)) {
 
         if ($checklimits && (time() - $timenow) >= $maxruntime) {
             if ($waiting) {
index a80612c..1a7ebc4 100644 (file)
@@ -1845,7 +1845,7 @@ function xmldb_main_upgrade($oldversion) {
                   FROM {event_subscriptions} es
              LEFT JOIN {user} u ON u.id = es.userid
                  WHERE u.deleted = 1 OR u.id IS NULL";
-        $deletedusers = $DB->get_field_sql($sql);
+        $deletedusers = $DB->get_fieldset_sql($sql);
         if ($deletedusers) {
             list($sql, $params) = $DB->get_in_or_equal($deletedusers);
 
@@ -2162,5 +2162,15 @@ function xmldb_main_upgrade($oldversion) {
 
         upgrade_main_savepoint(true, 2020010900.02);
     }
+
+    if ($oldversion < 2020011700.02) {
+        // Delete all orphaned subscription events.
+        $select = "subscriptionid IS NOT NULL
+                   AND subscriptionid NOT IN (SELECT id from {event_subscriptions})";
+        $DB->delete_records_select('event', $select);
+
+        upgrade_main_savepoint(true, 2020011700.02);
+    }
+
     return true;
 }
index f5d5690..c54384e 100644 (file)
@@ -410,7 +410,8 @@ class pgsql_native_moodle_database extends moodle_database {
 
         $tablename = $this->prefix.$table;
 
-        $sql = "SELECT a.attnum, a.attname AS field, t.typname AS type, a.attlen, a.atttypmod, a.attnotnull, a.atthasdef, d.adsrc
+        $sql = "SELECT a.attnum, a.attname AS field, t.typname AS type, a.attlen, a.atttypmod, a.attnotnull, a.atthasdef,
+                       CASE WHEN a.atthasdef THEN pg_catalog.pg_get_expr(d.adbin, d.adrelid) END AS adsrc
                   FROM pg_catalog.pg_class c
                   JOIN pg_catalog.pg_namespace as ns ON ns.oid = c.relnamespace
                   JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
index 8351c68..87997eb 100644 (file)
@@ -4,7 +4,7 @@
     <location>yui/src/codemirror</location>
     <name>codemirror</name>
     <license>MIT</license>
-    <version>5.37.0</version>
+    <version>5.49.2</version>
     <licenseversion></licenseversion>
   </library>
   <library>
index 549ffa9..5762bdc 100644 (file)
Binary files a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-debug.js and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-debug.js differ
index 41819a0..8201c1f 100644 (file)
Binary files a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-min.js and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-min.js differ
index 549ffa9..5762bdc 100644 (file)
Binary files a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror.js and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror.js differ
index bf69cee..8669220 100644 (file)
   (global.CodeMirror = factory());
 }(this, (function () { 'use strict';
 
-// Kludges for bugs and behavior differences that can't be feature
-// detected are enabled based on userAgent etc sniffing.
-var userAgent = navigator.userAgent
-var platform = navigator.platform
-
-var gecko = /gecko\/\d/i.test(userAgent)
-var ie_upto10 = /MSIE \d/.test(userAgent)
-var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent)
-var edge = /Edge\/(\d+)/.exec(userAgent)
-var ie = ie_upto10 || ie_11up || edge
-var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : +(edge || ie_11up)[1])
-var webkit = !edge && /WebKit\//.test(userAgent)
-var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent)
-var chrome = !edge && /Chrome\//.test(userAgent)
-var presto = /Opera\//.test(userAgent)
-var safari = /Apple Computer/.test(navigator.vendor)
-var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent)
-var phantom = /PhantomJS/.test(userAgent)
-
-var ios = !edge && /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent)
-var android = /Android/.test(userAgent)
-// This is woefully incomplete. Suggestions for alternative methods welcome.
-var mobile = ios || android || /webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent)
-var mac = ios || /Mac/.test(platform)
-var chromeOS = /\bCrOS\b/.test(userAgent)
-var windows = /win/i.test(platform)
-
-var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/)
-if (presto_version) { presto_version = Number(presto_version[1]) }
-if (presto_version && presto_version >= 15) { presto = false; webkit = true }
-// Some browsers use the wrong event properties to signal cmd/ctrl on OS X
-var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11))
-var captureRightClick = gecko || (ie && ie_version >= 9)
-
-function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") }
-
-var rmClass = function(node, cls) {
-  var current = node.className
-  var match = classTest(cls).exec(current)
-  if (match) {
-    var after = current.slice(match.index + match[0].length)
-    node.className = current.slice(0, match.index) + (after ? match[1] + after : "")
-  }
-}
-
-function removeChildren(e) {
-  for (var count = e.childNodes.length; count > 0; --count)
-    { e.removeChild(e.firstChild) }
-  return e
-}
-
-function removeChildrenAndAdd(parent, e) {
-  return removeChildren(parent).appendChild(e)
-}
-
-function elt(tag, content, className, style) {
-  var e = document.createElement(tag)
-  if (className) { e.className = className }
-  if (style) { e.style.cssText = style }
-  if (typeof content == "string") { e.appendChild(document.createTextNode(content)) }
-  else if (content) { for (var i = 0; i < content.length; ++i) { e.appendChild(content[i]) } }
-  return e
-}
-// wrapper for elt, which removes the elt from the accessibility tree
-function eltP(tag, content, className, style) {
-  var e = elt(tag, content, className, style)
-  e.setAttribute("role", "presentation")
-  return e
-}
-
-var range
-if (document.createRange) { range = function(node, start, end, endNode) {
-  var r = document.createRange()
-  r.setEnd(endNode || node, end)
-  r.setStart(node, start)
-  return r
-} }
-else { range = function(node, start, end) {
-  var r = document.body.createTextRange()
-  try { r.moveToElementText(node.parentNode) }
-  catch(e) { return r }
-  r.collapse(true)
-  r.moveEnd("character", end)
-  r.moveStart("character", start)
-  return r
-} }
-
-function contains(parent, child) {
-  if (child.nodeType == 3) // Android browser always returns false when child is a textnode
-    { child = child.parentNode }
-  if (parent.contains)
-    { return parent.contains(child) }
-  do {
-    if (child.nodeType == 11) { child = child.host }
-    if (child == parent) { return true }
-  } while (child = child.parentNode)
-}
-
-function activeElt() {
-  // IE and Edge may throw an "Unspecified Error" when accessing document.activeElement.
-  // IE < 10 will throw when accessed while the page is loading or in an iframe.
-  // IE > 9 and Edge will throw when accessed in an iframe if document.body is unavailable.
-  var activeElement
-  try {
-    activeElement = document.activeElement
-  } catch(e) {
-    activeElement = document.body || null
-  }
-  while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)
-    { activeElement = activeElement.shadowRoot.activeElement }
-  return activeElement
-}
-
-function addClass(node, cls) {
-  var current = node.className
-  if (!classTest(cls).test(current)) { node.className += (current ? " " : "") + cls }
-}
-function joinClasses(a, b) {
-  var as = a.split(" ")
-  for (var i = 0; i < as.length; i++)
-    { if (as[i] && !classTest(as[i]).test(b)) { b += " " + as[i] } }
-  return b
-}
-
-var selectInput = function(node) { node.select() }
-if (ios) // Mobile Safari apparently has a bug where select() is broken.
-  { selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length } }
-else if (ie) // Suppress mysterious IE10 errors
-  { selectInput = function(node) { try { node.select() } catch(_e) {} } }
-
-function bind(f) {
-  var args = Array.prototype.slice.call(arguments, 1)
-  return function(){return f.apply(null, args)}
-}
-
-function copyObj(obj, target, overwrite) {
-  if (!target) { target = {} }
-  for (var prop in obj)
-    { if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop)))
-      { target[prop] = obj[prop] } }
-  return target
-}
-
-// Counts the column offset in a string, taking tabs into account.
-// Used mostly to find indentation.
-function countColumn(string, end, tabSize, startIndex, startValue) {
-  if (end == null) {
-    end = string.search(/[^\s\u00a0]/)
-    if (end == -1) { end = string.length }
-  }
-  for (var i = startIndex || 0, n = startValue || 0;;) {
-    var nextTab = string.indexOf("\t", i)
-    if (nextTab < 0 || nextTab >= end)
-      { return n + (end - i) }
-    n += nextTab - i
-    n += tabSize - (n % tabSize)
-    i = nextTab + 1
-  }
-}
-
-var Delayed = function() {this.id = null};
-Delayed.prototype.set = function (ms, f) {
-  clearTimeout(this.id)
-  this.id = setTimeout(f, ms)
-};
-
-function indexOf(array, elt) {
-  for (var i = 0; i < array.length; ++i)
-    { if (array[i] == elt) { return i } }
-  return -1
-}
-
-// Number of pixels added to scroller and sizer to hide scrollbar
-var scrollerGap = 30
-
-// Returned or thrown by various protocols to signal 'I'm not
-// handling this'.
-var Pass = {toString: function(){return "CodeMirror.Pass"}}
-
-// Reused option objects for setSelection & friends
-var sel_dontScroll = {scroll: false};
-var sel_mouse = {origin: "*mouse"};
-var sel_move = {origin: "+move"};
-// The inverse of countColumn -- find the offset that corresponds to
-// a particular column.
-function findColumn(string, goal, tabSize) {
-  for (var pos = 0, col = 0;;) {
-    var nextTab = string.indexOf("\t", pos)
-    if (nextTab == -1) { nextTab = string.length }
-    var skipped = nextTab - pos
-    if (nextTab == string.length || col + skipped >= goal)
-      { return pos + Math.min(skipped, goal - col) }
-    col += nextTab - pos
-    col += tabSize - (col % tabSize)
-    pos = nextTab + 1
-    if (col >= goal) { return pos }
-  }
-}
-
-var spaceStrs = [""]
-function spaceStr(n) {
-  while (spaceStrs.length <= n)
-    { spaceStrs.push(lst(spaceStrs) + " ") }
-  return spaceStrs[n]
-}
-
-function lst(arr) { return arr[arr.length-1] }
-
-function map(array, f) {
-  var out = []
-  for (var i = 0; i < array.length; i++) { out[i] = f(array[i], i) }
-  return out
-}
-
-function insertSorted(array, value, score) {
-  var pos = 0, priority = score(value)
-  while (pos < array.length && score(array[pos]) <= priority) { pos++ }
-  array.splice(pos, 0, value)
-}
-
-function nothing() {}
-
-function createObj(base, props) {
-  var inst
-  if (Object.create) {
-    inst = Object.create(base)
-  } else {
-    nothing.prototype = base
-    inst = new nothing()
-  }
-  if (props) { copyObj(props, inst) }
-  return inst
-}
-
-var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/
-function isWordCharBasic(ch) {
-  return /\w/.test(ch) || ch > "\x80" &&
-    (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch))
-}
-function isWordChar(ch, helper) {
-  if (!helper) { return isWordCharBasic(ch) }
-  if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) { return true }
-  return helper.test(ch)
-}
-
-function isEmpty(obj) {
-  for (var n in obj) { if (obj.hasOwnProperty(n) && obj[n]) { return false } }
-  return true
-}
-
-// Extending unicode characters. A series of a non-extending char +
-// any number of extending chars is treated as a single unit as far
-// as editing and measuring is concerned. This is not fully correct,
-// since some scripts/fonts/browsers also treat other configurations
-// of code points as a group.
-var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/
-function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch) }
-
-// Returns a number from the range [`0`; `str.length`] unless `pos` is outside that range.
-function skipExtendingChars(str, pos, dir) {
-  while ((dir < 0 ? pos > 0 : pos < str.length) && isExtendingChar(str.charAt(pos))) { pos += dir }
-  return pos
-}
-
-// Returns the value from the range [`from`; `to`] that satisfies
-// `pred` and is closest to `from`. Assumes that at least `to`
-// satisfies `pred`. Supports `from` being greater than `to`.
-function findFirst(pred, from, to) {
-  // At any point we are certain `to` satisfies `pred`, don't know
-  // whether `from` does.
-  var dir = from > to ? -1 : 1
-  for (;;) {
-    if (from == to) { return from }
-    var midF = (from + to) / 2, mid = dir < 0 ? Math.ceil(midF) : Math.floor(midF)
-    if (mid == from) { return pred(mid) ? from : to }
-    if (pred(mid)) { to = mid }
-    else { from = mid + dir }
-  }
-}
-
-// The display handles the DOM integration, both for input reading
-// and content drawing. It holds references to DOM nodes and
-// display-related state.
-
-function Display(place, doc, input) {
-  var d = this
-  this.input = input
-
-  // Covers bottom-right square when both scrollbars are present.
-  d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler")
-  d.scrollbarFiller.setAttribute("cm-not-content", "true")
-  // Covers bottom of gutter when coverGutterNextToScrollbar is on
-  // and h scrollbar is present.
-  d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler")
-  d.gutterFiller.setAttribute("cm-not-content", "true")
-  // Will contain the actual code, positioned to cover the viewport.
-  d.lineDiv = eltP("div", null, "CodeMirror-code")
-  // Elements are added to these to represent selection and cursors.
-  d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1")
-  d.cursorDiv = elt("div", null, "CodeMirror-cursors")
-  // A visibility: hidden element used to find the size of things.
-  d.measure = elt("div", null, "CodeMirror-measure")
-  // When lines outside of the viewport are measured, they are drawn in this.
-  d.lineMeasure = elt("div", null, "CodeMirror-measure")
-  // Wraps everything that needs to exist inside the vertically-padded coordinate system
-  d.lineSpace = eltP("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv],
-                    null, "position: relative; outline: none")
-  var lines = eltP("div", [d.lineSpace], "CodeMirror-lines")
-  // Moved around its parent to cover visible view.
-  d.mover = elt("div", [lines], null, "position: relative")
-  // Set to the height of the document, allowing scrolling.
-  d.sizer = elt("div", [d.mover], "CodeMirror-sizer")
-  d.sizerWidth = null
-  // Behavior of elts with overflow: auto and padding is
-  // inconsistent across browsers. This is used to ensure the
-  // scrollable area is big enough.
-  d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;")
-  // Will contain the gutters, if any.
-  d.gutters = elt("div", null, "CodeMirror-gutters")
-  d.lineGutter = null
-  // Actual scrollable element.
-  d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll")
-  d.scroller.setAttribute("tabIndex", "-1")
-  // The element in which the editor lives.
-  d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror")
-
-  // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported)
-  if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0 }
-  if (!webkit && !(gecko && mobile)) { d.scroller.draggable = true }
-
-  if (place) {
-    if (place.appendChild) { place.appendChild(d.wrapper) }
-    else { place(d.wrapper) }
-  }
-
-  // Current rendered range (may be bigger than the view window).
-  d.viewFrom = d.viewTo = doc.first
-  d.reportedViewFrom = d.reportedViewTo = doc.first
-  // Information about the rendered lines.
-  d.view = []
-  d.renderedView = null
-  // Holds info about a single rendered line when it was rendered
-  // for measurement, while not in view.
-  d.externalMeasured = null
-  // Empty space (in pixels) above the view
-  d.viewOffset = 0
-  d.lastWrapHeight = d.lastWrapWidth = 0
-  d.updateLineNumbers = null
-
-  d.nativeBarWidth = d.barHeight = d.barWidth = 0
-  d.scrollbarsClipped = false
-
-  // Used to only resize the line number gutter when necessary (when
-  // the amount of lines crosses a boundary that makes its width change)
-  d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null
-  // Set to true when a non-horizontal-scrolling line widget is
-  // added. As an optimization, line widget aligning is skipped when
-  // this is false.
-  d.alignWidgets = false
-
-  d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null
-
-  // Tracks the maximum line length so that the horizontal scrollbar
-  // can be kept static when scrolling.
-  d.maxLine = null
-  d.maxLineLength = 0
-  d.maxLineChanged = false
-
-  // Used for measuring wheel scrolling granularity
-  d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null
-
-  // True when shift is held down.
-  d.shift = false
-
-  // Used to track whether anything happened since the context menu
-  // was opened.
-  d.selForContextMenu = null
-
-  d.activeTouch = null
-
-  input.init(d)
-}
-
-// Find the line object corresponding to the given line number.
-function getLine(doc, n) {
-  n -= doc.first
-  if (n < 0 || n >= doc.size) { throw new Error("There is no line " + (n + doc.first) + " in the document.") }
-  var chunk = doc
-  while (!chunk.lines) {
-    for (var i = 0;; ++i) {
-      var child = chunk.children[i], sz = child.chunkSize()
-      if (n < sz) { chunk = child; break }
-      n -= sz
-    }
-  }
-  return chunk.lines[n]
-}
-
-// Get the part of a document between two positions, as an array of
-// strings.
-function getBetween(doc, start, end) {
-  var out = [], n = start.line
-  doc.iter(start.line, end.line + 1, function (line) {
-    var text = line.text
-    if (n == end.line) { text = text.slice(0, end.ch) }
-    if (n == start.line) { text = text.slice(start.ch) }
-    out.push(text)
-    ++n
-  })
-  return out
-}
-// Get the lines between from and to, as array of strings.
-function getLines(doc, from, to) {
-  var out = []
-  doc.iter(from, to, function (line) { out.push(line.text) }) // iter aborts when callback returns truthy value
-  return out
-}
-
-// Update the height of a line, propagating the height change
-// upwards to parent nodes.
-function updateLineHeight(line, height) {
-  var diff = height - line.height
-  if (diff) { for (var n = line; n; n = n.parent) { n.height += diff } }
-}
-
-// Given a line object, find its line number by walking up through
-// its parent links.
-function lineNo(line) {
-  if (line.parent == null) { return null }
-  var cur = line.parent, no = indexOf(cur.lines, line)
-  for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {
-    for (var i = 0;; ++i) {
-      if (chunk.children[i] == cur) { break }
-      no += chunk.children[i].chunkSize()
-    }
-  }
-  return no + cur.first
-}
-
-// Find the line at the given vertical position, using the height
-// information in the document tree.
-function lineAtHeight(chunk, h) {
-  var n = chunk.first
-  outer: do {
-    for (var i$1 = 0; i$1 < chunk.children.length; ++i$1) {
-      var child = chunk.children[i$1], ch = child.height
-      if (h < ch) { chunk = child; continue outer }
-      h -= ch
-      n += child.chunkSize()
-    }
-    return n
-  } while (!chunk.lines)
-  var i = 0
-  for (; i < chunk.lines.length; ++i) {
-    var line = chunk.lines[i], lh = line.height
-    if (h < lh) { break }
-    h -= lh
-  }
-  return n + i
-}
-
-function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size}
-
-function lineNumberFor(options, i) {
-  return String(options.lineNumberFormatter(i + options.firstLineNumber))
-}
-
-// A Pos instance represents a position within the text.
-function Pos(line, ch, sticky) {
-  if ( sticky === void 0 ) sticky = null;
-
-  if (!(this instanceof Pos)) { return new Pos(line, ch, sticky) }
-  this.line = line
-  this.ch = ch
-  this.sticky = sticky
-}
-
-// Compare two positions, return 0 if they are the same, a negative
-// number when a is less, and a positive number otherwise.
-function cmp(a, b) { return a.line - b.line || a.ch - b.ch }
-
-function equalCursorPos(a, b) { return a.sticky == b.sticky && cmp(a, b) == 0 }
-
-function copyPos(x) {return Pos(x.line, x.ch)}
-function maxPos(a, b) { return cmp(a, b) < 0 ? b : a }
-function minPos(a, b) { return cmp(a, b) < 0 ? a : b }
-
-// Most of the external API clips given positions to make sure they
-// actually exist within the document.
-function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1))}
-function clipPos(doc, pos) {
-  if (pos.line < doc.first) { return Pos(doc.first, 0) }
-  var last = doc.first + doc.size - 1
-  if (pos.line > last) { return Pos(last, getLine(doc, last).text.length) }
-  return clipToLen(pos, getLine(doc, pos.line).text.length)
-}
-function clipToLen(pos, linelen) {
-  var ch = pos.ch
-  if (ch == null || ch > linelen) { return Pos(pos.line, linelen) }
-  else if (ch < 0) { return Pos(pos.line, 0) }
-  else { return pos }
-}
-function clipPosArray(doc, array) {
-  var out = []
-  for (var i = 0; i < array.length; i++) { out[i] = clipPos(doc, array[i]) }
-  return out
-}
-
-// Optimize some code when these features are not used.
-var sawReadOnlySpans = false;
-var sawCollapsedSpans = false;
-function seeReadOnlySpans() {
-  sawReadOnlySpans = true
-}
-
-function seeCollapsedSpans() {
-  sawCollapsedSpans = true
-}
-
-// TEXTMARKER SPANS
-
-function MarkedSpan(marker, from, to) {
-  this.marker = marker
-  this.from = from; this.to = to
-}
-
-// Search an array of spans for a span matching the given marker.
-function getMarkedSpanFor(spans, marker) {
-  if (spans) { for (var i = 0; i < spans.length; ++i) {
-    var span = spans[i]
-    if (span.marker == marker) { return span }
-  } }
-}
-// Remove a span from an array, returning undefined if no spans are
-// left (we don't store arrays for lines without spans).
-function removeMarkedSpan(spans, span) {
-  var r
-  for (var i = 0; i < spans.length; ++i)
-    { if (spans[i] != span) { (r || (r = [])).push(spans[i]) } }
-  return r
-}
-// Add a span to a line.
-function addMarkedSpan(line, span) {
-  line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]
-  span.marker.attachLine(line)
-}
-
-// Used for the algorithm that adjusts markers for a change in the
-// document. These functions cut an array of spans at a given
-// character position, returning an array of remaining chunks (or
-// undefined if nothing remains).
-function markedSpansBefore(old, startCh, isInsert) {
-  var nw
-  if (old) { for (var i = 0; i < old.length; ++i) {
-    var span = old[i], marker = span.marker
-    var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh)
-    if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) {
-      var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh)
-      ;(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to))
-    }
-  } }
-  return nw
-}
-function markedSpansAfter(old, endCh, isInsert) {
-  var nw
-  if (old) { for (var i = 0; i < old.length; ++i) {
-    var span = old[i], marker = span.marker
-    var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh)
-    if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) {
-      var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh)
-      ;(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh,
-                                            span.to == null ? null : span.to - endCh))
-    }
-  } }
-  return nw
-}
-
-// Given a change object, compute the new set of marker spans that
-// cover the line in which the change took place. Removes spans
-// entirely within the change, reconnects spans belonging to the
-// same marker that appear on both sides of the change, and cuts off
-// spans partially within the change. Returns an array of span
-// arrays with one element for each line in (after) the change.
-function stretchSpansOverChange(doc, change) {
-  if (change.full) { return null }
-  var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans
-  var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans
-  if (!oldFirst && !oldLast) { return null }
-
-  var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0
-  // Get the spans that 'stick out' on both sides
-  var first = markedSpansBefore(oldFirst, startCh, isInsert)
-  var last = markedSpansAfter(oldLast, endCh, isInsert)
-
-  // Next, merge those two ends
-  var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0)
-  if (first) {
-    // Fix up .to properties of first
-    for (var i = 0; i < first.length; ++i) {
-      var span = first[i]
-      if (span.to == null) {
-        var found = getMarkedSpanFor(last, span.marker)
-        if (!found) { span.to = startCh }
-        else if (sameLine) { span.to = found.to == null ? null : found.to + offset }
-      }
-    }
-  }
-  if (last) {
-    // Fix up .from in last (or move them into first in case of sameLine)
-    for (var i$1 = 0; i$1 < last.length; ++i$1) {
-      var span$1 = last[i$1]
-      if (span$1.to != null) { span$1.to += offset }
-      if (span$1.from == null) {
-        var found$1 = getMarkedSpanFor(first, span$1.marker)
-        if (!found$1) {
-          span$1.from = offset
-          if (sameLine) { (first || (first = [])).push(span$1) }
-        }
-      } else {
-        span$1.from += offset
-        if (sameLine) { (first || (first = [])).push(span$1) }
-      }
-    }
-  }
-  // Make sure we didn't create any zero-length spans
-  if (first) { first = clearEmptySpans(first) }
-  if (last && last != first) { last = clearEmptySpans(last) }
-
-  var newMarkers = [first]
-  if (!sameLine) {
-    // Fill gap with whole-line-spans
-    var gap = change.text.length - 2, gapMarkers
-    if (gap > 0 && first)
-      { for (var i$2 = 0; i$2 < first.length; ++i$2)
-        { if (first[i$2].to == null)
-          { (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i$2].marker, null, null)) } } }
-    for (var i$3 = 0; i$3 < gap; ++i$3)
-      { newMarkers.push(gapMarkers) }
-    newMarkers.push(last)
-  }
-  return newMarkers
-}
-
-// Remove spans that are empty and don't have a clearWhenEmpty
-// option of false.
-function clearEmptySpans(spans) {
-  for (var i = 0; i < spans.length; ++i) {
-    var span = spans[i]
-    if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false)
-      { spans.splice(i--, 1) }
-  }
-  if (!spans.length) { return null }
-  return spans
-}
-
-// Used to 'clip' out readOnly ranges when making a change.
-function removeReadOnlyRanges(doc, from, to) {
-  var markers = null
-  doc.iter(from.line, to.line + 1, function (line) {
-    if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) {
-      var mark = line.markedSpans[i].marker
-      if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))
-        { (markers || (markers = [])).push(mark) }
-    } }
-  })
-  if (!markers) { return null }
-  var parts = [{from: from, to: to}]
-  for (var i = 0; i < markers.length; ++i) {
-    var mk = markers[i], m = mk.find(0)
-    for (var j = 0; j < parts.length; ++j) {
-      var p = parts[j]
-      if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) { continue }
-      var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to)
-      if (dfrom < 0 || !mk.inclusiveLeft && !dfrom)
-        { newParts.push({from: p.from, to: m.from}) }
-      if (dto > 0 || !mk.inclusiveRight && !dto)
-        { newParts.push({from: m.to, to: p.to}) }
-      parts.splice.apply(parts, newParts)
-      j += newParts.length - 3
-    }
-  }
-  return parts
-}
-
-// Connect or disconnect spans from a line.
-function detachMarkedSpans(line) {
-  var spans = line.markedSpans
-  if (!spans) { return }
-  for (var i = 0; i < spans.length; ++i)
-    { spans[i].marker.detachLine(line) }
-  line.markedSpans = null
-}
-function attachMarkedSpans(line, spans) {
-  if (!spans) { return }
-  for (var i = 0; i < spans.length; ++i)
-    { spans[i].marker.attachLine(line) }
-  line.markedSpans = spans
-}
-
-// Helpers used when computing which overlapping collapsed span
-// counts as the larger one.
-function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 }
-function extraRight(marker) { return marker.inclusiveRight ? 1 : 0 }
-
-// Returns a number indicating which of two overlapping collapsed
-// spans is larger (and thus includes the other). Falls back to
-// comparing ids when the spans cover exactly the same range.
-function compareCollapsedMarkers(a, b) {
-  var lenDiff = a.lines.length - b.lines.length
-  if (lenDiff != 0) { return lenDiff }
-  var aPos = a.find(), bPos = b.find()
-  var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b)
-  if (fromCmp) { return -fromCmp }
-  var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b)
-  if (toCmp) { return toCmp }
-  return b.id - a.id
-}
-
-// Find out whether a line ends or starts in a collapsed span. If
-// so, return the marker for that span.
-function collapsedSpanAtSide(line, start) {
-  var sps = sawCollapsedSpans && line.markedSpans, found
-  if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) {
-    sp = sps[i]
-    if (sp.marker.collapsed && (start ? sp.from : sp.to) == null &&
-        (!found || compareCollapsedMarkers(found, sp.marker) < 0))
-      { found = sp.marker }
-  } }
-  return found
-}
-function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) }
-function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) }
-
-function collapsedSpanAround(line, ch) {
-  var sps = sawCollapsedSpans && line.markedSpans, found
-  if (sps) { for (var i = 0; i < sps.length; ++i) {
-    var sp = sps[i]
-    if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) &&
-        (!found || compareCollapsedMarkers(found, sp.marker) < 0)) { found = sp.marker }
-  } }
-  return found
-}
-
-// Test whether there exists a collapsed span that partially
-// overlaps (covers the start or end, but not both) of a new span.
-// Such overlap is not allowed.
-function conflictingCollapsedRange(doc, lineNo, from, to, marker) {
-  var line = getLine(doc, lineNo)
-  var sps = sawCollapsedSpans && line.markedSpans
-  if (sps) { for (var i = 0; i < sps.length; ++i) {
-    var sp = sps[i]
-    if (!sp.marker.collapsed) { continue }
-    var found = sp.marker.find(0)
-    var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker)
-    var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker)
-    if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) { continue }
-    if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) ||
-        fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0))
-      { return true }
-  } }
-}
-
-// A visual line is a line as drawn on the screen. Folding, for
-// example, can cause multiple logical lines to appear on the same
-// visual line. This finds the start of the visual line that the
-// given line is part of (usually that is the line itself).
-function visualLine(line) {
-  var merged
-  while (merged = collapsedSpanAtStart(line))
-    { line = merged.find(-1, true).line }
-  return line
-}
-
-function visualLineEnd(line) {
-  var merged
-  while (merged = collapsedSpanAtEnd(line))
-    { line = merged.find(1, true).line }
-  return line
-}
-
-// Returns an array of logical lines that continue the visual line
-// started by the argument, or undefined if there are no such lines.
-function visualLineContinued(line) {
-  var merged, lines
-  while (merged = collapsedSpanAtEnd(line)) {
-    line = merged.find(1, true).line
-    ;(lines || (lines = [])).push(line)
-  }
-  return lines
-}
-
-// Get the line number of the start of the visual line that the
-// given line number is part of.
-function visualLineNo(doc, lineN) {
-  var line = getLine(doc, lineN), vis = visualLine(line)
-  if (line == vis) { return lineN }
-  return lineNo(vis)
-}
-
-// Get the line number of the start of the next visual line after
-// the given line.
-function visualLineEndNo(doc, lineN) {
-  if (lineN > doc.lastLine()) { return lineN }
-  var line = getLine(doc, lineN), merged
-  if (!lineIsHidden(doc, line)) { return lineN }
-  while (merged = collapsedSpanAtEnd(line))
-    { line = merged.find(1, true).line }
-  return lineNo(line) + 1
-}
-
-// Compute whether a line is hidden. Lines count as hidden when they
-// are part of a visual line that starts with another line, or when
-// they are entirely covered by collapsed, non-widget span.
-function lineIsHidden(doc, line) {
-  var sps = sawCollapsedSpans && line.markedSpans
-  if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) {
-    sp = sps[i]
-    if (!sp.marker.collapsed) { continue }
-    if (sp.from == null) { return true }
-    if (sp.marker.widgetNode) { continue }
-    if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp))
-      { return true }
-  } }
-}
-function lineIsHiddenInner(doc, line, span) {
-  if (span.to == null) {
-    var end = span.marker.find(1, true)
-    return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker))
-  }
-  if (span.marker.inclusiveRight && span.to == line.text.length)
-    { return true }
-  for (var sp = (void 0), i = 0; i < line.markedSpans.length; ++i) {
-    sp = line.markedSpans[i]
-    if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to &&
-        (sp.to == null || sp.to != span.from) &&
-        (sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
-        lineIsHiddenInner(doc, line, sp)) { return true }
-  }
-}
-
-// Find the height above the given line.
-function heightAtLine(lineObj) {
-  lineObj = visualLine(lineObj)
-
-  var h = 0, chunk = lineObj.parent
-  for (var i = 0; i < chunk.lines.length; ++i) {
-    var line = chunk.lines[i]
-    if (line == lineObj) { break }
-    else { h += line.height }
-  }
-  for (var p = chunk.parent; p; chunk = p, p = chunk.parent) {
-    for (var i$1 = 0; i$1 < p.children.length; ++i$1) {
-      var cur = p.children[i$1]
-      if (cur == chunk) { break }
-      else { h += cur.height }
-    }
-  }
-  return h
-}
-
-// Compute the character length of a line, taking into account
-// collapsed ranges (see markText) that might hide parts, and join
-// other lines onto it.
-function lineLength(line) {
-  if (line.height == 0) { return 0 }
-  var len = line.text.length, merged, cur = line
-  while (merged = collapsedSpanAtStart(cur)) {
-    var found = merged.find(0, true)
-    cur = found.from.line
-    len += found.from.ch - found.to.ch
-  }
-  cur = line
-  while (merged = collapsedSpanAtEnd(cur)) {
-    var found$1 = merged.find(0, true)
-    len -= cur.text.length - found$1.from.ch
-    cur = found$1.to.line
-    len += cur.text.length - found$1.to.ch
-  }
-  return len
-}
-
-// Find the longest line in the document.
-function findMaxLine(cm) {
-  var d = cm.display, doc = cm.doc
-  d.maxLine = getLine(doc, doc.first)
-  d.maxLineLength = lineLength(d.maxLine)
-  d.maxLineChanged = true
-  doc.iter(function (line) {
-    var len = lineLength(line)
-    if (len > d.maxLineLength) {
-      d.maxLineLength = len
-      d.maxLine = line
-    }
-  })
-}
-
-// BIDI HELPERS
-
-function iterateBidiSections(order, from, to, f) {
-  if (!order) { return f(from, to, "ltr", 0) }
-  var found = false
-  for (var i = 0; i < order.length; ++i) {
-    var part = order[i]
-    if (part.from < to && part.to > from || from == to && part.to == from) {
-      f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr", i)
-      found = true
-    }
-  }
-  if (!found) { f(from, to, "ltr") }
-}
-
-var bidiOther = null
-function getBidiPartAt(order, ch, sticky) {
-  var found
-  bidiOther = null
-  for (var i = 0; i < order.length; ++i) {
-    var cur = order[i]
-    if (cur.from < ch && cur.to > ch) { return i }
-    if (cur.to == ch) {
-      if (cur.from != cur.to && sticky == "before") { found = i }
-      else { bidiOther = i }
-    }
-    if (cur.from == ch) {
-      if (cur.from != cur.to && sticky != "before") { found = i }
-      else { bidiOther = i }
-    }
-  }
-  return found != null ? found : bidiOther
-}
-
-// Bidirectional ordering algorithm
-// See http://unicode.org/reports/tr9/tr9-13.html for the algorithm
-// that this (partially) implements.
-
-// One-char codes used for character types:
-// L (L):   Left-to-Right
-// R (R):   Right-to-Left
-// r (AL):  Right-to-Left Arabic
-// 1 (EN):  European Number
-// + (ES):  European Number Separator
-// % (ET):  European Number Terminator
-// n (AN):  Arabic Number
-// , (CS):  Common Number Separator
-// m (NSM): Non-Spacing Mark
-// b (BN):  Boundary Neutral
-// s (B):   Paragraph Separator
-// t (S):   Segment Separator
-// w (WS):  Whitespace
-// N (ON):  Other Neutrals
-
-// Returns null if characters are ordered as they appear
-// (left-to-right), or an array of sections ({from, to, level}
-// objects) in the order in which they occur visually.
-var bidiOrdering = (function() {
-  // Character types for codepoints 0 to 0xff
-  var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"
-  // Character types for codepoints 0x600 to 0x6f9
-  var arabicTypes = "nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111"
-  function charType(code) {
-    if (code <= 0xf7) { return lowTypes.charAt(code) }
-    else if (0x590 <= code && code <= 0x5f4) { return "R" }
-    else if (0x600 <= code && code <= 0x6f9) { return arabicTypes.charAt(code - 0x600) }
-    else if (0x6ee <= code && code <= 0x8ac) { return "r" }
-    else if (0x2000 <= code && code <= 0x200b) { return "w" }
-    else if (code == 0x200c) { return "b" }
-    else { return "L" }
-  }
-
-  var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/
-  var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/
-
-  function BidiSpan(level, from, to) {
-    this.level = level
-    this.from = from; this.to = to
-  }
-
-  return function(str, direction) {
-    var outerType = direction == "ltr" ? "L" : "R"
-
-    if (str.length == 0 || direction == "ltr" && !bidiRE.test(str)) { return false }
-    var len = str.length, types = []
-    for (var i = 0; i < len; ++i)
-      { types.push(charType(str.charCodeAt(i))) }
-
-    // W1. Examine each non-spacing mark (NSM) in the level run, and
-    // change the type of the NSM to the type of the previous
-    // character. If the NSM is at the start of the level run, it will
-    // get the type of sor.
-    for (var i$1 = 0, prev = outerType; i$1 < len; ++i$1) {
-      var type = types[i$1]
-      if (type == "m") { types[i$1] = prev }
-      else { prev = type }
-    }
-
-    // W2. Search backwards from each instance of a European number
-    // until the first strong type (R, L, AL, or sor) is found. If an
-    // AL is found, change the type of the European number to Arabic
-    // number.
-    // W3. Change all ALs to R.
-    for (var i$2 = 0, cur = outerType; i$2 < len; ++i$2) {
-      var type$1 = types[i$2]
-      if (type$1 == "1" && cur == "r") { types[i$2] = "n" }
-      else if (isStrong.test(type$1)) { cur = type$1; if (type$1 == "r") { types[i$2] = "R" } }
-    }
-
-    // W4. A single European separator between two European numbers
-    // changes to a European number. A single common separator between
-    // two numbers of the same type changes to that type.
-    for (var i$3 = 1, prev$1 = types[0]; i$3 < len - 1; ++i$3) {
-      var type$2 = types[i$3]
-      if (type$2 == "+" && prev$1 == "1" && types[i$3+1] == "1") { types[i$3] = "1" }
-      else if (type$2 == "," && prev$1 == types[i$3+1] &&
-               (prev$1 == "1" || prev$1 == "n")) { types[i$3] = prev$1 }
-      prev$1 = type$2
-    }
-
-    // W5. A sequence of European terminators adjacent to European
-    // numbers changes to all European numbers.
-    // W6. Otherwise, separators and terminators change to Other
-    // Neutral.
-    for (var i$4 = 0; i$4 < len; ++i$4) {
-      var type$3 = types[i$4]
-      if (type$3 == ",") { types[i$4] = "N" }
-      else if (type$3 == "%") {
-        var end = (void 0)
-        for (end = i$4 + 1; end < len && types[end] == "%"; ++end) {}
-        var replace = (i$4 && types[i$4-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"
-        for (var j = i$4; j < end; ++j) { types[j] = replace }
-        i$4 = end - 1
-      }
-    }
-
-    // W7. Search backwards from each instance of a European number
-    // until the first strong type (R, L, or sor) is found. If an L is
-    // found, then change the type of the European number to L.
-    for (var i$5 = 0, cur$1 = outerType; i$5 < len; ++i$5) {
-      var type$4 = types[i$5]
-      if (cur$1 == "L" && type$4 == "1") { types[i$5] = "L" }
-      else if (isStrong.test(type$4)) { cur$1 = type$4 }
-    }
-
-    // N1. A sequence of neutrals takes the direction of the
-    // surrounding strong text if the text on both sides has the same
-    // direction. European and Arabic numbers act as if they were R in
-    // terms of their influence on neutrals. Start-of-level-run (sor)
-    // and end-of-level-run (eor) are used at level run boundaries.
-    // N2. Any remaining neutrals take the embedding direction.
-    for (var i$6 = 0; i$6 < len; ++i$6) {
-      if (isNeutral.test(types[i$6])) {
-        var end$1 = (void 0)
-        for (end$1 = i$6 + 1; end$1 < len && isNeutral.test(types[end$1]); ++end$1) {}
-        var before = (i$6 ? types[i$6-1] : outerType) == "L"
-        var after = (end$1 < len ? types[end$1] : outerType) == "L"
-        var replace$1 = before == after ? (before ? "L" : "R") : outerType
-        for (var j$1 = i$6; j$1 < end$1; ++j$1) { types[j$1] = replace$1 }
-        i$6 = end$1 - 1
-      }
-    }
-
-    // Here we depart from the documented algorithm, in order to avoid
-    // building up an actual levels array. Since there are only three
-    // levels (0, 1, 2) in an implementation that doesn't take
-    // explicit embedding into account, we can build up the order on
-    // the fly, without following the level-based algorithm.
-    var order = [], m
-    for (var i$7 = 0; i$7 < len;) {
-      if (countsAsLeft.test(types[i$7])) {
-        var start = i$7
-        for (++i$7; i$7 < len && countsAsLeft.test(types[i$7]); ++i$7) {}
-        order.push(new BidiSpan(0, start, i$7))
-      } else {
-        var pos = i$7, at = order.length
-        for (++i$7; i$7 < len && types[i$7] != "L"; ++i$7) {}
-        for (var j$2 = pos; j$2 < i$7;) {
-          if (countsAsNum.test(types[j$2])) {
-            if (pos < j$2) { order.splice(at, 0, new BidiSpan(1, pos, j$2)) }
-            var nstart = j$2
-            for (++j$2; j$2 < i$7 && countsAsNum.test(types[j$2]); ++j$2) {}
-            order.splice(at, 0, new BidiSpan(2, nstart, j$2))
-            pos = j$2
-          } else { ++j$2 }
-        }
-        if (pos < i$7) { order.splice(at, 0, new BidiSpan(1, pos, i$7)) }
-      }
-    }
-    if (direction == "ltr") {
-      if (order[0].level == 1 && (m = str.match(/^\s+/))) {
-        order[0].from = m[0].length
-        order.unshift(new BidiSpan(0, 0, m[0].length))
-      }
-      if (lst(order).level == 1 && (m = str.match(/\s+$/))) {
-        lst(order).to -= m[0].length
-        order.push(new BidiSpan(0, len - m[0].length, len))
-      }
-    }
-
-    return direction == "rtl" ? order.reverse() : order
-  }
-})()
-
-// Get the bidi ordering for the given line (and cache it). Returns
-// false for lines that are fully left-to-right, and an array of
-// BidiSpan objects otherwise.
-function getOrder(line, direction) {
-  var order = line.order
-  if (order == null) { order = line.order = bidiOrdering(line.text, direction) }
-  return order
-}
-
-// EVENT HANDLING
-
-// Lightweight event framework. on/off also work on DOM nodes,
-// registering native DOM handlers.
-
-var noHandlers = []
-
-var on = function(emitter, type, f) {
-  if (emitter.addEventListener) {
-    emitter.addEventListener(type, f, false)
-  } else if (emitter.attachEvent) {
-    emitter.attachEvent("on" + type, f)
-  } else {
-    var map = emitter._handlers || (emitter._handlers = {})
-    map[type] = (map[type] || noHandlers).concat(f)
-  }
-}
-
-function getHandlers(emitter, type) {
-  return emitter._handlers && emitter._handlers[type] || noHandlers
-}
-
-function off(emitter, type, f) {
-  if (emitter.removeEventListener) {
-    emitter.removeEventListener(type, f, false)
-  } else if (emitter.detachEvent) {
-    emitter.detachEvent("on" + type, f)
-  } else {
-    var map = emitter._handlers, arr = map && map[type]
-    if (arr) {
-      var index = indexOf(arr, f)
-      if (index > -1)
-        { map[type] = arr.slice(0, index).concat(arr.slice(index + 1)) }
-    }
-  }
-}
-
-function signal(emitter, type /*, values...*/) {
-  var handlers = getHandlers(emitter, type)
-  if (!handlers.length) { return }
-  var args = Array.prototype.slice.call(arguments, 2)
-  for (var i = 0; i < handlers.length; ++i) { handlers[i].apply(null, args) }
-}
-
-// The DOM events that CodeMirror handles can be overridden by
-// registering a (non-DOM) handler on the editor for the event name,
-// and preventDefault-ing the event in that handler.
-function signalDOMEvent(cm, e, override) {
-  if (typeof e == "string")
-    { e = {type: e, preventDefault: function() { this.defaultPrevented = true }} }
-  signal(cm, override || e.type, cm, e)
-  return e_defaultPrevented(e) || e.codemirrorIgnore
-}
-
-function signalCursorActivity(cm) {
-  var arr = cm._handlers && cm._handlers.cursorActivity
-  if (!arr) { return }
-  var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = [])
-  for (var i = 0; i < arr.length; ++i) { if (indexOf(set, arr[i]) == -1)
-    { set.push(arr[i]) } }
-}
-
-function hasHandler(emitter, type) {
-  return getHandlers(emitter, type).length > 0
-}
-
-// Add on and off methods to a constructor's prototype, to make
-// registering events on such objects more convenient.
-function eventMixin(ctor) {
-  ctor.prototype.on = function(type, f) {on(this, type, f)}
-  ctor.prototype.off = function(type, f) {off(this, type, f)}
-}
-
-// Due to the fact that we still support jurassic IE versions, some
-// compatibility wrappers are needed.
-
-function e_preventDefault(e) {
-  if (e.preventDefault) { e.preventDefault() }
-  else { e.returnValue = false }
-}
-function e_stopPropagation(e) {
-  if (e.stopPropagation) { e.stopPropagation() }
-  else { e.cancelBubble = true }
-}
-function e_defaultPrevented(e) {
-  return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false
-}
-function e_stop(e) {e_preventDefault(e); e_stopPropagation(e)}
-
-function e_target(e) {return e.target || e.srcElement}
-function e_button(e) {
-  var b = e.which
-  if (b == null) {
-    if (e.button & 1) { b = 1 }
-    else if (e.button & 2) { b = 3 }
-    else if (e.button & 4) { b = 2 }
-  }
-  if (mac && e.ctrlKey && b == 1) { b = 3 }
-  return b
-}
-
-// Detect drag-and-drop
-var dragAndDrop = function() {
-  // There is *some* kind of drag-and-drop support in IE6-8, but I
-  // couldn't get it to work yet.
-  if (ie && ie_version < 9) { return false }
-  var div = elt('div')
-  return "draggable" in div || "dragDrop" in div
-}()
-
-var zwspSupported
-function zeroWidthElement(measure) {
-  if (zwspSupported == null) {
-    var test = elt("span", "\u200b")
-    removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")]))
-    if (measure.firstChild.offsetHeight != 0)
-      { zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8) }
-  }
-  var node = zwspSupported ? elt("span", "\u200b") :
-    elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px")
-  node.setAttribute("cm-text", "")
-  return node
-}
-
-// Feature-detect IE's crummy client rect reporting for bidi text
-var badBidiRects
-function hasBadBidiRects(measure) {
-  if (badBidiRects != null) { return badBidiRects }
-  var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA"))
-  var r0 = range(txt, 0, 1).getBoundingClientRect()
-  var r1 = range(txt, 1, 2).getBoundingClientRect()
-  removeChildren(measure)
-  if (!r0 || r0.left == r0.right) { return false } // Safari returns null in some cases (#2780)
-  return badBidiRects = (r1.right - r0.right < 3)
-}
-
-// See if "".split is the broken IE version, if so, provide an
-// alternative way to split lines.
-var splitLinesAuto = "\n\nb".split(/\n/).length != 3 ? function (string) {
-  var pos = 0, result = [], l = string.length
-  while (pos <= l) {
-    var nl = string.indexOf("\n", pos)
-    if (nl == -1) { nl = string.length }
-    var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl)
-    var rt = line.indexOf("\r")
-    if (rt != -1) {
-      result.push(line.slice(0, rt))
-      pos += rt + 1
-    } else {
-      result.push(line)
-      pos = nl + 1
-    }
-  }
-  return result
-} : function (string) { return string.split(/\r\n?|\n/); }
-
-var hasSelection = window.getSelection ? function (te) {
-  try { return te.selectionStart != te.selectionEnd }
-  catch(e) { return false }
-} : function (te) {
-  var range
-  try {range = te.ownerDocument.selection.createRange()}
-  catch(e) {}
-  if (!range || range.parentElement() != te) { return false }
-  return range.compareEndPoints("StartToEnd", range) != 0
-}
-
-var hasCopyEvent = (function () {
-  var e = elt("div")
-  if ("oncopy" in e) { return true }
-  e.setAttribute("oncopy", "return;")
-  return typeof e.oncopy == "function"
-})()
-
-var badZoomedRects = null
-function hasBadZoomedRects(measure) {
-  if (badZoomedRects != null) { return badZoomedRects }
-  var node = removeChildrenAndAdd(measure, elt("span", "x"))
-  var normal = node.getBoundingClientRect()
-  var fromRange = range(node, 0, 1).getBoundingClientRect()
-  return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1
-}
-
-var modes = {};
-var mimeModes = {};
-// Extra arguments are stored as the mode's dependencies, which is
-// used by (legacy) mechanisms like loadmode.js to automatically
-// load a mode. (Preferred mechanism is the require/define calls.)
-function defineMode(name, mode) {
-  if (arguments.length > 2)
-    { mode.dependencies = Array.prototype.slice.call(arguments, 2) }
-  modes[name] = mode
-}
-
-function defineMIME(mime, spec) {
-  mimeModes[mime] = spec
-}
-
-// Given a MIME type, a {name, ...options} config object, or a name
-// string, return a mode config object.
-function resolveMode(spec) {
-  if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) {
-    spec = mimeModes[spec]
-  } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) {
-    var found = mimeModes[spec.name]
-    if (typeof found == "string") { found = {name: found} }
-    spec = createObj(found, spec)
-    spec.name = found.name
-  } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) {
-    return resolveMode("application/xml")
-  } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+json$/.test(spec)) {
-    return resolveMode("application/json")
-  }
-  if (typeof spec == "string") { return {name: spec} }
-  else { return spec || {name: "null"} }
-}
-
-// Given a mode spec (anything that resolveMode accepts), find and
-// initialize an actual mode object.
-function getMode(options, spec) {
-  spec = resolveMode(spec)
-  var mfactory = modes[spec.name]
-  if (!mfactory) { return getMode(options, "text/plain") }
-  var modeObj = mfactory(options, spec)
-  if (modeExtensions.hasOwnProperty(spec.name)) {
-    var exts = modeExtensions[spec.name]
-    for (var prop in exts) {
-      if (!exts.hasOwnProperty(prop)) { continue }
-      if (modeObj.hasOwnProperty(prop)) { modeObj["_" + prop] = modeObj[prop] }
-      modeObj[prop] = exts[prop]
-    }
-  }
-  modeObj.name = spec.name
-  if (spec.helperType) { modeObj.helperType = spec.helperType }
-  if (spec.modeProps) { for (var prop$1 in spec.modeProps)
-    { modeObj[prop$1] = spec.modeProps[prop$1] } }
-
-  return modeObj
-}
-
-// This can be used to attach properties to mode objects from
-// outside the actual mode definition.
-var modeExtensions = {}
-function extendMode(mode, properties) {
-  var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {})
-  copyObj(properties, exts)
-}
-
-function copyState(mode, state) {
-  if (state === true) { return state }
-  if (mode.copyState) { return mode.copyState(state) }
-  var nstate = {}
-  for (var n in state) {
-    var val = state[n]
-    if (val instanceof Array) { val = val.concat([]) }
-    nstate[n] = val
-  }
-  return nstate
-}
-
-// Given a mode and a state (for that mode), find the inner mode and
-// state at the position that the state refers to.
-function innerMode(mode, state) {
-  var info
-  while (mode.innerMode) {
-    info = mode.innerMode(state)
-    if (!info || info.mode == mode) { break }
-    state = info.state
-    mode = info.mode
-  }
-  return info || {mode: mode, state: state}
-}
-
-function startState(mode, a1, a2) {
-  return mode.startState ? mode.startState(a1, a2) : true
-}
-
-// STRING STREAM
-
-// Fed to the mode parsers, provides helper functions to make
-// parsers more succinct.
-
-var StringStream = function(string, tabSize, lineOracle) {
-  this.pos = this.start = 0
-  this.string = string
-  this.tabSize = tabSize || 8
-  this.lastColumnPos = this.lastColumnValue = 0
-  this.lineStart = 0
-  this.lineOracle = lineOracle
-};
-
-StringStream.prototype.eol = function () {return this.pos >= this.string.length};
-StringStream.prototype.sol = function () {return this.pos == this.lineStart};
-StringStream.prototype.peek = function () {return this.string.charAt(this.pos) || undefined};
-StringStream.prototype.next = function () {
-  if (this.pos < this.string.length)
-    { return this.string.charAt(this.pos++) }
-};
-StringStream.prototype.eat = function (match) {
-  var ch = this.string.charAt(this.pos)
-  var ok
-  if (typeof match == "string") { ok = ch == match }
-  else { ok = ch && (match.test ? match.test(ch) : match(ch)) }
-  if (ok) {++this.pos; return ch}
-};
-StringStream.prototype.eatWhile = function (match) {
-  var start = this.pos
-  while (this.eat(match)){}
-  return this.pos > start
-};
-StringStream.prototype.eatSpace = function () {
-    var this$1 = this;
-
-  var start = this.pos
-  while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) { ++this$1.pos }
-  return this.pos > start
-};
-StringStream.prototype.skipToEnd = function () {this.pos = this.string.length};
-StringStream.prototype.skipTo = function (ch) {
-  var found = this.string.indexOf(ch, this.pos)
-  if (found > -1) {this.pos = found; return true}
-};
-StringStream.prototype.backUp = function (n) {this.pos -= n};
-StringStream.prototype.column = function () {
-  if (this.lastColumnPos < this.start) {
-    this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue)
-    this.lastColumnPos = this.start
-  }
-  return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0)
-};
-StringStream.prototype.indentation = function () {
-  return countColumn(this.string, null, this.tabSize) -
-    (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0)
-};
-StringStream.prototype.match = function (pattern, consume, caseInsensitive) {
-  if (typeof pattern == "string") {
-    var cased = function (str) { return caseInsensitive ? str.toLowerCase() : str; }
-    var substr = this.string.substr(this.pos, pattern.length)
-    if (cased(substr) == cased(pattern)) {
-      if (consume !== false) { this.pos += pattern.length }
-      return true
+  // Kludges for bugs and behavior differences that can't be feature
+  // detected are enabled based on userAgent etc sniffing.
+  var userAgent = navigator.userAgent;
+  var platform = navigator.platform;
+
+  var gecko = /gecko\/\d/i.test(userAgent);
+  var ie_upto10 = /MSIE \d/.test(userAgent);
+  var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent);
+  var edge = /Edge\/(\d+)/.exec(userAgent);
+  var ie = ie_upto10 || ie_11up || edge;
+  var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : +(edge || ie_11up)[1]);
+  var webkit = !edge && /WebKit\//.test(userAgent);
+  var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent);
+  var chrome = !edge && /Chrome\//.test(userAgent);
+  var presto = /Opera\//.test(userAgent);
+  var safari = /Apple Computer/.test(navigator.vendor);
+  var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent);
+  var phantom = /PhantomJS/.test(userAgent);
+
+  var ios = !edge && /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent);
+  var android = /Android/.test(userAgent);
+  // This is woefully incomplete. Suggestions for alternative methods welcome.
+  var mobile = ios || android || /webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent);
+  var mac = ios || /Mac/.test(platform);
+  var chromeOS = /\bCrOS\b/.test(userAgent);
+  var windows = /win/i.test(platform);
+
+  var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/);
+  if (presto_version) { presto_version = Number(presto_version[1]); }
+  if (presto_version && presto_version >= 15) { presto = false; webkit = true; }
+  // Some browsers use the wrong event properties to signal cmd/ctrl on OS X
+  var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11));
+  var captureRightClick = gecko || (ie && ie_version >= 9);
+
+  function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") }
+
+  var rmClass = function(node, cls) {
+    var current = node.className;
+    var match = classTest(cls).exec(current);
+    if (match) {
+      var after = current.slice(match.index + match[0].length);
+      node.className = current.slice(0, match.index) + (after ? match[1] + after : "");
     }
-  } else {
-    var match = this.string.slice(this.pos).match(pattern)
-    if (match && match.index > 0) { return null }
-    if (match && consume !== false) { this.pos += match[0].length }
-    return match
-  }
-};
-StringStream.prototype.current = function (){return this.string.slice(this.start, this.pos)};
-StringStream.prototype.hideFirstChars = function (n, inner) {
-  this.lineStart += n
-  try { return inner() }
-  finally { this.lineStart -= n }
-};
-StringStream.prototype.lookAhead = function (n) {
-  var oracle = this.lineOracle
-  return oracle && oracle.lookAhead(n)
-};
-StringStream.prototype.baseToken = function () {
-  var oracle = this.lineOracle
-  return oracle && oracle.baseToken(this.pos)
-};
-
-var SavedContext = function(state, lookAhead) {
-  this.state = state
-  this.lookAhead = lookAhead
-};
-
-var Context = function(doc, state, line, lookAhead) {
-  this.state = state
-  this.doc = doc
-  this.line = line
-  this.maxLookAhead = lookAhead || 0
-  this.baseTokens = null
-  this.baseTokenPos = 1
-};
-
-Context.prototype.lookAhead = function (n) {
-  var line = this.doc.getLine(this.line + n)
-  if (line != null && n > this.maxLookAhead) { this.maxLookAhead = n }
-  return line
-};
-
-Context.prototype.baseToken = function (n) {
-    var this$1 = this;
-
-  if (!this.baseTokens) { return null }
-  while (this.baseTokens[this.baseTokenPos] <= n)
-    { this$1.baseTokenPos += 2 }
-  var type = this.baseTokens[this.baseTokenPos + 1]
-  return {type: type && type.replace(/( |^)overlay .*/, ""),
-          size: this.baseTokens[this.baseTokenPos] - n}
-};
-
-Context.prototype.nextLine = function () {
-  this.line++
-  if (this.maxLookAhead > 0) { this.maxLookAhead-- }
-};
-
-Context.fromSaved = function (doc, saved, line) {
-  if (saved instanceof SavedContext)
-    { return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead) }
-  else
-    { return new Context(doc, copyState(doc.mode, saved), line) }
-};
-
-Context.prototype.save = function (copy) {
-  var state = copy !== false ? copyState(this.doc.mode, this.state) : this.state
-  return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state
-};
-
-
-// Compute a style array (an array starting with a mode generation
-// -- for invalidation -- followed by pairs of end positions and
-// style strings), which is used to highlight the tokens on the
-// line.
-function highlightLine(cm, line, context, forceToEnd) {
-  // A styles array always starts with a number identifying the
-  // mode/overlays that it is based on (for easy invalidation).
-  var st = [cm.state.modeGen], lineClasses = {}
-  // Compute the base array of styles
-  runMode(cm, line.text, cm.doc.mode, context, function (end, style) { return st.push(end, style); },
-          lineClasses, forceToEnd)
-  var state = context.state
-
-  // Run overlays, adjust style array.
-  var loop = function ( o ) {
-    context.baseTokens = st
-    var overlay = cm.state.overlays[o], i = 1, at = 0
-    context.state = true
-    runMode(cm, line.text, overlay.mode, context, function (end, style) {
-      var start = i
-      // Ensure there's a token end at the current position, and that i points at it
-      while (at < end) {
-        var i_end = st[i]
-        if (i_end > end)
-          { st.splice(i, 1, end, st[i+1], i_end) }
-        i += 2
-        at = Math.min(end, i_end)
-      }
-      if (!style) { return }
-      if (overlay.opaque) {
-        st.splice(start, i - start, end, "overlay " + style)
-        i = start + 2
-      } else {
-        for (; start < i; start += 2) {
-          var cur = st[start+1]
-          st[start+1] = (cur ? cur + " " : "") + "overlay " + style
-        }
-      }
-    }, lineClasses)
-    context.state = state
-    context.baseTokens = null
-    context.baseTokenPos = 1
   };
 
-  for (var o = 0; o < cm.state.overlays.length; ++o) loop( o );
-
-  return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}
-}
-
-function getLineStyles(cm, line, updateFrontier) {
-  if (!line.styles || line.styles[0] != cm.state.modeGen) {
-    var context = getContextBefore(cm, lineNo(line))
-    var resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state)
-    var result = highlightLine(cm, line, context)
-    if (resetState) { context.state = resetState }
-    line.stateAfter = context.save(!resetState)
-    line.styles = result.styles
-    if (result.classes) { line.styleClasses = result.classes }
-    else if (line.styleClasses) { line.styleClasses = null }
-    if (updateFrontier === cm.doc.highlightFrontier)
-      { cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier) }
-  }
-  return line.styles
-}
-
-function getContextBefore(cm, n, precise) {
-  var doc = cm.doc, display = cm.display
-  if (!doc.mode.startState) { return new Context(doc, true, n) }
-  var start = findStartLine(cm, n, precise)
-  var saved = start > doc.first && getLine(doc, start - 1).stateAfter
-  var context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start)
-
-  doc.iter(start, n, function (line) {
-    processLine(cm, line.text, context)
-    var pos = context.line
-    line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null
-    context.nextLine()
-  })
-  if (precise) { doc.modeFrontier = context.line }
-  return context
-}
-
-// Lightweight form of highlight -- proceed over this line and
-// update state, but don't save a style array. Used for lines that
-// aren't currently visible.
-function processLine(cm, text, context, startAt) {
-  var mode = cm.doc.mode
-  var stream = new StringStream(text, cm.options.tabSize, context)
-  stream.start = stream.pos = startAt || 0
-  if (text == "") { callBlankLine(mode, context.state) }
-  while (!stream.eol()) {
-    readToken(mode, stream, context.state)
-    stream.start = stream.pos
-  }
-}
-
-function callBlankLine(mode, state) {
-  if (mode.blankLine) { return mode.blankLine(state) }
-  if (!mode.innerMode) { return }
-  var inner = innerMode(mode, state)
-  if (inner.mode.blankLine) { return inner.mode.blankLine(inner.state) }
-}
-
-function readToken(mode, stream, state, inner) {
-  for (var i = 0; i < 10; i++) {
-    if (inner) { inner[0] = innerMode(mode, state).mode }
-    var style = mode.token(stream, state)
-    if (stream.pos > stream.start) { return style }
-  }
-  throw new Error("Mode " + mode.name + " failed to advance stream.")
-}
-
-var Token = function(stream, type, state) {
-  this.start = stream.start; this.end = stream.pos
-  this.string = stream.current()
-  this.type = type || null
-  this.state = state
-};
-
-// Utility for getTokenAt and getLineTokens
-function takeToken(cm, pos, precise, asArray) {
-  var doc = cm.doc, mode = doc.mode, style
-  pos = clipPos(doc, pos)
-  var line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise)
-  var stream = new StringStream(line.text, cm.options.tabSize, context), tokens
-  if (asArray) { tokens = [] }
-  while ((asArray || stream.pos < pos.ch) && !stream.eol()) {
-    stream.start = stream.pos
-    style = readToken(mode, stream, context.state)
-    if (asArray) { tokens.push(new Token(stream, style, copyState(doc.mode, context.state))) }
-  }
-  return asArray ? tokens : new Token(stream, style, context.state)
-}
-
-function extractLineClasses(type, output) {
-  if (type) { for (;;) {
-    var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/)
-    if (!lineClass) { break }
-    type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length)
-    var prop = lineClass[1] ? "bgClass" : "textClass"
-    if (output[prop] == null)
-      { output[prop] = lineClass[2] }
-    else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop]))
-      { output[prop] += " " + lineClass[2] }
-  } }
-  return type
-}
-
-// Run the given mode's parser over a line, calling f for each token.
-function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) {
-  var flattenSpans = mode.flattenSpans
-  if (flattenSpans == null) { flattenSpans = cm.options.flattenSpans }
-  var curStart = 0, curStyle = null
-  var stream = new StringStream(text, cm.options.tabSize, context), style
-  var inner = cm.options.addModeClass && [null]
-  if (text == "") { extractLineClasses(callBlankLine(mode, context.state), lineClasses) }
-  while (!stream.eol()) {
-    if (stream.pos > cm.options.maxHighlightLength) {
-      flattenSpans = false
-      if (forceToEnd) { processLine(cm, text, context, stream.pos) }
-      stream.pos = text.length
-      style = null
-    } else {
-      style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses)
-    }
-    if (inner) {
-      var mName = inner[0].name
-      if (mName) { style = "m-" + (style ? mName + " " + style : mName) }
-    }
-    if (!flattenSpans || curStyle != style) {
-      while (curStart < stream.start) {
-        curStart = Math.min(stream.start, curStart + 5000)
-        f(curStart, curStyle)
-      }
-      curStyle = style
-    }
-    stream.start = stream.pos
-  }
-  while (curStart < stream.pos) {
-    // Webkit seems to refuse to render text nodes longer than 57444
-    // characters, and returns inaccurate measurements in nodes
-    // starting around 5000 chars.
-    var pos = Math.min(stream.pos, curStart + 5000)
-    f(pos, curStyle)
-    curStart = pos
-  }
-}
-
-// Finds the line to start with when starting a parse. Tries to
-// find a line with a stateAfter, so that it can start with a
-// valid state. If that fails, it returns the line with the
-// smallest indentation, which tends to need the least context to
-// parse correctly.
-function findStartLine(cm, n, precise) {
-  var minindent, minline, doc = cm.doc
-  var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100)
-  for (var search = n; search > lim; --search) {
-    if (search <= doc.first) { return doc.first }
-    var line = getLine(doc, search - 1), after = line.stateAfter
-    if (after &&&n