Merge branch 'wip-MDL-62755-master' of git://github.com/abgreeve/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 27 Jun 2018 23:55:53 +0000 (01:55 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 27 Jun 2018 23:55:53 +0000 (01:55 +0200)
60 files changed:
admin/searchareas.php
admin/tool/dataprivacy/amd/src/data_registry.js
admin/tool/dataprivacy/amd/src/expand_contract.js
admin/tool/dataprivacy/categories.php
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/data_registry.php
admin/tool/dataprivacy/classes/expired_contexts_manager.php
admin/tool/dataprivacy/classes/expired_user_contexts.php
admin/tool/dataprivacy/classes/form/context_instance.php
admin/tool/dataprivacy/classes/local/helper.php
admin/tool/dataprivacy/classes/purpose.php
admin/tool/dataprivacy/datadeletion.php
admin/tool/dataprivacy/dataregistry.php
admin/tool/dataprivacy/datarequests.php
admin/tool/dataprivacy/defaults.php
admin/tool/dataprivacy/editcategory.php
admin/tool/dataprivacy/editpurpose.php
admin/tool/dataprivacy/pluginregistry.php
admin/tool/dataprivacy/purposes.php
admin/tool/dataprivacy/templates/component_status.mustache
admin/tool/dataprivacy/templates/data_deletion.mustache
admin/tool/dataprivacy/templates/data_registry_compliance.mustache
admin/tool/dataprivacy/templates/data_request_email.mustache
admin/tool/dataprivacy/templates/data_request_modal.mustache
admin/tool/dataprivacy/tests/expired_contexts_test.php
admin/tool/xmldb/actions/edit_table/edit_table.class.php
admin/tool/xmldb/actions/edit_xml_file/edit_xml_file.class.php
admin/tool/xmldb/actions/view_table_php/view_table_php.class.php
filter/emoticon/filter.php
filter/emoticon/tests/filter_test.php
lang/en/admin.php
lang/en/search.php
lib/accesslib.php
lib/environmentlib.php
lib/evalmath/evalmath.class.php
lib/evalmath/readme_moodle.txt
lib/excellib.class.php
lib/filelib.php
lib/mathslib.php
lib/tests/accesslib_test.php
lib/tests/mathslib_test.php
lib/xmldb/xmldb_index.php
lib/xmldb/xmldb_key.php
mod/page/lang/en/page.php
mod/page/lib.php
mod/page/mod_form.php
mod/page/settings.php
mod/page/tests/behat/page_appearance.feature [new file with mode: 0644]
mod/page/tests/generator/lib.php
mod/page/version.php
mod/page/view.php
mod/quiz/report/overview/db/install.xml
mod/quiz/report/overview/db/upgrade.php
mod/quiz/report/overview/version.php
question/type/shortanswer/renderer.php
theme/boost/scss/moodle/course.scss
theme/boost/templates/core_form/element-date_time_selector-inline.mustache
theme/boost/templates/core_form/element-date_time_selector.mustache
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/style/moodle.css

index bf178b5..911a86b 100644 (file)
@@ -28,113 +28,139 @@ admin_externalpage_setup('searchareas');
 
 $areaid = optional_param('areaid', null, PARAM_ALPHAEXT);
 $action = optional_param('action', null, PARAM_ALPHA);
+$indexingenabled = \core_search\manager::is_indexing_enabled(); // This restricts many of the actions on this page.
 
+// Get a search manager instance, which we'll need for display and to handle some actions.
 try {
     $searchmanager = \core_search\manager::instance();
 } catch (core_search\engine_exception $searchmanagererror) {
-    // Continue, we return an error later depending on the requested action.
+    // In action cases, we'll throw this exception below. In non-action cases, we produce a lang string error.
 }
 
+// Handle all the actions.
 if ($action) {
-
+    // If dealing with an areaid, we need to check that the area exists.
     if ($areaid) {
-        // We need to check that the area exists.
         $area = \core_search\manager::get_search_area($areaid);
         if ($area === false) {
             throw new moodle_exception('invalidrequest');
         }
     }
 
-    if ($action !== 'enable' && $action !== 'disable') {
-        // All actions but enable/disable need the search engine to be ready.
-        if (!empty($searchmanagererror)) {
-            throw $searchmanagererror;
-        }
+    // All the indexing actions.
+    if (in_array($action, ['delete', 'indexall', 'reindexall', 'deleteall'])) {
 
-        // Show confirm prompt for all these actions as they may be inadvisable, or may cause
-        // an interruption in search functionality, on production systems.
-        if (!optional_param('confirm', 0, PARAM_INT)) {
-            // Display confirmation prompt.
-            $a = null;
-            if ($areaid) {
-                $a = html_writer::tag('strong', $area->get_visible_name());
-            }
+        // All of these actions require that indexing is enabled.
+        if ($indexingenabled) {
 
-            $actionparams = ['sesskey' => sesskey(), 'action' => $action, 'confirm' => 1];
-            if ($areaid) {
-                $actionparams['areaid'] = $areaid;
+            // For all of these actions, we strictly need a manager instance.
+            if (isset($searchmanagererror)) {
+                throw $searchmanagererror;
             }
-            $actionurl = new moodle_url('/admin/searchareas.php', $actionparams);
-            $cancelurl = new moodle_url('/admin/searchareas.php');
-            echo $OUTPUT->header();
-            echo $OUTPUT->confirm(get_string('confirm_' . $action, 'search', $a),
+
+            // Show confirm prompt for all these actions as they may be inadvisable, or may cause
+            // an interruption in search functionality, on production systems.
+            if (!optional_param('confirm', 0, PARAM_INT)) {
+                // Display confirmation prompt.
+                $a = null;
+                if ($areaid) {
+                    $a = html_writer::tag('strong', $area->get_visible_name());
+                }
+
+                $actionparams = ['sesskey' => sesskey(), 'action' => $action, 'confirm' => 1];
+                if ($areaid) {
+                    $actionparams['areaid'] = $areaid;
+                }
+                $actionurl = new moodle_url('/admin/searchareas.php', $actionparams);
+                $cancelurl = new moodle_url('/admin/searchareas.php');
+                echo $OUTPUT->header();
+                echo $OUTPUT->confirm(get_string('confirm_' . $action, 'search', $a),
                     new single_button($actionurl, get_string('continue'), 'post', true),
                     new single_button($cancelurl, get_string('cancel'), 'get'));
-            echo $OUTPUT->footer();
-            exit;
+                echo $OUTPUT->footer();
+                exit;
+            } else {
+                // Confirmed, so run the required action.
+                require_sesskey();
+
+                switch ($action) {
+                    case 'delete':
+                        $searchmanager->delete_index($areaid);
+                        \core\notification::success(get_string('searchindexdeleted', 'admin'));
+                        break;
+                    case 'indexall':
+                        $searchmanager->index();
+                        \core\notification::success(get_string('searchindexupdated', 'admin'));
+                        break;
+                    case 'reindexall':
+                        $searchmanager->index(true);
+                        \core\notification::success(get_string('searchreindexed', 'admin'));
+                        break;
+                    case 'deleteall':
+                        $searchmanager->delete_index();
+                        \core\notification::success(get_string('searchalldeleted', 'admin'));
+                        break;
+                    default:
+                        break;
+                }
+
+                // Redirect back to the main page after taking action.
+                redirect(new moodle_url('/admin/searchareas.php'));
+            }
         }
-    }
+    } else if (in_array($action, ['enable', 'disable'])) {
+        // Toggling search areas requires no confirmation.
+        require_sesskey();
 
-    // We are now taking an actual action, so require sesskey.
-    require_sesskey();
-
-    switch ($action) {
-        case 'enable':
-            $area->set_enabled(true);
-            \core\notification::add(get_string('searchareaenabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'disable':
-            $area->set_enabled(false);
-            \core\notification::add(get_string('searchareadisabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'delete':
-            $search = \core_search\manager::instance();
-            $search->delete_index($areaid);
-            \core\notification::add(get_string('searchindexdeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'indexall':
-            $searchmanager->index();
-            \core\notification::add(get_string('searchindexupdated', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'reindexall':
-            $searchmanager->index(true);
-            \core\notification::add(get_string('searchreindexed', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'deleteall':
-            $searchmanager->delete_index();
-            \core\notification::add(get_string('searchalldeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        default:
-            throw new moodle_exception('invalidaction');
-            break;
-    }
+        switch ($action) {
+            case 'enable':
+                $area->set_enabled(true);
+                \core\notification::success(get_string('searchareaenabled', 'admin'));
+                break;
+            case 'disable':
+                $area->set_enabled(false);
+                core\notification::success(get_string('searchareadisabled', 'admin'));
+                break;
+            default:
+                break;
+        }
 
-    // Redirect back to the main page after taking action.
-    redirect(new moodle_url('/admin/searchareas.php'));
+        redirect(new moodle_url('/admin/searchareas.php'));
+    } else {
+        // Invalid action.
+        throw new moodle_exception('invalidaction');
+    }
 }
 
-echo $OUTPUT->header();
 
-$searchareas = \core_search\manager::get_search_areas_list();
-if (empty($searchmanagererror)) {
-    $areasconfig = $searchmanager->get_areas_config($searchareas);
+// Display.
+if (isset($searchmanager) && $indexingenabled) {
+    \core\notification::info(get_string('indexinginfo', 'admin'));
+} else if (isset($searchmanager)) {
+    $params = (object) [
+        'url' => (new moodle_url("/admin/settings.php?section=manageglobalsearch#admin-searchindexwhendisabled"))->out(false)
+    ];
+    \core\notification::error(get_string('indexwhendisabledfullnotice', 'search', $params));
 } else {
-    $areasconfig = false;
-}
-
-if (!empty($searchmanagererror)) {
+    // In non-action cases, init errors are translated and displayed to the user as error notifications.
     $errorstr = get_string($searchmanagererror->errorcode, $searchmanagererror->module, $searchmanagererror->a);
-    echo $OUTPUT->notification($errorstr, \core\output\notification::NOTIFY_ERROR);
-} else {
-    echo $OUTPUT->notification(get_string('indexinginfo', 'admin'), \core\output\notification::NOTIFY_INFO);
+    \core\notification::error($errorstr);
 }
 
+echo $OUTPUT->header();
+
 $table = new html_table();
 $table->id = 'core-search-areas';
+$table->head = [
+    get_string('searcharea', 'search'),
+    get_string('enable'),
+    get_string('newestdocindexed', 'admin'),
+    get_string('searchlastrun', 'admin'),
+    get_string('searchindexactions', 'admin')
+];
 
-$table->head = array(get_string('searcharea', 'search'), get_string('enable'), get_string('newestdocindexed', 'admin'),
-    get_string('searchlastrun', 'admin'), get_string('searchindexactions', 'admin'));
-
+$searchareas = \core_search\manager::get_search_areas_list();
+$areasconfig = isset($searchmanager) ? $searchmanager->get_areas_config($searchareas) : false;
 foreach ($searchareas as $area) {
     $areaid = $area->get_area_id();
     $columns = array(new html_table_cell($area->get_visible_name()));
@@ -144,7 +170,7 @@ foreach ($searchareas as $area) {
             new pix_icon('t/hide', get_string('disable'), 'moodle', array('title' => '', 'class' => 'iconsmall')),
             null, array('title' => get_string('disable')));
 
-        if ($areasconfig) {
+        if ($areasconfig && $indexingenabled) {
             $columns[] = $areasconfig[$areaid]->lastindexrun;
 
             if ($areasconfig[$areaid]->indexingstart) {
@@ -173,7 +199,11 @@ foreach ($searchareas as $area) {
             $columns[] = html_writer::alist($actions, ['class' => 'unstyled list-unstyled']);
 
         } else {
-            $blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
+            if (!$areasconfig) {
+                $blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
+            } else {
+                $blankrow = new html_table_cell(get_string('indexwhendisabledshortnotice', 'search'));
+            }
             $blankrow->colspan = 3;
             $columns[] = $blankrow;
         }
@@ -192,10 +222,7 @@ foreach ($searchareas as $area) {
 }
 
 // Cross-search area tasks.
-$options = array();
-if (!empty($searchmanagererror)) {
-    $options['disabled'] = true;
-}
+$options = (isset($searchmanager) && $indexingenabled) ? [] : ['disabled' => true];
 echo $OUTPUT->box_start('search-areas-actions');
 echo $OUTPUT->single_button(admin_searcharea_action_url('indexall'), get_string('searchupdateindex', 'admin'), 'get', $options);
 echo $OUTPUT->single_button(admin_searcharea_action_url('reindexall'), get_string('searchreindexindex', 'admin'), 'get', $options);
@@ -204,7 +231,7 @@ echo $OUTPUT->box_end();
 
 echo html_writer::table($table);
 
-if (empty($searchmanagererror)) {
+if (isset($searchmanager)) {
     // Show information about queued index requests for specific contexts.
     $searchrenderer = $PAGE->get_renderer('core_search');
     echo $searchrenderer->render_index_requests_info($searchmanager->get_index_requests_info());
index 76f00ee..07eb18d 100644 (file)
@@ -234,6 +234,7 @@ define(['jquery', 'core/str', 'core/ajax', 'core/notification', 'core/templates'
                     },
                     fail: Notification.exception
                 }]);
+                return;
             }).catch(Notification.exception);
 
         };
index 41b7e50..cf509b5 100644 (file)
@@ -34,7 +34,6 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
          *
          * @param  {object} targetnode The node that we want to expand / collapse
          * @param  {object} thisnode The node that was clicked.
-         * @return {null}
          */
         expandCollapse: function(targetnode, thisnode) {
             if (targetnode.hasClass('hide')) {
@@ -58,7 +57,6 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
          * Expand or collapse all nodes on this page.
          *
          * @param  {string} nextstate The next state to change to.
-         * @return {null}
          */
         expandCollapseAll: function(nextstate) {
             var currentstate = (nextstate == 'visible') ? 'hide' : 'visible';
@@ -75,6 +73,7 @@ define(['jquery', 'core/url', 'core/str'], function($, url, str) {
 
             str.get_string(currentstate, 'tool_dataprivacy').then(function(langString) {
                 $('.tool_dataprivacy-expand-all').html(langString);
+                return;
             }).catch(Notification.exception);
 
             $(':header i.fa').each(function() {
index b160ff3..f323278 100644 (file)
@@ -24,6 +24,8 @@
 
 require_once(__DIR__ . '/../../../config.php');
 
+require_login(null, false);
+
 $url = new moodle_url("/admin/tool/dataprivacy/categories.php");
 $title = get_string('editcategories', 'tool_dataprivacy');
 
index 05efe9c..275044a 100644 (file)
@@ -197,7 +197,7 @@ class api {
             } else {
                 // If not a DPO, only users with the capability to make data requests for the user should be allowed.
                 // (e.g. users with the Parent role, etc).
-                if (!api::can_create_data_request_for_user($foruser)) {
+                if (!self::can_create_data_request_for_user($foruser)) {
                     $forusercontext = \context_user::instance($foruser);
                     throw new required_capability_exception($forusercontext,
                             'tool/dataprivacy:makedatarequestsforchildren', 'nopermissions', '');
index 39e3e53..1435fcd 100644 (file)
@@ -289,7 +289,8 @@ class data_registry {
      * @param int $forcedcategoryvalue Use this value as if this was this context level category.
      * @return int[]
      */
-    public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false, $forcedcategoryvalue = false) {
+    public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
+                                                                                   $forcedcategoryvalue = false) {
 
         list($purposeid, $categoryid) = self::get_defaults($contextlevel);
 
index 3d20e5d..539fc28 100644 (file)
  */
 namespace tool_dataprivacy;
 
-use tool_dataprivacy\api;
-use tool_dataprivacy\purpose;
-use tool_dataprivacy\context_instance;
-use tool_dataprivacy\data_registry;
+use core_privacy\manager;
 use tool_dataprivacy\expired_context;
 
 defined('MOODLE_INTERNAL') || die();
@@ -90,7 +87,7 @@ abstract class expired_contexts_manager {
             return $numprocessed;
         }
 
-        $privacymanager = new \core_privacy\manager();
+        $privacymanager = new manager();
         $privacymanager->set_observer(new \tool_dataprivacy\manager_observer());
 
         foreach ($this->get_context_levels() as $level) {
@@ -118,11 +115,11 @@ abstract class expired_contexts_manager {
     /**
      * Deletes user data from the provided context.
      *
-     * @param \core_privacy\manager $privacymanager
-     * @param \tool_dataprivacy\expired_context $expiredctx
+     * @param manager $privacymanager
+     * @param expired_context $expiredctx
      * @return \context|false
      */
-    protected function delete_expired_context(\core_privacy\manager $privacymanager, \tool_dataprivacy\expired_context $expiredctx) {
+    protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
 
         $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
         if (!$context) {
index e4b40e8..924d565 100644 (file)
@@ -23,8 +23,7 @@
  */
 namespace tool_dataprivacy;
 
-use tool_dataprivacy\purpose;
-use tool_dataprivacy\context_instance;
+use core_privacy\manager;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -110,11 +109,11 @@ class expired_user_contexts extends \tool_dataprivacy\expired_contexts_manager {
      *
      * Overwritten to delete the user.
      *
-     * @param \core_privacy\manager $privacymanager
-     * @param \tool_dataprivacy\expired_context $expiredctx
+     * @param manager $privacymanager
+     * @param expired_context $expiredctx
      * @return \context|false
      */
-    protected function delete_expired_context(\core_privacy\manager $privacymanager, \tool_dataprivacy\expired_context $expiredctx) {
+    protected function delete_expired_context(manager $privacymanager, expired_context $expiredctx) {
         $context = \context::instance_by_id($expiredctx->get('contextid'), IGNORE_MISSING);
         if (!$context) {
             api::delete_expired_context($expiredctx->get('contextid'));
index 14790b5..bd1204f 100644 (file)
@@ -27,6 +27,7 @@ defined('MOODLE_INTERNAL') || die();
 
 use tool_dataprivacy\api;
 use tool_dataprivacy\data_registry;
+use tool_dataprivacy\purpose;
 
 /**
  * Context instance data form.
@@ -186,12 +187,12 @@ class context_instance extends \core\form\persistent {
     /**
      * Returns the purpose display text.
      *
-     * @param \tool_dataprivacy\purpose $effectivepurpose
+     * @param purpose $effectivepurpose
      * @param int $retentioncontextlevel
      * @param \context $context The context, just for displaying (filters) purposes.
      * @return string
      */
-    protected static function get_retention_display_text(\tool_dataprivacy\purpose $effectivepurpose, $retentioncontextlevel, \context $context) {
+    protected static function get_retention_display_text(purpose $effectivepurpose, $retentioncontextlevel, \context $context) {
         global $PAGE;
 
         $renderer = $PAGE->get_renderer('tool_dataprivacy');
index d7c3436..c68c994 100644 (file)
@@ -132,7 +132,7 @@ class helper {
             'contextlevel' => CONTEXT_USER
         ];
 
-        // The final list of users that we will return;
+        // The final list of users that we will return.
         $finalresults = [];
 
         // Our prospective list of users.
index c1631e4..ca4fb17 100644 (file)
@@ -64,7 +64,7 @@ class purpose extends \core\persistent {
                 // Replicate self::read.
                 $this->from_record($data);
 
-                // Using validate() as self::$validated is private.
+                // Validate the purpose record.
                 $this->validate();
 
                 // Now replicate the parent constructor.
index eaa5056..f622e5b 100644 (file)
@@ -25,6 +25,8 @@
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
 
+require_login(null, false);
+
 $filter = optional_param('filter', CONTEXT_COURSE, PARAM_INT);
 
 $url = new moodle_url('/admin/tool/dataprivacy/datadeletion.php');
index 58da785..52eff0a 100644 (file)
@@ -25,6 +25,8 @@
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
 
+require_login(null, false);
+
 $contextlevel = optional_param('contextlevel', CONTEXT_SYSTEM, PARAM_INT);
 $contextid = optional_param('contextid', 0, PARAM_INT);
 
index d2d9123..a3e613a 100644 (file)
@@ -25,6 +25,8 @@
 require_once("../../../config.php");
 require_once('lib.php');
 
+require_login(null, false);
+
 $url = new moodle_url('/admin/tool/dataprivacy/datarequests.php');
 
 $title = get_string('datarequests', 'tool_dataprivacy');
index 0611908..d936ba2 100644 (file)
@@ -25,6 +25,8 @@
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
 
+require_login(null, false);
+
 $url = new \moodle_url('/admin/tool/dataprivacy/defaults.php');
 $title = get_string('setdefaults', 'tool_dataprivacy');
 
index a2f59fb..0ee630f 100644 (file)
@@ -24,6 +24,8 @@
 
 require_once(__DIR__ . '/../../../config.php');
 
+require_login(null, false);
+
 $id = optional_param('id', 0, PARAM_INT);
 
 $url = new \moodle_url('/admin/tool/dataprivacy/editcategory.php', array('id' => $id));
index d7013db..20ee631 100644 (file)
@@ -24,6 +24,8 @@
 
 require_once(__DIR__ . '/../../../config.php');
 
+require_login(null, false);
+
 $id = optional_param('id', 0, PARAM_INT);
 
 $url = new \moodle_url('/admin/tool/dataprivacy/editpurpose.php', array('id' => $id));
index 060bd81..3b3f1c4 100644 (file)
@@ -25,6 +25,8 @@
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
 
+require_login(null, false);
+
 $contextlevel = optional_param('contextlevel', CONTEXT_SYSTEM, PARAM_INT);
 $contextid = optional_param('contextid', 0, PARAM_INT);
 
index 81d1517..5fad922 100644 (file)
@@ -24,6 +24,8 @@
 
 require_once(__DIR__ . '/../../../config.php');
 
+require_login(null, false);
+
 $url = new moodle_url("/admin/tool/dataprivacy/purposes.php");
 $title = get_string('editpurposes', 'tool_dataprivacy');
 
index 79c9063..c62dc0b 100644 (file)
@@ -46,7 +46,7 @@
     }
 }}
 
-<div classs="container-fluid">
+<div class="container-fluid">
     <hr />
     <div class="row">
         {{#compliant}}
     </div>
 
     {{#compliant}}
-        <div class="hide" data-section="{{raw_component}}" aria-expanded="false">
+        <div class="hide" data-section="{{raw_component}}" aria-expanded="false" role="contentinfo">
             {{#metadata}}
                 <hr />
                 <div class="p-l-3">
                     <dl class="row">
                         <dt class="span3 col-xs-3">
                             {{#link}}
-                                <a href="#{{name}}"><h5 style="word-wrap:break-word">{{name}}</h5></a>
+                                <a href="#{{name}}"><strong style="word-wrap:break-word">{{name}}</strong></a>
                             {{/link}}
                             {{^link}}
-                                <h5 style="word-wrap:break-word">{{name}}</h5>
+                                <strong style="word-wrap:break-word">{{name}}</strong>
                             {{/link}}
                             <div class="small text-muted" style="word-wrap:break-word">{{type}}</div>
                         </dt>
index d87f231..cbf5ed3 100644 (file)
@@ -50,7 +50,7 @@
             "labelattributes": [],
             "helpicon": false
         },
-        "expiredcontexts": "<table class='table'><thead><tr><th class='header c0' scope='col'>Name<div class='commands'><a title='Hide Name' aria-expanded='true' aria-controls='expired-contexts-table_r0_c0 expired-contexts-table_r1_c0 expired-contexts-table_r2_c0 expired-contexts-table_r3_c0 expired-contexts-table_r4_c0 expired-contexts-table_r5_c0 expired-contexts-table_r6_c0 expired-contexts-table_r7_c0 expired-contexts-table_r8_c0 expired-contexts-table_r9_c0 expired-contexts-table_r10_c0 expired-contexts-table_r11_c0 expired-contexts-table_r12_c0 expired-contexts-table_r13_c0 expired-contexts-table_r14_c0 expired-contexts-table_r15_c0 expired-contexts-table_r16_c0 expired-contexts-table_r17_c0 expired-contexts-table_r18_c0 expired-contexts-table_r19_c0' href='#?thide=name'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c1' scope='col'>Information<div class='commands'><a title='Hide Information' aria-expanded='true' aria-controls='expired-contexts-table_r0_c1 expired-contexts-table_r1_c1 expired-contexts-table_r2_c1 expired-contexts-table_r3_c1 expired-contexts-table_r4_c1 expired-contexts-table_r5_c1 expired-contexts-table_r6_c1 expired-contexts-table_r7_c1 expired-contexts-table_r8_c1 expired-contexts-table_r9_c1 expired-contexts-table_r10_c1 expired-contexts-table_r11_c1 expired-contexts-table_r12_c1 expired-contexts-table_r13_c1 expired-contexts-table_r14_c1 expired-contexts-table_r15_c1 expired-contexts-table_r16_c1 expired-contexts-table_r17_c1 expired-contexts-table_r18_c1 expired-contexts-table_r19_c1'    href='#?thide=info'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c2' scope='col'>Purpose<div class='commands'><a title='Hide Purpose' aria-expanded='true' aria-controls='expired-contexts-table_r0_c2 expired-contexts-table_r1_c2 expired-contexts-table_r2_c2 expired-contexts-table_r3_c2 expired-contexts-table_r4_c2 expired-contexts-table_r5_c2 expired-contexts-table_r6_c2 expired-contexts-table_r7_c2 expired-contexts-table_r8_c2 expired-contexts-table_r9_c2 expired-contexts-table_r10_c2 expired-contexts-table_r11_c2 expired-contexts-table_r12_c2 expired-contexts-table_r13_c2 expired-contexts-table_r14_c2 expired-contexts-table_r15_c2 expired-contexts-table_r16_c2 expired-contexts-table_r17_c2 expired-contexts-table_r18_c2 expired-contexts-table_r19_c2'    href='#?thide=purpose'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c3' scope='col'>Category<div class='commands'><a title='Hide Category' aria-expanded='true' aria-controls='expired-contexts-table_r0_c3 expired-contexts-table_r1_c3 expired-contexts-table_r2_c3 expired-contexts-table_r3_c3 expired-contexts-table_r4_c3 expired-contexts-table_r5_c3 expired-contexts-table_r6_c3 expired-contexts-table_r7_c3 expired-contexts-table_r8_c3 expired-contexts-table_r9_c3 expired-contexts-table_r10_c3 expired-contexts-table_r11_c3 expired-contexts-table_r12_c3 expired-contexts-table_r13_c3 expired-contexts-table_r14_c3 expired-contexts-table_r15_c3 expired-contexts-table_r16_c3 expired-contexts-table_r17_c3 expired-contexts-table_r18_c3 expired-contexts-table_r19_c3'    href='#?thide=category'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c4' scope='col'>Retention period<div class='commands'><a title='Hide Retention period' aria-expanded='true' aria-controls='expired-contexts-table_r0_c4 expired-contexts-table_r1_c4 expired-contexts-table_r2_c4 expired-contexts-table_r3_c4 expired-contexts-table_r4_c4 expired-contexts-table_r5_c4 expired-contexts-table_r6_c4 expired-contexts-table_r7_c4 expired-contexts-table_r8_c4 expired-contexts-table_r9_c4 expired-contexts-table_r10_c4 expired-contexts-table_r11_c4 expired-contexts-table_r12_c4 expired-contexts-table_r13_c4 expired-contexts-table_r14_c4 expired-contexts-table_r15_c4 expired-contexts-table_r16_c4 expired-contexts-table_r17_c4 expired-contexts-table_r18_c4 expired-contexts-table_r19_c4'    href='#?thide=retentionperiod'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c5' scope='col'><a href='#?tsort=timecreated'>Expiry<span class='accesshide '>Sort by Expiry Ascending</span></a>  <i class='icon fa fa-sort-asc fa-fw ' aria-hidden='true' title='Ascending'aria-label='Ascending'></i><div class='commands'><a title='Hide Expiry' aria-expanded='true' aria-controls='expired-contexts-table_r0_c5 expired-contexts-table_r1_c5 expired-contexts-table_r2_c5 expired-contexts-table_r3_c5 expired-contexts-table_r4_c5 expired-contexts-table_r5_c5 expired-contexts-table_r6_c5 expired-contexts-table_r7_c5 expired-contexts-table_r8_c5 expired-contexts-table_r9_c5 expired-contexts-table_r10_c5 expired-contexts-table_r11_c5 expired-contexts-table_r12_c5 expired-contexts-table_r13_c5 expired-contexts-table_r14_c5 expired-contexts-table_r15_c5 expired-contexts-table_r16_c5 expired-contexts-table_r17_c5 expired-contexts-table_r18_c5 expired-contexts-table_r19_c5'    href='#?thide=timecreated'><i class='icon fa fa-minus fa-fw ' aria-hidden='true' title='Hide' aria-label='Hide'></i></a></div>                    </th>                    <th class='header c6' scope='col'><input title='Select all' type='checkbox' value='1' name='selectall' checked='checked'><div class='commands'></div>                    </th>                </tr>            </thead>            <tbody>                <tr class='' id='expired-contexts-table_r0'>                    <td class='cell c0' id='expired-contexts-table_r0_c0'><span class='m-r-1'>Miscellaneous / TC 1</span><i class='icon fa fa-info fa-fw ' aria-hidden='true' title='Miscellaneous / System' aria-label='Miscellaneous / System'></i></td>                    <td class='cell c1' id='expired-contexts-table_r0_c1'><span class='m-r-1'>7 children</span><i class='icon fa fa-info fa-fw ' aria-hidden='true' title='Test book, Glossary 1, Assignment 1, Page 1, Small files, Big file 0, Forum' aria-label='Test book, Glossary 1, Assignment 1, Page 1, Small files, Big file 0, Forum'></i></td>                    <td class='cell c2' id='expired-contexts-table_r0_c2'>Default purpose</td>                    <td class='cell c3' id='expired-contexts-table_r0_c3'>Default category</td>                    <td class='cell c4' id='expired-contexts-table_r0_c4'>1 days</td>                    <td class='cell c5' id='expired-contexts-table_r0_c5'>Thursday, 5 April 2018, 10:29 AM</td>                    <td class='cell c6' id='expired-contexts-table_r0_c6'><input type='checkbox' class='usercheckbox' name='expiredcontext_3' checked='true'></td></tr></tbody></table>"
+        "expiredcontexts": "<table class='table'><tbody><tr><td>This is the table that will contain the list of expired contexts</td></tr></tbody></table>"
     }
 }}
 <div class="container-fluid" data-region="data-deletion">
index 259c59c..caadac1 100644 (file)
@@ -55,7 +55,7 @@
             <h3 id="{{plugin_type_raw}}">{{#pix}}t/collapsed, moodle, {{#str}}expandplugintype, tool_dataprivacy{{/str}}{{/pix}}{{plugin_type}}</h3>
             </a>
         </div>
-        <div class="hide p-b-1" data-plugintarget="{{plugin_type_raw}}" aria-expanded="false">
+        <div class="hide p-b-1" data-plugintarget="{{plugin_type_raw}}" aria-expanded="false" role="contentinfo">
             {{#plugins}}
                 {{> tool_dataprivacy/component_status}}
             {{/plugins}}
index 8875b6c..1c993b2 100644 (file)
         "datarequestsurl": "#"
     }
 }}
-<style>
-    table, th, td {
-        border: 1px solid black;
-        padding: 0.5em;
-    }
-</style>
-<div>
-    <p>{{#str}}emailsalutation, tool_dataprivacy, {{dponame}}{{/str}}</p>
-    <p>{{#str}}requestemailintro, tool_dataprivacy{{/str}}</p>
-    <table>
-        <tr>
-            <th scope="row">
-                {{#str}}requesttype, tool_dataprivacy{{/str}}
-            </th>
-            <td>
-                {{requesttype}}
-            </td>
-        </tr>
-        <tr>
-            <th scope="row">
-                {{#str}}requestfor, tool_dataprivacy{{/str}}
-            </th>
-            <td>
-                {{requestfor}}
-            </td>
-        </tr>
-        {{^forself}}
-        <tr>
-            <th scope="row">
-                {{#str}}requestby, tool_dataprivacy{{/str}}
-            </th>
-            <td>
-                {{requestedby}}
-            </td>
-        </tr>
-        {{/forself}}
-        <tr>
-            <th scope="row">
-                {{#str}}requestcomments, tool_dataprivacy{{/str}}
-            </th>
-            <td>
-                {{{requestcomments}}}
-            </td>
-        </tr>
-        <tr>
-            <th scope="row">
-                {{#str}}daterequested, tool_dataprivacy{{/str}}
-            </th>
-            <td>
-                {{requestdate}}
-            </td>
-        </tr>
-    </table>
-    <hr>
-    <a href="{{datarequestsurl}}">{{#str}}viewrequest, tool_dataprivacy{{/str}}</a>
-</div>
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <style>
+        table, th, td {
+            border: 1px solid black;
+            padding: 0.5em;
+        }
+    </style>
+    <title>{{#str}}datarequestemailsubject, tool_dataprivacy, {{requesttype}}{{/str}}</title>
+</head>
+<body>
+    <div>
+        <p>{{#str}}emailsalutation, tool_dataprivacy, {{dponame}}{{/str}}</p>
+        <p>{{#str}}requestemailintro, tool_dataprivacy{{/str}}</p>
+        <table>
+            <tr>
+                <th scope="row">
+                    {{#str}}requesttype, tool_dataprivacy{{/str}}
+                </th>
+                <td>
+                    {{requesttype}}
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    {{#str}}requestfor, tool_dataprivacy{{/str}}
+                </th>
+                <td>
+                    {{requestfor}}
+                </td>
+            </tr>
+            {{^forself}}
+                <tr>
+                    <th scope="row">
+                        {{#str}}requestby, tool_dataprivacy{{/str}}
+                    </th>
+                    <td>
+                        {{requestedby}}
+                    </td>
+                </tr>
+            {{/forself}}
+            <tr>
+                <th scope="row">
+                    {{#str}}requestcomments, tool_dataprivacy{{/str}}
+                </th>
+                <td>
+                    {{{requestcomments}}}
+                </td>
+            </tr>
+            <tr>
+                <th scope="row">
+                    {{#str}}daterequested, tool_dataprivacy{{/str}}
+                </th>
+                <td>
+                    {{requestdate}}
+                </td>
+            </tr>
+        </table>
+        <hr>
+        <a href="{{datarequestsurl}}">{{#str}}viewrequest, tool_dataprivacy{{/str}}</a>
+    </div>
+</body>
+</html>
index b61bce1..cae5022 100644 (file)
@@ -30,6 +30,7 @@
 
     Example context (json):
     {
+        "title": "Data request modal title"
     }
 }}
 {{< core/modal }}
index 1e434a7..d480e57 100644 (file)
@@ -142,7 +142,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         global $DB;
 
         $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
-        $purpose2 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'P1000Y', 'lawfulbases' => 'gdpr_art_6_1_b']);
+        $purpose2 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'P1Y', 'lawfulbases' => 'gdpr_art_6_1_b']);
         $cat = api::create_category((object)['name' => 'a']);
 
         $record = (object)[
@@ -165,8 +165,12 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
 
         $course1 = $this->getDataGenerator()->create_course();
 
-        // Old course.
-        $course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => '2']);
+        // Course finished last week (so purpose1 retention period does delete stuff but purpose2 retention period does not).
+        $dt = new \DateTime();
+        $di = new \DateInterval('P7D');
+        $dt->sub($di);
+
+        $course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => $dt->getTimestamp()]);
         $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
         $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
 
index 5c2e7b9..32f6fea 100644 (file)
@@ -124,7 +124,7 @@ class edit_table extends XMLDBAction {
         $o.= '    <input type="hidden" name ="action" value="edit_table_save" />';
         $o.= '    <input type="hidden" name ="sesskey" value="' . sesskey() .'" />';
         $o.= '    <input type="hidden" name ="postaction" value="edit_table" />';
-        $o.= '    <table id="formelements" class="boxaligncenter">';
+        $o .= '    <table id="formelements">';
         // If the table is being used, we cannot rename it
         if ($structure->getTableUses($table->getName())) {
             $o.= '      <tr valign="top"><td>Name:</td><td><input type="hidden" name ="name" value="' . s($table->getName()) . '" />' . s($table->getName()) .'</td></tr>';
@@ -243,7 +243,7 @@ class edit_table extends XMLDBAction {
                 // The readable info
                 $r = '</td><td class="readableinfo cell">' . $field->readableInfo() . '</td>';
                 // Print table row
-                $o .= '<tr class="r' . $row . '"><td class="table cell">' . $f . $b . $r . '</tr>';
+                $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $f . $b . $r . '</tr>';
                 $row = ($row + 1) % 2;
             }
             $o .= '</table>';
@@ -296,7 +296,7 @@ class edit_table extends XMLDBAction {
                 // The readable info
                 $r = '</td><td class="readableinfo cell">' . $key->readableInfo() . '</td>';
                 // Print table row
-            $o .= '<tr class="r' . $row . '"><td class="table cell">' . $k . $b . $r .'</tr>';
+                $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $k . $b . $r .'</tr>';
                 $row = ($row + 1) % 2;
             }
             $o .= '</table>';
@@ -337,7 +337,7 @@ class edit_table extends XMLDBAction {
                 // The readable info
                 $r = '</td><td class="readableinfo cell">' . $index->readableInfo() . '</td>';
                 // Print table row
-            $o .= '<tr class="r' . $row . '"><td class="table cell">' . $i . $b . $r .'</tr>';
+                $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $i . $b . $r .'</tr>';
                 $row = ($row + 1) % 2;
             }
             $o .= '</table>';
index 1fdd4cb..f053c08 100644 (file)
@@ -115,7 +115,7 @@ class edit_xml_file extends XMLDBAction {
                 $o.= '    <input type="hidden" name ="path" value="' . s($structure->getPath()) .'" />';
                 $o.= '    <input type="hidden" name ="version" value="' . s($structure->getVersion()) .'" />';
                 $o.= '    <input type="hidden" name ="sesskey" value="' . sesskey() .'" />';
-                $o.= '    <table id="formelements" class="boxaligncenter">';
+                $o .= '    <table id="formelements">';
                 $o.= '      <tr valign="top"><td>Path:</td><td>' . s($structure->getPath()) . '</td></tr>';
                 $o.= '      <tr valign="top"><td>Version:</td><td>' . s($structure->getVersion()) . '</td></tr>';
                 $o.= '      <tr valign="top"><td><label for="comment" accesskey="c">Comment:</label></td><td><textarea name="comment" rows="3" cols="80" id="comment">' . $structure->getComment() . '</textarea></td></tr>';
@@ -216,7 +216,7 @@ class edit_xml_file extends XMLDBAction {
                          }
                         $b .= '</td>';
                         // Print table row
-                        $o .= '<tr class="r' . $row . '"><td class="table cell">' . $t . $b . '</tr>';
+                        $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $t . $b . '</tr>';
                         $row = ($row + 1) % 2;
                     }
                     $o .= '</table>';
index e975201..d337572 100644 (file)
@@ -162,7 +162,7 @@ class view_table_php extends XMLDBAction {
         $o.= '    <input type="hidden" name ="dir" value="' . str_replace($CFG->dirroot, '', $dirpath) . '" />';
         $o.= '    <input type="hidden" name ="table" value="' . s($tableparam) . '" />';
         $o.= '    <input type="hidden" name ="action" value="view_table_php" />';
-        $o.= '    <table id="formelements" class="boxaligncenter" cellpadding="5">';
+        $o .= '    <table id="formelements" cellpadding="5">';
         $o.= '      <tr><td><label for="menucommand" accesskey="c">' . $this->str['selectaction'] .' </label>' . html_writer::select($popcommands, 'command', $commandparam, false) . '&nbsp;<label for="menufieldkeyindex" accesskey="f">' . $this->str['selectfieldkeyindex'] . ' </label>' .html_writer::select($popfields, 'fieldkeyindex', $origfieldkeyindexparam, false) . '</td></tr>';
         $o.= '      <tr><td colspan="2" align="center"><input type="submit" value="' .$this->str['view'] . '" /></td></tr>';
         $o.= '    </table>';
index d579bf6..5f5b3ff 100644 (file)
@@ -32,6 +32,22 @@ defined('MOODLE_INTERNAL') || die();
 
 class filter_emoticon extends moodle_text_filter {
 
+    /**
+     * Internal cache used for replacing. Multidimensional array;
+     * - dimension 1: language,
+     * - dimension 2: theme.
+     * @var array
+     */
+    protected static $emoticontexts = array();
+
+    /**
+     * Internal cache used for replacing. Multidimensional array;
+     * - dimension 1: language,
+     * - dimension 2: theme.
+     * @var array
+     */
+    protected static $emoticonimgs = array();
+
     /**
      * Apply the filter to the text
      *
@@ -49,7 +65,7 @@ class filter_emoticon extends moodle_text_filter {
             return $text;
         }
         if (in_array($options['originalformat'], explode(',', get_config('filter_emoticon', 'formats')))) {
-            $this->replace_emoticons($text);
+            return $this->replace_emoticons($text);
         }
         return $text;
     }
@@ -62,51 +78,73 @@ class filter_emoticon extends moodle_text_filter {
      * Replace emoticons found in the text with their images
      *
      * @param string $text to modify
-     * @return void
+     * @return string the modified result
      */
-    protected function replace_emoticons(&$text) {
+    protected function replace_emoticons($text) {
         global $CFG, $OUTPUT, $PAGE;
-        static $emoticontexts = array();    // internal cache used for replacing
-        static $emoticonimgs = array();     // internal cache used for replacing
 
         $lang = current_language();
         $theme = $PAGE->theme->name;
 
-        if (!isset($emoticontexts[$lang][$theme]) or !isset($emoticonimgs[$lang][$theme])) {
+        if (!isset(self::$emoticontexts[$lang][$theme]) or !isset(self::$emoticonimgs[$lang][$theme])) {
             // prepare internal caches
             $manager = get_emoticon_manager();
             $emoticons = $manager->get_emoticons();
-            $emoticontexts[$lang][$theme] = array();
-            $emoticonimgs[$lang][$theme] = array();
+            self::$emoticontexts[$lang][$theme] = array();
+            self::$emoticonimgs[$lang][$theme] = array();
             foreach ($emoticons as $emoticon) {
-                $emoticontexts[$lang][$theme][] = $emoticon->text;
-                $emoticonimgs[$lang][$theme][] = $OUTPUT->render($manager->prepare_renderable_emoticon($emoticon));
+                self::$emoticontexts[$lang][$theme][] = $emoticon->text;
+                self::$emoticonimgs[$lang][$theme][] = $OUTPUT->render($manager->prepare_renderable_emoticon($emoticon));
             }
             unset($emoticons);
         }
 
-        if (empty($emoticontexts[$lang][$theme])) { // No emoticons defined, nothing to process here
-            return;
+        if (empty(self::$emoticontexts[$lang][$theme])) { // No emoticons defined, nothing to process here.
+            return $text;
         }
 
-        // detect all the <script> zones to take out
-        $excludes = array();
-        preg_match_all('/<script language(.+?)<\/script>/is', $text, $listofexcludes);
+        // Detect all zones that we should not handle (including the nested tags).
+        $processing = preg_split('/(<\/?(?:span|script)[^>]*>)/is', $text, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
 
-        // take out all the <script> zones from text
-        foreach (array_unique($listofexcludes[0]) as $key => $value) {
-            $excludes['<+'.$key.'+>'] = $value;
-        }
-        if ($excludes) {
-            $text = str_replace($excludes, array_keys($excludes), $text);
-        }
+        // Initialize the results.
+        $resulthtml = "";
+        $exclude = 0;
 
-        // this is the meat of the code - this is run every time
-        $text = str_replace($emoticontexts[$lang][$theme], $emoticonimgs[$lang][$theme], $text);
+        // Define the patterns that mark the start of the forbidden zones.
+        $excludepattern = array('/^<script/is', '/^<span[^>]+class="nolink[^"]*"/is');
 
-        // Recover all the <script> zones to text
-        if ($excludes) {
-            $text = str_replace(array_keys($excludes), $excludes, $text);
+        // Loop through the fragments.
+        foreach ($processing as $fragment) {
+            // If we are not ignoring, we MUST test if we should.
+            if ($exclude == 0) {
+                foreach ($excludepattern as $exp) {
+                    if (preg_match($exp, $fragment)) {
+                        $exclude = $exclude + 1;
+                        break;
+                    }
+                }
+            }
+            if ($exclude > 0) {
+                // If we are ignoring the fragment, then we must check if we may have reached the end of the zone.
+                if (strpos($fragment, '</span') !== false || strpos($fragment, '</script') !== false) {
+                    $exclude -= 1;
+                    // This is needed because of a double increment at the first element.
+                    if ($exclude == 1) {
+                        $exclude -= 1;
+                    }
+                } else if (strpos($fragment, '<span') !== false || strpos($fragment, '<script') !== false) {
+                    // If we find a nested tag we increase the exclusion level.
+                    $exclude = $exclude + 1;
+                }
+            } else if (strpos($fragment, '<span') === false ||
+                       strpos($fragment, '</span') === false) {
+                // This is the meat of the code - this is run every time.
+                // This code only runs for fragments that are not ignored (including the tags themselves).
+                $fragment = str_replace(self::$emoticontexts[$lang][$theme], self::$emoticonimgs[$lang][$theme], $fragment);
+            }
+            $resulthtml .= $fragment;
         }
+
+        return $resulthtml;
     }
 }
index da3576a..10ee76e 100644 (file)
@@ -34,29 +34,124 @@ require_once($CFG->dirroot . '/filter/emoticon/filter.php'); // Include the code
 class filter_emoticon_testcase extends advanced_testcase {
 
     /**
-     * Verify configured target formats are observed. Just that.
+     * Tests the filter doesn't affect nolink classes.
+     *
+     * @dataProvider filter_emoticon_provider
      */
-    public function test_filter_emoticon_formats() {
-
-        $this->resetAfterTest(true); // We are modifying the config.
+    public function test_filter_emoticon($input, $format, $expected) {
+        $this->resetAfterTest();
 
         $filter = new testable_filter_emoticon();
+        $this->assertEquals($expected, $filter->filter($input, [
+                'originalformat' => $format,
+            ]));
+    }
+
+    /**
+     * The data provider for filter emoticon tests.
+     *
+     * @return  array
+     */
+    public function filter_emoticon_provider() {
+        $grr = '(grr)';
+        return [
+            'FORMAT_MOODLE is not filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_MOODLE,
+                'expected' => $grr,
+            ],
+            'FORMAT_MARKDOWN is not filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_MARKDOWN,
+                'expected' => $grr,
+            ],
+            'FORMAT_PLAIN is not filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_PLAIN,
+                'expected' => $grr,
+            ],
+            'FORMAT_HTML is filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_HTML,
+                'expected' => $this->get_converted_content_for_emoticon($grr),
+            ],
+            'Script tag should not be processed' => [
+                'input' => "<script language='javascript'>alert('{$grr}');</script>",
+                'format' => FORMAT_HTML,
+                'expected' => "<script language='javascript'>alert('{$grr}');</script>",
+            ],
+            'Basic nolink should not be processed' => [
+                'input' => '<span class="nolink">(n)</span>',
+                'format' => FORMAT_HTML,
+                'expected' => '<span class="nolink">(n)</span>',
+            ],
+            'Nested nolink should not be processed' => [
+                'input' => '<span class="nolink"><span>(n)</span>(n)</span>',
+                'format' => FORMAT_HTML,
+                'expected' => '<span class="nolink"><span>(n)</span>(n)</span>',
+            ],
+            'Nested nolink should not be processed but following emoticon' => [
+                'input' => '<span class="nolink"><span>(n)</span>(n)</span>(n)',
+                'format' => FORMAT_HTML,
+                'expected' => '<span class="nolink"><span>(n)</span>(n)</span>' . $this->get_converted_content_for_emoticon('(n)'),
+            ],
+        ];
+    }
+
+    /**
+     * Translate the text for a single emoticon into the rendered value.
+     *
+     * @param   string  $text The text to translate.
+     * @return  string
+     */
+    public function get_converted_content_for_emoticon($text) {
+        global $OUTPUT;
+        $manager = get_emoticon_manager();
+        $emoticons = $manager->get_emoticons();
+        foreach ($emoticons as $emoticon) {
+            if ($emoticon->text == $text) {
+                return $OUTPUT->render($manager->prepare_renderable_emoticon($emoticon));
+            }
+        }
+
+        return $text;
+    }
 
-        // Verify texts not matching target formats aren't filtered.
+    /**
+     * Tests the filter doesn't break anything if activated but invalid format passed.
+     *
+     */
+    public function test_filter_invalidformat() {
+        global $PAGE;
+        $this->resetAfterTest();
+
+        $filter = new testable_filter_emoticon();
+        $input = '(grr)';
         $expected = '(grr)';
-        $options = array('originalformat' => FORMAT_MOODLE); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
 
-        $options = array('originalformat' => FORMAT_MARKDOWN); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+        $this->assertEquals($expected, $filter->filter($input, [
+            'originalformat' => 'ILLEGALFORMAT',
+        ]));
+    }
+
+    /**
+     * Tests the filter doesn't break anything if activated but no emoticons available.
+     *
+     */
+    public function test_filter_emptyemoticons() {
+        global $CFG;
+        $this->resetAfterTest();
+        // Empty the emoticons array.
+        $CFG->emoticons = null;
 
-        $options = array('originalformat' => FORMAT_PLAIN); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+        $filter = new filter_emoticon(context_system::instance(), array('originalformat' => FORMAT_HTML));
+
+        $input = '(grr)';
+        $expected = '(grr)';
 
-        // And texts matching target formats are filtered.
-        $expected = '<img class="icon emoticon" alt="angry" title="angry" src="https://www.example.com/moodle/theme/image.php/_s/boost/core/1/s/angry" />';
-        $options = array('originalformat' => FORMAT_HTML); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+        $this->assertEquals($expected, $filter->filter($input, [
+            'originalformat' => FORMAT_HTML,
+        ]));
     }
 }
 
@@ -65,6 +160,9 @@ class filter_emoticon_testcase extends advanced_testcase {
  */
 class testable_filter_emoticon extends filter_emoticon {
     public function __construct() {
+        // Reset static emoticon caches.
+        parent::$emoticontexts = array();
+        parent::$emoticonimgs = array();
         // Use this context for filtering.
         $this->context = context_system::instance();
         // Define FORMAT_HTML as only one filtering in DB.
index 0103646..880a8c5 100644 (file)
@@ -529,6 +529,7 @@ $string['environmentrequireversion'] = 'version {$a->needed} is required and you
 $string['environmentsettingok'] = 'recommended setting detected';
 $string['environmentshouldfixsetting'] = 'PHP setting should be changed.';
 $string['environmentxmlerror'] = 'Error reading environment data ({$a->error_code})';
+$string['environmentmariadbwrongdbtype'] = 'Wrong <code>$CFG->dbtype</code>: you need to change it in your <code>config.php</code> file, from \'<code>mysql</code>\' to \'<code>mariadb</code>\'.';
 $string['errordeletingconfig'] = 'An error occurred while deleting the configuration records for plugin \'{$a}\'.';
 $string['errorsetting'] = 'Could not save setting:';
 $string['errorwithsettings'] = 'Some settings were not changed due to an error.';
index db7c5d6..09917b6 100644 (file)
@@ -75,6 +75,8 @@ $string['checkdir'] = 'Check dir';
 $string['checkdiradvice'] = 'Ensure the data directory exists and is writable.';
 $string['incourse'] = 'in course {$a}';
 $string['index'] = 'Index';
+$string['indexwhendisabledfullnotice'] = 'Indexing is currently not permitted when search is disabled. To enable this, please see the <a href="{$a->url}">searchindexwhendisabled</a> setting.';
+$string['indexwhendisabledshortnotice'] = 'Indexing is not available.';
 $string['invalidindexerror'] = 'Index directory either contains an invalid index, or nothing at all.';
 $string['ittook'] = 'It took';
 $string['matchingfile'] = 'Matched from file <span class="filename">{$a}</span>';
index fc93ae5..536504c 100644 (file)
@@ -286,7 +286,11 @@ function get_role_definitions(array $roleids) {
     // Grab all keys we have not yet got in our static cache.
     if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
         $cache = cache::make('core', 'roledefs');
-        $ACCESSLIB_PRIVATE->cacheroledefs += array_filter($cache->get_many($uncached));
+        foreach ($cache->get_many($uncached) as $roleid => $cachedroledef) {
+            if (is_array($cachedroledef)) {
+                $ACCESSLIB_PRIVATE->cacheroledefs[$roleid] = $cachedroledef;
+            }
+        }
 
         // Check we have the remaining keys from the MUC.
         if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
@@ -313,20 +317,25 @@ function get_role_definitions_uncached(array $roleids) {
         return array();
     }
 
-    list($sql, $params) = $DB->get_in_or_equal($roleids);
+    // Create a blank results array: even if a role has no capabilities,
+    // we need to ensure it is included in the results to show we have
+    // loaded all the capabilities that there are.
     $rdefs = array();
+    foreach ($roleids as $roleid) {
+        $rdefs[$roleid] = array();
+    }
 
+    // Load all the capabilities for these roles in all contexts.
+    list($sql, $params) = $DB->get_in_or_equal($roleids);
     $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
               FROM {role_capabilities} rc
               JOIN {context} ctx ON rc.contextid = ctx.id
              WHERE rc.roleid $sql";
     $rs = $DB->get_recordset_sql($sql, $params);
 
+    // Store the capabilities into the expected data structure.
     foreach ($rs as $rd) {
         if (!isset($rdefs[$rd->roleid][$rd->path])) {
-            if (!isset($rdefs[$rd->roleid])) {
-                $rdefs[$rd->roleid] = array();
-            }
             $rdefs[$rd->roleid][$rd->path] = array();
         }
         $rdefs[$rd->roleid][$rd->path][$rd->capability] = (int) $rd->permission;
index 270d79c..51ea48d 100644 (file)
@@ -1048,6 +1048,19 @@ function environment_check_database($version, $env_select) {
         return $result;
     }
 
+    // Check if the DB Vendor has been properly configured.
+    // Hack: this is required when playing with MySQL and MariaDB since they share the same PHP module and base DB classes,
+    // whilst they are slowly evolving using separate directions though MariaDB is still an "almost" drop-in replacement.
+    $dbvendorismysql = ($current_vendor === 'mysql');
+    $dbtypeismariadb = (stripos($dbinfo['description'], 'mariadb') !== false);
+    if ($dbvendorismysql && $dbtypeismariadb) {
+        $result->setStatus(false);
+        $result->setLevel($level);
+        $result->setInfo($current_vendor . ' (' . $dbinfo['description'] . ')');
+        $result->setFeedbackStr('environmentmariadbwrongdbtype');
+        return $result;
+    }
+
 /// And finally compare them, saving results
     if (version_compare($current_version, $needed_version, '>=')) {
         $result->setStatus(true);
index af8a77d..d8875b5 100644 (file)
@@ -89,7 +89,8 @@ LICENSE
 /**
  * This class was heavily modified in order to get usefull spreadsheet emulation ;-)
  * skodak
- *
+ * This class was modified to allow comparison operators (<, <=, ==, >=, >)
+ * and synonyms functions (for the 'if' function). See MDL-14274 for more details.
  */
 
 class EvalMath {
@@ -113,7 +114,8 @@ class EvalMath {
         'average'=>array(-1), 'max'=>array(-1),  'min'=>array(-1),
         'mod'=>array(2),      'pi'=>array(0),    'power'=>array(2),
         'round'=>array(1, 2), 'sum'=>array(-1), 'rand_int'=>array(2),
-        'rand_float'=>array(0));
+        'rand_float'=>array(0), 'ifthenelse'=>array(3));
+    var $fcsynonyms = array('if' => 'ifthenelse');
 
     var $allowimplicitmultiplication;
 
@@ -207,20 +209,25 @@ class EvalMath {
         $stack = new EvalMathStack;
         $output = array(); // postfix form of expression, to be passed to pfx()
         $expr = trim(strtolower($expr));
-
-        $ops   = array('+', '-', '*', '/', '^', '_');
+        // MDL-14274: new operators for comparison added.
+        $ops   = array('+', '-', '*', '/', '^', '_', '>', '<', '<=', '>=', '==');
         $ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?
-        $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence
+        $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2, '>'=>3, '<'=>3, '<='=>3, '>='=>3, '=='=>3); // operator precedence
 
         $expecting_op = false; // we use this in syntax-checking the expression
                                // and determining when a - is a negation
 
-        if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
+        if (preg_match("/[^\w\s+*^\/()\.,-<>=]/", $expr, $matches)) { // make sure the characters are all good
             return $this->trigger(get_string('illegalcharactergeneral', 'mathslib', $matches[0]));
         }
 
         while(1) { // 1 Infinite Loop ;)
-            $op = substr($expr, $index, 1); // get the first character at the current index
+            // MDL-14274 Test two character operators.
+            $op = substr($expr, $index, 2);
+            if (!in_array($op, $ops)) {
+                // MDL-14274 Get one character operator.
+                $op = substr($expr, $index, 1); // get the first character at the current index
+            }
             // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
             $ex = preg_match('/^('.self::$namepat.'\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr($expr, $index), $match);
             //===============
@@ -245,7 +252,7 @@ class EvalMath {
                 }
                 // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
                 $stack->push($op); // finally put OUR operator onto the stack
-                $index++;
+                $index += strlen($op);
                 $expecting_op = false;
             //===============
             } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
@@ -265,7 +272,9 @@ class EvalMath {
                             $a->given = $arg_count;
                             return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
                         }
-                    } elseif (array_key_exists($fnn, $this->fc)) {
+                    } elseif ($this->get_native_function_name($fnn)) {
+                        $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
+
                         $counts = $this->fc[$fnn];
                         if (in_array(-1, $counts) and $arg_count > 0) {}
                         elseif (!in_array($arg_count, $counts)) {
@@ -309,7 +318,9 @@ class EvalMath {
                 $expecting_op = true;
                 $val = $match[1];
                 if (preg_match('/^('.self::$namepat.')\($/', $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
-                    if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f) or array_key_exists($matches[1], $this->fc)) { // it's a func
+                    if (in_array($matches[1], $this->fb) or
+                                array_key_exists($matches[1], $this->f) or
+                                $this->get_native_function_name($matches[1])){ // it's a func
                         $stack->push($val);
                         $stack->push(1);
                         $stack->push('(');
@@ -331,6 +342,7 @@ class EvalMath {
                     $stack->pop();// 1
                     $fn = $stack->pop();
                     $fnn = $matches[1]; // get the function name
+                    $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
                     $counts = $this->fc[$fnn];
                     if (!in_array(0, $counts)){
                         $a= new stdClass();
@@ -368,7 +380,20 @@ class EvalMath {
         }
         return $output;
     }
-
+    /**
+     *
+     * @param string $fnn
+     * @return string|boolean false if function name unknown.
+     */
+    function get_native_function_name($fnn) {
+        if (array_key_exists($fnn, $this->fcsynonyms)) {
+            return $this->fcsynonyms[$fnn];
+        } else if (array_key_exists($fnn, $this->fc)) {
+            return $fnn;
+        } else {
+            return false;
+        }
+    }
     // evaluate postfix notation
     function pfx($tokens, $vars = array()) {
 
@@ -387,7 +412,8 @@ class EvalMath {
                     $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
                     if ($fnn == 'ln') $fnn = 'log';
                     eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval()
-                } elseif (array_key_exists($fnn, $this->fc)) { // calc emulation function
+                } elseif ($this->get_native_function_name($fnn)) { // calc emulation function
+                    $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
                     // get args
                     $args = array();
                     for ($i = $count-1; $i >= 0; $i--) {
@@ -407,7 +433,7 @@ class EvalMath {
                     $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
                 }
             // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
-            } elseif (in_array($token, array('+', '-', '*', '/', '^'), true)) {
+            } elseif (in_array($token, array('+', '-', '*', '/', '^', '>', '<', '==', '<=', '>='), true)) {
                 if (is_null($op2 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
                 if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
                 switch ($token) {
@@ -422,6 +448,16 @@ class EvalMath {
                         $stack->push($op1/$op2); break;
                     case '^':
                         $stack->push(pow($op1, $op2)); break;
+                    case '>':
+                        $stack->push((int)($op1 > $op2)); break;
+                    case '<':
+                        $stack->push((int)($op1 < $op2)); break;
+                    case '==':
+                        $stack->push((int)($op1 == $op2)); break;
+                    case '<=':
+                        $stack->push((int)($op1 <= $op2)); break;
+                    case '>=':
+                        $stack->push((int)($op1 >= $op2)); break;
                 }
             // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
             } elseif ($token == "_") {
@@ -483,7 +519,21 @@ class EvalMathStack {
 
 // spreadsheet functions emulation
 class EvalMathFuncs {
-
+    /**
+     * MDL-14274 new conditional function.
+     * @param boolean $condition boolean for conditional.
+     * @param variant $then value if condition is true.
+     * @param unknown $else value if condition is false.
+     * @author Juan Pablo de Castro <juan.pablo.de.castro@gmail.com>
+     * @return unknown
+     */
+    static function ifthenelse($condition, $then, $else) {
+        if ($condition == true) {
+            return $then;
+        } else {
+            return $else;
+        }
+    }
     static function average() {
         $args = func_get_args();
         return (call_user_func_array(array('self', 'sum'), $args) / count($args));
index e9b1a80..c9935c5 100644 (file)
@@ -18,3 +18,7 @@ To see all changes diff against version 1.1, available from:
 http://www.phpclasses.org/browse/package/2695.html
 
 skodak, Tim Hunt
+
+Changes by Juan Pablo de Castro (MDL-14274):
+* operators >,<,>=,<=,== added.
+* function if[thenelse](condition, true_value, false_value)
index ff38252..ef612b4 100644 (file)
@@ -116,7 +116,7 @@ class MoodleExcelWorkbook {
             header('Pragma: no-cache');
         }
 
-        if (core_useragent::is_ie()) {
+        if (core_useragent::is_ie() || core_useragent::is_edge()) {
             $filename = rawurlencode($filename);
         } else {
             $filename = s($filename);
index 53f07db..a8c844a 100644 (file)
@@ -2115,7 +2115,7 @@ function send_temp_file($path, $filename, $pathisstring=false) {
     }
 
     // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
-    if (core_useragent::is_ie()) {
+    if (core_useragent::is_ie() || core_useragent::is_edge()) {
         $filename = urlencode($filename);
     }
 
@@ -2264,7 +2264,7 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring
     }
 
     // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
-    if (core_useragent::is_ie()) {
+    if (core_useragent::is_ie() || core_useragent::is_edge()) {
         $filename = rawurlencode($filename);
     }
 
index aa2e866..8753695 100644 (file)
@@ -56,10 +56,7 @@ class calc_formula {
             return;
         }
         $formula = substr($formula, 1);
-        if (strpos($formula, '=') !== false) {
-            $this->_error = "too many '='";
-            return;
-        }
+
         $this->_nfx = $this->_em->nfx($formula);
         if ($this->_nfx == false) {
             $this->_error = $this->_em->last_error;
index 514e002..3ff6192 100644 (file)
@@ -1805,6 +1805,74 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertFalse(has_all_capabilities($sca, $coursecontext, 0));
     }
 
+    /**
+     * Test that the caching in get_role_definitions() and get_role_definitions_uncached()
+     * works as intended.
+     */
+    public function test_role_definition_caching() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Get some role ids.
+        $authenticatedrole = $DB->get_record('role', array('shortname' => 'user'), '*', MUST_EXIST);
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+        $emptyroleid = create_role('No capabilities', 'empty', 'A role with no capabilties');
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        // Instantiate the cache instance, since that does DB queries (get_config)
+        // and we don't care about those.
+        cache::make('core', 'roledefs');
+
+        // One database query is not necessarily one database read, it seems. Find out how many.
+        $startdbreads = $DB->perf_get_reads();
+        $rs = $DB->get_recordset('user');
+        $rs->close();
+        $readsperquery = $DB->perf_get_reads() - $startdbreads;
+
+        // Now load some role definitions, and check when it queries the database.
+
+        // Load the capabilities for two roles. Should be one query.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id]);
+        $this->assertEquals(1 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+
+        // Load the capabilities for same two roles. Should not query the DB.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id]);
+        $this->assertEquals(0 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+
+        // Include a third role. Should do one DB query.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id, $emptyroleid]);
+        $this->assertEquals(1 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+
+        // Repeat call. No DB queries.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id, $emptyroleid]);
+        $this->assertEquals(0 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+
+        // Alter a role.
+        role_change_permission($studentrole->id, $coursecontext, 'moodle/course:tag', CAP_ALLOW);
+
+        // Should now know to do one query.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id]);
+        $this->assertEquals(1 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+
+        // Now clear the in-memory cache, and verify that it does not query the DB.
+        // Cannot use accesslib_clear_all_caches_for_unit_testing since that also
+        // clears the MUC cache.
+        global $ACCESSLIB_PRIVATE;
+        $ACCESSLIB_PRIVATE->cacheroledefs = array();
+
+        // Get all roles. Should not need the DB.
+        $startdbreads = $DB->perf_get_reads();
+        get_role_definitions([$authenticatedrole->id, $studentrole->id, $emptyroleid]);
+        $this->assertEquals(0 * $readsperquery, $DB->perf_get_reads() - $startdbreads);
+    }
+
     /**
      * Tests get_user_capability_course() which checks a capability across all courses.
      */
index fe3b1d2..6d37bcf 100644 (file)
@@ -81,6 +81,47 @@ class core_mathslib_testcase extends basic_testcase {
         $this->assertSame(8, $formula->evaluate());
     }
 
+    public function test_conditional_functions() {
+        $formula = new calc_formula('=ifthenelse(1,2,3)');
+        $this->assertSame(2, (int)$formula->evaluate());
+
+        $formula = new calc_formula('=ifthenelse(0,2,3)');
+        $this->assertSame(3, (int) $formula->evaluate());
+
+        $formula = new calc_formula('=ifthenelse(2<3,2,3)');
+        $this->assertSame(2, (int) $formula->evaluate());
+
+        // Test synonim if.
+        $formula = new calc_formula('=if(1,2,3)');
+        $this->assertSame(2, (int)$formula->evaluate());
+
+        $formula = new calc_formula('=if(0,2,3)');
+        $this->assertSame(3, (int) $formula->evaluate());
+
+        $formula = new calc_formula('=if(2<3,2,3)');
+        $this->assertSame(2, (int) $formula->evaluate());
+    }
+
+    public function test_conditional_operators() {
+        $formula = new calc_formula('=2==2');
+        $this->assertSame(1, $formula->evaluate());
+
+        $formula = new calc_formula('=2>3');
+        $this->assertSame(0, $formula->evaluate());
+        $formula = new calc_formula('=2<3');
+        $this->assertSame(1, $formula->evaluate());
+
+        $formula = new calc_formula('=(2<=3)');
+        $this->assertSame(1, $formula->evaluate());
+
+        $formula = new calc_formula('=(2<=3)*10');
+        $this->assertSame(10, $formula->evaluate());
+
+        $formula = new calc_formula('=(2>=3)*10');
+        $this->assertSame(0, $formula->evaluate());
+        $formula = new calc_formula('=2<3*10');
+        $this->assertSame(10, $formula->evaluate());
+    }
     /**
      * Tests the min and max functions.
      */
index e8c82d8..72eae49 100644 (file)
@@ -291,14 +291,14 @@ class xmldb_index extends xmldb_object {
         // The fields
         $indexfields = $this->getFields();
         if (!empty($indexfields)) {
-            $result .= 'array(' . "'".  implode("', '", $indexfields) . "')";
+            $result .= "['".  implode("', '", $indexfields) . "']";
         } else {
             $result .= 'null';
         }
         // Hints
         $hints = $this->getHints();
         if (!empty($hints)) {
-            $result .= ', array(' . "'".  implode("', '", $hints) . "')";
+            $result .= ", ['".  implode("', '", $hints) . "']";
         }
 
         // Return result
index 0d47aa0..c27ef71 100644 (file)
@@ -430,7 +430,7 @@ class xmldb_key extends xmldb_object {
         // The fields
         $keyfields = $this->getFields();
         if (!empty($keyfields)) {
-            $result .= 'array(' . "'".  implode("', '", $keyfields) . "')";
+            $result .= "['".  implode("', '", $keyfields) . "']";
         } else {
             $result .= 'null';
         }
@@ -447,7 +447,7 @@ class xmldb_key extends xmldb_object {
             // The reffields
             $reffields = $this->getRefFields();
             if (!empty($reffields)) {
-                $result .= 'array(' . "'".  implode("', '", $reffields) . "')";
+                $result .= "['".  implode("', '", $reffields) . "']";
             } else {
                 $result .= 'null';
             }
index c105fd7..7400a71 100644 (file)
@@ -64,5 +64,7 @@ $string['printheading'] = 'Display page name';
 $string['printheadingexplain'] = 'Display page name above content?';
 $string['printintro'] = 'Display page description';
 $string['printintroexplain'] = 'Display page description above content?';
+$string['printlastmodified'] = 'Display last modified date';
+$string['printlastmodifiedexplain'] = 'Display last modified date below content?';
 $string['privacy:metadata'] = 'The Page resource plugin does not store any personal data.';
 $string['search:activity'] = 'Page';
index 67135ad..3b6f596 100644 (file)
@@ -113,6 +113,7 @@ function page_add_instance($data, $mform = null) {
     }
     $displayoptions['printheading'] = $data->printheading;
     $displayoptions['printintro']   = $data->printintro;
+    $displayoptions['printlastmodified'] = $data->printlastmodified;
     $data->displayoptions = serialize($displayoptions);
 
     if ($mform) {
@@ -162,6 +163,7 @@ function page_update_instance($data, $mform) {
     }
     $displayoptions['printheading'] = $data->printheading;
     $displayoptions['printintro']   = $data->printintro;
+    $displayoptions['printlastmodified'] = $data->printlastmodified;
     $data->displayoptions = serialize($displayoptions);
 
     $data->content       = $data->page['text'];
@@ -495,6 +497,7 @@ function page_dndupload_handle($uploadinfo) {
     $data->popupwidth = $config->popupwidth;
     $data->printheading = $config->printheading;
     $data->printintro = $config->printintro;
+    $data->printlastmodified = $config->printlastmodified;
 
     return page_add_instance($data, null);
 }
index a411f1a..b8ff4ab 100644 (file)
@@ -92,6 +92,8 @@ class mod_page_mod_form extends moodleform_mod {
         $mform->setDefault('printheading', $config->printheading);
         $mform->addElement('advcheckbox', 'printintro', get_string('printintro', 'page'));
         $mform->setDefault('printintro', $config->printintro);
+        $mform->addElement('advcheckbox', 'printlastmodified', get_string('printlastmodified', 'page'));
+        $mform->setDefault('printlastmodified', $config->printlastmodified);
 
         // add legacy files flag only if used
         if (isset($this->current->legacyfiles) and $this->current->legacyfiles != RESOURCELIB_LEGACYFILES_NO) {
@@ -113,26 +115,36 @@ class mod_page_mod_form extends moodleform_mod {
         $mform->setDefault('revision', 1);
     }
 
-    function data_preprocessing(&$default_values) {
+    /**
+     * Enforce defaults here.
+     *
+     * @param array $defaultvalues Form defaults
+     * @return void
+     **/
+    public function data_preprocessing(&$defaultvalues) {
         if ($this->current->instance) {
             $draftitemid = file_get_submitted_draft_itemid('page');
-            $default_values['page']['format'] = $default_values['contentformat'];
-            $default_values['page']['text']   = file_prepare_draft_area($draftitemid, $this->context->id, 'mod_page', 'content', 0, page_get_editor_options($this->context), $default_values['content']);
-            $default_values['page']['itemid'] = $draftitemid;
+            $defaultvalues['page']['format'] = $defaultvalues['contentformat'];
+            $defaultvalues['page']['text']   = file_prepare_draft_area($draftitemid, $this->context->id, 'mod_page',
+                    'content', 0, page_get_editor_options($this->context), $defaultvalues['content']);
+            $defaultvalues['page']['itemid'] = $draftitemid;
         }
-        if (!empty($default_values['displayoptions'])) {
-            $displayoptions = unserialize($default_values['displayoptions']);
+        if (!empty($defaultvalues['displayoptions'])) {
+            $displayoptions = unserialize($defaultvalues['displayoptions']);
             if (isset($displayoptions['printintro'])) {
-                $default_values['printintro'] = $displayoptions['printintro'];
+                $defaultvalues['printintro'] = $displayoptions['printintro'];
             }
             if (isset($displayoptions['printheading'])) {
-                $default_values['printheading'] = $displayoptions['printheading'];
+                $defaultvalues['printheading'] = $displayoptions['printheading'];
+            }
+            if (isset($displayoptions['printlastmodified'])) {
+                $defaultvalues['printlastmodified'] = $displayoptions['printlastmodified'];
             }
             if (!empty($displayoptions['popupwidth'])) {
-                $default_values['popupwidth'] = $displayoptions['popupwidth'];
+                $defaultvalues['popupwidth'] = $displayoptions['popupwidth'];
             }
             if (!empty($displayoptions['popupheight'])) {
-                $default_values['popupheight'] = $displayoptions['popupheight'];
+                $defaultvalues['popupheight'] = $displayoptions['popupheight'];
             }
         }
     }
index 6424204..a1c9e06 100644 (file)
@@ -43,6 +43,8 @@ if ($ADMIN->fulltree) {
         get_string('printheading', 'page'), get_string('printheadingexplain', 'page'), 1));
     $settings->add(new admin_setting_configcheckbox('page/printintro',
         get_string('printintro', 'page'), get_string('printintroexplain', 'page'), 0));
+    $settings->add(new admin_setting_configcheckbox('page/printlastmodified',
+        get_string('printlastmodified', 'page'), get_string('printlastmodifiedexplain', 'page'), 1));
     $settings->add(new admin_setting_configselect('page/display',
         get_string('displayselect', 'page'), get_string('displayselectexplain', 'page'), RESOURCELIB_DISPLAY_OPEN, $displayoptions));
     $settings->add(new admin_setting_configtext('page/popupwidth',
diff --git a/mod/page/tests/behat/page_appearance.feature b/mod/page/tests/behat/page_appearance.feature
new file mode 100644 (file)
index 0000000..efcee45
--- /dev/null
@@ -0,0 +1,62 @@
+@mod @mod_page
+Feature: Configure page appearance
+  In order to change the appearance of the page resource
+  As an admin
+  I need to configure the page appearance settings
+
+  Background:
+    Given the following "courses" exist:
+      | shortname | fullname   |
+      | C1        | Course 1 |
+    And the following "activities" exist:
+      | activity | name       | intro      | course | idnumber |
+      | page     | PageName1  | PageDesc1  | C1     | PAGE1    |
+    And I log in as "admin"
+
+  @javascript
+  Scenario: Hide and display the page name
+    Given I am on "Course 1" course homepage
+    When I follow "PageName1"
+    Then I should see "PageName1" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display page name" "checkbox"
+    And I press "Save and display"
+    Then I should not see "PageName1" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display page name" "checkbox"
+    And I press "Save and display"
+    Then I should see "PageName1" in the "region-main" "region"
+
+  @javascript
+  Scenario: Display and hide the page description
+    Given I am on "Course 1" course homepage
+    When I follow "PageName1"
+    Then I should not see "PageDesc1" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display page description" "checkbox"
+    And I press "Save and display"
+    Then I should see "PageDesc1" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display page description" "checkbox"
+    And I press "Save and display"
+    Then I should not see "PageDesc1" in the "region-main" "region"
+
+  @javascript
+  Scenario: Display and hide the last modified date
+    Given I am on "Course 1" course homepage
+    When I follow "PageName1"
+    Then I should see "Last modified:" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display last modified date" "checkbox"
+    And I press "Save and display"
+    Then I should not see "Last modified:" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display last modified date" "checkbox"
+    And I press "Save and display"
+    Then I should see "Last modified:" in the "region-main" "region"
index 9ca99ee..2121a9e 100644 (file)
@@ -57,6 +57,9 @@ class mod_page_generator extends testing_module_generator {
         if (!isset($record->printintro)) {
             $record->printintro = 0;
         }
+        if (!isset($record->printlastmodified)) {
+            $record->printlastmodified = 1;
+        }
 
         return parent::create_instance($record, (array)$options);
     }
index 93ffa65..6033a00 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018051400;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2018051401;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2018050800;    // Requires this Moodle version
 $plugin->component = 'mod_page';       // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 149e0b1..4297a7f 100644 (file)
@@ -88,7 +88,9 @@ $formatoptions->context = $context;
 $content = format_text($content, $page->contentformat, $formatoptions);
 echo $OUTPUT->box($content, "generalbox center clearfix");
 
-$strlastmodified = get_string("lastmodified");
-echo "<div class=\"modified\">$strlastmodified: ".userdate($page->timemodified)."</div>";
+if (!isset($options['printlastmodified']) || !empty($options['printlastmodified'])) {
+    $strlastmodified = get_string("lastmodified");
+    echo html_writer::div("$strlastmodified: " . userdate($page->timemodified), 'modified');
+}
 
 echo $OUTPUT->footer();
index 6eb80f6..6f2391b 100644 (file)
@@ -16,7 +16,7 @@
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
-        <KEY NAME="questionusageid-slot" TYPE="foreign-unique" FIELDS="questionusageid, slot" REFTABLE="question_attempts" REFFIELDS="questionusageid, slot"/>
+        <KEY NAME="questionusageid-slot" TYPE="foreign" FIELDS="questionusageid, slot" REFTABLE="question_attempts" REFFIELDS="questionusageid, slot"/>
       </KEYS>
     </TABLE>
   </TABLES>
index 81efd89..0cc8919 100644 (file)
@@ -42,21 +42,42 @@ function xmldb_quiz_overview_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
-    if ($oldversion < 2018021800) {
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
+    // Upgrade 2018021800 (now removed) incorrectly added this key
+    // with a unique constraint, which breaks things because those
+    // columns are not, in fact, unique. So drop (if it exists) then recreate.
+    if ($oldversion < 2018061800) {
+
+        // Define key questionusageid-slot (foreign) to be dropped form quiz_overview_regrades.
+        $table = new xmldb_table('quiz_overview_regrades');
+
+        // There is no key_exists, so test the equivalent index.
+        $oldindex = new xmldb_index('questionusageid-slot', XMLDB_KEY_UNIQUE, array('questionusageid', 'slot'));
+
+        // Launch drop key questionusageid-slot.
+        if ($dbman->index_exists($table, $oldindex)) {
+            $key = new xmldb_key('questionusageid-slot', XMLDB_KEY_FOREIGN, array('questionusageid', 'slot'), 'question_attempts', array('questionusageid', 'slot'));
+            $dbman->drop_key($table, $key);
+        }
 
-        // Define key questionusageid-slot (foreign-unique) to be added to quiz_overview_regrades.
+        // Overview savepoint reached.
+        upgrade_plugin_savepoint(true, 2018061800, 'quiz', 'overview');
+    }
+
+    if ($oldversion < 2018061801) {
+
+        // Define key questionusageid-slot (foreign) to be added to quiz_overview_regrades.
         $table = new xmldb_table('quiz_overview_regrades');
-        $key = new xmldb_key('questionusageid-slot', XMLDB_KEY_FOREIGN_UNIQUE, array('questionusageid', 'slot'), 'question_attempts', array('questionusageid', 'slot'));
+        $key = new xmldb_key('questionusageid-slot', XMLDB_KEY_FOREIGN, array('questionusageid', 'slot'), 'question_attempts', array('questionusageid', 'slot'));
 
         // Launch add key questionusageid-slot.
         $dbman->add_key($table, $key);
 
         // Overview savepoint reached.
-        upgrade_plugin_savepoint(true, 2018021800, 'quiz', 'overview');
+        upgrade_plugin_savepoint(true, 2018061801, 'quiz', 'overview');
     }
 
-    // Automatically generated Moodle v3.5.0 release upgrade line.
-    // Put any upgrade step following this.
-
     return true;
 }
index b3a68de..76d0534 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version  = 2018051400;
+$plugin->version  = 2018061801;
 $plugin->requires = 2018050800;
 $plugin->component = 'quiz_overview';
index 0a724eb..5ab0b76 100644 (file)
@@ -47,7 +47,7 @@ class qtype_shortanswer_renderer extends qtype_renderer {
             'value' => $currentanswer,
             'id' => $inputname,
             'size' => 80,
-            'class' => 'form-control',
+            'class' => 'form-control d-inline',
         );
 
         if ($options->readonly) {
index fde60b3..c368d44 100644 (file)
@@ -709,10 +709,6 @@ span.editinstructions {
     &.even {
         background-color: $table-bg-accent;
     }
-
-    &:hover {
-        background-color: $table-bg-hover;
-    }
 }
 
 .courses > .paging.paging-morelink {
index c01bff2..8adcac2 100644 (file)
@@ -1,6 +1,6 @@
 {{< core_form/element-template-inline }}
     {{$element}}
-        <div class="fdate_time_selector d-flex">
+        <div class="fdate_time_selector d-flex align-items-center">
         {{#element.elements}}
             {{{separator}}}
             {{{html}}}
index fb4d48f..225c4ee 100644 (file)
@@ -1,6 +1,6 @@
 {{< core_form/element-template }}
     {{$element}}
-        <div class="fdate_time_selector d-flex flex-wrap">
+        <div class="fdate_time_selector d-flex flex-wrap align-items-center">
         {{#element.elements}}
             {{{separator}}}
             {{{html}}}
index 770d95c..9c26f9a 100644 (file)
@@ -578,7 +578,6 @@ span.editinstructions {
 .courses .coursebox.even {
     background-color: @tableBackgroundAccent;
 }
-.courses .coursebox:hover,
 .course_category_tree .courses > .paging.paging-morelink:hover {
     background-color: @tableBackgroundHover;
 }
index 32cad6e..ce1163d 100644 (file)
@@ -6480,7 +6480,6 @@ span.editinstructions {
 .courses .coursebox.even {
   background-color: #f9f9f9;
 }
-.courses .coursebox:hover,
 .course_category_tree .courses > .paging.paging-morelink:hover {
   background-color: #f5f5f5;
 }