Merge branch 'MDL-63009-master' of git://github.com/mickhawkins/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 17 Sep 2018 21:30:13 +0000 (23:30 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 17 Sep 2018 21:30:13 +0000 (23:30 +0200)
100 files changed:
.eslintignore
.stylelintignore
Gruntfile.js
admin/tool/dataprivacy/amd/build/defaultsactions.min.js [new file with mode: 0644]
admin/tool/dataprivacy/amd/src/defaultsactions.js [new file with mode: 0644]
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/data_registry.php
admin/tool/dataprivacy/classes/external.php
admin/tool/dataprivacy/classes/external/category_exporter.php
admin/tool/dataprivacy/classes/external/purpose_exporter.php
admin/tool/dataprivacy/classes/form/defaults.php [deleted file]
admin/tool/dataprivacy/classes/output/data_registry_page.php
admin/tool/dataprivacy/classes/output/defaults_page.php [new file with mode: 0644]
admin/tool/dataprivacy/db/services.php
admin/tool/dataprivacy/defaults.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/styles.css
admin/tool/dataprivacy/templates/category_purpose_form.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/defaults_display.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/defaults_page.mustache [new file with mode: 0644]
admin/tool/dataprivacy/templates/delete_activity_defaults.mustache [new file with mode: 0644]
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php [new file with mode: 0644]
admin/tool/dataprivacy/tests/behat/manage_defaults.feature [new file with mode: 0644]
admin/tool/dataprivacy/tests/external_test.php
admin/tool/dataprivacy/tests/generator/lib.php [new file with mode: 0644]
admin/tool/dataprivacy/version.php
blocks/calendar_month/block_calendar_month.php
blocks/rss_client/classes/output/item.php
cache/stores/memcache/addinstanceform.php [deleted file]
cache/stores/memcache/classes/privacy/provider.php [deleted file]
cache/stores/memcache/lang/en/cachestore_memcache.php [deleted file]
cache/stores/memcache/lib.php [deleted file]
cache/stores/memcache/settings.php [deleted file]
cache/stores/memcache/tests/memcache_test.php [deleted file]
cache/stores/memcache/version.php [deleted file]
cache/upgrade.txt
config-dist.php
group/assign.php
install/lang/el/admin.php
iplookup/tests/geoplugin_test.php
lib/classes/plugin_manager.php
lib/classes/session/memcache.php [deleted file]
lib/classes/session/memcached.php
lib/classes/session/util.php [deleted file]
lib/db/upgrade.php
lib/editor/atto/plugins/html/styles.css [new file with mode: 0644]
lib/editor/atto/plugins/html/thirdpartylibs.xml [new file with mode: 0644]
lib/editor/atto/plugins/html/version.php
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-debug.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-min.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-debug.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-min.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button.js
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/moodle-atto_html-codemirror-core.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/skins/sam/moodle-atto_html-codemirror-skin.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/skins/sam/moodle-atto_html-codemirror.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-debug.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-min.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/LICENSE [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/build.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-css.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-html.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/beautify.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/js/wrapper.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/meta/beautify.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/beautify/readme_moodle.txt [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/button/build.json
lib/editor/atto/plugins/html/yui/src/button/js/button.js
lib/editor/atto/plugins/html/yui/src/button/meta/button.json
lib/editor/atto/plugins/html/yui/src/codemirror/LICENSE [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/assets/moodle-atto_html-codemirror-core.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/assets/skins/sam/moodle-atto_html-codemirror-skin.css [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/build.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/codemirror.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/css.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/htmlmixed.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/javascript.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/wrapper-end.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/wrapper-mid.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/wrapper.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/js/xml.js [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/meta/codemirror.json [new file with mode: 0644]
lib/editor/atto/plugins/html/yui/src/codemirror/readme_moodle.txt [new file with mode: 0644]
lib/form/filemanager.php
lib/upgrade.txt
message/lib.php
message/tests/messagelib_test.php
mod/data/lib.php
mod/data/tests/lib_test.php
mod/quiz/report/attemptsreport.php
question/engine/questionattempt.php
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/drawer.scss
theme/boost/style/moodle.css
user/lib.php
user/tests/behat/filter_participants.feature
version.php

index 6345306..420eb21 100644 (file)
@@ -9,6 +9,8 @@ auth/cas/CAS/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
+lib/editor/atto/plugins/html/yui/src/codemirror/
+lib/editor/atto/plugins/html/yui/src/beautify/
 lib/editor/atto/yui/src/rangy/js/*.*
 lib/editor/tinymce/plugins/pdw/tinymce/
 lib/editor/tinymce/plugins/spellchecker/rpc.php
index b0dc0dc..be8cec8 100644 (file)
@@ -1,4 +1,5 @@
 # Generated by "grunt ignorefiles"
+**/yui/build/*
 theme/bootstrapbase/style/
 theme/clean/style/custom.css
 theme/more/style/custom.css
@@ -11,6 +12,8 @@ auth/cas/CAS/
 enrol/lti/ims-blti/
 filter/algebra/AlgParser.pm
 filter/tex/mimetex.*
+lib/editor/atto/plugins/html/yui/src/codemirror/
+lib/editor/atto/plugins/html/yui/src/beautify/
 lib/editor/atto/yui/src/rangy/js/*.*
 lib/editor/tinymce/plugins/pdw/tinymce/
 lib/editor/tinymce/plugins/spellchecker/rpc.php
index 78583d0..29d271c 100644 (file)
@@ -222,6 +222,7 @@ module.exports = function(grunt) {
       // Generate .stylelintignore.
       var stylelintIgnores = [
           '# Generated by "grunt ignorefiles"',
+          '**/yui/build/*',
           'theme/bootstrapbase/style/',
           'theme/clean/style/custom.css',
           'theme/more/style/custom.css',
diff --git a/admin/tool/dataprivacy/amd/build/defaultsactions.min.js b/admin/tool/dataprivacy/amd/build/defaultsactions.min.js
new file mode 100644 (file)
index 0000000..b8f0bc3
Binary files /dev/null and b/admin/tool/dataprivacy/amd/build/defaultsactions.min.js differ
diff --git a/admin/tool/dataprivacy/amd/src/defaultsactions.js b/admin/tool/dataprivacy/amd/src/defaultsactions.js
new file mode 100644 (file)
index 0000000..76cbb89
--- /dev/null
@@ -0,0 +1,315 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * AMD module for data registry defaults actions.
+ *
+ * @module     tool_dataprivacy/defaultsactions
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define([
+    'jquery',
+    'core/ajax',
+    'core/notification',
+    'core/str',
+    'core/modal_factory',
+    'core/modal_events',
+    'core/templates'],
+function($, Ajax, Notification, Str, ModalFactory, ModalEvents, Templates) {
+
+    /**
+     * List of action selectors.
+     *
+     * @type {{EDIT_LEVEL_DEFAULTS: string}}
+     * @type {{NEW_ACTIVITY_DEFAULTS: string}}
+     * @type {{EDIT_ACTIVITY_DEFAULTS: string}}
+     * @type {{DELETE_ACTIVITY_DEFAULTS: string}}
+     */
+    var ACTIONS = {
+        EDIT_LEVEL_DEFAULTS: '[data-action="edit-level-defaults"]',
+        NEW_ACTIVITY_DEFAULTS: '[data-action="new-activity-defaults"]',
+        EDIT_ACTIVITY_DEFAULTS: '[data-action="edit-activity-defaults"]',
+        DELETE_ACTIVITY_DEFAULTS: '[data-action="delete-activity-defaults"]'
+    };
+
+    /** @type {{INHERIT: Number}} **/
+    var INHERIT = -1;
+
+    /**
+     * DefaultsActions class.
+     */
+    var DefaultsActions = function() {
+        this.registerEvents();
+    };
+
+    /**
+     * Register event listeners.
+     */
+    DefaultsActions.prototype.registerEvents = function() {
+        $(ACTIONS.EDIT_LEVEL_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+            var category = button.data('category');
+            var purpose = button.data('purpose');
+
+            // Get options.
+            var requests = [
+                {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_purpose_options', args: {}}
+            ];
+
+            var promises = Ajax.call(requests);
+            var titlePromise = Str.get_string('editdefaults', 'tool_dataprivacy', $('#defaults-header').text());
+            $.when(promises[0], promises[1], titlePromise).then(function(categoryResponse, purposeResponse, title) {
+                var categories = categoryResponse.options;
+                var purposes = purposeResponse.options;
+                showDefaultsFormModal(title, contextLevel, category, purpose, null, categories, purposes, null);
+
+                return true;
+            }).catch(Notification.exception);
+        });
+
+        $(ACTIONS.NEW_ACTIVITY_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+
+            // Get options.
+            var requests = [
+                {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_activity_options', args: {'nodefaults': true}}
+            ];
+
+            var promises = Ajax.call(requests);
+            var titlePromise = Str.get_string('addnewdefaults', 'tool_dataprivacy');
+
+            $.when(promises[0], promises[1], promises[2], titlePromise).then(
+                function(categoryResponse, purposeResponse, activityResponse, title) {
+                    var categories = categoryResponse.options;
+                    var purposes = purposeResponse.options;
+                    var activities = activityResponse.options;
+
+                    showDefaultsFormModal(title, contextLevel, null, null, null, categories, purposes, activities);
+
+                    return true;
+
+                }).catch(Notification.exception);
+            }
+        );
+
+        $(ACTIONS.EDIT_ACTIVITY_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+            var category = button.data('category');
+            var purpose = button.data('purpose');
+            var activity = button.data('activityname');
+
+            // Get options.
+            var requests = [
+                {methodname: 'tool_dataprivacy_get_category_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_purpose_options', args: {}},
+                {methodname: 'tool_dataprivacy_get_activity_options', args: {}}
+            ];
+
+            var promises = Ajax.call(requests);
+            var titlePromise = Str.get_string('editmoduledefaults', 'tool_dataprivacy');
+
+            $.when(promises[0], promises[1], promises[2], titlePromise).then(
+                function(categoryResponse, purposeResponse, activityResponse, title) {
+                    var categories = categoryResponse.options;
+                    var purposes = purposeResponse.options;
+                    var activities = activityResponse.options;
+
+                    showDefaultsFormModal(title, contextLevel, category, purpose, activity, categories, purposes, activities);
+
+                    return true;
+
+                }).catch(Notification.exception);
+            }
+        );
+
+        $(ACTIONS.DELETE_ACTIVITY_DEFAULTS).click(function(e) {
+            e.preventDefault();
+
+            var button = $(this);
+            var contextLevel = button.data('contextlevel');
+            var activity = button.data('activityname');
+            var activityDisplayName = button.data('activitydisplayname');
+            // Set category and purpose to inherit (-1).
+            var category = INHERIT;
+            var purpose = INHERIT;
+
+            ModalFactory.create({
+                title: Str.get_string('deletedefaults', 'tool_dataprivacy', activityDisplayName),
+                body: Templates.render('tool_dataprivacy/delete_activity_defaults', {"activityname": activityDisplayName}),
+                type: ModalFactory.types.SAVE_CANCEL,
+                large: true
+            }).then(function(modal) {
+                modal.setSaveButtonText(Str.get_string('delete'));
+
+                // Handle save event.
+                modal.getRoot().on(ModalEvents.save, function() {
+                    setContextDefaults(contextLevel, category, purpose, activity, false);
+                });
+
+                // Handle hidden event.
+                modal.getRoot().on(ModalEvents.hidden, function() {
+                    // Destroy when hidden.
+                    modal.destroy();
+                });
+
+                modal.show();
+
+                return true;
+            }).catch(Notification.exception);
+        });
+    };
+
+    /**
+     * Prepares and renders the modal for setting the defaults for the given context level/plugin.
+     *
+     * @param {String} title The modal's title.
+     * @param {Number} contextLevel The context level to set defaults for.
+     * @param {Number} category The current category ID.
+     * @param {Number} purpose The current purpose ID.
+     * @param {String} activity The plugin name of the activity. Optional.
+     * @param {Array} categoryOptions The list of category options.
+     * @param {Array} purposeOptions The list of purpose options.
+     * @param {Array} activityOptions The list of activity options. Optional.
+     */
+    function showDefaultsFormModal(title, contextLevel, category, purpose, activity,
+                                   categoryOptions, purposeOptions, activityOptions) {
+
+        if (category !== null) {
+            categoryOptions.forEach(function(currentValue) {
+                if (currentValue.id === category) {
+                    currentValue.selected = true;
+                }
+            });
+        }
+
+        if (purpose !== null) {
+            purposeOptions.forEach(function(currentValue) {
+                if (currentValue.id === purpose) {
+                    currentValue.selected = true;
+                }
+            });
+        }
+
+        var templateContext = {
+            "contextlevel": contextLevel,
+            "categoryoptions": categoryOptions,
+            "purposeoptions": purposeOptions
+        };
+
+        // Check the activityOptions parameter that was passed.
+        if (activityOptions !== null && activityOptions.length) {
+            // Check the activity parameter that was passed.
+            if (activity === null) {
+                // We're setting a new defaults for a module.
+                templateContext.newactivitydefaults = true;
+
+            } else {
+                // Edit mode. Set selection.
+                activityOptions.forEach(function(currentValue) {
+                    if (activity === currentValue.name) {
+                        currentValue.selected = true;
+                    }
+                });
+            }
+
+            templateContext.modemodule = true;
+            templateContext.activityoptions = activityOptions;
+        }
+
+        ModalFactory.create({
+            title: title,
+            body: Templates.render('tool_dataprivacy/category_purpose_form', templateContext),
+            type: ModalFactory.types.SAVE_CANCEL,
+            large: true
+        }).then(function(modal) {
+
+            // Handle save event.
+            modal.getRoot().on(ModalEvents.save, function() {
+                var activity = $('#activity');
+                var activityVal = typeof activity !== 'undefined' ? activity.val() : null;
+                var override = $('#override');
+                var overrideVal = typeof override !== 'undefined' ? override.is(':checked') : false;
+
+                setContextDefaults($('#contextlevel').val(), $('#category').val(), $('#purpose').val(), activityVal, overrideVal);
+            });
+
+            // Handle hidden event.
+            modal.getRoot().on(ModalEvents.hidden, function() {
+                // Destroy when hidden.
+                modal.destroy();
+            });
+
+            modal.show();
+
+            return modal;
+        }).catch(Notification.exception);
+    }
+
+    /**
+     * Calls a the tool_dataprivacy_set_context_defaults WS function.
+     *
+     * @param {Number} contextLevel The context level.
+     * @param {Number} category The category ID.
+     * @param {Number} purpose The purpose ID.
+     * @param {String} activity The plugin name of the activity module.
+     * @param {Boolean} override Whether to override custom instances.
+     */
+    function setContextDefaults(contextLevel, category, purpose, activity, override) {
+        var request = {
+            methodname: 'tool_dataprivacy_set_context_defaults',
+            args: {
+                'contextlevel': contextLevel,
+                'category': category,
+                'purpose': purpose,
+                'override': override,
+                'activity': activity
+            }
+        };
+
+        Ajax.call([request])[0].done(function(data) {
+            if (data.result) {
+                window.location.reload();
+            }
+        });
+    }
+
+    return /** @alias module:tool_dataprivacy/defaultsactions */ {
+        // Public variables and functions.
+
+        /**
+         * Initialise the module.
+         *
+         * @method init
+         * @return {DefaultsActions}
+         */
+        'init': function() {
+            return new DefaultsActions();
+        }
+    };
+});
index 40b3a4e..212f91a 100644 (file)
@@ -24,7 +24,7 @@
 namespace tool_dataprivacy;
 
 use coding_exception;
-use context_course;
+use context_helper;
 use context_system;
 use core\invalid_persistent_exception;
 use core\message\message;
@@ -105,18 +105,14 @@ class api {
     }
 
     /**
-     * Check's whether the current user has the capability to manage data requests.
+     * Checks whether the current user has the capability to manage data requests.
      *
      * @param int $userid The user ID.
      * @return bool
-     * @throws coding_exception
-     * @throws dml_exception
      */
     public static function can_manage_data_requests($userid) {
-        $context = context_system::instance();
-
-        // A user can manage data requests if he/she has the site DPO role and has the capability to manage data requests.
-        return self::is_site_dpo($userid) && has_capability('tool/dataprivacy:managedatarequests', $context, $userid);
+        // Privacy officers can manage data requests.
+        return self::is_site_dpo($userid);
     }
 
     /**
@@ -136,6 +132,31 @@ class api {
         require_capability('tool/dataprivacy:managedataregistry', $context);
     }
 
+    /**
+     * Fetches the list of configured privacy officer roles.
+     *
+     * Every time this function is called, it checks each role if they have the 'managedatarequests' capability and removes
+     * any role that doesn't have the required capability anymore.
+     *
+     * @return int[]
+     * @throws dml_exception
+     */
+    public static function get_assigned_privacy_officer_roles() {
+        $roleids = [];
+
+        // Get roles from config.
+        $configroleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
+        if (!empty($configroleids)) {
+            // Fetch roles that have the capability to manage data requests.
+            $capableroles = array_keys(get_roles_with_capability('tool/dataprivacy:managedatarequests'));
+
+            // Extract the configured roles that have the capability from the list of capable roles.
+            $roleids = array_intersect($capableroles, $configroleids);
+        }
+
+        return $roleids;
+    }
+
     /**
      * Fetches the role shortnames of Data Protection Officer roles.
      *
@@ -144,7 +165,7 @@ class api {
     public static function get_dpo_role_names() : array {
         global $DB;
 
-        $dporoleids = explode(',', str_replace(' ', '', get_config('tool_dataprivacy', 'dporoles')));
+        $dporoleids = self::get_assigned_privacy_officer_roles();
         $dponames = array();
 
         if (!empty($dporoleids)) {
@@ -156,20 +177,15 @@ class api {
     }
 
     /**
-     * Fetches the list of users with the Data Protection Officer role.
-     *
-     * @throws dml_exception
+     * Fetches the list of users with the Privacy Officer role.
      */
     public static function get_site_dpos() {
         // Get role(s) that can manage data requests.
-        $dporoles = explode(',', get_config('tool_dataprivacy', 'dporoles'));
+        $dporoles = self::get_assigned_privacy_officer_roles();
 
         $dpos = [];
         $context = context_system::instance();
         foreach ($dporoles as $roleid) {
-            if (empty($roleid)) {
-                continue;
-            }
             $allnames = get_all_user_name_fields(true, 'u');
             $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
                       'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
@@ -189,15 +205,14 @@ class api {
     }
 
     /**
-     * Checks whether a given user is a site DPO.
+     * Checks whether a given user is a site Privacy Officer.
      *
      * @param int $userid The user ID.
      * @return bool
-     * @throws dml_exception
      */
     public static function is_site_dpo($userid) {
         $dpos = self::get_site_dpos();
-        return array_key_exists($userid, $dpos);
+        return array_key_exists($userid, $dpos) || is_siteadmin();
     }
 
     /**
@@ -1143,4 +1158,91 @@ class api {
 
         return $approvedcollection;
     }
+
+    /**
+     * Updates the default category and purpose for a given context level (and optionally, a plugin).
+     *
+     * @param int $contextlevel The context level.
+     * @param int $categoryid The ID matching the category.
+     * @param int $purposeid The ID matching the purpose record.
+     * @param int $activity The name of the activity that we're making a defaults configuration for.
+     * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
+     * @return boolean True if set/unset config succeeds. Otherwise, it throws an exception.
+     */
+    public static function set_context_defaults($contextlevel, $categoryid, $purposeid, $activity = null, $override = false) {
+        global $DB;
+
+        self::check_can_manage_data_registry();
+
+        // Get the class name associated with this context level.
+        $classname = context_helper::get_class_for_level($contextlevel);
+        list($purposevar, $categoryvar) = data_registry::var_names_from_context($classname, $activity);
+
+        // Check the default category to be set.
+        if ($categoryid == context_instance::INHERIT) {
+            unset_config($categoryvar, 'tool_dataprivacy');
+
+        } else {
+            // Make sure the given category ID exists first.
+            $categorypersistent = new category($categoryid);
+            $categorypersistent->read();
+
+            // Then set the new default value.
+            set_config($categoryvar, $categoryid, 'tool_dataprivacy');
+        }
+
+        // Check the default purpose to be set.
+        if ($purposeid == context_instance::INHERIT) {
+            // If the defaults is set to inherit, just unset the config value.
+            unset_config($purposevar, 'tool_dataprivacy');
+
+        } else {
+            // Make sure the given purpose ID exists first.
+            $purposepersistent = new purpose($purposeid);
+            $purposepersistent->read();
+
+            // Then set the new default value.
+            set_config($purposevar, $purposeid, 'tool_dataprivacy');
+        }
+
+        // Unset instances that have been assigned with custom purpose and category, if override was specified.
+        if ($override) {
+            // We'd like to find context IDs that we want to unset.
+            $statements = ["SELECT c.id as contextid FROM {context} c"];
+            // Based on this context level.
+            $params = ['contextlevel' => $contextlevel];
+
+            if ($contextlevel == CONTEXT_MODULE) {
+                // If we're deleting module context instances, we need to make sure the instance ID is in the course modules table.
+                $statements[] = "JOIN {course_modules} cm ON cm.id = c.instanceid";
+                // And that the module is listed on the modules table.
+                $statements[] = "JOIN {modules} m ON m.id = cm.module";
+
+                if ($activity) {
+                    // If we're overriding for an activity module, make sure that the context instance matches that activity.
+                    $statements[] = "AND m.name = :modname";
+                    $params['modname'] = $activity;
+                }
+            }
+            // Make sure this context instance exists in the tool_dataprivacy_ctxinstance table.
+            $statements[] = "JOIN {tool_dataprivacy_ctxinstance} tdc ON tdc.contextid = c.id";
+            // And that the context level of this instance matches the given context level.
+            $statements[] = "WHERE c.contextlevel = :contextlevel";
+
+            // Build our SQL query by gluing the statements.
+            $sql = implode("\n", $statements);
+
+            // Get the context records matching our query.
+            $contextids = $DB->get_fieldset_sql($sql, $params);
+
+            // Delete the matching context instances.
+            foreach ($contextids as $contextid) {
+                if ($instance = context_instance::get_record_by_contextid($contextid, false)) {
+                    self::unset_context_instance($instance);
+                }
+            }
+        }
+
+        return true;
+    }
 }
index 1fac2f5..7b46b4d 100644 (file)
 namespace tool_dataprivacy;
 
 use coding_exception;
-use tool_dataprivacy\purpose;
-use tool_dataprivacy\category;
-use tool_dataprivacy\contextlevel;
-use tool_dataprivacy\context_instance;
+use core\persistent;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -57,13 +54,22 @@ class data_registry {
     /**
      * Returns purpose and category var names from a context class name
      *
-     * @param string $classname
+     * @param string $classname The context level's class.
+     * @param string $pluginname The name of the plugin associated with the context level.
      * @return string[]
      */
-    public static function var_names_from_context($classname) {
+    public static function var_names_from_context($classname, $pluginname = '') {
+        $pluginname = trim($pluginname);
+        if (!empty($pluginname)) {
+            $categoryvar = $classname . '_' . $pluginname . '_category';
+            $purposevar = $classname . '_' . $pluginname . '_purpose';
+        } else {
+            $categoryvar = $classname . '_category';
+            $purposevar = $classname . '_purpose';
+        }
         return [
-            $classname . '_purpose',
-            $classname . '_category',
+            $purposevar,
+            $categoryvar
         ];
     }
 
@@ -72,17 +78,31 @@ class data_registry {
      *
      * The caller code is responsible of checking that $contextlevel is an integer.
      *
-     * @param int $contextlevel
-     * @return int|false[]
+     * @param int $contextlevel The context level.
+     * @param string $pluginname The name of the plugin associated with the context level.
+     * @return int[]|false[]
      */
-    public static function get_defaults($contextlevel) {
+    public static function get_defaults($contextlevel, $pluginname = '') {
 
         $classname = \context_helper::get_class_for_level($contextlevel);
-        list($purposevar, $categoryvar) = self::var_names_from_context($classname);
+        list($purposevar, $categoryvar) = self::var_names_from_context($classname, $pluginname);
 
         $purposeid = get_config('tool_dataprivacy', $purposevar);
         $categoryid = get_config('tool_dataprivacy', $categoryvar);
 
+        if (!empty($pluginname)) {
+            list($purposevar, $categoryvar) = self::var_names_from_context($classname);
+            // If the plugin-level doesn't have a default purpose set, try the context level.
+            if ($purposeid === false) {
+                $purposeid = get_config('tool_dataprivacy', $purposevar);
+            }
+
+            // If the plugin-level doesn't have a default category set, try the context level.
+            if ($categoryid === false) {
+                $categoryid = get_config('tool_dataprivacy', $categoryvar);
+            }
+        }
+
         if (empty($purposeid)) {
             $purposeid = false;
         }
@@ -197,9 +217,15 @@ class data_registry {
                 // Use the context level value as we don't allow people to set specific instances values.
                 return self::get_effective_contextlevel_value($context->contextlevel, $element);
             } else {
+                // Check if we need to pass the plugin name of an activity.
+                $forplugin = '';
+                if ($context->contextlevel == CONTEXT_MODULE) {
+                    list($course, $cm) = get_course_and_cm_from_cmid($context->instanceid);
+                    $forplugin = $cm->modname;
+                }
                 // Use the default context level value.
                 list($purposeid, $categoryid) = self::get_effective_default_contextlevel_purpose_and_category(
-                    $context->contextlevel
+                    $context->contextlevel, false, false, $forplugin
                 );
                 return self::get_element_instance($element, $$fieldname);
             }
@@ -283,14 +309,15 @@ class data_registry {
      * Returns the effective default purpose and category for a context level.
      *
      * @param int $contextlevel
-     * @param int $forcedpurposevalue Use this value as if this was this context level purpose.
-     * @param int $forcedcategoryvalue Use this value as if this was this context level category.
+     * @param int|bool $forcedpurposevalue Use this value as if this was this context level purpose.
+     * @param int|bool $forcedcategoryvalue Use this value as if this was this context level category.
+     * @param string $activity The plugin name of the activity.
      * @return int[]
      */
     public static function get_effective_default_contextlevel_purpose_and_category($contextlevel, $forcedpurposevalue = false,
-                                                                                   $forcedcategoryvalue = false) {
+                                                                                   $forcedcategoryvalue = false, $activity = '') {
 
-        list($purposeid, $categoryid) = self::get_defaults($contextlevel);
+        list($purposeid, $categoryid) = self::get_defaults($contextlevel, $activity);
 
         // Honour forced values.
         if ($forcedpurposevalue) {
index e14e072..692e072 100644 (file)
@@ -1110,6 +1110,287 @@ class external extends external_api {
         ]);
     }
 
+    /**
+     * Parameters for set_context_defaults().
+     *
+     * @return external_function_parameters
+     */
+    public static function set_context_defaults_parameters() {
+        return new external_function_parameters([
+            'contextlevel' => new external_value(PARAM_INT, 'The context level', VALUE_REQUIRED),
+            'category' => new external_value(PARAM_INT, 'The default category for the given context level', VALUE_REQUIRED),
+            'purpose' => new external_value(PARAM_INT, 'The default purpose for the given context level', VALUE_REQUIRED),
+            'activity' => new external_value(PARAM_PLUGIN, 'The plugin name of the activity', VALUE_DEFAULT, null),
+            'override' => new external_value(PARAM_BOOL, 'Whether to override existing instances with the defaults', VALUE_DEFAULT,
+                false),
+        ]);
+    }
+
+    /**
+     * Updates the default category and purpose for a given context level (and optionally, a plugin).
+     *
+     * @param int $contextlevel The context level.
+     * @param int $category The ID matching the category.
+     * @param int $purpose The ID matching the purpose record.
+     * @param int $activity The name of the activity that we're making a defaults configuration for.
+     * @param bool $override Whether to override the purpose/categories of existing instances to these defaults.
+     * @return array
+     */
+    public static function set_context_defaults($contextlevel, $category, $purpose, $activity, $override) {
+        $warnings = [];
+
+        $params = external_api::validate_parameters(self::set_context_defaults_parameters(), [
+            'contextlevel' => $contextlevel,
+            'category' => $category,
+            'purpose' => $purpose,
+            'activity' => $activity,
+            'override' => $override,
+        ]);
+        $contextlevel = $params['contextlevel'];
+        $category = $params['category'];
+        $purpose = $params['purpose'];
+        $activity = $params['activity'];
+        $override = $params['override'];
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        // Set the context defaults.
+        $result = api::set_context_defaults($contextlevel, $category, $purpose, $activity, $override);
+
+        return [
+            'result' => $result,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for set_context_defaults().
+     *
+     * @return external_single_structure
+     */
+    public static function set_context_defaults_returns() {
+        return new external_single_structure([
+            'result' => new external_value(PARAM_BOOL, 'Whether the context defaults were successfully set or not'),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Parameters for get_category_options().
+     *
+     * @return external_function_parameters
+     */
+    public static function get_category_options_parameters() {
+        return new external_function_parameters([
+            'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
+            'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
+        ]);
+    }
+
+    /**
+     * Fetches a list of data category options containing category IDs as keys and the category name for the value.
+     *
+     * @param bool $includeinherit Whether to include the "Inherit" option.
+     * @param bool $includenotset Whether to include the "Not set" option.
+     * @return array
+     */
+    public static function get_category_options($includeinherit, $includenotset) {
+        $warnings = [];
+
+        $params = self::validate_parameters(self::get_category_options_parameters(), [
+            'includeinherit' => $includeinherit,
+            'includenotset' => $includenotset
+        ]);
+        $includeinherit = $params['includeinherit'];
+        $includenotset = $params['includenotset'];
+
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $categories = api::get_categories();
+        $options = data_registry_page::category_options($categories, $includenotset, $includeinherit);
+        $categoryoptions = [];
+        foreach ($options as $id => $name) {
+            $categoryoptions[] = [
+                'id' => $id,
+                'name' => $name,
+            ];
+        }
+
+        return [
+            'options' => $categoryoptions,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for get_category_options().
+     *
+     * @return external_single_structure
+     */
+    public static function get_category_options_returns() {
+        $optiondefinition = new external_single_structure(
+            [
+                'id' => new external_value(PARAM_INT, 'The category ID'),
+                'name' => new external_value(PARAM_TEXT, 'The category name'),
+            ]
+        );
+
+        return new external_single_structure([
+            'options' => new external_multiple_structure($optiondefinition),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Parameters for get_purpose_options().
+     *
+     * @return external_function_parameters
+     */
+    public static function get_purpose_options_parameters() {
+        return new external_function_parameters([
+            'includeinherit' => new external_value(PARAM_BOOL, 'Include option "Inherit"', VALUE_DEFAULT, true),
+            'includenotset' => new external_value(PARAM_BOOL, 'Include option "Not set"', VALUE_DEFAULT, false),
+        ]);
+    }
+
+    /**
+     * Fetches a list of data storage purposes containing purpose IDs as keys and the purpose name for the value.
+     *
+     * @param bool $includeinherit Whether to include the "Inherit" option.
+     * @param bool $includenotset Whether to include the "Not set" option.
+     * @return array
+     */
+    public static function get_purpose_options($includeinherit, $includenotset) {
+        $warnings = [];
+
+        $params = self::validate_parameters(self::get_category_options_parameters(), [
+            'includeinherit' => $includeinherit,
+            'includenotset' => $includenotset
+        ]);
+        $includeinherit = $params['includeinherit'];
+        $includenotset = $params['includenotset'];
+
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $purposes = api::get_purposes();
+        $options = data_registry_page::purpose_options($purposes, $includenotset, $includeinherit);
+        $purposeoptions = [];
+        foreach ($options as $id => $name) {
+            $purposeoptions[] = [
+                'id' => $id,
+                'name' => $name,
+            ];
+        }
+
+        return [
+            'options' => $purposeoptions,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for get_purpose_options().
+     *
+     * @return external_single_structure
+     */
+    public static function get_purpose_options_returns() {
+        $optiondefinition = new external_single_structure(
+            [
+                'id' => new external_value(PARAM_INT, 'The purpose ID'),
+                'name' => new external_value(PARAM_TEXT, 'The purpose name'),
+            ]
+        );
+
+        return new external_single_structure([
+            'options' => new external_multiple_structure($optiondefinition),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
+    /**
+     * Parameters for get_activity_options().
+     *
+     * @return external_function_parameters
+     */
+    public static function get_activity_options_parameters() {
+        return new external_function_parameters([
+            'nodefaults' => new external_value(PARAM_BOOL, 'Whether to fetch all activities or only those without defaults',
+                VALUE_DEFAULT, false),
+        ]);
+    }
+
+    /**
+     * Fetches a list of activity options for setting data registry defaults.
+     *
+     * @param boolean $nodefaults If false, it will fetch all of the activities. Otherwise, it will only fetch the activities
+     *                            that don't have defaults yet (e.g. when adding a new activity module defaults).
+     * @return array
+     */
+    public static function get_activity_options($nodefaults) {
+        $warnings = [];
+
+        $params = self::validate_parameters(self::get_activity_options_parameters(), [
+            'nodefaults' => $nodefaults,
+        ]);
+        $nodefaults = $params['nodefaults'];
+
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        // Get activity module plugin info.
+        $pluginmanager = \core_plugin_manager::instance();
+        $modplugins = $pluginmanager->get_enabled_plugins('mod');
+        $modoptions = [];
+
+        // Get the module-level defaults. data_registry::get_defaults falls back to this when there are no activity defaults.
+        list($levelpurpose, $levelcategory) = data_registry::get_defaults(CONTEXT_MODULE);
+        foreach ($modplugins as $name) {
+            // Check if we have default purpose and category for this module if we want don't want to fetch everything.
+            if ($nodefaults) {
+                list($purpose, $category) = data_registry::get_defaults(CONTEXT_MODULE, $name);
+                // Compare this with the module-level defaults.
+                if ($purpose !== $levelpurpose || $category !== $levelcategory) {
+                    // If the defaults for this activity has been already set, there's no need to add this in the list of options.
+                    continue;
+                }
+            }
+
+            $displayname = $pluginmanager->plugin_name('mod_' . $name);
+            $modoptions[] = (object)[
+                'name' => $name,
+                'displayname' => $displayname
+            ];
+        }
+
+        return [
+            'options' => $modoptions,
+            'warnings' => $warnings
+        ];
+    }
+
+    /**
+     * Returns for get_category_options().
+     *
+     * @return external_single_structure
+     */
+    public static function get_activity_options_returns() {
+        $optionsdefinition = new external_single_structure(
+            [
+                'name' => new external_value(PARAM_TEXT, 'The plugin name of the activity'),
+                'displayname' => new external_value(PARAM_TEXT, 'The display name of the activity'),
+            ]
+        );
+
+        return new external_single_structure([
+            'options' => new external_multiple_structure($optionsdefinition),
+            'warnings' => new external_warnings()
+        ]);
+    }
+
     /**
      * Gets the structure of a tree node (link + child branches).
      *
index c5c350c..06d2469 100644 (file)
@@ -25,6 +25,8 @@ namespace tool_dataprivacy\external;
 defined('MOODLE_INTERNAL') || die();
 
 use core\external\persistent_exporter;
+use tool_dataprivacy\category;
+use tool_dataprivacy\context_instance;
 
 /**
  * Class for exporting field data.
@@ -53,4 +55,25 @@ class category_exporter extends persistent_exporter {
             'context' => 'context',
         );
     }
+
+    /**
+     * Utility function that fetches a category name from the given ID.
+     *
+     * @param int $categoryid The category ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
+     * @return string The purpose name.
+     */
+    public static function get_name($categoryid) {
+        global $PAGE;
+        if ($categoryid === false || $categoryid == context_instance::INHERIT) {
+            return get_string('inherit', 'tool_dataprivacy');
+        } else if ($categoryid == context_instance::NOTSET) {
+            return get_string('notset', 'tool_dataprivacy');
+        } else {
+            $purpose = new category($categoryid);
+            $output = $PAGE->get_renderer('tool_dataprivacy');
+            $exporter = new self($purpose, ['context' => \context_system::instance()]);
+            $data = $exporter->export($output);
+            return $data->name;
+        }
+    }
 }
index f91e2f2..a6519c6 100644 (file)
@@ -29,6 +29,7 @@ use core\external\persistent_exporter;
 use DateInterval;
 use Exception;
 use renderer_base;
+use tool_dataprivacy\context_instance;
 use tool_dataprivacy\purpose;
 
 /**
@@ -143,4 +144,25 @@ class purpose_exporter extends persistent_exporter {
 
         return $values;
     }
+
+    /**
+     * Utility function that fetches a purpose name from the given ID.
+     *
+     * @param int $purposeid The purpose ID. Could be INHERIT (false, -1), NOT_SET (0), or the actual ID.
+     * @return string The purpose name.
+     */
+    public static function get_name($purposeid) {
+        global $PAGE;
+        if ($purposeid === false || $purposeid == context_instance::INHERIT) {
+            return get_string('inherit', 'tool_dataprivacy');
+        } else if ($purposeid == context_instance::NOTSET) {
+            return get_string('notset', 'tool_dataprivacy');
+        } else {
+            $purpose = new purpose($purposeid);
+            $output = $PAGE->get_renderer('tool_dataprivacy');
+            $exporter = new self($purpose, ['context' => \context_system::instance()]);
+            $data = $exporter->export($output);
+            return $data->name;
+        }
+    }
 }
diff --git a/admin/tool/dataprivacy/classes/form/defaults.php b/admin/tool/dataprivacy/classes/form/defaults.php
deleted file mode 100644 (file)
index 4e599c3..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * This file contains the defaults form.
- *
- * @package   tool_dataprivacy
- * @copyright 2018 David Monllao
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace tool_dataprivacy\form;
-defined('MOODLE_INTERNAL') || die();
-
-use \tool_dataprivacy\output\data_registry_page;
-
-require_once($CFG->libdir . '/formslib.php');
-require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
-
-/**
- * Context levels defaults form.
- *
- * @package   tool_dataprivacy
- * @copyright 2018 David Monllao
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class defaults extends \moodleform {
-
-    /**
-     * Define the form.
-     */
-    public function definition() {
-        global $OUTPUT;
-
-        $mform = $this->_form;
-        $mform->setDisableShortforms();
-
-        $notification = $OUTPUT->notification(get_string('defaultsinfo', 'tool_dataprivacy'),
-            \core\output\notification::NOTIFY_INFO);
-        $mform->addElement('html', $notification);
-
-        foreach ($this->_customdata['levels'] as $level => $classname) {
-
-            $mform->addElement('header', $classname . '-header',
-                get_string('contextlevelname' . $level, 'tool_dataprivacy'));
-
-            list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
-
-            $includeinherit = true;
-            if ($level == CONTEXT_SYSTEM) {
-                $includeinherit = false;
-            }
-
-            $categoryoptions = data_registry_page::category_options($this->_customdata['categories'], false, $includeinherit);
-            $purposeoptions = data_registry_page::category_options($this->_customdata['purposes'], false, $includeinherit);
-
-            $mform->addElement('select', $categoryvar, get_string('category', 'tool_dataprivacy'), $categoryoptions);
-            $mform->addHelpButton($categoryvar, 'categorydefault', 'tool_dataprivacy');
-            $mform->setType($categoryvar, PARAM_INT);
-
-            $mform->addElement('select', $purposevar, get_string('purpose', 'tool_dataprivacy'), $purposeoptions);
-            $mform->addHelpButton($purposevar, 'purposedefault', 'tool_dataprivacy');
-            $mform->setType($purposevar, PARAM_INT);
-        }
-
-        $this->add_action_buttons(true, get_string('savechanges'));
-    }
-}
index 8d5fd35..be0e996 100644 (file)
@@ -425,7 +425,7 @@ class data_registry_page implements renderable, templatable {
     /**
      * From a list of purpose persistents to a list of id => name purposes.
      *
-     * @param \tool_dataprivacy\purpose $purposes
+     * @param \tool_dataprivacy\purpose[] $purposes
      * @param bool $includenotset
      * @param bool $includeinherit
      * @return string[]
@@ -442,7 +442,7 @@ class data_registry_page implements renderable, templatable {
     /**
      * From a list of category persistents to a list of id => name categories.
      *
-     * @param \tool_dataprivacy\category $categories
+     * @param \tool_dataprivacy\category[] $categories
      * @param bool $includenotset
      * @param bool $includeinherit
      * @return string[]
diff --git a/admin/tool/dataprivacy/classes/output/defaults_page.php b/admin/tool/dataprivacy/classes/output/defaults_page.php
new file mode 100644 (file)
index 0000000..166228f
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class containing data for the data registry defaults.
+ *
+ * @package    tool_dataprivacy
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace tool_dataprivacy\output;
+defined('MOODLE_INTERNAL') || die();
+
+use action_menu_link_primary;
+use coding_exception;
+use moodle_exception;
+use moodle_url;
+use renderable;
+use renderer_base;
+use stdClass;
+use templatable;
+use tool_dataprivacy\data_registry;
+use tool_dataprivacy\external\category_exporter;
+use tool_dataprivacy\external\purpose_exporter;
+
+/**
+ * Class containing data for the data registry defaults.
+ *
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class defaults_page implements renderable, templatable {
+
+    /** @var int $mode The display mode. */
+    protected $mode = null;
+
+    /** @var int $category The default category for the given mode. */
+    protected $category = null;
+
+    /** @var int $purpose The default purpose for the given mode. */
+    protected $purpose = null;
+
+    /** @var stdClass[] $otherdefaults Other defaults for the given mode. */
+    protected $otherdefaults = [];
+
+    /** @var bool $canedit Whether editing is allowed. */
+    protected $canedit = false;
+
+    /**
+     * Construct this renderable.
+     *
+     * @param int $mode The display mode.
+     * @param int $category The default category for the given mode.
+     * @param int $purpose The default purpose for the given mode.
+     * @param stdClass[] $otherdefaults Other defaults for the given mode.
+     * @param bool $canedit Whether editing is allowed.
+     */
+    public function __construct($mode, $category, $purpose, $otherdefaults = [], $canedit = false) {
+        $this->mode = $mode;
+        $this->category = $category;
+        $this->purpose = $purpose;
+        $this->otherdefaults = $otherdefaults;
+        $this->canedit = $canedit;
+    }
+
+    /**
+     * Export this data so it can be used as the context for a mustache template.
+     *
+     * @param renderer_base $output
+     * @return stdClass
+     * @throws coding_exception
+     * @throws moodle_exception
+     */
+    public function export_for_template(renderer_base $output) {
+        $data = new stdClass();
+
+        // Set tab URLs.
+        $coursecaturl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSECAT]);
+        $courseurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_COURSE]);
+        $moduleurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_MODULE]);
+        $blockurl = new moodle_url('/admin/tool/dataprivacy/defaults.php', ['mode' => CONTEXT_BLOCK]);
+        $data->coursecaturl = $coursecaturl;
+        $data->courseurl = $courseurl;
+        $data->moduleurl = $moduleurl;
+        $data->blockurl = $blockurl;
+
+        // Set display mode.
+        switch ($this->mode) {
+            case CONTEXT_COURSECAT:
+                $data->modecoursecat = true;
+                break;
+            case CONTEXT_COURSE:
+                $data->modecourse = true;
+                break;
+            case CONTEXT_MODULE:
+                $data->modemodule = true;
+                break;
+            case CONTEXT_BLOCK:
+                $data->modeblock = true;
+                break;
+            default:
+                $data->modecoursecat = true;
+                break;
+        }
+
+        // Set config variables.
+        $configname = \context_helper::get_class_for_level($this->mode);
+        list($purposevar, $categoryvar) = data_registry::var_names_from_context($configname);
+        $data->categoryvar = $categoryvar;
+        $data->purposevar = $purposevar;
+
+        // Set default category.
+        $data->categoryid = $this->category;
+        $data->category = category_exporter::get_name($this->category);
+
+        // Set default purpose.
+        $data->purposeid = $this->purpose;
+        $data->purpose = purpose_exporter::get_name($this->purpose);
+
+        // Set other defaults.
+        $otherdefaults = [];
+        $url = new moodle_url('#');
+        foreach ($this->otherdefaults as $pluginname => $values) {
+            $defaults = [
+                'name' => $values->name,
+                'category' => category_exporter::get_name($values->category),
+                'purpose' => purpose_exporter::get_name($values->purpose),
+            ];
+            if ($this->canedit) {
+                $actions = [];
+                // Edit link.
+                $editattrs = [
+                    'data-action' => 'edit-activity-defaults',
+                    'data-contextlevel' => $this->mode,
+                    'data-activityname' => $pluginname,
+                    'data-category' => $values->category,
+                    'data-purpose' => $values->purpose,
+                ];
+                $editlink = new action_menu_link_primary($url, new \pix_icon('t/edit', get_string('edit')),
+                    get_string('edit'), $editattrs);
+                $actions[] = $editlink->export_for_template($output);
+
+                // Delete link.
+                $deleteattrs = [
+                    'data-action' => 'delete-activity-defaults',
+                    'data-contextlevel' => $this->mode,
+                    'data-activityname' => $pluginname,
+                    'data-activitydisplayname' => $values->name,
+                ];
+                $deletelink = new action_menu_link_primary($url, new \pix_icon('t/delete', get_string('delete')),
+                    get_string('delete'), $deleteattrs);
+                $actions[] = $deletelink->export_for_template($output);
+
+                $defaults['actions'] = $actions;
+            }
+            $otherdefaults[] = (object)$defaults;
+        }
+        $data->otherdefaults = $otherdefaults;
+
+        $data->canedit = $this->canedit;
+        $data->contextlevel = $this->mode;
+
+        return $data;
+    }
+}
index 9c71e8c..b9e6077 100644 (file)
@@ -173,4 +173,44 @@ $functions = [
         'ajax'          => true,
         'loginrequired' => true,
     ],
+    'tool_dataprivacy_set_context_defaults' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'set_context_defaults',
+        'classpath'     => '',
+        'description'   => 'Updates the default category and purpose for a given context level (and optionally, a plugin)',
+        'type'          => 'write',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
+    'tool_dataprivacy_get_category_options' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'get_category_options',
+        'classpath'     => '',
+        'description'   => 'Fetches a list of data category options',
+        'type'          => 'read',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
+    'tool_dataprivacy_get_purpose_options' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'get_purpose_options',
+        'classpath'     => '',
+        'description'   => 'Fetches a list of data storage purpose options',
+        'type'          => 'read',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
+    'tool_dataprivacy_get_activity_options' => [
+        'classname'     => 'tool_dataprivacy\external',
+        'methodname'    => 'get_activity_options',
+        'classpath'     => '',
+        'description'   => 'Fetches a list of activity options',
+        'type'          => 'read',
+        'capabilities'  => 'tool/dataprivacy:managedataregistry',
+        'ajax'          => true,
+        'loginrequired' => true,
+    ],
 ];
index d936ba2..9faad87 100644 (file)
@@ -32,47 +32,39 @@ $title = get_string('setdefaults', 'tool_dataprivacy');
 
 \tool_dataprivacy\page_helper::setup($url, $title, 'dataregistry');
 
-$levels = \context_helper::get_all_levels();
-// They are set through the context level site and user.
-unset($levels[CONTEXT_SYSTEM]);
-unset($levels[CONTEXT_USER]);
+$mode = optional_param('mode', CONTEXT_COURSECAT, PARAM_INT);
+$classname = context_helper::get_class_for_level($mode);
+list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
+$purpose = get_config('tool_dataprivacy', $purposevar);
+$category = get_config('tool_dataprivacy', $categoryvar);
 
-$customdata = [
-    'levels' => $levels,
-    'purposes' => \tool_dataprivacy\api::get_purposes(),
-    'categories' => \tool_dataprivacy\api::get_categories(),
-];
-$form = new \tool_dataprivacy\form\defaults($PAGE->url->out(false), $customdata);
+$otherdefaults = [];
+if ($mode == CONTEXT_MODULE) {
+    // Get activity module plugin info.
+    $pluginmanager = core_plugin_manager::instance();
+    $modplugins = $pluginmanager->get_enabled_plugins('mod');
 
-$toform = new stdClass();
-foreach ($levels as $level => $classname) {
-    list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
-    $toform->{$purposevar} = get_config('tool_dataprivacy', $purposevar);
-    $toform->{$categoryvar} = get_config('tool_dataprivacy', $categoryvar);
-}
-$form->set_data($toform);
-
-$returnurl = new \moodle_url('/admin/tool/dataprivacy/dataregistry.php');
-if ($form->is_cancelled()) {
-    redirect($returnurl);
-} else if ($data = $form->get_data()) {
-
-    foreach ($levels as $level => $classname) {
-
-        list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname);
-
-        if (isset($data->{$purposevar})) {
-            set_config($purposevar, $data->{$purposevar}, 'tool_dataprivacy');
-        }
-        if (isset($data->{$categoryvar})) {
-            set_config($categoryvar, $data->{$categoryvar}, 'tool_dataprivacy');
+    foreach ($modplugins as $name) {
+        list($purposevar, $categoryvar) = \tool_dataprivacy\data_registry::var_names_from_context($classname, $name);
+        $plugincategory = get_config('tool_dataprivacy', $categoryvar);
+        $pluginpurpose = get_config('tool_dataprivacy', $purposevar);
+        if ($plugincategory === false && $pluginpurpose === false) {
+            // If no purpose and category has been set for this plugin, then there's no need to show this on the list.
+            continue;
         }
+
+        $displayname = $pluginmanager->plugin_name('mod_' . $name);
+        $otherdefaults[$name] = (object)[
+            'name' => $displayname,
+            'category' => $plugincategory,
+            'purpose' => $pluginpurpose,
+        ];
     }
-    redirect($returnurl, get_string('defaultssaved', 'tool_dataprivacy'),
-        0, \core\output\notification::NOTIFY_SUCCESS);
 }
 
+$defaultspage = new \tool_dataprivacy\output\defaults_page($mode, $category, $purpose, $otherdefaults, true);
+
 $output = $PAGE->get_renderer('tool_dataprivacy');
 echo $output->header();
-$form->display();
+echo $output->render_from_template('tool_dataprivacy/defaults_page', $defaultspage->export_for_template($output));
 echo $output->footer();
index 822f251..a5cfb5b 100644 (file)
@@ -28,6 +28,7 @@ $string['pluginname'] = 'Data privacy';
 $string['pluginname_help'] = 'Data privacy plugin';
 $string['activitiesandresources'] = 'Activities and resources';
 $string['addcategory'] = 'Add category';
+$string['addnewdefaults'] = 'Add a new module default';
 $string['addpurpose'] = 'Add purpose';
 $string['approve'] = 'Approve';
 $string['approverequest'] = 'Approve request';
@@ -76,9 +77,12 @@ $string['datarequests'] = 'Data requests';
 $string['datecomment'] = '[{$a->date}]: ' . PHP_EOL . ' {$a->comment}';
 $string['daterequested'] = 'Date requested';
 $string['daterequesteddetail'] = 'Date requested:';
-$string['defaultsinfo'] = 'Default categories and purposes are applied to all newly created instances.';
+$string['defaultsinfo'] = 'Default categories and purposes are applied to all new and existing instances where a value is not set.';
+$string['defaultswarninginfo'] = 'Warning: Changing these defaults may affect the retention period of existing instances.';
 $string['deletecategory'] = 'Delete category';
 $string['deletecategorytext'] = 'Are you sure you want to delete the category \'{$a}\'?';
+$string['deletedefaults'] = 'Delete defaults: {$a}';
+$string['deletedefaultsconfirmation'] = 'Are you sure you want to delete the default category and purpose for {$a} modules?';
 $string['deleteexpiredcontextstask'] = 'Delete expired contexts';
 $string['deleteexpireddatarequeststask'] = 'Delete files from completed data requests that have expired';
 $string['deletepurpose'] = 'Delete purpose';
@@ -95,6 +99,8 @@ $string['dporolemapping_desc'] = 'The privacy officer can manage data requests.
 $string['editcategories'] = 'Edit categories';
 $string['editcategory'] = 'Edit category';
 $string['editcategories'] = 'Edit categories';
+$string['editdefaults'] = 'Edit defaults: {$a}';
+$string['editmoduledefaults'] = 'Edit module defaults';
 $string['editpurpose'] = 'Edit purpose';
 $string['editpurposes'] = 'Edit purposes';
 $string['effectiveretentionperiodcourse'] = '{$a} (after the course end date)';
@@ -185,6 +191,7 @@ $string['nopurposes'] = 'There are no purposes yet';
 $string['nosubjectaccessrequests'] = 'There are no data requests that you need to act on';
 $string['nosystemdefaults'] = 'Site purpose and category have not yet been defined.';
 $string['notset'] = 'Not set (use the default value)';
+$string['overrideinstances'] = 'Reset instances with custom values';
 $string['pluginregistry'] = 'Plugin privacy registry';
 $string['pluginregistrytitle'] = 'Plugin privacy compliance registry';
 $string['privacy'] = 'Privacy';
index e6ddf93..7a7015a 100644 (file)
@@ -28,3 +28,7 @@ dd a.contactdpo {
 [data-region="data-requests-table"] .moodle-actionmenu {
     min-width: 150px;
 }
+
+.context-level-view {
+    margin: 1em;
+}
\ No newline at end of file
diff --git a/admin/tool/dataprivacy/templates/category_purpose_form.mustache b/admin/tool/dataprivacy/templates/category_purpose_form.mustache
new file mode 100644 (file)
index 0000000..c5d0ed4
--- /dev/null
@@ -0,0 +1,99 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_dataprivacy/category_purpose_form
+
+    Manage data registry defaults.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+
+    Context variables required for this template:
+    * actionurl String The action URL.
+    * contextlevel Number - The context level.
+    * modemodule Boolean - Whether to display the activity select element.
+    * activityoptions Array - An array of objects for the activity select element.
+    * categoryoptions Array - An array of objects for the category select element.
+    * purposeoptions Array - An array of objects for the purpose select element.
+
+    Example context (json):
+    {
+        "actionurl": "#",
+        "contextlevel": 70,
+        "newactivitydefaults": true,
+        "modemodule": true,
+        "activityoptions": [
+            { "name": "assign", "displayname": "Assignment" },
+            { "name": "forum", "displayname": "Forum", "selected": true },
+            { "name": "lesson", "displayname": "Lesson" },
+            { "name": "quiz", "displayname": "Quiz" }
+        ],
+        "categoryoptions": [
+            { "id": 1, "name": "Category 1" },
+            { "id": 2, "name": "Category 2", "selected": true },
+            { "id": 3, "name": "Category 3" }
+        ],
+        "purposeoptions": [
+            { "id": 1, "name": "Purpose 1" },
+            { "id": 2, "name": "Purpose 2" },
+            { "id": 3, "name": "Purpose 3", "selected": true }
+        ]
+    }
+}}
+
+<div class="alert alert-warning" role="alert">
+    {{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
+</div>
+<form method="post" action="{{actionurl}}" id="category_purpose_form">
+    <input type="hidden" value="{{contextlevel}}" id="contextlevel" />
+    {{#modemodule}}
+    <div class="form-group">
+        <label for="activity">{{#str}}activitymodule{{/str}}</label>
+        {{^newactivitydefaults}}
+            <input type="hidden" id="activity" value="{{#activityoptions}}{{#selected}}{{name}}{{/selected}}{{/activityoptions}}" />
+        {{/newactivitydefaults}}
+        <select class="form-control" {{#newactivitydefaults}}id="activity" {{/newactivitydefaults}}{{^newactivitydefaults}}disabled{{/newactivitydefaults}}>
+            {{#activityoptions}}
+                <option value="{{name}}" {{#selected}}selected{{/selected}}>{{displayname}}</option>
+            {{/activityoptions}}
+        </select>
+    </div>
+    {{/modemodule}}
+    <div class="form-group">
+        <label for="category">{{#str}}category, tool_dataprivacy{{/str}}</label>
+        <select class="form-control" id="category">
+        {{#categoryoptions}}
+            <option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+        {{/categoryoptions}}
+        </select>
+    </div>
+    <div class="form-group">
+        <label for="purpose">{{#str}}purpose, tool_dataprivacy{{/str}}</label>
+        <select class="form-control" id="purpose">
+        {{#purposeoptions}}
+            <option value="{{id}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+        {{/purposeoptions}}
+        </select>
+    </div>
+    <div class="form-check">
+        <input class="form-check-input" type="checkbox" value="1" id="override">
+        <label class="form-check-label" for="override">
+            {{#str}}overrideinstances, tool_dataprivacy{{/str}}
+        </label>
+    </div>
+</form>
diff --git a/admin/tool/dataprivacy/templates/defaults_display.mustache b/admin/tool/dataprivacy/templates/defaults_display.mustache
new file mode 100644 (file)
index 0000000..39a29aa
--- /dev/null
@@ -0,0 +1,74 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_dataprivacy/defaults_display
+
+    Renders the current default category and purpose.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+
+    Context variables required for this template:
+    * contextlevel Number - The context level.
+    * categoryid Number - The category ID.
+    * purposeid Number - The purpose ID.
+    * category String - The category name.
+    * purpose String - The purpose name.
+    * canedit Boolean - Whether this is being rendered for editing purposes.
+
+    Example context (json):
+    {
+        "category": "Awesome default category",
+        "categoryid": 1,
+        "purpose": "Awesome default purpose",
+        "purposeid": 2,
+        "canedit": true,
+        "contextlevel": 70
+    }
+}}
+<div class="row-fluid rtl-compatible m-t-1 m-b-1">
+    <div class="col-md-9 span9">
+        <div class="row-fluid rtl-compatible m-t-1 m-b-1">
+            <div class="col-md-3 span3">
+                <strong>{{#str}}category, tool_dataprivacy{{/str}}</strong>
+            </div>
+            <div class="col-md-9 span9">
+                {{category}}
+            </div>
+        </div>
+        <div class="row-fluid rtl-compatible m-t-1 m-b-1">
+            <div class="col-md-3 span3">
+                <strong>{{#str}}purpose, tool_dataprivacy{{/str}}</strong>
+            </div>
+            <div class="col-md-9 span9">
+                {{purpose}}
+            </div>
+        </div>
+    </div>
+    <div class="col-md-3 span3 align-self-center">
+    {{#canedit}}
+        <button class="btn btn-default" {{!
+            }}data-action="edit-level-defaults" {{!
+            }}data-contextlevel="{{contextlevel}}" {{!
+            }}data-category="{{categoryid}}" {{!
+            }}data-purpose="{{purposeid}}">
+            {{#str}}edit{{/str}}
+        </button>
+    {{/canedit}}
+    </div>
+</div>
diff --git a/admin/tool/dataprivacy/templates/defaults_page.mustache b/admin/tool/dataprivacy/templates/defaults_page.mustache
new file mode 100644 (file)
index 0000000..bb531da
--- /dev/null
@@ -0,0 +1,147 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_dataprivacy/defaults_page
+
+    Manage data registry defaults.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+
+    Context variables required for this template:
+    * contextlevel Number - The context level.
+    * modecoursecat Boolean - Whether we're displaying defaults for course categories.
+    * modecourse Boolean - Whether we're displaying defaults for courses.
+    * modemodule Boolean - Whether we're displaying defaults for activity modules.
+    * modeblock Boolean - Whether we're displaying defaults for blocks.
+    * coursecaturl String - The URL for the course category defaults tab.
+    * courseurl String - The URL for the course defaults tab.
+    * moduleurl String - The URL for the activity module defaults tab.
+    * blockurl String - The URL for the block defaults tab.
+    * purposeid Number - The purpose ID for this context level.
+    * canedit Boolean - Whether this is being rendered for editing purposes.
+    * categoryid Number - The ID of the default category for this context level.
+    * purposeid Number - The ID of the default purpose for this context level.
+    * category String - The category name.
+    * purpose String - The purpose name.
+    * otherdefaults Array - An array containing the defaults for the activity modules.
+
+    Example context (json):
+    {
+        "contextlevel": 70,
+        "modecoursecat": false,
+        "modecourse": false,
+        "modemodule": true,
+        "modeblock": false,
+        "coursecaturl": "#",
+        "courseurl": "#",
+        "moduleurl": "#",
+        "blockurl": "#",
+        "category": "Awesome default category",
+        "purpose": "Awesome default purpose",
+        "canedit": true,
+        "otherdefaults": [
+            {
+                "name": "Assignment",
+                "category": "Category for activity modules",
+                "purpose": "Assessments"
+            },
+            {
+                "name": "Forum",
+                "category": "Category for activity modules",
+                "purpose": "Social interactions"
+            }
+        ]
+    }
+}}
+<div class="card">
+    <div class="card-header">
+        <ul class="nav nav-tabs card-header-tabs">
+            <li class="nav-item">
+                <a class="nav-link {{#modecoursecat}}active{{/modecoursecat}}" href="{{coursecaturl}}">{{#str}}categories{{/str}}</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link {{#modecourse}}active{{/modecourse}}" href="{{courseurl}}">{{#str}}courses{{/str}}</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link {{#modemodule}}active{{/modemodule}}" href="{{moduleurl}}">{{#str}}activitymodules{{/str}}</a>
+            </li>
+            <li class="nav-item">
+                <a class="nav-link {{#modeblock}}active{{/modeblock}}" href="{{blockurl}}">{{#str}}blocks{{/str}}</a>
+            </li>
+        </ul>
+    </div>
+    <div class="card-body context-level-view">
+        <div class="alert alert-primary" role="alert">
+            {{#str}}defaultsinfo, tool_dataprivacy{{/str}}
+        </div>
+        <h4 class="card-title" id="defaults-header">
+            {{#modecoursecat}}{{#str}}categories{{/str}}{{/modecoursecat}}
+            {{#modecourse}}{{#str}}courses{{/str}}{{/modecourse}}
+            {{#modemodule}}{{#str}}activitymodules{{/str}}{{/modemodule}}
+            {{#modeblock}}{{#str}}blocks{{/str}}{{/modeblock}}
+        </h4>
+        <div>
+            {{> tool_dataprivacy/defaults_display}}
+            {{#canedit}}
+                {{#modemodule}}
+                <button class="btn btn-primary" data-action="new-activity-defaults" data-contextlevel="{{contextlevel}}">
+                    {{#str}}addnewdefaults, tool_dataprivacy{{/str}}
+                </button>
+                {{/modemodule}}
+            {{/canedit}}
+            {{#modemodule}}
+            <table class="m-t-1 table table-striped">
+                <thead>
+                    <tr>
+                        <th>&nbsp;</th>
+                        <th scope="col">{{#str}}category, tool_dataprivacy{{/str}}</th>
+                        <th scope="col">{{#str}}purpose, tool_dataprivacy{{/str}}</th>
+                        {{#canedit}}
+                            <th scope="col">{{#str}}actions{{/str}}</th>
+                        {{/canedit}}
+                    </tr>
+                </thead>
+                <tbody>
+                    {{#otherdefaults}}
+                    <tr>
+                        <th scope="row">{{name}}</th>
+                        <td>{{category}}</td>
+                        <td>{{purpose}}</td>
+                        {{#canedit}}
+                            <td>
+                                {{#actions}}
+                                    {{> core/action_menu_link}}
+                                {{/actions}}
+                            </td>
+                        {{/canedit}}
+                    </tr>
+                    {{/otherdefaults}}
+                </tbody>
+            </table>
+            {{/modemodule}}
+        </div>
+    </div>
+</div>
+
+{{#js}}
+// Initialise the JS.
+require(['tool_dataprivacy/defaultsactions'], function(ActionsMod) {
+    ActionsMod.init();
+});
+{{/js}}
diff --git a/admin/tool/dataprivacy/templates/delete_activity_defaults.mustache b/admin/tool/dataprivacy/templates/delete_activity_defaults.mustache
new file mode 100644 (file)
index 0000000..c0220d3
--- /dev/null
@@ -0,0 +1,41 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template tool_dataprivacy/delete_activity_defaults
+
+    Renders the confirmation.
+
+    Classes required for JS:
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * activityname String - The activity name.
+
+    Example context (json):
+    {
+        "activityname": "Assignment"
+    }
+}}
+
+<div class="alert alert-warning" role="alert">
+    {{#str}}defaultswarninginfo, tool_dataprivacy{{/str}}
+</div>
+<div>
+    {{#str}}deletedefaultsconfirmation, tool_dataprivacy, {{activityname}}{{/str}}
+</div>
index 9feebcf..820611c 100644 (file)
@@ -124,6 +124,47 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->assertEquals($u1->id, $dpo->id);
     }
 
+    /**
+     * Test for \tool_dataprivacy\api::get_assigned_privacy_officer_roles().
+     */
+    public function test_get_assigned_privacy_officer_roles() {
+        global $DB;
+
+        // Erroneously set the manager roles as the PO, even if it doesn't have the managedatarequests capability yet.
+        $managerroleid = $DB->get_field('role', 'id', array('shortname' => 'manager'));
+        set_config('dporoles', $managerroleid, 'tool_dataprivacy');
+        // Get the assigned PO roles when nothing has been set yet.
+        $roleids = api::get_assigned_privacy_officer_roles();
+        // Confirm that the returned list is empty.
+        $this->assertEmpty($roleids);
+
+        $context = context_system::instance();
+
+        // Give the manager role with the capability to manage data requests.
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $managerroleid, $context->id, true);
+
+        // Give the editing teacher role with the capability to manage data requests.
+        $editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
+        assign_capability('tool/dataprivacy:managedatarequests', CAP_ALLOW, $editingteacherroleid, $context->id, true);
+
+        // Get the non-editing teacher role ID.
+        $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'teacher'));
+
+        // Erroneously map the manager and the non-editing teacher roles to the PO role.
+        $badconfig = $managerroleid . ',' . $teacherroleid;
+        set_config('dporoles', $badconfig, 'tool_dataprivacy');
+
+        // Get the assigned PO roles.
+        $roleids = api::get_assigned_privacy_officer_roles();
+
+        // There should only be one PO role.
+        $this->assertCount(1, $roleids);
+        // Confirm it contains the manager role.
+        $this->assertContains($managerroleid, $roleids);
+        // And it does not contain the editing teacher role.
+        $this->assertNotContains($editingteacherroleid, $roleids);
+    }
+
     /**
      * Test for api::approve_data_request().
      */
@@ -1321,6 +1362,164 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
         $this->assertEquals($data->contexts->used, $contextids, '', 0.0, 10, true);
     }
 
+    /**
+     * Data provider for \tool_dataprivacy_api_testcase::test_set_context_defaults
+     */
+    public function set_context_defaults_provider() {
+        $contextlevels = [
+            [CONTEXT_COURSECAT],
+            [CONTEXT_COURSE],
+            [CONTEXT_MODULE],
+            [CONTEXT_BLOCK],
+        ];
+        $paramsets = [
+            [true, true, false, false], // Inherit category and purpose, Not for activity, Don't override.
+            [true, false, false, false], // Inherit category but not purpose, Not for activity, Don't override.
+            [false, true, false, false], // Inherit purpose but not category, Not for activity, Don't override.
+            [false, false, false, false], // Don't inherit both category and purpose, Not for activity, Don't override.
+            [false, false, false, true], // Don't inherit both category and purpose, Not for activity, Override instances.
+        ];
+        $data = [];
+        foreach ($contextlevels as $level) {
+            foreach ($paramsets as $set) {
+                $data[] = array_merge($level, $set);
+            }
+            if ($level == CONTEXT_MODULE) {
+                // Add a combination where defaults for activity is being set.
+                $data[] = [CONTEXT_MODULE, false, false, true, false];
+                $data[] = [CONTEXT_MODULE, false, false, true, true];
+            }
+        }
+        return $data;
+    }
+
+    /**
+     * Test for \tool_dataprivacy\api::set_context_defaults()
+     *
+     * @dataProvider set_context_defaults_provider
+     * @param int $contextlevel The context level
+     * @param bool $inheritcategory Whether to set category value as INHERIT.
+     * @param bool $inheritpurpose Whether to set purpose value as INHERIT.
+     * @param bool $foractivity Whether to set defaults for an activity.
+     * @param bool $override Whether to override instances.
+     */
+    public function test_set_context_defaults($contextlevel, $inheritcategory, $inheritpurpose, $foractivity, $override) {
+        $this->setAdminUser();
+
+        $generator = $this->getDataGenerator();
+
+        // Generate course cat, course, block, assignment, forum instances.
+        $coursecat = $generator->create_category();
+        $course = $generator->create_course(['category' => $coursecat->id]);
+        $block = $generator->create_block('online_users');
+        $assign = $generator->create_module('assign', ['course' => $course->id]);
+        $forum = $generator->create_module('forum', ['course' => $course->id]);
+
+        $coursecatcontext = context_coursecat::instance($coursecat->id);
+        $coursecontext = context_course::instance($course->id);
+        $blockcontext = context_block::instance($block->id);
+
+        list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
+        list($course, $forumcm) = get_course_and_cm_from_instance($forum->id, 'forum');
+        $assigncontext = context_module::instance($assigncm->id);
+        $forumcontext = context_module::instance($forumcm->id);
+
+        // Generate purposes and categories.
+        $category1 = api::create_category((object)['name' => 'Test category 1']);
+        $category2 = api::create_category((object)['name' => 'Test category 2']);
+        $purpose1 = api::create_purpose((object)[
+            'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+        $purpose2 = api::create_purpose((object)[
+            'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+
+        // Assign purposes and categories to contexts.
+        $coursecatctxinstance = api::set_context_instance((object) [
+            'contextid' => $coursecatcontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $coursectxinstance = api::set_context_instance((object) [
+            'contextid' => $coursecontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $blockctxinstance = api::set_context_instance((object) [
+            'contextid' => $blockcontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $assignctxinstance = api::set_context_instance((object) [
+            'contextid' => $assigncontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+        $forumctxinstance = api::set_context_instance((object) [
+            'contextid' => $forumcontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+
+        $categoryid = $inheritcategory ? context_instance::INHERIT : $category2->get('id');
+        $purposeid = $inheritpurpose ? context_instance::INHERIT : $purpose2->get('id');
+        $activity = '';
+        if ($contextlevel == CONTEXT_MODULE && $foractivity) {
+            $activity = 'assign';
+        }
+        $result = api::set_context_defaults($contextlevel, $categoryid, $purposeid, $activity, $override);
+        $this->assertTrue($result);
+
+        $targetctxinstance = false;
+        switch ($contextlevel) {
+            case CONTEXT_COURSECAT:
+                $targetctxinstance = $coursecatctxinstance;
+                break;
+            case CONTEXT_COURSE:
+                $targetctxinstance = $coursectxinstance;
+                break;
+            case CONTEXT_MODULE:
+                $targetctxinstance = $assignctxinstance;
+                break;
+            case CONTEXT_BLOCK:
+                $targetctxinstance = $blockctxinstance;
+                break;
+        }
+        $this->assertNotFalse($targetctxinstance);
+
+        // Check the context instances.
+        $instanceexists = context_instance::record_exists($targetctxinstance->get('id'));
+        if ($override) {
+            // If overridden, context instances on this context level would have been deleted.
+            $this->assertFalse($instanceexists);
+
+            // Check forum context instance.
+            $forumctxexists = context_instance::record_exists($forumctxinstance->get('id'));
+            if ($contextlevel != CONTEXT_MODULE || $foractivity) {
+                // The forum context instance won't be affected in this test if:
+                // - The overridden defaults are not for context modules.
+                // - Only the defaults for assign have been set.
+                $this->assertTrue($forumctxexists);
+            } else {
+                // If we're overriding for the whole course module context level,
+                // then this forum context instance will be deleted as well.
+                $this->assertFalse($forumctxexists);
+            }
+        } else {
+            // Otherwise, the context instance record remains.
+            $this->assertTrue($instanceexists);
+        }
+
+        // Check defaults.
+        list($defaultpurpose, $defaultcategory) = data_registry::get_defaults($contextlevel, $activity);
+        if (!$inheritpurpose) {
+            $this->assertEquals($purposeid, $defaultpurpose);
+        }
+        if (!$inheritcategory) {
+            $this->assertEquals($categoryid, $defaultcategory);
+        }
+    }
+
     /**
      * Perform setup for the test_add_request_contexts_with_status_xxxxx tests.
      *
diff --git a/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php b/admin/tool/dataprivacy/tests/behat/behat_tool_dataprivacy.php
new file mode 100644 (file)
index 0000000..857bb33
--- /dev/null
@@ -0,0 +1,284 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Step definitions to generate database fixtures for the data privacy tool.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+
+use Behat\Gherkin\Node\TableNode as TableNode;
+use Behat\Behat\Tester\Exception\PendingException as PendingException;
+use tool_dataprivacy\api;
+
+/**
+ * Step definitions to generate database fixtures for the data privacy tool.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_tool_dataprivacy extends behat_base {
+
+    /**
+     * Each element specifies:
+     * - The data generator suffix used.
+     * - The required fields.
+     * - The mapping between other elements references and database field names.
+     * @var array
+     */
+    protected static $elements = array(
+        'categories' => array(
+            'datagenerator' => 'category',
+            'required' => array()
+        ),
+        'purposes' => array(
+            'datagenerator' => 'purpose',
+            'required' => array()
+        ),
+    );
+
+    /**
+     * Creates the specified element. More info about available elements in http://docs.moodle.org/dev/Acceptance_testing#Fixtures.
+     *
+     * @Given /^the following data privacy "(?P<element_string>(?:[^"]|\\")*)" exist:$/
+     *
+     * @param string    $elementname The name of the entity to add
+     * @param TableNode $data
+     */
+    public function the_following_data_categories_exist($elementname, TableNode $data) {
+
+        // Now that we need them require the data generators.
+        require_once(__DIR__.'/../../../../../lib/phpunit/classes/util.php');
+
+        if (empty(self::$elements[$elementname])) {
+            throw new PendingException($elementname . ' data generator is not implemented');
+        }
+
+        $datagenerator = testing_util::get_data_generator();
+        $dataprivacygenerator = $datagenerator->get_plugin_generator('tool_dataprivacy');
+
+        $elementdatagenerator = self::$elements[$elementname]['datagenerator'];
+        $requiredfields = self::$elements[$elementname]['required'];
+        if (!empty(self::$elements[$elementname]['switchids'])) {
+            $switchids = self::$elements[$elementname]['switchids'];
+        }
+
+        foreach ($data->getHash() as $elementdata) {
+
+            // Check if all the required fields are there.
+            foreach ($requiredfields as $requiredfield) {
+                if (!isset($elementdata[$requiredfield])) {
+                    throw new Exception($elementname . ' requires the field ' . $requiredfield . ' to be specified');
+                }
+            }
+
+            // Switch from human-friendly references to ids.
+            if (isset($switchids)) {
+                foreach ($switchids as $element => $field) {
+                    $methodname = 'get_' . $element . '_id';
+
+                    // Not all the switch fields are required, default vars will be assigned by data generators.
+                    if (isset($elementdata[$element])) {
+                        // Temp $id var to avoid problems when $element == $field.
+                        $id = $this->{$methodname}($elementdata[$element]);
+                        unset($elementdata[$element]);
+                        $elementdata[$field] = $id;
+                    }
+                }
+            }
+
+            // Preprocess the entities that requires a special treatment.
+            if (method_exists($this, 'preprocess_' . $elementdatagenerator)) {
+                $elementdata = $this->{'preprocess_' . $elementdatagenerator}($elementdata);
+            }
+
+            // Creates element.
+            $methodname = 'create_' . $elementdatagenerator;
+            if (method_exists($dataprivacygenerator, $methodname)) {
+                // Using data generators directly.
+                $dataprivacygenerator->{$methodname}($elementdata);
+
+            } else if (method_exists($this, 'process_' . $elementdatagenerator)) {
+                // Using an alternative to the direct data generator call.
+                $this->{'process_' . $elementdatagenerator}($elementdata);
+            } else {
+                throw new PendingException($elementname . ' data generator is not implemented');
+            }
+        }
+    }
+
+    /**
+     * Sets the data category and data storage purpose for the site.
+     *
+     * @Given /^I set the site category and purpose to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_site_category_and_purpose($category, $purpose) {
+        $category = \tool_dataprivacy\category::get_record(['name' => $category]);
+        $purpose = \tool_dataprivacy\purpose::get_record(['name' => $purpose]);
+        $data = (object)[
+            'contextlevel' => CONTEXT_SYSTEM,
+            'categoryid' => $category->get('id'),
+            'purposeid' => $purpose->get('id'),
+        ];
+        api::set_contextlevel($data);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course category instance.
+     *
+     * @Given /^I set the category and purpose for the course category "(?P<categoryname_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the name or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_course_category($name, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'name' => $name,
+            'idnumber' => $name,
+        ];
+        $select = 'name = :name OR idnumber = :idnumber';
+        $coursecatid = $DB->get_field_select('course_categories', 'id', $select, $params, MUST_EXIST);
+        $context = context_coursecat::instance($coursecatid);
+
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course instance.
+     *
+     * @Given /^I set the category and purpose for the course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the fullname or the shortname, or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_course($name, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'shortname' => $name,
+            'fullname' => $name,
+            'idnumber' => $name,
+        ];
+        $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+        $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+        $context = context_course::instance($courseid);
+
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course instance.
+     *
+     * @Given /^I set the category and purpose for the "(?P<activityname_string>(?:[^"]|\\")*)" "(?P<activitytype_string>(?:[^"]|\\")*)" in course "(?P<coursename_string>(?:[^"]|\\")*)" to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the name of the activity.
+     * @param string $type The activity type. E.g. assign, quiz, forum, etc.
+     * @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_activity($name, $type, $coursename, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'shortname' => $coursename,
+            'fullname' => $coursename,
+            'idnumber' => $coursename,
+        ];
+        $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+        $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+
+        $cmid = null;
+        $cms = get_coursemodules_in_course($type, $courseid);
+        foreach ($cms as $cm) {
+            if ($cm->name === $name || $cm->idnumber === $name) {
+                $cmid = $cm->id;
+                break;
+            }
+        }
+        if ($cmid === null) {
+            throw new coding_exception("Activity module '{$name}' of type '{$type}' not found!");
+        }
+        $context = context_module::instance($cmid);
+
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the data category and data storage purpose for a course instance.
+     *
+     * @Given /^I set the category and purpose for the "(?P<blockname_string>(?:[^"]|\\")*)" block in the "(?P<coursename_string>(?:[^"]|\\")*)" course to "(?P<category_string>(?:[^"]|\\")*)" and "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     *
+     * @param string $name The instance name. It should match the name of the block. (e.g. online_users)
+     * @param string $coursename The course name. It should match the fullname or the shortname, or the idnumber.
+     * @param string $category The ID of the category to be set for the instance.
+     * @param string $purpose The ID of the purpose to be set for the instance.
+     */
+    public function i_set_the_category_and_purpose_for_block($name, $coursename, $category, $purpose) {
+        global $DB;
+
+        $params = [
+            'shortname' => $coursename,
+            'fullname' => $coursename,
+            'idnumber' => $coursename,
+        ];
+        $select = 'shortname = :shortname OR fullname = :fullname OR idnumber = :idnumber';
+        $courseid = $DB->get_field_select('course', 'id', $select, $params, MUST_EXIST);
+
+        // Fetch the course context.
+        $coursecontext = context_course::instance($courseid);
+
+        // Fetch the block record and context.
+        $blockid = $DB->get_field('block_instances', 'id', ['blockname' => $name, 'parentcontextid' => $coursecontext->id]);
+        $context = context_block::instance($blockid);
+
+        // Set the category and purpose.
+        $this->set_category_and_purpose($context->id, $category, $purpose);
+    }
+
+    /**
+     * Sets the category and purpose for a context instance.
+     *
+     * @param int $contextid The context ID.
+     * @param int $categoryname The category name.
+     * @param int $purposename The purpose name.
+     * @throws coding_exception
+     */
+    protected function set_category_and_purpose($contextid, $categoryname, $purposename) {
+        $category = \tool_dataprivacy\category::get_record(['name' => $categoryname]);
+        $purpose = \tool_dataprivacy\purpose::get_record(['name' => $purposename]);
+
+        api::set_context_instance((object) [
+            'contextid' => $contextid,
+            'purposeid' => $purpose->get('id'),
+            'categoryid' => $category->get('id'),
+        ]);
+    }
+}
diff --git a/admin/tool/dataprivacy/tests/behat/manage_defaults.feature b/admin/tool/dataprivacy/tests/behat/manage_defaults.feature
new file mode 100644 (file)
index 0000000..a6287d5
--- /dev/null
@@ -0,0 +1,298 @@
+@tool @tool_dataprivacy @javascript
+Feature: Manage data registry defaults
+  As the privacy officer
+  In order to manage the data registry
+  I need to be able to manage the default data categories and data storage purposes for various context levels.
+
+  Background:
+    Given I log in as "admin"
+    And the following "categories" exist:
+      | name                   | idnumber | category |
+      | Science and technology | scitech  |          |
+      | Physics                | st-phys  | scitech  |
+    And the following "courses" exist:
+      | fullname                  | shortname   | category |
+      | Fundamentals of physics 1 | Physics 101 | st-phys  |
+    And the following "activities" exist:
+      | activity | name         | idnumber | course      |
+      | assign   | Assignment 1 | assign1  | Physics 101 |
+      | forum    | Forum 1      | forum1   | Physics 101 |
+    And the following "blocks" exist:
+      | blockname    | contextlevel | reference   | pagetypepattern | defaultregion |
+      | online_users | Course       | Physics 101 | course-view-*   | site-post     |
+    And the following data privacy "categories" exist:
+      | name          |
+      | Site category |
+      | Category 1    |
+      | Category 2    |
+    And the following data privacy "purposes" exist:
+      | name         | retentionperiod |
+      | Site purpose | P10Y           |
+      | Purpose 1    | P3Y            |
+      | Purpose 2    | P5Y            |
+    And I set the site category and purpose to "Site category" and "Site purpose"
+
+  Scenario: Set course category data registry defaults
+    Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years"
+
+  Scenario: Set course category data registry defaults with override
+    Given I set the category and purpose for the course category "scitech" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years"
+
+  Scenario: Set course data registry defaults
+    Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Courses" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set course data registry defaults with override
+    Given I set the category and purpose for the course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Courses" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
+
+  Scenario: Set module level data registry defaults
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set module level data registry defaults with override
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I click on "Forum 1 (Forum)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
+
+  Scenario: Set data registry defaults for an activity module
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Add a new module default"
+    And I set the field "Activity module" to "Assignment"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1" in the "Assignment" "table_row"
+    And I should see "Purpose 1" in the "Assignment" "table_row"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set data registry defaults for an activity module with override
+    Given I set the category and purpose for the "assign1" "assign" in course "Physics 101" to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Activity modules" "link"
+    And I should see "Inherit"
+    And I should see "Add a new module default"
+    And I press "Add a new module default"
+    And I set the field "Activity module" to "Assignment"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1" in the "Assignment" "table_row"
+    And I should see "Purpose 1" in the "Assignment" "table_row"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Activities and resources" "link"
+    And I wait until the page is ready
+    And I click on "Assignment 1 (Assignment)" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
+
+  Scenario: Set block category data registry defaults
+    Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Blocks" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Blocks" "link"
+    And I wait until the page is ready
+    And I click on "Online users" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Category 2"
+    And the field "purposeid" matches value "Purpose 2"
+    And I should see "5 years (after the course end date)"
+
+  Scenario: Set course category data registry defaults with override
+    Given I set the category and purpose for the "online_users" block in the "Physics 101" course to "Category 2" and "Purpose 2"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Set defaults" "link"
+    And I click on "Blocks" "link"
+    And I should see "Inherit"
+    And I should not see "Add a new module default"
+    And I press "Edit"
+    And I set the field "Category" to "Category 1"
+    And I set the field "Purpose" to "Purpose 1"
+    And I click on "Reset instances with custom values" "checkbox"
+    When I press "Save changes"
+    Then I should see "Category 1"
+    And I should see "Purpose 1"
+    And I navigate to "Users > Privacy and policies > Data registry" in site administration
+    And I click on "Science and technology" "link"
+    And I wait until the page is ready
+    And I click on "Courses" "link"
+    And I wait until the page is ready
+    And I click on "Physics 101" "link"
+    And I wait until the page is ready
+    And I click on "Blocks" "link"
+    And I wait until the page is ready
+    And I click on "Online users" "link"
+    And I wait until the page is ready
+    And the field "categoryid" matches value "Not set (use the default value)"
+    And the field "purposeid" matches value "Not set (use the default value)"
+    And I should see "3 years (after the course end date)"
index 71d0111..5448d5f 100644 (file)
@@ -28,6 +28,7 @@ global $CFG;
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
 
 use tool_dataprivacy\api;
+use tool_dataprivacy\context_instance;
 use tool_dataprivacy\external;
 
 /**
@@ -282,4 +283,281 @@ class tool_dataprivacy_external_testcase extends externallib_advanced_testcase {
         $this->expectException(dml_missing_record_exception::class);
         external::get_data_request($this->requestid + 1);
     }
+
+    /**
+     * Test for \tool_dataprivacy\external::set_context_defaults()
+     * when called by a user that doesn't have the manage registry capability.
+     */
+    public function test_set_context_defaults_no_capability() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $this->setUser($user);
+        $this->expectException(required_capability_exception::class);
+        external::set_context_defaults(CONTEXT_COURSECAT, context_instance::INHERIT, context_instance::INHERIT, '', false);
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::set_context_defaults().
+     *
+     * We're just checking the module context level here to test the WS function.
+     * More testing is done in \tool_dataprivacy_api_testcase::test_set_context_defaults().
+     *
+     * @dataProvider get_options_provider
+     * @param bool $modulelevel Whether defaults are to be applied on the module context level or for an activity only.
+     * @param bool $override Whether to override instances.
+     */
+    public function test_set_context_defaults($modulelevel, $override) {
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+
+        // Generate course cat, course, block, assignment, forum instances.
+        $coursecat = $generator->create_category();
+        $course = $generator->create_course(['category' => $coursecat->id]);
+        $assign = $generator->create_module('assign', ['course' => $course->id]);
+        list($course, $assigncm) = get_course_and_cm_from_instance($assign->id, 'assign');
+        $assigncontext = context_module::instance($assigncm->id);
+
+        // Generate purpose and category.
+        $category1 = api::create_category((object)['name' => 'Test category 1']);
+        $category2 = api::create_category((object)['name' => 'Test category 2']);
+        $purpose1 = api::create_purpose((object)[
+            'name' => 'Test purpose 1', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+        $purpose2 = api::create_purpose((object)[
+            'name' => 'Test purpose 2', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+
+        // Set a custom purpose and ID for this assignment instance.
+        $assignctxinstance = api::set_context_instance((object) [
+            'contextid' => $assigncontext->id,
+            'purposeid' => $purpose1->get('id'),
+            'categoryid' => $category1->get('id'),
+        ]);
+
+        $modulename = $modulelevel ? 'assign' : '';
+        $categoryid = $category2->get('id');
+        $purposeid = $purpose2->get('id');
+        $result = external::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, $modulename, $override);
+
+        // Extract the result.
+        $return = external_api::clean_returnvalue(external::set_context_defaults_returns(), $result);
+        $this->assertTrue($return['result']);
+
+        // Check the assignment context instance.
+        $instanceexists = context_instance::record_exists($assignctxinstance->get('id'));
+        if ($override) {
+            // The custom assign instance should have been deleted.
+            $this->assertFalse($instanceexists);
+        } else {
+            // The custom assign instance should still exist.
+            $this->assertTrue($instanceexists);
+        }
+
+        // Check the saved defaults.
+        list($savedpurpose, $savedcategory) = \tool_dataprivacy\data_registry::get_defaults(CONTEXT_MODULE, $modulename);
+        $this->assertEquals($categoryid, $savedcategory);
+        $this->assertEquals($purposeid, $savedpurpose);
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_category_options()
+     * when called by a user that doesn't have the manage registry capability.
+     */
+    public function test_get_category_options_no_capability() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $this->setUser($user);
+        $this->expectException(required_capability_exception::class);
+        external::get_category_options(true, true);
+    }
+
+    /**
+     * Data provider for \tool_dataprivacy_external_testcase::test_XX_options().
+     */
+    public function get_options_provider() {
+        return [
+            [false, false],
+            [false, true],
+            [true, false],
+            [true, true],
+        ];
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_category_options().
+     *
+     * @dataProvider get_options_provider
+     * @param bool $includeinherit Whether "Inherit" would be included to the options.
+     * @param bool $includenotset Whether "Not set" would be included to the options.
+     */
+    public function test_get_category_options($includeinherit, $includenotset) {
+        $this->setAdminUser();
+
+        // Prepare our expected options.
+        $expectedoptions = [];
+        if ($includeinherit) {
+            $expectedoptions[] = [
+                'id' => context_instance::INHERIT,
+                'name' => get_string('inherit', 'tool_dataprivacy'),
+            ];
+        }
+
+        if ($includenotset) {
+            $expectedoptions[] = [
+                'id' => context_instance::NOTSET,
+                'name' => get_string('notset', 'tool_dataprivacy'),
+            ];
+        }
+
+        for ($i = 1; $i <= 3; $i++) {
+            $category = api::create_category((object)['name' => 'Category ' . $i]);
+            $expectedoptions[] = [
+                'id' => $category->get('id'),
+                'name' => $category->get('name'),
+            ];
+        }
+
+        // Call the WS function.
+        $result = external::get_category_options($includeinherit, $includenotset);
+
+        // Extract the options.
+        $return = (object) external_api::clean_returnvalue(external::get_category_options_returns(), $result);
+        $options = $return->options;
+
+        // Make sure everything checks out.
+        $this->assertCount(count($expectedoptions), $options);
+        foreach ($options as $option) {
+            $this->assertContains($option, $expectedoptions);
+        }
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_purpose_options()
+     * when called by a user that doesn't have the manage registry capability.
+     */
+    public function test_get_purpose_options_no_capability() {
+        $generator = $this->getDataGenerator();
+        $user = $generator->create_user();
+        $this->setUser($user);
+        $this->expectException(required_capability_exception::class);
+        external::get_category_options(true, true);
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_purpose_options().
+     *
+     * @dataProvider get_options_provider
+     * @param bool $includeinherit Whether "Inherit" would be included to the options.
+     * @param bool $includenotset Whether "Not set" would be included to the options.
+     */
+    public function test_get_purpose_options($includeinherit, $includenotset) {
+        $this->setAdminUser();
+
+        // Prepare our expected options.
+        $expectedoptions = [];
+        if ($includeinherit) {
+            $expectedoptions[] = [
+                'id' => context_instance::INHERIT,
+                'name' => get_string('inherit', 'tool_dataprivacy'),
+            ];
+        }
+
+        if ($includenotset) {
+            $expectedoptions[] = [
+                'id' => context_instance::NOTSET,
+                'name' => get_string('notset', 'tool_dataprivacy'),
+            ];
+        }
+
+        for ($i = 1; $i <= 3; $i++) {
+            $purpose = api::create_purpose((object)[
+                'name' => 'Purpose ' . $i, 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+            ]);
+            $expectedoptions[] = [
+                'id' => $purpose->get('id'),
+                'name' => $purpose->get('name'),
+            ];
+        }
+
+        // Call the WS function.
+        $result = external::get_purpose_options($includeinherit, $includenotset);
+
+        // Extract the options.
+        $return = (object) external_api::clean_returnvalue(external::get_purpose_options_returns(), $result);
+        $options = $return->options;
+
+        // Make sure everything checks out.
+        $this->assertCount(count($expectedoptions), $options);
+        foreach ($options as $option) {
+            $this->assertContains($option, $expectedoptions);
+        }
+    }
+
+    /**
+     * Data provider for \tool_dataprivacy_external_testcase::get_activity_options().
+     */
+    public function get_activity_options_provider() {
+        return [
+            [false, false, true],
+            [false, true, true],
+            [true, false, true],
+            [true, true, true],
+            [false, false, false],
+            [false, true, false],
+            [true, false, false],
+            [true, true, false],
+        ];
+    }
+
+    /**
+     * Test for \tool_dataprivacy\external::get_activity_options().
+     *
+     * @dataProvider get_activity_options_provider
+     * @param bool $inheritcategory Whether the category would be set to "Inherit".
+     * @param bool $inheritpurpose Whether the purpose would be set to "Inherit".
+     * @param bool $nodefaults Whether to fetch only activities that don't have defaults.
+     */
+    public function test_get_activity_options($inheritcategory, $inheritpurpose, $nodefaults) {
+        $this->setAdminUser();
+
+        $category = api::create_category((object)['name' => 'Test category']);
+        $purpose = api::create_purpose((object)[
+            'name' => 'Test purpose ', 'retentionperiod' => 'PT1M', 'lawfulbases' => 'gdpr_art_6_1_a'
+        ]);
+        $categoryid = $category->get('id');
+        $purposeid = $purpose->get('id');
+
+        if ($inheritcategory) {
+            $categoryid = context_instance::INHERIT;
+        }
+        if ($inheritpurpose) {
+            $purposeid = context_instance::INHERIT;
+        }
+
+        // Set the context default for the assignment module.
+        api::set_context_defaults(CONTEXT_MODULE, $categoryid, $purposeid, 'assign');
+
+        // Call the WS function.
+        $result = external::get_activity_options($nodefaults);
+
+        // Extract the options.
+        $return = (object) external_api::clean_returnvalue(external::get_activity_options_returns(), $result);
+        $options = $return->options;
+
+        // Make sure the options list is not empty.
+        $this->assertNotEmpty($options);
+
+        $pluginwithdefaults = [
+            'name' => 'assign',
+            'displayname' => get_string('pluginname', 'assign')
+        ];
+
+        // If we don't want plugins with defaults to be listed or if both of the category and purpose are set to inherit,
+        // the assign module should be listed.
+        if (!$nodefaults || ($inheritcategory && $inheritpurpose)) {
+            $this->assertContains($pluginwithdefaults, $options);
+        } else {
+            $this->assertNotContains($pluginwithdefaults, $options);
+        }
+    }
 }
diff --git a/admin/tool/dataprivacy/tests/generator/lib.php b/admin/tool/dataprivacy/tests/generator/lib.php
new file mode 100644 (file)
index 0000000..744e34a
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Data privacy tool data generator.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use tool_dataprivacy\api;
+use tool_dataprivacy\category;
+use tool_dataprivacy\purpose;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Data privacy tool data generator class.
+ *
+ * @package    tool_dataprivacy
+ * @category   test
+ * @copyright  2018 Jun Pataleta
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_dataprivacy_generator extends component_generator_base {
+
+    /** @var int Number of created categories. */
+    protected $categorycount = 0;
+
+    /** @var int Number of created purposes. */
+    protected $purposecount = 0;
+
+    /**
+     * Reset process.
+     *
+     * Do not call directly.
+     *
+     * @return void
+     */
+    public function reset() {
+        $this->categorycount = 0;
+        $this->purposecount = 0;
+    }
+
+    /**
+     * Create a new category.
+     *
+     * @param array|stdClass $record
+     * @return category
+     */
+    public function create_category($record = null) {
+        $this->categorycount++;
+        $i = $this->categorycount;
+        $record = (object)$record;
+
+        if (!isset($record->name)) {
+            $record->name = "Test purpose $i";
+        }
+
+        if (!isset($record->description)) {
+            $record->description = "{$record->name} description";
+        }
+
+        $category = api::create_category($record);
+
+        return $category;
+    }
+
+    /**
+     * Create a new purpose.
+     *
+     * @param array|stdClass $record
+     * @return purpose
+     */
+    public function create_purpose($record = null) {
+        $this->purposecount++;
+        $i = $this->purposecount;
+        $record = (object)$record;
+
+        if (!isset($record->name)) {
+            $record->name = "Test purpose $i";
+        }
+
+        if (!isset($record->description)) {
+            $record->description = "{$record->name} $i description";
+        }
+
+        if (!isset($record->retentionperiod)) {
+            $record->retentionperiod = 'PT1M';
+        }
+
+        if (!isset($record->lawfulbases)) {
+            $record->lawfulbases = 'gdpr_art_6_1_a';
+        }
+
+        $purpose = api::create_purpose($record);
+
+        return $purpose;
+    }
+}
index ca666e3..e8cf60c 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018082100;
+$plugin->version   = 2018091000;
 $plugin->requires  = 2018050800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
index 00547c6..64c2cd2 100644 (file)
@@ -49,7 +49,8 @@ class block_calendar_month extends block_base {
         $this->content->footer = '';
 
         $courseid = $this->page->course->id;
-        $categoryid = ($this->page->context->contextlevel === CONTEXT_COURSECAT) ? $this->page->category->id : null;
+        $categoryid = ($this->page->context->contextlevel === CONTEXT_COURSECAT && !empty($this->page->category)) ?
+            $this->page->category->id : null;
         $calendar = \calendar_information::create(time(), $courseid, $categoryid);
         list($data, $template) = calendar_get_view($calendar, 'mini', isloggedin(), isloggedin());
 
index e988304..4bb603a 100644 (file)
@@ -131,7 +131,7 @@ class item implements \renderable, \templatable {
         }
 
         // Allow the renderer to format the title and description.
-        $data['title']          = $output->format_title($title);
+        $data['title']          = strip_tags($output->format_title($title));
         $data['description']    = $this->showdescription ? $output->format_description($this->description) : null;
 
         return $data;
diff --git a/cache/stores/memcache/addinstanceform.php b/cache/stores/memcache/addinstanceform.php
deleted file mode 100644 (file)
index 27135ef..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * The library file for the memcache cache store.
- *
- * This file is part of the memcache cache store, it contains the API for interacting with an instance of the store.
- *
- * @package    cachestore_memcache
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-require_once($CFG->dirroot.'/cache/forms.php');
-
-/**
- * Form for adding a memcache instance.
- *
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class cachestore_memcache_addinstance_form extends cachestore_addinstance_form {
-
-    /**
-     * Add the desired form elements.
-     */
-    protected function configuration_definition() {
-        $form = $this->_form;
-        $form->addElement('textarea', 'servers', get_string('servers', 'cachestore_memcache'), array('cols' => 75, 'rows' => 5));
-        $form->addHelpButton('servers', 'servers', 'cachestore_memcache');
-        $form->addRule('servers', get_string('required'), 'required');
-        $form->setType('servers', PARAM_RAW);
-
-        $form->addElement('text', 'prefix', get_string('prefix', 'cachestore_memcache'),
-                array('maxlength' => 5, 'size' => 5));
-        $form->addHelpButton('prefix', 'prefix', 'cachestore_memcache');
-        $form->setType('prefix', PARAM_TEXT); // We set to text but we have a rule to limit to alphanumext.
-        $form->setDefault('prefix', 'mdl_');
-        $form->addRule('prefix', get_string('prefixinvalid', 'cachestore_memcache'), 'regex', '#^[a-zA-Z0-9\-_]+$#');
-        $form->setForceLtr('prefix');
-
-        $form->addElement('header', 'clusteredheader', get_string('clustered', 'cachestore_memcache'));
-
-        $form->addElement('checkbox', 'clustered', get_string('clustered', 'cachestore_memcache'));
-        $form->setDefault('checkbox', false);
-        $form->addHelpButton('clustered', 'clustered', 'cachestore_memcache');
-
-        $form->addElement('textarea', 'setservers', get_string('setservers', 'cachestore_memcache'),
-                array('cols' => 75, 'rows' => 5));
-        $form->addHelpButton('setservers', 'setservers', 'cachestore_memcache');
-        $form->disabledIf('setservers', 'clustered');
-        $form->setType('setservers', PARAM_RAW);
-    }
-
-    /**
-     * Perform minimal validation on the settings form.
-     *
-     * @param array $data
-     * @param array $files
-     */
-    public function validation($data, $files) {
-        $errors = parent::validation($data, $files);
-
-        if (isset($data['clustered']) && ($data['clustered'] == 1)) {
-            // Set servers is required with in cluster mode.
-            if (!isset($data['setservers'])) {
-                $errors['setservers'] = get_string('required');
-            } else {
-                $trimmed = trim($data['setservers']);
-                if (empty($trimmed)) {
-                    $errors['setservers'] = get_string('required');
-                }
-            }
-
-            $validservers = false;
-            if (isset($data['servers'])) {
-                $servers = trim($data['servers']);
-                $servers = explode("\n", $servers);
-                if (count($servers) === 1) {
-                    $validservers = true;
-                }
-            }
-
-            if (!$validservers) {
-                $errors['servers'] = get_string('serversclusterinvalid', 'cachestore_memcache');
-            }
-        }
-
-        return $errors;
-    }
-}
\ No newline at end of file
diff --git a/cache/stores/memcache/classes/privacy/provider.php b/cache/stores/memcache/classes/privacy/provider.php
deleted file mode 100644 (file)
index e7e3779..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Privacy Subsystem implementation for cachestore_memcache.
- *
- * @package    cachestore_memcache
- * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace cachestore_memcache\privacy;
-
-use core_privacy\local\metadata\collection;
-use core_privacy\local\request\contextlist;
-use core_privacy\local\request\approved_contextlist;
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Privacy Subsystem for cachestore_memcache.
- *
- * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider {
-
-    /**
-     * Returns meta data about this system.
-     *
-     * @param   collection $collection The initialised collection to add items to.
-     * @return  collection A listing of user data stored through this system.
-     */
-    public static function get_metadata(collection $collection) : collection {
-        $collection->add_external_location_link('memcache', [
-                'data' => 'privacy:metadata:memcache:data',
-            ], 'privacy:metadata:memcache');
-        return $collection;
-    }
-
-    /**
-     * Get the list of contexts that contain user information for the specified user.
-     *
-     * @param   int         $userid     The user to search.
-     * @return  contextlist $contextlist  The contextlist containing the list of contexts used in this plugin.
-     */
-    public static function get_contexts_for_userid(int $userid) : contextlist {
-        return new contextlist();
-    }
-
-    /**
-     * Export all user data for the specified user, in the specified contexts.
-     *
-     * @param approved_contextlist $contextlist The approved contexts to export information for.
-     */
-    public static function export_user_data(approved_contextlist $contextlist) {
-    }
-
-    /**
-     * Delete all use data which matches the specified deletion_criteria.
-     *
-     * @param \context $context A user context.
-     */
-    public static function delete_data_for_all_users_in_context(\context $context) {
-    }
-
-    /**
-     * Delete all user data for the specified user, in the specified contexts.
-     *
-     * @param   approved_contextlist    $contextlist    The approved contexts and user information to delete information for.
-     */
-    public static function delete_data_for_user(approved_contextlist $contextlist) {
-    }
-}
diff --git a/cache/stores/memcache/lang/en/cachestore_memcache.php b/cache/stores/memcache/lang/en/cachestore_memcache.php
deleted file mode 100644 (file)
index 9095825..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * The library file for the memcache cache store.
- *
- * This file is part of the memcache cache store, it contains the API for interacting with an instance of the store.
- *
- * @package    cachestore_memcache
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-$string['clustered'] = 'Enable clustered servers';
-$string['clustered_help'] = 'This is used to allow read-one, set-multi functionality.
-
-The intended use case is to create an improved store for load-balanced configurations. The store will fetch from one server (usually localhost), but set to many (all the servers in the load-balance pool). For caches with very high read to set ratios, this saves a significant amount of network overhead.
-
-When this setting is enabled, the server listed above will be used for fetching.';
-$string['clusteredheader'] = 'Split servers';
-$string['pluginname'] = 'Memcache';
-$string['prefix'] = 'Key prefix';
-$string['prefix_help'] = 'This prefix is used for all key names on the memcache server.
-* If you only have one Moodle instance using this server, you can leave this value default.
-* Due to key length restrictions, a maximum of 5 characters is permitted.';
-$string['prefixinvalid'] = 'Invalid prefix. You can only use a-z A-Z 0-9-_.';
-$string['privacy:metadata:memcache'] = 'The Memcache cachestore plugin stores data briefly as part of its caching functionality. This data is stored on an Memcache server where data is regularly removed.';
-$string['privacy:metadata:memcache:data'] = 'The various data stored in the cache';
-$string['servers'] = 'Servers';
-$string['servers_help'] = 'This sets the servers that should be utilised by this memcache adapter.
-Servers should be defined one per line and consist of a server address and optionally a port and weight.
-If no port is provided then the default port (11211) is used.
-
-For example:
-<pre>
-server.url.com
-ipaddress:port
-servername:port:weight
-</pre>
-
-If *Enable clustered servers* is enabled below, there must be only one server listed here. This would usually be a name that always resolves to the local machine, like 127.0.0.1 or localhost.';
-$string['serversclusterinvalid'] = 'Exactly one server is required when clustering is enabled.';
-$string['setservers'] = 'Set Servers';
-$string['setservers_help'] = 'This is the list of servers that will updated when data is modified in the cache. Generally the fully qualified name of each server in the pool.
-It **must** include the server listed in *Servers* above, even if by a different hostname.
-Servers should be defined one per line and consist of a server address and optionally a port.
-If no port is provided then the default port (11211) is used.
-
-For example:
-<pre>
-server.url.com
-ipaddress:port
-</pre>';
-$string['sessionhandlerconflict'] = 'Warning: A memcache instance ({$a}) has being configured to use the same memcache server as sessions. Purging all caches will lead to sessions also being purged.';
-$string['testservers'] = 'Test servers';
-$string['testservers_desc'] = 'One or more connection strings for memcache servers to test against. If a test server has been specified then memcache performance can be tested using the cache performance page in the administration block.
-As an example: 127.0.0.1:11211';
diff --git a/cache/stores/memcache/lib.php b/cache/stores/memcache/lib.php
deleted file mode 100644 (file)
index 60ee49b..0000000
+++ /dev/null
@@ -1,642 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * The library file for the memcache cache store.
- *
- * This file is part of the memcache cache store, it contains the API for interacting with an instance of the store.
- *
- * @package    cachestore_memcache
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * The memcache store class.
- *
- * (Not to be confused with memcached store)
- *
- * Configuration options:
- *      servers:        string: host:port:weight , ...
- *
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class cachestore_memcache extends cache_store implements cache_is_configurable {
-
-    /**
-     * The name of the store
-     * @var store
-     */
-    protected $name;
-
-    /**
-     * The memcache connection once established.
-     * @var Memcache
-     */
-    protected $connection;
-
-    /**
-     * Key prefix for this memcache.
-     * @var string
-     */
-    protected $prefix;
-
-    /**
-     * An array of servers to use in the connection args.
-     * @var array
-     */
-    protected $servers = array();
-
-    /**
-     * An array of options used when establishing the connection.
-     * @var array
-     */
-    protected $options = array();
-
-    /**
-     * Set to true when things are ready to be initialised.
-     * @var bool
-     */
-    protected $isready = false;
-
-    /**
-     * Set to true once this store instance has been initialised.
-     * @var bool
-     */
-    protected $isinitialised = false;
-
-    /**
-     * The cache definition this store was initialised for.
-     * @var cache_definition
-     */
-    protected $definition;
-
-    /**
-     * Set to true when this store is clustered.
-     * @var bool
-     */
-    protected $clustered = false;
-
-    /**
-     * Array of servers to set when in clustered mode.
-     * @var array
-     */
-    protected $setservers = array();
-
-    /**
-     * The an array of memcache connections for the set servers, once established.
-     * @var array
-     */
-    protected $setconnections = array();
-
-    /**
-     * If true data going in and out will be encoded.
-     * @var bool
-     */
-    protected $encode = true;
-
-    /**
-     * Default prefix for key names.
-     * @var string
-     */
-    const DEFAULT_PREFIX = 'mdl_';
-
-    /**
-     * Constructs the store instance.
-     *
-     * Noting that this function is not an initialisation. It is used to prepare the store for use.
-     * The store will be initialised when required and will be provided with a cache_definition at that time.
-     *
-     * @param string $name
-     * @param array $configuration
-     */
-    public function __construct($name, array $configuration = array()) {
-        $this->name = $name;
-        if (!array_key_exists('servers', $configuration) || empty($configuration['servers'])) {
-            // Nothing configured.
-            return;
-        }
-        if (!is_array($configuration['servers'])) {
-            $configuration['servers'] = array($configuration['servers']);
-        }
-        foreach ($configuration['servers'] as $server) {
-            if (!is_array($server)) {
-                $server = explode(':', $server, 3);
-            }
-            if (!array_key_exists(1, $server)) {
-                $server[1] = 11211;
-                $server[2] = 100;
-            } else if (!array_key_exists(2, $server)) {
-                $server[2] = 100;
-            }
-            $this->servers[] = $server;
-        }
-
-        $this->clustered = array_key_exists('clustered', $configuration) ? (bool)$configuration['clustered'] : false;
-
-        if ($this->clustered) {
-            if (!array_key_exists('setservers', $configuration) || (count($configuration['setservers']) < 1)) {
-                // Can't setup clustering without set servers.
-                return;
-            }
-            if (count($this->servers) !== 1) {
-                // Can only setup cluster with exactly 1 get server.
-                return;
-            }
-            foreach ($configuration['setservers'] as $server) {
-                // We do not use weights (3rd part) on these servers.
-                if (!is_array($server)) {
-                    $server = explode(':', $server, 3);
-                }
-                if (!array_key_exists(1, $server)) {
-                    $server[1] = 11211;
-                }
-                $this->setservers[] = $server;
-            }
-        }
-
-        if (empty($configuration['prefix'])) {
-            $this->prefix = self::DEFAULT_PREFIX;
-        } else {
-            $this->prefix = $configuration['prefix'];
-        }
-
-        $this->connection = new Memcache;
-        foreach ($this->servers as $server) {
-            $this->connection->addServer($server[0], (int) $server[1], true, (int) $server[2]);
-        }
-
-        if ($this->clustered) {
-            foreach ($this->setservers as $setserver) {
-                // Since we will have a number of them with the same name, append server and port.
-                $connection = new Memcache;
-                $connection->addServer($setserver[0], $setserver[1]);
-                $this->setconnections[] = $connection;
-            }
-        }
-
-        // Test the connection to the pool of servers.
-        $this->isready = @$this->connection->set($this->parse_key('ping'), 'ping', MEMCACHE_COMPRESSED, 1);
-    }
-
-    /**
-     * Initialises the cache.
-     *
-     * Once this has been done the cache is all set to be used.
-     *
-     * @param cache_definition $definition
-     */
-    public function initialise(cache_definition $definition) {
-        if ($this->is_initialised()) {
-            throw new coding_exception('This memcache instance has already been initialised.');
-        }
-        $this->definition = $definition;
-        $this->isinitialised = true;
-        $this->encode = self::require_encoding();
-    }
-
-    /**
-     * Tests if encoding is going to be required.
-     *
-     * Prior to memcache 3.0.3 scalar data types were not preserved.
-     * For earlier versions of the memcache extension we need to encode and decode scalar types
-     * to ensure that it is preserved.
-     *
-     * @param string $version The version to check, if null it is fetched from PHP.
-     * @return bool
-     */
-    public static function require_encoding($version = null) {
-        if (!$version) {
-            $version = phpversion('memcache');
-        }
-        return (version_compare($version, '3.0.3', '<'));
-    }
-
-    /**
-     * Returns true once this instance has been initialised.
-     *
-     * @return bool
-     */
-    public function is_initialised() {
-        return ($this->isinitialised);
-    }
-
-    /**
-     * Returns true if this store instance is ready to be used.
-     * @return bool
-     */
-    public function is_ready() {
-        return $this->isready;
-    }
-
-    /**
-     * Returns true if the store requirements are met.
-     *
-     * @return bool
-     */
-    public static function are_requirements_met() {
-        return class_exists('Memcache');
-    }
-
-    /**
-     * Returns true if the given mode is supported by this store.
-     *
-     * @param int $mode One of cache_store::MODE_*
-     * @return bool
-     */
-    public static function is_supported_mode($mode) {
-        return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
-    }
-
-    /**
-     * Returns the supported features as a combined int.
-     *
-     * @param array $configuration
-     * @return int
-     */
-    public static function get_supported_features(array $configuration = array()) {
-        return self::SUPPORTS_NATIVE_TTL + self::DEREFERENCES_OBJECTS;
-    }
-
-    /**
-     * Returns false as this store does not support multiple identifiers.
-     * (This optional function is a performance optimisation; it must be
-     * consistent with the value from get_supported_features.)
-     *
-     * @return bool False
-     */
-    public function supports_multiple_identifiers() {
-        return false;
-    }
-
-    /**
-     * Returns the supported modes as a combined int.
-     *
-     * @param array $configuration
-     * @return int
-     */
-    public static function get_supported_modes(array $configuration = array()) {
-        return self::MODE_APPLICATION;
-    }
-
-    /**
-     * Parses the given key to make it work for this memcache backend.
-     *
-     * @param string $key The raw key.
-     * @return string The resulting key.
-     */
-    protected function parse_key($key) {
-        if (strlen($key) > 245) {
-            $key = '_sha1_'.sha1($key);
-        }
-        $key = $this->prefix . $key;
-        return $key;
-    }
-
-    /**
-     * Retrieves an item from the cache store given its key.
-     *
-     * @param string $key The key to retrieve
-     * @return mixed The data that was associated with the key, or false if the key did not exist.
-     */
-    public function get($key) {
-        $result = $this->connection->get($this->parse_key($key));
-        if ($this->encode && $result !== false) {
-            return @unserialize($result);
-        }
-        return $result;
-    }
-
-    /**
-     * Retrieves several items from the cache store in a single transaction.
-     *
-     * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
-     *
-     * @param array $keys The array of keys to retrieve
-     * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
-     *      be set to false.
-     */
-    public function get_many($keys) {
-        $mkeys = array();
-        foreach ($keys as $key) {
-            $mkeys[$key] = $this->parse_key($key);
-        }
-        $result = $this->connection->get($mkeys);
-        if (!is_array($result)) {
-            $result = array();
-        }
-        $return = array();
-        foreach ($mkeys as $key => $mkey) {
-            if (!array_key_exists($mkey, $result)) {
-                $return[$key] = false;
-            } else {
-                $return[$key] = $result[$mkey];
-                if ($this->encode && $return[$key] !== false) {
-                    $return[$key] = @unserialize($return[$key]);
-                }
-            }
-        }
-        return $return;
-    }
-
-    /**
-     * Sets an item in the cache given its key and data value.
-     *
-     * @param string $key The key to use.
-     * @param mixed $data The data to set.
-     * @return bool True if the operation was a success false otherwise.
-     */
-    public function set($key, $data) {
-        if ($this->encode) {
-            // We must serialise this data.
-            $data = serialize($data);
-        }
-
-        if ($this->clustered) {
-            $status = true;
-            foreach ($this->setconnections as $connection) {
-                $status = $connection->set($this->parse_key($key), $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl())
-                        && $status;
-            }
-            return $status;
-        }
-
-        return $this->connection->set($this->parse_key($key), $data, MEMCACHE_COMPRESSED, $this->definition->get_ttl());
-    }
-
-    /**
-     * Sets many items in the cache in a single transaction.
-     *
-     * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
-     *      keys, 'key' and 'value'.
-     * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
-     *      sent ... if they care that is.
-     */
-    public function set_many(array $keyvaluearray) {
-        $count = 0;
-        foreach ($keyvaluearray as $pair) {
-            if ($this->set($pair['key'], $pair['value'])) {
-                $count++;
-            }
-        }
-        return $count;
-    }
-
-    /**
-     * Deletes an item from the cache store.
-     *
-     * @param string $key The key to delete.
-     * @return bool Returns true if the operation was a success, false otherwise.
-     */
-    public function delete($key) {
-        if ($this->clustered) {
-            $status = true;
-            foreach ($this->setconnections as $connection) {
-                $status = $connection->delete($this->parse_key($key)) && $status;
-            }
-            return $status;
-        }
-
-        return $this->connection->delete($this->parse_key($key));
-    }
-
-    /**
-     * Deletes several keys from the cache in a single action.
-     *
-     * @param array $keys The keys to delete
-     * @return int The number of items successfully deleted.
-     */
-    public function delete_many(array $keys) {
-        $count = 0;
-        foreach ($keys as $key) {
-            if ($this->delete($key)) {
-                $count++;
-            }
-        }
-        return $count;
-    }
-
-    /**
-     * Purges the cache deleting all items within it.
-     *
-     * @return boolean True on success. False otherwise.
-     */
-    public function purge() {
-        if ($this->isready) {
-            if ($this->clustered) {
-                foreach ($this->setconnections as $connection) {
-                    $connection->flush();
-                }
-            } else {
-                $this->connection->flush();
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Given the data from the add instance form this function creates a configuration array.
-     *
-     * @param stdClass $data
-     * @return array
-     */
-    public static function config_get_configuration_array($data) {
-        $lines = explode("\n", $data->servers);
-        $servers = array();
-        foreach ($lines as $line) {
-            // Trim surrounding colons and default whitespace.
-            $line = trim(trim($line), ":");
-            // Skip blank lines.
-            if ($line === '') {
-                continue;
-            }
-            $servers[] = explode(':', $line, 3);
-        }
-
-        $clustered = false;
-        $setservers = array();
-        if (isset($data->clustered)) {
-            $clustered = true;
-
-            $lines = explode("\n", $data->setservers);
-            foreach ($lines as $line) {
-                // Trim surrounding colons and default whitespace.
-                $line = trim(trim($line), ":");
-                if ($line === '') {
-                    continue;
-                }
-                $setserver = explode(':', $line, 3);
-                // We don't use weights, so display a debug message.
-                if (count($setserver) > 2) {
-                    debugging('Memcache Set Server '.$setserver[0].' has too many parameters.');
-                }
-                $setservers[] = $setserver;
-            }
-        }
-
-        return array(
-            'servers' => $servers,
-            'prefix' => $data->prefix,
-            'clustered' => $clustered,
-            'setservers' => $setservers
-        );
-    }
-
-    /**
-     * Allows the cache store to set its data against the edit form before it is shown to the user.
-     *
-     * @param moodleform $editform
-     * @param array $config
-     */
-    public static function config_set_edit_form_data(moodleform $editform, array $config) {
-        $data = array();
-        if (!empty($config['servers'])) {
-            $servers = array();
-            foreach ($config['servers'] as $server) {
-                $servers[] = join(":", $server);
-            }
-            $data['servers'] = join("\n", $servers);
-        }
-        if (!empty($config['prefix'])) {
-            $data['prefix'] = $config['prefix'];
-        } else {
-            $data['prefix'] = self::DEFAULT_PREFIX;
-        }
-        if (isset($config['clustered'])) {
-            $data['clustered'] = (bool)$config['clustered'];
-        }
-        if (!empty($config['setservers'])) {
-            $servers = array();
-            foreach ($config['setservers'] as $server) {
-                $servers[] = join(":", $server);
-            }
-            $data['setservers'] = join("\n", $servers);
-        }
-
-        $editform->set_data($data);
-    }
-
-    /**
-     * Performs any necessary clean up when the store instance is being deleted.
-     */
-    public function instance_deleted() {
-        if ($this->connection) {
-            $connection = $this->connection;
-        } else {
-            $connection = new Memcache;
-            foreach ($this->servers as $server) {
-                $connection->addServer($server[0], $server[1], true, $server[2]);
-            }
-        }
-        @$connection->flush();
-        unset($connection);
-        unset($this->connection);
-    }
-
-    /**
-     * Generates an instance of the cache store that can be used for testing.
-     *
-     * @param cache_definition $definition
-     * @return cachestore_memcache|false
-     */
-    public static function initialise_test_instance(cache_definition $definition) {
-        if (!self::are_requirements_met()) {
-            return false;
-        }
-
-        $config = get_config('cachestore_memcache');
-        if (empty($config->testservers)) {
-            return false;
-        }
-
-        $configuration = array();
-        $configuration['servers'] = explode("\n", $config->testservers);
-        if (!empty($config->testclustered)) {
-            $configuration['clustered'] = $config->testclustered;
-        }
-        if (!empty($config->testsetservers)) {
-            $configuration['setservers'] = explode("\n", $config->testsetservers);
-        }
-
-        $store = new cachestore_memcache('Test memcache', $configuration);
-        if ($store->is_ready()) {
-            $store->initialise($definition);
-        }
-
-        return $store;
-    }
-
-    /**
-     * Generates the appropriate configuration required for unit testing.
-     *
-     * @return array Array of unit test configuration data to be used by initialise().
-     */
-    public static function unit_test_configuration() {
-        // If the configuration is not defined correctly, return only the configuration know about.
-        if (!defined('TEST_CACHESTORE_MEMCACHE_TESTSERVERS')) {
-            return [];
-        }
-        return ['servers' => explode("\n", TEST_CACHESTORE_MEMCACHE_TESTSERVERS)];
-    }
-
-    /**
-     * Returns the name of this instance.
-     * @return string
-     */
-    public function my_name() {
-        return $this->name;
-    }
-
-    /**
-     * Used to notify of configuration conflicts.
-     *
-     * The warnings returned here will be displayed on the cache configuration screen.
-     *
-     * @return string[] Returns an array of warnings (strings)
-     */
-    public function get_warnings() {
-        global $CFG;
-        $warnings = array();
-        if (isset($CFG->session_memcached_save_path) && count($this->servers)) {
-            $bits = explode(':', $CFG->session_memcached_save_path, 3);
-            $host = array_shift($bits);
-            $port = (count($bits)) ? array_shift($bits) : '11211';
-            foreach ($this->servers as $server) {
-                if ($server[0] === $host && $server[1] == $port) {
-                    $warnings[] = get_string('sessionhandlerconflict', 'cachestore_memcache', $this->my_name());
-                    break;
-                }
-            }
-        }
-        return $warnings;
-    }
-
-    /**
-     * Returns true if this cache store instance is both suitable for testing, and ready for testing.
-     *
-     * Cache stores that support being used as the default store for unit and acceptance testing should
-     * override this function and return true if there requirements have been met.
-     *
-     * @return bool
-     */
-    public static function ready_to_be_used_for_testing() {
-        return defined('TEST_CACHESTORE_MEMCACHE_TESTSERVERS');
-    }
-}
diff --git a/cache/stores/memcache/settings.php b/cache/stores/memcache/settings.php
deleted file mode 100644 (file)
index 26d8984..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * The settings for the memcache store.
- *
- * This file is part of the memcache cache store, it contains the API for interacting with an instance of the store.
- *
- * @package    cachestore_memcache
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die;
-
-$settings->add(new admin_setting_configtextarea(
-        'cachestore_memcache/testservers',
-        new lang_string('testservers', 'cachestore_memcache'),
-        new lang_string('testservers_desc', 'cachestore_memcache'),
-        '', PARAM_RAW, 60, 3));
diff --git a/cache/stores/memcache/tests/memcache_test.php b/cache/stores/memcache/tests/memcache_test.php
deleted file mode 100644 (file)
index f5d4f08..0000000
+++ /dev/null
@@ -1,295 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Memcache unit tests.
- *
- * If you wish to use these unit tests all you need to do is add the following definition to
- * your config.php file.
- *
- * define('TEST_CACHESTORE_MEMCACHE_TESTSERVERS', '127.0.0.1:11211');
- *
- * @package    cachestore_memcache
- * @copyright  2013 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-// Include the necessary evils.
-global $CFG;
-require_once($CFG->dirroot.'/cache/tests/fixtures/stores.php');
-require_once($CFG->dirroot.'/cache/stores/memcache/lib.php');
-
-/**
- * Memcache unit test class.
- *
- * @package    cachestore_memcache
- * @copyright  2013 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class cachestore_memcache_test extends cachestore_tests {
-    /**
-     * Returns the memcache class name
-     * @return string
-     */
-    protected function get_class_name() {
-        return 'cachestore_memcache';
-    }
-
-    /**
-     * Tests the valid keys to ensure they work.
-     */
-    public function test_valid_keys() {
-        $this->resetAfterTest(true);
-
-        $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcache', 'phpunit_test');
-        $instance = new cachestore_memcache('Memcache Test', cachestore_memcache::unit_test_configuration());
-
-        if (!$instance->is_ready()) {
-            // Something prevented memcache store to be inited (extension, TEST_CACHESTORE_MEMCACHE_TESTSERVERS...).
-            $this->markTestSkipped();
-        }
-        $instance->initialise($definition);
-
-        $keys = array(
-            // Alphanumeric.
-            'abc', 'ABC', '123', 'aB1', '1aB',
-            // Hyphens.
-            'a-1', '1-a', '-a1', 'a1-',
-            // Underscores.
-            'a_1', '1_a', '_a1', 'a1_'
-        );
-
-        // Set some keys.
-        foreach ($keys as $key) {
-            $this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
-        }
-
-        // Get some keys.
-        foreach ($keys as $key) {
-            $this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
-        }
-
-        // Try get many.
-        $values = $instance->get_many($keys);
-        foreach ($values as $key => $value) {
-            $this->assertEquals($key, $value);
-        }
-
-        // Reset a key.
-        $this->assertTrue($instance->set($keys[0], 'New'), "Failed to reset key `$key`");
-        $this->assertEquals('New', $instance->get($keys[0]), "Failed to get reset key `$key`");
-
-        // Delete and check that we can't retrieve.
-        foreach ($keys as $key) {
-            $this->assertTrue($instance->delete($key), "Failed to delete key `$key`");
-            $this->assertFalse($instance->get($key), "Retrieved deleted key `$key`");
-        }
-
-        // Try set many, and check that count is correct.
-        $many = array();
-        foreach ($keys as $key) {
-            $many[] = array('key' => $key, 'value' => $key);
-        }
-        $returncount = $instance->set_many($many);
-        $this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
-
-        // Check keys retrieved with get_many.
-        $values = $instance->get_many($keys);
-        foreach ($keys as $key) {
-            $this->assertTrue(isset($values[$key]), "Failed to get_many key `$key`");
-            $this->assertEquals($key, $values[$key], "Failed to match get_many key `$key`");
-        }
-
-        // Delete many, make sure count matches.
-        $returncount = $instance->delete_many($keys);
-        $this->assertEquals(count($many), $returncount, 'Delete many count didn\'t match');
-
-        // Check that each key was deleted.
-        foreach ($keys as $key) {
-            $this->assertFalse($instance->get($key), "Retrieved many deleted key `$key`");
-        }
-
-        // Set the keys again.
-        $returncount = $instance->set_many($many);
-        $this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
-
-        // Purge.
-        $this->assertTrue($instance->purge(), 'Failure to purge');
-
-        // Delete and check that we can't retrieve.
-        foreach ($keys as $key) {
-            $this->assertFalse($instance->get($key), "Retrieved purged key `$key`");
-        }
-    }
-
-    /**
-     * Tests the clustering feature.
-     */
-    public function test_clustered() {
-        $this->resetAfterTest(true);
-
-        if (!defined('TEST_CACHESTORE_MEMCACHE_TESTSERVERS')) {
-            $this->markTestSkipped();
-        }
-
-        $testservers = explode("\n", trim(TEST_CACHESTORE_MEMCACHE_TESTSERVERS));
-
-        if (count($testservers) < 2) {
-            $this->markTestSkipped();
-        }
-
-        // User the first server as our primary.
-        set_config('testservers', $testservers[0], 'cachestore_memcache');
-        set_config('testsetservers', TEST_CACHESTORE_MEMCACHE_TESTSERVERS, 'cachestore_memcache');
-        set_config('testclustered', true, 'cachestore_memcache');
-
-        // First and instance that we can use to test the second server.
-        $definition = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cachestore_memcache', 'phpunit_test');
-        $instance = cachestore_memcache::initialise_test_instance($definition);
-
-        if (!$instance) {
-            $this->markTestSkipped();
-        }
-
-        // Now we are going to setup a connection to each independent server.
-        set_config('testclustered', false, 'cachestore_memcache');
-        set_config('testsetservers', '', 'cachestore_memcache');
-        $checkinstances = array();
-        foreach ($testservers as $testserver) {
-            set_config('testservers', $testserver, 'cachestore_memcache');
-            $checkinstance = cachestore_memcache::initialise_test_instance($definition);
-            if (!$checkinstance) {
-                $this->markTestSkipped();
-            }
-            $checkinstances[] = $checkinstance;
-        }
-
-        $keys = array(
-            // Alphanumeric.
-            'abc', 'ABC', '123', 'aB1', '1aB',
-            // Hyphens.
-            'a-1', '1-a', '-a1', 'a1-',
-            // Underscores.
-            'a_1', '1_a', '_a1', 'a1_'
-        );
-
-        // Set each key.
-        foreach ($keys as $key) {
-            $this->assertTrue($instance->set($key, $key), "Failed to set key `$key`");
-        }
-
-        // Check each key.
-        foreach ($keys as $key) {
-            $this->assertEquals($key, $instance->get($key), "Failed to get key `$key`");
-            foreach ($checkinstances as $id => $checkinstance) {
-                $this->assertEquals($key, $checkinstance->get($key), "Failed to get key `$key` from server $id");
-            }
-        }
-
-        // Reset a key.
-        $this->assertTrue($instance->set($keys[0], 'New'), "Failed to reset key `$key`");
-        $this->assertEquals('New', $instance->get($keys[0]), "Failed to get reset key `$key`");
-        foreach ($checkinstances as $id => $checkinstance) {
-            $this->assertEquals('New', $checkinstance->get($keys[0]), "Failed to get reset key `$key` from server $id");
-        }
-
-        // Delete and check that we can't retrieve.
-        foreach ($keys as $key) {
-            $this->assertTrue($instance->delete($key), "Failed to delete key `$key`");
-            $this->assertFalse($instance->get($key), "Retrieved deleted key `$key`");
-            foreach ($checkinstances as $id => $checkinstance) {
-                $this->assertFalse($checkinstance->get($key), "Retrieved deleted key `$key` from server $id");
-            }
-        }
-
-        // Try set many, and check that count is correct.
-        $many = array();
-        foreach ($keys as $key) {
-            $many[] = array('key' => $key, 'value' => $key);
-        }
-        $returncount = $instance->set_many($many);
-        $this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
-
-        // Check keys retrieved with get_many.
-        $values = $instance->get_many($keys);
-        foreach ($keys as $key) {
-            $this->assertTrue(isset($values[$key]), "Failed to get_many key `$key`");
-            $this->assertEquals($key, $values[$key], "Failed to match get_many key `$key`");
-        }
-        foreach ($checkinstances as $id => $checkinstance) {
-            $values = $checkinstance->get_many($keys);
-            foreach ($keys as $key) {
-                $this->assertTrue(isset($values[$key]), "Failed to get_many key `$key` from server $id");
-                $this->assertEquals($key, $values[$key], "Failed to get_many key `$key` from server $id");
-            }
-        }
-
-        // Delete many, make sure count matches.
-        $returncount = $instance->delete_many($keys);
-        $this->assertEquals(count($many), $returncount, 'Delete many count didn\'t match');
-
-        // Check that each key was deleted.
-        foreach ($keys as $key) {
-            $this->assertFalse($instance->get($key), "Retrieved many deleted key `$key`");
-            foreach ($checkinstances as $id => $checkinstance) {
-                $this->assertFalse($checkinstance->get($key), "Retrieved many deleted key `$key` from server $id");
-            }
-        }
-
-        // Set the keys again.
-        $returncount = $instance->set_many($many);
-        $this->assertEquals(count($many), $returncount, 'Set many count didn\'t match');
-
-        // Purge.
-        $this->assertTrue($instance->purge(), 'Failure to purge');
-
-        // Delete and check that we can't retrieve.
-        foreach ($keys as $key) {
-            $this->assertFalse($instance->get($key), "Retrieved purged key `$key`");
-            foreach ($checkinstances as $id => $checkinstance) {
-                $this->assertFalse($checkinstance->get($key), "Retrieved purged key `$key` from server 2");
-            }
-        }
-    }
-
-    /**
-     * Test our checks for encoding.
-     */
-    public function test_require_encoding() {
-        $this->assertTrue(cachestore_memcache::require_encoding('dev'));
-        $this->assertTrue(cachestore_memcache::require_encoding('1.0'));
-        $this->assertTrue(cachestore_memcache::require_encoding('1.0.0'));
-        $this->assertTrue(cachestore_memcache::require_encoding('2.0'));
-        $this->assertTrue(cachestore_memcache::require_encoding('2.0.8'));
-        $this->assertTrue(cachestore_memcache::require_encoding('2.2.8'));
-        $this->assertTrue(cachestore_memcache::require_encoding('3.0'));
-        $this->assertTrue(cachestore_memcache::require_encoding('3.0-dev'));
-        $this->assertTrue(cachestore_memcache::require_encoding('3.0.0'));
-        $this->assertTrue(cachestore_memcache::require_encoding('3.0.1'));
-        $this->assertTrue(cachestore_memcache::require_encoding('3.0.2-dev'));
-        $this->assertTrue(cachestore_memcache::require_encoding('3.0.2'));
-        $this->assertTrue(cachestore_memcache::require_encoding('3.0.3-dev'));
-        $this->assertFalse(cachestore_memcache::require_encoding('3.0.3'));
-        $this->assertFalse(cachestore_memcache::require_encoding('3.0.4'));
-        $this->assertFalse(cachestore_memcache::require_encoding('3.0.4-dev'));
-        $this->assertFalse(cachestore_memcache::require_encoding('3.0.8'));
-        $this->assertFalse(cachestore_memcache::require_encoding('3.1.0'));
-        $this->assertFalse(cachestore_memcache::require_encoding('3.1.2'));
-
-    }
-}
diff --git a/cache/stores/memcache/version.php b/cache/stores/memcache/version.php
deleted file mode 100644 (file)
index fffc295..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Cache memcache store version information.
- *
- * Not to be confused with the memcached plugin.
- *
- * @package    cachestore_memcache
- * @copyright  2012 Sam Hemelryk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die;
-
-$plugin->version = 2018051400;    // The current module version (Date: YYYYMMDDXX)
-$plugin->requires = 2018050800;    // Requires this Moodle version.
-$plugin->component = 'cachestore_memcache';  // Full name of the plugin.
index 8c3e287..f6c02bd 100644 (file)
@@ -4,6 +4,7 @@ Information provided here is intended especially for developers.
 === 3.6 ===
 * The `cache::now()` function now takes an optional boolean parameter to indicate that the cache should return a more
   accurate time, generated by the PHP `microtime` function.
+* The memcache store was removed as it is not compatible with PHP 7.0.
 
 === 3.3 ===
 * Identifiers and invalidation events have been explictly been marked as incompatible and will
index fa43382..0b9c4e8 100644 (file)
@@ -284,16 +284,8 @@ $CFG->admin = 'admin';
 //      igbinary support to make the setting to work. Also, if you change the serializer you have to flush the database!
 //      $CFG->session_redis_serializer_use_igbinary = false; // Optional, default is PHP builtin serializer.
 //
-//   Memcache session handler (requires memcached server and memcache extension):
-//      $CFG->session_handler_class = '\core\session\memcache';
-//      $CFG->session_memcache_save_path = '127.0.0.1:11211';
-//      $CFG->session_memcache_acquire_lock_timeout = 120;
-//      ** NOTE: Memcache extension has less features than memcached and may be
-//         less reliable. Use memcached where possible or if you encounter
-//         session problems. **
-//
-// Please be aware that when selecting either Memcached or Memcache for sessions that it is advised to use a dedicated
-// memcache server. The memcache and memcached extensions do not provide isolated environments for individual uses.
+// Please be aware that when selecting Memcached for sessions that it is advised to use a dedicated
+// memcache server. The memcached extension does not provide isolated environments for individual uses.
 // Using the same server for other purposes (MUC for example) can lead to sessions being prematurely removed should
 // the other uses of the server purge the cache.
 //
index 806646f..fe3ec49 100644 (file)
@@ -92,7 +92,8 @@ $currentmembersoptions = '';
 $currentmemberscount = 0;
 if ($currentmembers) {
     foreach($currentmembers as $group) {
-        $currentmembersoptions .= '<option value="'.$group->id.'.">'.format_string($group->name).'</option>';
+        $currentmembersoptions .= '<option value="' . $group->id . '." title="' . format_string($group->name) . '">' .
+                format_string($group->name) . '</option>';
         $currentmemberscount ++;
     }
 
@@ -112,7 +113,8 @@ $potentialmembersoptions = '';
 $potentialmemberscount = 0;
 if ($potentialmembers) {
     foreach($potentialmembers as $group) {
-        $potentialmembersoptions .= '<option value="'.$group->id.'.">'.format_string($group->name).'</option>';
+        $potentialmembersoptions .= '<option value="' . $group->id . '." title="' . format_string($group->name) . '">' .
+                format_string($group->name) . '</option>';
         $potentialmemberscount ++;
     }
 } else {
index 6a9b92b..33faa8b 100644 (file)
@@ -34,8 +34,8 @@ $string['clianswerno'] = 'ο';
 $string['cliansweryes'] = 'ν';
 $string['cliincorrectvalueerror'] = 'Σφάλμα, λανθασμένη τιμή "{$a->value}" για το "{$a->option}"';
 $string['cliincorrectvalueretry'] = 'Λανθασμένη τιμή. Παρακαλούμε προσπαθήστε ξανά.';
-$string['clitypevalue'] = 'δώσε μία τιμή';
-$string['clitypevaluedefault'] = 'δώσε μία τιμή, πάτησε Enter για να χρησιμοποιήσεις τη προκαθορισμένη τιμή ({$a})';
+$string['clitypevalue'] = 'πληκτρολογήστε μια τιμή';
+$string['clitypevaluedefault'] = 'πληκτρολογήστε μια τιμή· πατήστε Enter για να χρησιμοποιήσετε την προκαθορισμένη τιμή ({$a})';
 $string['cliunknowoption'] = 'Μη αναγνωρίσιμες επιλογές: {$a}<br />
 Παρακαλούμε χρησιμοποιήστε την επιλογή --βοήθεια';
 $string['cliyesnoprompt'] = 'πατώντας ν (σημαίνει ναι) αλλιώς πατώντας ο (σημαίνει όχι)';
index 864d944..79466bc 100644 (file)
@@ -47,7 +47,7 @@ class core_iplookup_geoplugin_testcase extends advanced_testcase {
     }
 
     public function test_ipv4() {
-        $result = iplookup_find_location('192.30.255.112');
+        $result = iplookup_find_location('50.0.184.0');
 
         $this->assertEquals('array', gettype($result));
         $this->assertEquals('San Francisco', $result['city']);
index 7f9665d..e319dc3 100644 (file)
@@ -1648,6 +1648,7 @@ class core_plugin_manager {
             'qformat' => array('blackboard', 'learnwise'),
             'auth' => array('radius', 'fc', 'nntp', 'pam', 'pop3', 'imap'),
             'block' => array('course_overview', 'messages'),
+            'cachestore' => array('memcache'),
             'enrol' => array('authorize'),
             'report' => array('search'),
             'repository' => array('alfresco'),
@@ -1732,7 +1733,7 @@ class core_plugin_manager {
             ),
 
             'cachestore' => array(
-                'file', 'memcache', 'memcached', 'mongodb', 'session', 'static', 'apcu', 'redis'
+                'file', 'memcached', 'mongodb', 'session', 'static', 'apcu', 'redis'
             ),
 
             'calendartype' => array(
diff --git a/lib/classes/session/memcache.php b/lib/classes/session/memcache.php
deleted file mode 100644 (file)
index 62e88bf..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Memcache based session handler.
- *
- * This is based on the memcached code. It lacks some features, such as
- * locking options, but appears to work in practice.
- *
- * Note: You may need to manually configure redundancy and fail-over
- * if you specify multiple servers.
- *
- * @package core
- * @copyright 2014 The Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core\session;
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Memcache based session handler.
- *
- * @package core
- * @copyright 2014 The Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class memcache extends handler {
-    /** @var string $savepath save_path string  */
-    protected $savepath;
-    /** @var array $servers list of servers parsed from save_path */
-    protected $servers;
-    /** @var int $acquiretimeout how long to wait for session lock */
-    protected $acquiretimeout = 120;
-
-    /**
-     * Creates new instance of handler.
-     */
-    public function __construct() {
-        global $CFG;
-
-        if (empty($CFG->session_memcache_save_path)) {
-            $this->savepath = '';
-        } else {
-            $this->savepath = $CFG->session_memcache_save_path;
-        }
-
-        if (empty($this->savepath)) {
-            $this->servers = array();
-        } else {
-            $this->servers = util::connection_string_to_memcache_servers($this->savepath);
-        }
-
-        if (!empty($CFG->session_memcache_acquire_lock_timeout)) {
-            $this->acquiretimeout = (int)$CFG->session_memcache_acquire_lock_timeout;
-        }
-    }
-
-    /**
-     * Starts the session.
-     *
-     * @return bool success
-     */
-    public function start() {
-        $default = ini_get('max_execution_time');
-        set_time_limit($this->acquiretimeout);
-
-        $result = parent::start();
-
-        set_time_limit($default);
-        return $result;
-    }
-
-    /**
-     * Inits session handler.
-     */
-    public function init() {
-        if (!extension_loaded('memcache')) {
-            throw new exception('sessionhandlerproblem', 'error', '', null,
-                    'memcache extension is not loaded');
-        }
-        $version = phpversion('memcache');
-        if (!$version or version_compare($version, '2.2') < 0) {
-            throw new exception('sessionhandlerproblem', 'error', '', null,
-                    'memcache extension version must be at least 2.2');
-        }
-        if (empty($this->savepath)) {
-            throw new exception('sessionhandlerproblem', 'error', '', null,
-                    '$CFG->session_memcache_save_path must be specified in config.php');
-        }
-        // Check in case anybody mistakenly includes tcp://, which you
-        // would do in the raw PHP config. We require the same format as
-        // for memcached (without tcp://). Otherwse the code that splits it into
-        // individual servers won't have worked properly.
-        if (strpos($this->savepath, 'tcp://') !== false) {
-            throw new exception('sessionhandlerproblem', 'error', '', null,
-                    '$CFG->session_memcache_save_path should not contain tcp://');
-        }
-
-        ini_set('session.save_handler', 'memcache');
-
-        // The format of save_path is different for memcache (compared to memcached).
-        // We are using the same format in config.php to avoid confusion.
-        // It has to have tcp:// at the start of each entry.
-        $memcacheformat = preg_replace('~(^|,\s*)~','$1tcp://', $this->savepath);
-        ini_set('session.save_path', $memcacheformat);
-    }
-
-    /**
-     * Check the backend contains data for this session id.
-     *
-     * Note: this is intended to be called from manager::session_exists() only.
-     *
-     * @param string $sid PHP session ID
-     * @return bool true if session found.
-     */
-    public function session_exists($sid) {
-        $result = false;
-
-        foreach ($this->get_memcaches() as $memcache) {
-            if ($result === false) {
-                $value = $memcache->get($sid);
-                if ($value !== false) {
-                    $result = true;
-                }
-            }
-            $memcache->close();
-        }
-
-        return $result;
-    }
-
-    /**
-     * Gets the Memcache objects, one for each server.
-     * The connects must be closed manually after use.
-     *
-     * Note: the servers are not automatically synchronised
-     *       when accessed via Memcache class, it needs to be
-     *       done manually by accessing all configured servers.
-     *
-     * @return \Memcache[] Array of initialised memcache objects
-     */
-    protected function get_memcaches() {
-        $result = array();
-        foreach ($this->servers as $server) {
-            $memcache = new \Memcache();
-            $memcache->addServer($server[0], $server[1]);
-            $result[] = $memcache;
-        }
-        return $result;
-    }
-
-    /**
-     * Kills all active sessions, the core sessions table is purged afterwards.
-     */
-    public function kill_all_sessions() {
-        global $DB;
-        if (!$this->servers) {
-            return;
-        }
-
-        $memcaches = $this->get_memcaches();
-
-        // Note: this can be significantly improved by fetching keys from memcache,
-        // but we need to make sure we are not deleting somebody else's sessions.
-
-        $rs = $DB->get_recordset('sessions', array(), 'id DESC', 'id, sid');
-        foreach ($rs as $record) {
-            foreach ($memcaches as $memcache) {
-                $memcache->delete($record->sid);
-            }
-        }
-        $rs->close();
-
-        foreach ($memcaches as $memcache) {
-            $memcache->close();
-        }
-    }
-
-    /**
-     * Kills one session, the session record is removed afterwards.
-     *
-     * @param string $sid PHP session ID
-     */
-    public function kill_session($sid) {
-        foreach ($this->get_memcaches() as $memcache) {
-            $memcache->delete($sid);
-            $memcache->close();
-        }
-    }
-}
index 5854f45..643e10e 100644 (file)
@@ -74,7 +74,7 @@ class memcached extends handler {
         if (empty($this->savepath)) {
             $this->servers = array();
         } else {
-            $this->servers = util::connection_string_to_memcache_servers($this->savepath);
+            $this->servers = self::connection_string_to_memcache_servers($this->savepath);
         }
 
         if (empty($CFG->session_memcached_prefix)) {
@@ -268,4 +268,33 @@ class memcached extends handler {
         }
     }
 
+    /**
+     * Convert a connection string to an array of servers.
+     *
+     * "abc:123, xyz:789" to
+     *  [
+     *      ['abc', '123'],
+     *      ['xyz', '789'],
+     *  ]
+     *
+     * @param   string  $str save_path value containing memcached connection string
+     * @return  array[]
+     */
+    protected static function connection_string_to_memcache_servers(string $str) : array {
+        $servers = [];
+        $parts   = explode(',', $str);
+        foreach ($parts as $part) {
+            $part = trim($part);
+            $pos  = strrpos($part, ':');
+            if ($pos !== false) {
+                $host = substr($part, 0, $pos);
+                $port = substr($part, ($pos + 1));
+            } else {
+                $host = $part;
+                $port = 11211;
+            }
+            $servers[] = [$host, $port];
+        }
+        return $servers;
+    }
 }
diff --git a/lib/classes/session/util.php b/lib/classes/session/util.php
deleted file mode 100644 (file)
index 72c476a..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-// This file is part of Moodle - http://moodle.org/
-//
-// Moodle is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// Moodle is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-
-/**
- * Shared utility functions for session handlers.
- *
- * This contains functions that are shared between two or more handlers.
- *
- * @package core
- * @copyright 2014 The Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core\session;
-
-defined('MOODLE_INTERNAL') || die();
-
-/**
- * Shared utility functions for session handlers.
- *
- * This contains functions that are shared between two or more handlers.
- *
- * @package core
- * @copyright 2014 The Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-abstract class util {
-    /**
-     * Convert a connection string to an array of servers
-     *
-     * EG: Converts: "abc:123, xyz:789" to
-     *
-     *  array(
-     *      array('abc', '123'),
-     *      array('xyz', '789'),
-     *  )
-     *
-     * @copyright  2013 Moodlerooms Inc. (http://www.moodlerooms.com)
-     * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
-     * @author     Mark Nielsen
-     *
-     * @param string $str save_path value containing memcached connection string
-     * @return array
-     */
-    public static function connection_string_to_memcache_servers($str) {
-        $servers = array();
-        $parts   = explode(',', $str);
-        foreach ($parts as $part) {
-            $part = trim($part);
-            $pos  = strrpos($part, ':');
-            if ($pos !== false) {
-                $host = substr($part, 0, $pos);
-                $port = substr($part, ($pos + 1));
-            } else {
-                $host = $part;
-                $port = 11211;
-            }
-            $servers[] = array($host, $port);
-        }
-        return $servers;
-    }
-}
index 80f33f8..df3883a 100644 (file)
@@ -2328,5 +2328,13 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2018083100.01);
     }
 
+    if ($oldversion < 2018091200.00) {
+        if (!file_exists($CFG->dirroot.'/cache/stores/memcache/settings.php')) {
+            unset_all_config_for_plugin('cachestore_memcache');
+        }
+
+        upgrade_main_savepoint(true, 2018091200.00);
+    }
+
     return true;
 }
diff --git a/lib/editor/atto/plugins/html/styles.css b/lib/editor/atto/plugins/html/styles.css
new file mode 100644 (file)
index 0000000..f6e60ce
--- /dev/null
@@ -0,0 +1,4 @@
+.dir-rtl .CodeMirror {
+    text-align: left;
+    direction: ltr;
+}
diff --git a/lib/editor/atto/plugins/html/thirdpartylibs.xml b/lib/editor/atto/plugins/html/thirdpartylibs.xml
new file mode 100644 (file)
index 0000000..8351c68
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<libraries>
+  <library>
+    <location>yui/src/codemirror</location>
+    <name>codemirror</name>
+    <license>MIT</license>
+    <version>5.37.0</version>
+    <licenseversion></licenseversion>
+  </library>
+  <library>
+    <location>yui/src/beautify</location>
+    <name>jsbeautify</name>
+    <license>MIT</license>
+    <version>1.7.5</version>
+    <licenseversion></licenseversion>
+  </library>
+</libraries>
index 3504e83..8139bbf 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018051400;        // The current plugin version (Date: YYYYMMDDXX).
-$plugin->requires  = 2018050800;        // Requires this Moodle version.
+$plugin->version   = 2018090500;        // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2018090500;        // Requires this Moodle version.
 $plugin->component = 'atto_html';  // Full name of the plugin (used for diagnostics).
diff --git a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-debug.js b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-debug.js
new file mode 100644 (file)
index 0000000..39d28f6
Binary files /dev/null and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-debug.js differ
diff --git a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-min.js b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-min.js
new file mode 100644 (file)
index 0000000..202ba15
Binary files /dev/null and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify-min.js differ
diff --git a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify.js b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify.js
new file mode 100644 (file)
index 0000000..39d28f6
Binary files /dev/null and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-beautify/moodle-atto_html-beautify.js differ
index 5bb7914..a3d932a 100644 (file)
Binary files a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-debug.js and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-debug.js differ
index 16fb192..4405900 100644 (file)
Binary files a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-min.js and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button-min.js differ
index 5bb7914..a3d932a 100644 (file)
Binary files a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button.js and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-button/moodle-atto_html-button.js differ
diff --git a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/moodle-atto_html-codemirror-core.css b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/moodle-atto_html-codemirror-core.css
new file mode 100644 (file)
index 0000000..c7a8ae7
Binary files /dev/null and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/moodle-atto_html-codemirror-core.css differ
diff --git a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/skins/sam/moodle-atto_html-codemirror-skin.css b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/skins/sam/moodle-atto_html-codemirror-skin.css
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/skins/sam/moodle-atto_html-codemirror.css b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/skins/sam/moodle-atto_html-codemirror.css
new file mode 100644 (file)
index 0000000..77424f5
Binary files /dev/null and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/assets/skins/sam/moodle-atto_html-codemirror.css differ
diff --git a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-debug.js b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-debug.js
new file mode 100644 (file)
index 0000000..549ffa9
Binary files /dev/null and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-debug.js differ
diff --git a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-min.js b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-min.js
new file mode 100644 (file)
index 0000000..41819a0
Binary files /dev/null and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror-min.js differ
diff --git a/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror.js b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror.js
new file mode 100644 (file)
index 0000000..549ffa9
Binary files /dev/null and b/lib/editor/atto/plugins/html/yui/build/moodle-atto_html-codemirror/moodle-atto_html-codemirror.js differ
diff --git a/lib/editor/atto/plugins/html/yui/src/beautify/LICENSE b/lib/editor/atto/plugins/html/yui/src/beautify/LICENSE
new file mode 100644 (file)
index 0000000..a92a60e
--- /dev/null
@@ -0,0 +1,9 @@
+The MIT License (MIT)
+
+Copyright (c) 2007-2013 Einar Lielmanis and contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/editor/atto/plugins/html/yui/src/beautify/build.json b/lib/editor/atto/plugins/html/yui/src/beautify/build.json
new file mode 100644 (file)
index 0000000..3d79421
--- /dev/null
@@ -0,0 +1,16 @@
+{
+    "name": "moodle-atto_html-beautify",
+    "builds": {
+        "moodle-atto_html-beautify": {
+            "jsfiles": [
+                "wrapper.js",
+                "beautify.js",
+                "beautify-css.js",
+                "beautify-html.js"
+            ]
+        }
+    },
+    "shifter": {
+        "lint": false
+    }
+}
diff --git a/lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-css.js b/lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-css.js
new file mode 100644 (file)
index 0000000..589d691
--- /dev/null
@@ -0,0 +1,1048 @@
+/*jshint curly:false, eqeqeq:true, laxbreak:true, noempty:false */
+/* AUTO-GENERATED. DO NOT MODIFY. */
+/*
+
+  The MIT License (MIT)
+
+  Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation files
+  (the "Software"), to deal in the Software without restriction,
+  including without limitation the rights to use, copy, modify, merge,
+  publish, distribute, sublicense, and/or sell copies of the Software,
+  and to permit persons to whom the Software is furnished to do so,
+  subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+  ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+
+
+ CSS Beautifier
+---------------
+
+    Written by Harutyun Amirjanyan, (amirjanyan@gmail.com)
+
+    Based on code initially developed by: Einar Lielmanis, <einar@jsbeautifier.org>
+        http://jsbeautifier.org/
+
+    Usage:
+        css_beautify(source_text);
+        css_beautify(source_text, options);
+
+    The options are (default in brackets):
+        indent_size (4)                         — indentation size,
+        indent_char (space)                     — character to indent with,
+        selector_separator_newline (true)       - separate selectors with newline or
+                                                  not (e.g. "a,\nbr" or "a, br")
+        end_with_newline (false)                - end with a newline
+        newline_between_rules (true)            - add a new line after every css rule
+        space_around_selector_separator (false) - ensure space around selector separators:
+                                                  '>', '+', '~' (e.g. "a>b" -> "a > b")
+    e.g
+
+    css_beautify(css_source_text, {
+      'indent_size': 1,
+      'indent_char': '\t',
+      'selector_separator': ' ',
+      'end_with_newline': false,
+      'newline_between_rules': true,
+      'space_around_selector_separator': true
+    });
+*/
+
+// http://www.w3.org/TR/CSS21/syndata.html#tokenization
+// http://www.w3.org/TR/css3-syntax/
+
+(function() {
+var legacy_beautify_css =
+/******/ (function(modules) { // webpackBootstrap
+/******/       // The module cache
+/******/       var installedModules = {};
+/******/
+/******/       // The require function
+/******/       function __webpack_require__(moduleId) {
+/******/
+/******/               // Check if module is in cache
+/******/               if(installedModules[moduleId]) {
+/******/                       return installedModules[moduleId].exports;
+/******/               }
+/******/               // Create a new module (and put it into the cache)
+/******/               var module = installedModules[moduleId] = {
+/******/                       i: moduleId,
+/******/                       l: false,
+/******/                       exports: {}
+/******/               };
+/******/
+/******/               // Execute the module function
+/******/               modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/               // Flag the module as loaded
+/******/               module.l = true;
+/******/
+/******/               // Return the exports of the module
+/******/               return module.exports;
+/******/       }
+/******/
+/******/
+/******/       // expose the modules object (__webpack_modules__)
+/******/       __webpack_require__.m = modules;
+/******/
+/******/       // expose the module cache
+/******/       __webpack_require__.c = installedModules;
+/******/
+/******/       // identity function for calling harmony imports with the correct context
+/******/       __webpack_require__.i = function(value) { return value; };
+/******/
+/******/       // define getter function for harmony exports
+/******/       __webpack_require__.d = function(exports, name, getter) {
+/******/               if(!__webpack_require__.o(exports, name)) {
+/******/                       Object.defineProperty(exports, name, {
+/******/                               configurable: false,
+/******/                               enumerable: true,
+/******/                               get: getter
+/******/                       });
+/******/               }
+/******/       };
+/******/
+/******/       // getDefaultExport function for compatibility with non-harmony modules
+/******/       __webpack_require__.n = function(module) {
+/******/               var getter = module && module.__esModule ?
+/******/                       function getDefault() { return module['default']; } :
+/******/                       function getModuleExports() { return module; };
+/******/               __webpack_require__.d(getter, 'a', getter);
+/******/               return getter;
+/******/       };
+/******/
+/******/       // Object.prototype.hasOwnProperty.call
+/******/       __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/       // __webpack_public_path__
+/******/       __webpack_require__.p = "";
+/******/
+/******/       // Load entry module and return exports
+/******/       return __webpack_require__(__webpack_require__.s = 4);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
+/*
+
+  The MIT License (MIT)
+
+  Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation files
+  (the "Software"), to deal in the Software without restriction,
+  including without limitation the rights to use, copy, modify, merge,
+  publish, distribute, sublicense, and/or sell copies of the Software,
+  and to permit persons to whom the Software is furnished to do so,
+  subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+  ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+*/
+
+var mergeOpts = __webpack_require__(2).mergeOpts;
+var acorn = __webpack_require__(1);
+var Output = __webpack_require__(3).Output;
+
+
+var lineBreak = acorn.lineBreak;
+var allLineBreaks = acorn.allLineBreaks;
+
+function Beautifier(source_text, options) {
+    options = options || {};
+
+    // Allow the setting of language/file-type specific options
+    // with inheritance of overall settings
+    options = mergeOpts(options, 'css');
+
+    source_text = source_text || '';
+
+    var newlinesFromLastWSEat = 0;
+    var indentSize = options.indent_size ? parseInt(options.indent_size, 10) : 4;
+    var indentCharacter = options.indent_char || ' ';
+    var preserve_newlines = (options.preserve_newlines === undefined) ? false : options.preserve_newlines;
+    var selectorSeparatorNewline = (options.selector_separator_newline === undefined) ? true : options.selector_separator_newline;
+    var end_with_newline = (options.end_with_newline === undefined) ? false : options.end_with_newline;
+    var newline_between_rules = (options.newline_between_rules === undefined) ? true : options.newline_between_rules;
+    var space_around_combinator = (options.space_around_combinator === undefined) ? false : options.space_around_combinator;
+    space_around_combinator = space_around_combinator || ((options.space_around_selector_separator === undefined) ? false : options.space_around_selector_separator);
+    var eol = options.eol ? options.eol : 'auto';
+
+    if (options.indent_with_tabs) {
+        indentCharacter = '\t';
+        indentSize = 1;
+    }
+
+    if (eol === 'auto') {
+        eol = '\n';
+        if (source_text && lineBreak.test(source_text || '')) {
+            eol = source_text.match(lineBreak)[0];
+        }
+    }
+
+    eol = eol.replace(/\\r/, '\r').replace(/\\n/, '\n');
+
+    // HACK: newline parsing inconsistent. This brute force normalizes the input.
+    source_text = source_text.replace(allLineBreaks, '\n');
+
+    // tokenizer
+    var whiteRe = /^\s+$/;
+
+    var pos = -1,
+        ch;
+    var parenLevel = 0;
+
+    function next() {
+        ch = source_text.charAt(++pos);
+        return ch || '';
+    }
+
+    function peek(skipWhitespace) {
+        var result = '';
+        var prev_pos = pos;
+        if (skipWhitespace) {
+            eatWhitespace();
+        }
+        result = source_text.charAt(pos + 1) || '';
+        pos = prev_pos - 1;
+        next();
+        return result;
+    }
+
+    function eatString(endChars) {
+        var start = pos;
+        while (next()) {
+            if (ch === "\\") {
+                next();
+            } else if (endChars.indexOf(ch) !== -1) {
+                break;
+            } else if (ch === "\n") {
+                break;
+            }
+        }
+        return source_text.substring(start, pos + 1);
+    }
+
+    function peekString(endChar) {
+        var prev_pos = pos;
+        var str = eatString(endChar);
+        pos = prev_pos - 1;
+        next();
+        return str;
+    }
+
+    function eatWhitespace(preserve_newlines_local) {
+        var result = 0;
+        while (whiteRe.test(peek())) {
+            next();
+            if (ch === '\n' && preserve_newlines_local && preserve_newlines) {
+                output.add_new_line(true);
+                result++;
+            }
+        }
+        newlinesFromLastWSEat = result;
+        return result;
+    }
+
+    function skipWhitespace() {
+        var result = '';
+        if (ch && whiteRe.test(ch)) {
+            result = ch;
+        }
+        while (whiteRe.test(next())) {
+            result += ch;
+        }
+        return result;
+    }
+
+    function eatComment() {
+        var start = pos;
+        var singleLine = peek() === "/";
+        next();
+        while (next()) {
+            if (!singleLine && ch === "*" && peek() === "/") {
+                next();
+                break;
+            } else if (singleLine && ch === "\n") {
+                return source_text.substring(start, pos);
+            }
+        }
+
+        return source_text.substring(start, pos) + ch;
+    }
+
+
+    function lookBack(str) {
+        return source_text.substring(pos - str.length, pos).toLowerCase() ===
+            str;
+    }
+
+    // Nested pseudo-class if we are insideRule
+    // and the next special character found opens
+    // a new block
+    function foundNestedPseudoClass() {
+        var openParen = 0;
+        for (var i = pos + 1; i < source_text.length; i++) {
+            var ch = source_text.charAt(i);
+            if (ch === "{") {
+                return true;
+            } else if (ch === '(') {
+                // pseudoclasses can contain ()
+                openParen += 1;
+            } else if (ch === ')') {
+                if (openParen === 0) {
+                    return false;
+                }
+                openParen -= 1;
+            } else if (ch === ";" || ch === "}") {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    // printer
+    var baseIndentString = '';
+    var preindent_index = 0;
+    if (source_text && source_text.length) {
+        while ((source_text.charAt(preindent_index) === ' ' ||
+                source_text.charAt(preindent_index) === '\t')) {
+            preindent_index += 1;
+        }
+        baseIndentString = source_text.substring(0, preindent_index);
+        source_text = source_text.substring(preindent_index);
+    }
+
+
+    var singleIndent = new Array(indentSize + 1).join(indentCharacter);
+    var indentLevel;
+    var nestedLevel;
+    var output;
+
+    function print_string(output_string) {
+        if (output.just_added_newline()) {
+            output.set_indent(indentLevel);
+        }
+        output.add_token(output_string);
+    }
+
+    function preserveSingleSpace(isAfterSpace) {
+        if (isAfterSpace) {
+            output.space_before_token = true;
+        }
+    }
+
+    function indent() {
+        indentLevel++;
+    }
+
+    function outdent() {
+        if (indentLevel > 0) {
+            indentLevel--;
+        }
+    }
+
+    /*_____________________--------------------_____________________*/
+
+    this.beautify = function() {
+        // reset
+        output = new Output(singleIndent, baseIndentString);
+        indentLevel = 0;
+        nestedLevel = 0;
+
+        pos = -1;
+        ch = null;
+        parenLevel = 0;
+
+        var insideRule = false;
+        var insidePropertyValue = false;
+        var enteringConditionalGroup = false;
+        var top_ch = '';
+        var last_top_ch = '';
+
+        while (true) {
+            var whitespace = skipWhitespace();
+            var isAfterSpace = whitespace !== '';
+            var isAfterNewline = whitespace.indexOf('\n') !== -1;
+            last_top_ch = top_ch;
+            top_ch = ch;
+
+            if (!ch) {
+                break;
+            } else if (ch === '/' && peek() === '*') { /* css comment */
+                var header = indentLevel === 0;
+
+                if (isAfterNewline || header) {
+                    output.add_new_line();
+                }
+
+                print_string(eatComment());
+                output.add_new_line();
+                if (header) {
+                    output.add_new_line(true);
+                }
+            } else if (ch === '/' && peek() === '/') { // single line comment
+                if (!isAfterNewline && last_top_ch !== '{') {
+                    output.trim(true);
+                }
+                output.space_before_token = true;
+                print_string(eatComment());
+                output.add_new_line();
+            } else if (ch === '@') {
+                preserveSingleSpace(isAfterSpace);
+
+                // deal with less propery mixins @{...}
+                if (peek() === '{') {
+                    print_string(eatString('}'));
+                } else {
+                    print_string(ch);
+
+                    // strip trailing space, if present, for hash property checks
+                    var variableOrRule = peekString(": ,;{}()[]/='\"");
+
+                    if (variableOrRule.match(/[ :]$/)) {
+                        // we have a variable or pseudo-class, add it and insert one space before continuing
+                        next();
+                        variableOrRule = eatString(": ").replace(/\s$/, '');
+                        print_string(variableOrRule);
+                        output.space_before_token = true;
+                    }
+
+                    variableOrRule = variableOrRule.replace(/\s$/, '');
+
+                    // might be a nesting at-rule
+                    if (variableOrRule in this.NESTED_AT_RULE) {
+                        nestedLevel += 1;
+                        if (variableOrRule in this.CONDITIONAL_GROUP_RULE) {
+                            enteringConditionalGroup = true;
+                        }
+                    }
+                }
+            } else if (ch === '#' && peek() === '{') {
+                preserveSingleSpace(isAfterSpace);
+                print_string(eatString('}'));
+            } else if (ch === '{') {
+                if (peek(true) === '}') {
+                    eatWhitespace();
+                    next();
+                    output.space_before_token = true;
+                    print_string("{}");
+                    if (!eatWhitespace(true)) {
+                        output.add_new_line();
+                    }
+
+                    if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) {
+                        output.add_new_line(true);
+                    }
+                } else {
+                    indent();
+                    output.space_before_token = true;
+                    print_string(ch);
+                    if (!eatWhitespace(true)) {
+                        output.add_new_line();
+                    }
+
+                    // when entering conditional groups, only rulesets are allowed
+                    if (enteringConditionalGroup) {
+                        enteringConditionalGroup = false;
+                        insideRule = (indentLevel > nestedLevel);
+                    } else {
+                        // otherwise, declarations are also allowed
+                        insideRule = (indentLevel >= nestedLevel);
+                    }
+                }
+            } else if (ch === '}') {
+                outdent();
+                output.add_new_line();
+                print_string(ch);
+                insideRule = false;
+                insidePropertyValue = false;
+                if (nestedLevel) {
+                    nestedLevel--;
+                }
+
+                if (!eatWhitespace(true)) {
+                    output.add_new_line();
+                }
+
+                if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) {
+                    output.add_new_line(true);
+                }
+            } else if (ch === ":") {
+                eatWhitespace();
+                if ((insideRule || enteringConditionalGroup) &&
+                    !(lookBack("&") || foundNestedPseudoClass()) &&
+                    !lookBack("(")) {
+                    // 'property: value' delimiter
+                    // which could be in a conditional group query
+                    print_string(':');
+                    if (!insidePropertyValue) {
+                        insidePropertyValue = true;
+                        output.space_before_token = true;
+                    }
+                } else {
+                    // sass/less parent reference don't use a space
+                    // sass nested pseudo-class don't use a space
+
+                    // preserve space before pseudoclasses/pseudoelements, as it means "in any child"
+                    if (lookBack(" ")) {
+                        output.space_before_token = true;
+                    }
+                    if (peek() === ":") {
+                        // pseudo-element
+                        next();
+                        print_string("::");
+                    } else {
+                        // pseudo-class
+                        print_string(':');
+                    }
+                }
+            } else if (ch === '"' || ch === '\'') {
+                preserveSingleSpace(isAfterSpace);
+                print_string(eatString(ch));
+            } else if (ch === ';') {
+                insidePropertyValue = false;
+                print_string(ch);
+                if (!eatWhitespace(true)) {
+                    output.add_new_line();
+                }
+            } else if (ch === '(') { // may be a url
+                if (lookBack("url")) {
+                    print_string(ch);
+                    eatWhitespace();
+                    if (next()) {
+                        if (ch !== ')' && ch !== '"' && ch !== '\'') {
+                            print_string(eatString(')'));
+                        } else {
+                            pos--;
+                        }
+                    }
+                } else {
+                    parenLevel++;
+                    preserveSingleSpace(isAfterSpace);
+                    print_string(ch);
+                    eatWhitespace();
+                }
+            } else if (ch === ')') {
+                print_string(ch);
+                parenLevel--;
+            } else if (ch === ',') {
+                print_string(ch);
+                if (!eatWhitespace(true) && selectorSeparatorNewline && !insidePropertyValue && parenLevel < 1) {
+                    output.add_new_line();
+                } else {
+                    output.space_before_token = true;
+                }
+            } else if ((ch === '>' || ch === '+' || ch === '~') &&
+                !insidePropertyValue && parenLevel < 1) {
+                //handle combinator spacing
+                if (space_around_combinator) {
+                    output.space_before_token = true;
+                    print_string(ch);
+                    output.space_before_token = true;
+                } else {
+                    print_string(ch);
+                    eatWhitespace();
+                    // squash extra whitespace
+                    if (ch && whiteRe.test(ch)) {
+                        ch = '';
+                    }
+                }
+            } else if (ch === ']') {
+                print_string(ch);
+            } else if (ch === '[') {
+                preserveSingleSpace(isAfterSpace);
+                print_string(ch);
+            } else if (ch === '=') { // no whitespace before or after
+                eatWhitespace();
+                print_string('=');
+                if (whiteRe.test(ch)) {
+                    ch = '';
+                }
+            } else if (ch === '!') { // !important
+                print_string(' ');
+                print_string(ch);
+            } else {
+                preserveSingleSpace(isAfterSpace);
+                print_string(ch);
+            }
+        }
+
+        var sweetCode = output.get_code(end_with_newline, eol);
+
+        return sweetCode;
+    };
+
+    // https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
+    this.NESTED_AT_RULE = {
+        "@page": true,
+        "@font-face": true,
+        "@keyframes": true,
+        // also in CONDITIONAL_GROUP_RULE below
+        "@media": true,
+        "@supports": true,
+        "@document": true
+    };
+    this.CONDITIONAL_GROUP_RULE = {
+        "@media": true,
+        "@supports": true,
+        "@document": true
+    };
+}
+
+module.exports.Beautifier = Beautifier;
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports) {
+
+/* jshint curly: false */
+// This section of code is taken from acorn.
+//
+// Acorn was written by Marijn Haverbeke and released under an MIT
+// license. The Unicode regexps (for identifiers and whitespace) were
+// taken from [Esprima](http://esprima.org) by Ariya Hidayat.
+//
+// Git repositories for Acorn are available at
+//
+//     http://marijnhaverbeke.nl/git/acorn
+//     https://github.com/marijnh/acorn.git
+
+// ## Character categories
+
+// Big ugly regular expressions that match characters in the
+// whitespace, identifier, and identifier-start categories. These
+// are only applied when a character is found to actually have a
+// code point above 128.
+
+var nonASCIIwhitespace = /[\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]/; // jshint ignore:line
+var nonASCIIidentifierStartChars = "\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0\u08a2-\u08ac\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19c1-\u19c7\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa80-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc";
+var nonASCIIidentifierChars = "\u0300-\u036f\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u0620-\u0649\u0672-\u06d3\u06e7-\u06e8\u06fb-\u06fc\u0730-\u074a\u0800-\u0814\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0840-\u0857\u08e4-\u08fe\u0900-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962-\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09d7\u09df-\u09e0\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2-\u0ae3\u0ae6-\u0aef\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b5f-\u0b60\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c01-\u0c03\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62-\u0c63\u0c66-\u0c6f\u0c82\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2-\u0ce3\u0ce6-\u0cef\u0d02\u0d03\u0d46-\u0d48\u0d57\u0d62-\u0d63\u0d66-\u0d6f\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e34-\u0e3a\u0e40-\u0e45\u0e50-\u0e59\u0eb4-\u0eb9\u0ec8-\u0ecd\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f41-\u0f47\u0f71-\u0f84\u0f86-\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u1000-\u1029\u1040-\u1049\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u170e-\u1710\u1720-\u1730\u1740-\u1750\u1772\u1773\u1780-\u17b2\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u1920-\u192b\u1930-\u193b\u1951-\u196d\u19b0-\u19c0\u19c8-\u19c9\u19d0-\u19d9\u1a00-\u1a15\u1a20-\u1a53\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1b46-\u1b4b\u1b50-\u1b59\u1b6b-\u1b73\u1bb0-\u1bb9\u1be6-\u1bf3\u1c00-\u1c22\u1c40-\u1c49\u1c5b-\u1c7d\u1cd0-\u1cd2\u1d00-\u1dbe\u1e01-\u1f15\u200c\u200d\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2d81-\u2d96\u2de0-\u2dff\u3021-\u3028\u3099\u309a\ua640-\ua66d\ua674-\ua67d\ua69f\ua6f0-\ua6f1\ua7f8-\ua800\ua806\ua80b\ua823-\ua827\ua880-\ua881\ua8b4-\ua8c4\ua8d0-\ua8d9\ua8f3-\ua8f7\ua900-\ua909\ua926-\ua92d\ua930-\ua945\ua980-\ua983\ua9b3-\ua9c0\uaa00-\uaa27\uaa40-\uaa41\uaa4c-\uaa4d\uaa50-\uaa59\uaa7b\uaae0-\uaae9\uaaf2-\uaaf3\uabc0-\uabe1\uabec\uabed\uabf0-\uabf9\ufb20-\ufb28\ufe00-\ufe0f\ufe20-\ufe26\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f";
+var nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]");
+var nonASCIIidentifier = new RegExp("[" + nonASCIIidentifierStartChars + nonASCIIidentifierChars + "]");
+
+// Whether a single character denotes a newline.
+
+exports.newline = /[\n\r\u2028\u2029]/;
+
+// Matches a whole line break (where CRLF is considered a single
+// line break). Used to count lines.
+
+// in javascript, these two differ
+// in python they are the same, different methods are called on them
+exports.lineBreak = new RegExp('\r\n|' + exports.newline.source);
+exports.allLineBreaks = new RegExp(exports.lineBreak.source, 'g');
+
+
+// Test whether a given character code starts an identifier.
+
+exports.isIdentifierStart = function(code) {
+    // permit $ (36) and @ (64). @ is used in ES7 decorators.
+    if (code < 65) return code === 36 || code === 64;
+    // 65 through 91 are uppercase letters.
+    if (code < 91) return true;
+    // permit _ (95).
+    if (code < 97) return code === 95;
+    // 97 through 123 are lowercase letters.
+    if (code < 123) return true;
+    return code >= 0xaa && nonASCIIidentifierStart.test(String.fromCharCode(code));
+};
+
+// Test whether a given character is part of an identifier.
+
+exports.isIdentifierChar = function(code) {
+    if (code < 48) return code === 36;
+    if (code < 58) return true;
+    if (code < 65) return false;
+    if (code < 91) return true;
+    if (code < 97) return code === 95;
+    if (code < 123) return true;
+    return code >= 0xaa && nonASCIIidentifier.test(String.fromCharCode(code));
+};
+
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports) {
+
+/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
+/*
+
+    The MIT License (MIT)
+
+    Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
+
+    Permission is hereby granted, free of charge, to any person
+    obtaining a copy of this software and associated documentation files
+    (the "Software"), to deal in the Software without restriction,
+    including without limitation the rights to use, copy, modify, merge,
+    publish, distribute, sublicense, and/or sell copies of the Software,
+    and to permit persons to whom the Software is furnished to do so,
+    subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be
+    included in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+    BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+    ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+    CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+    SOFTWARE.
+*/
+
+function mergeOpts(allOptions, targetType) {
+    var finalOpts = {};
+    var name;
+
+    for (name in allOptions) {
+        if (name !== targetType) {
+            finalOpts[name] = allOptions[name];
+        }
+    }
+
+    //merge in the per type settings for the targetType
+    if (targetType in allOptions) {
+        for (name in allOptions[targetType]) {
+            finalOpts[name] = allOptions[targetType][name];
+        }
+    }
+    return finalOpts;
+}
+
+module.exports.mergeOpts = mergeOpts;
+
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports) {
+
+/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
+/*
+
+  The MIT License (MIT)
+
+  Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation files
+  (the "Software"), to deal in the Software without restriction,
+  including without limitation the rights to use, copy, modify, merge,
+  publish, distribute, sublicense, and/or sell copies of the Software,
+  and to permit persons to whom the Software is furnished to do so,
+  subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+  ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+*/
+
+function OutputLine(parent) {
+    var _character_count = 0;
+    // use indent_count as a marker for lines that have preserved indentation
+    var _indent_count = -1;
+
+    var _items = [];
+    var _empty = true;
+
+    this.set_indent = function(level) {
+        _character_count = parent.baseIndentLength + level * parent.indent_length;
+        _indent_count = level;
+    };
+
+    this.get_character_count = function() {
+        return _character_count;
+    };
+
+    this.is_empty = function() {
+        return _empty;
+    };
+
+    this.last = function() {
+        if (!this._empty) {
+            return _items[_items.length - 1];
+        } else {
+            return null;
+        }
+    };
+
+    this.push = function(input) {
+        _items.push(input);
+        _character_count += input.length;
+        _empty = false;
+    };
+
+    this.pop = function() {
+        var item = null;
+        if (!_empty) {
+            item = _items.pop();
+            _character_count -= item.length;
+            _empty = _items.length === 0;
+        }
+        return item;
+    };
+
+    this.remove_indent = function() {
+        if (_indent_count > 0) {
+            _indent_count -= 1;
+            _character_count -= parent.indent_length;
+        }
+    };
+
+    this.trim = function() {
+        while (this.last() === ' ') {
+            _items.pop();
+            _character_count -= 1;
+        }
+        _empty = _items.length === 0;
+    };
+
+    this.toString = function() {
+        var result = '';
+        if (!this._empty) {
+            if (_indent_count >= 0) {
+                result = parent.indent_cache[_indent_count];
+            }
+            result += _items.join('');
+        }
+        return result;
+    };
+}
+
+function Output(indent_string, baseIndentString) {
+    baseIndentString = baseIndentString || '';
+    this.indent_cache = [baseIndentString];
+    this.baseIndentLength = baseIndentString.length;
+    this.indent_length = indent_string.length;
+    this.raw = false;
+
+    var lines = [];
+    this.baseIndentString = baseIndentString;
+    this.indent_string = indent_string;
+    this.previous_line = null;
+    this.current_line = null;
+    this.space_before_token = false;
+
+    this.add_outputline = function() {
+        this.previous_line = this.current_line;
+        this.current_line = new OutputLine(this);
+        lines.push(this.current_line);
+    };
+
+    // initialize
+    this.add_outputline();
+
+
+    this.get_line_number = function() {
+        return lines.length;
+    };
+
+    // Using object instead of string to allow for later expansion of info about each line
+    this.add_new_line = function(force_newline) {
+        if (this.get_line_number() === 1 && this.just_added_newline()) {
+            return false; // no newline on start of file
+        }
+
+        if (force_newline || !this.just_added_newline()) {
+            if (!this.raw) {
+                this.add_outputline();
+            }
+            return true;
+        }
+
+        return false;
+    };
+
+    this.get_code = function(end_with_newline, eol) {
+        var sweet_code = lines.join('\n').replace(/[\r\n\t ]+$/, '');
+
+        if (end_with_newline) {
+            sweet_code += '\n';
+        }
+
+        if (eol !== '\n') {
+            sweet_code = sweet_code.replace(/[\n]/g, eol);
+        }
+
+        return sweet_code;
+    };
+
+    this.set_indent = function(level) {
+        // Never indent your first output indent at the start of the file
+        if (lines.length > 1) {
+            while (level >= this.indent_cache.length) {
+                this.indent_cache.push(this.indent_cache[this.indent_cache.length - 1] + this.indent_string);
+            }
+
+            this.current_line.set_indent(level);
+            return true;
+        }
+        this.current_line.set_indent(0);
+        return false;
+    };
+
+    this.add_raw_token = function(token) {
+        for (var x = 0; x < token.newlines; x++) {
+            this.add_outputline();
+        }
+        this.current_line.push(token.whitespace_before);
+        this.current_line.push(token.text);
+        this.space_before_token = false;
+    };
+
+    this.add_token = function(printable_token) {
+        this.add_space_before_token();
+        this.current_line.push(printable_token);
+    };
+
+    this.add_space_before_token = function() {
+        if (this.space_before_token && !this.just_added_newline()) {
+            this.current_line.push(' ');
+        }
+        this.space_before_token = false;
+    };
+
+    this.remove_indent = function(index) {
+        var output_length = lines.length;
+        while (index < output_length) {
+            lines[index].remove_indent();
+            index++;
+        }
+    };
+
+    this.trim = function(eat_newlines) {
+        eat_newlines = (eat_newlines === undefined) ? false : eat_newlines;
+
+        this.current_line.trim(indent_string, baseIndentString);
+
+        while (eat_newlines && lines.length > 1 &&
+            this.current_line.is_empty()) {
+            lines.pop();
+            this.current_line = lines[lines.length - 1];
+            this.current_line.trim();
+        }
+
+        this.previous_line = lines.length > 1 ? lines[lines.length - 2] : null;
+    };
+
+    this.just_added_newline = function() {
+        return this.current_line.is_empty();
+    };
+
+    this.just_added_blankline = function() {
+        if (this.just_added_newline()) {
+            if (lines.length === 1) {
+                return true; // start of the file and newline = blank
+            }
+
+            var line = lines[lines.length - 2];
+            return line.is_empty();
+        }
+        return false;
+    };
+}
+
+module.exports.Output = Output;
+
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports, __webpack_require__) {
+
+/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
+/*
+
+    The MIT License (MIT)
+
+    Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
+
+    Permission is hereby granted, free of charge, to any person
+    obtaining a copy of this software and associated documentation files
+    (the "Software"), to deal in the Software without restriction,
+    including without limitation the rights to use, copy, modify, merge,
+    publish, distribute, sublicense, and/or sell copies of the Software,
+    and to permit persons to whom the Software is furnished to do so,
+    subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be
+    included in all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+    BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+    ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+    CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+    SOFTWARE.
+*/
+
+var Beautifier = __webpack_require__(0).Beautifier;
+
+function css_beautify(source_text, options) {
+    var beautifier = new Beautifier(source_text, options);
+    return beautifier.beautify();
+}
+
+module.exports = css_beautify;
+
+/***/ })
+/******/ ]);
+var css_beautify = legacy_beautify_css;
+/* Footer */
+if (typeof define === "function" && define.amd) {
+    // Add support for AMD ( https://github.com/amdjs/amdjs-api/wiki/AMD#defineamd-property- )
+    define([], function() {
+        return {
+            css_beautify: css_beautify
+        };
+    });
+} else if (typeof exports !== "undefined") {
+    // Add support for CommonJS. Just put this file somewhere on your require.paths
+    // and you will be able to `var html_beautify = require("beautify").html_beautify`.
+    exports.css_beautify = css_beautify;
+} else if (typeof window !== "undefined") {
+    // If we're running a web page and don't have either of the above, add our one global
+    window.css_beautify = css_beautify;
+} else if (typeof global !== "undefined") {
+    // If we don't even have window, try global.
+    global.css_beautify = css_beautify;
+}
+
+}());
diff --git a/lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-html.js b/lib/editor/atto/plugins/html/yui/src/beautify/js/beautify-html.js
new file mode 100644 (file)
index 0000000..c7fcf6d
--- /dev/null
@@ -0,0 +1,1387 @@
+/*jshint curly:false, eqeqeq:true, laxbreak:true, noempty:false */
+/* AUTO-GENERATED. DO NOT MODIFY. */
+/*
+
+  The MIT License (MIT)
+
+  Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation files
+  (the "Software"), to deal in the Software without restriction,
+  including without limitation the rights to use, copy, modify, merge,
+  publish, distribute, sublicense, and/or sell copies of the Software,
+  and to permit persons to whom the Software is furnished to do so,
+  subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+  ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+
+
+ Style HTML
+---------------
+
+  Written by Nochum Sossonko, (nsossonko@hotmail.com)
+
+  Based on code initially developed by: Einar Lielmanis, <einar@jsbeautifier.org>
+    http://jsbeautifier.org/
+
+  Usage:
+    style_html(html_source);
+
+    style_html(html_source, options);
+
+  The options are:
+    indent_inner_html (default false)  — indent <head> and <body> sections,
+    indent_size (default 4)          — indentation size,
+    indent_char (default space)      — character to indent with,
+    wrap_line_length (default 250)            -  maximum amount of characters per line (0 = disable)
+    brace_style (default "collapse") - "collapse" | "expand" | "end-expand" | "none"
+            put braces on the same line as control statements (default), or put braces on own line (Allman / ANSI style), or just put end braces on own line, or attempt to keep them where they are.
+    unformatted (defaults to inline tags) - list of tags, that shouldn't be reformatted
+    content_unformatted (defaults to pre tag) - list of tags, whose content shouldn't be reformatted
+    indent_scripts (default normal)  - "keep"|"separate"|"normal"
+    preserve_newlines (default true) - whether existing line breaks before elements should be preserved
+                                        Only works before elements, not inside tags or for text.
+    max_preserve_newlines (default unlimited) - maximum number of line breaks to be preserved in one chunk
+    indent_handlebars (default false) - format and indent {{#foo}} and {{/foo}}
+    end_with_newline (false)          - end with a newline
+    extra_liners (default [head,body,/html]) -List of tags that should have an extra newline before them.
+
+    e.g.
+
+    style_html(html_source, {
+      'indent_inner_html': false,
+      'indent_size': 2,
+      'indent_char': ' ',
+      'wrap_line_length': 78,
+      'brace_style': 'expand',
+      'preserve_newlines': true,
+      'max_preserve_newlines': 5,
+      'indent_handlebars': false,
+      'extra_liners': ['/html']
+    });
+*/
+
+(function() {
+var legacy_beautify_html =
+/******/ (function(modules) { // webpackBootstrap
+/******/       // The module cache
+/******/       var installedModules = {};
+/******/
+/******/       // The require function
+/******/       function __webpack_require__(moduleId) {
+/******/
+/******/               // Check if module is in cache
+/******/               if(installedModules[moduleId]) {
+/******/                       return installedModules[moduleId].exports;
+/******/               }
+/******/               // Create a new module (and put it into the cache)
+/******/               var module = installedModules[moduleId] = {
+/******/                       i: moduleId,
+/******/                       l: false,
+/******/                       exports: {}
+/******/               };
+/******/
+/******/               // Execute the module function
+/******/               modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/               // Flag the module as loaded
+/******/               module.l = true;
+/******/
+/******/               // Return the exports of the module
+/******/               return module.exports;
+/******/       }
+/******/
+/******/
+/******/       // expose the modules object (__webpack_modules__)
+/******/       __webpack_require__.m = modules;
+/******/
+/******/       // expose the module cache
+/******/       __webpack_require__.c = installedModules;
+/******/
+/******/       // identity function for calling harmony imports with the correct context
+/******/       __webpack_require__.i = function(value) { return value; };
+/******/
+/******/       // define getter function for harmony exports
+/******/       __webpack_require__.d = function(exports, name, getter) {
+/******/               if(!__webpack_require__.o(exports, name)) {
+/******/                       Object.defineProperty(exports, name, {
+/******/                               configurable: false,
+/******/                               enumerable: true,
+/******/                               get: getter
+/******/                       });
+/******/               }
+/******/       };
+/******/
+/******/       // getDefaultExport function for compatibility with non-harmony modules
+/******/       __webpack_require__.n = function(module) {
+/******/               var getter = module && module.__esModule ?
+/******/                       function getDefault() { return module['default']; } :
+/******/                       function getModuleExports() { return module; };
+/******/               __webpack_require__.d(getter, 'a', getter);
+/******/               return getter;
+/******/       };
+/******/
+/******/       // Object.prototype.hasOwnProperty.call
+/******/       __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/       // __webpack_public_path__
+/******/       __webpack_require__.p = "";
+/******/
+/******/       // Load entry module and return exports
+/******/       return __webpack_require__(__webpack_require__.s = 3);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
+/*
+
+  The MIT License (MIT)
+
+  Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation files
+  (the "Software"), to deal in the Software without restriction,
+  including without limitation the rights to use, copy, modify, merge,
+  publish, distribute, sublicense, and/or sell copies of the Software,
+  and to permit persons to whom the Software is furnished to do so,
+  subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+  ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+*/
+
+var mergeOpts = __webpack_require__(2).mergeOpts;
+var acorn = __webpack_require__(1);
+
+
+var lineBreak = acorn.lineBreak;
+var allLineBreaks = acorn.allLineBreaks;
+
+// function trim(s) {
+//     return s.replace(/^\s+|\s+$/g, '');
+// }
+
+function ltrim(s) {
+    return s.replace(/^\s+/g, '');
+}
+
+function rtrim(s) {
+    return s.replace(/\s+$/g, '');
+}
+
+function Beautifier(html_source, options, js_beautify, css_beautify) {
+    //Wrapper function to invoke all the necessary constructors and deal with the output.
+    html_source = html_source || '';
+
+    var multi_parser,
+        indent_inner_html,
+        indent_body_inner_html,
+        indent_head_inner_html,
+        indent_size,
+        indent_character,
+        wrap_line_length,
+        brace_style,
+        unformatted,
+        content_unformatted,
+        preserve_newlines,
+        max_preserve_newlines,
+        indent_handlebars,
+        wrap_attributes,
+        wrap_attributes_indent_size,
+        is_wrap_attributes_force,
+        is_wrap_attributes_force_expand_multiline,
+        is_wrap_attributes_force_aligned,
+        end_with_newline,
+        extra_liners,
+        eol;
+
+    options = options || {};
+
+    // Allow the setting of language/file-type specific options
+    // with inheritance of overall settings
+    options = mergeOpts(options, 'html');
+
+    // backwards compatibility to 1.3.4
+    if ((options.wrap_line_length === undefined || parseInt(options.wrap_line_length, 10) === 0) &&
+        (options.max_char !== undefined && parseInt(options.max_char, 10) !== 0)) {
+        options.wrap_line_length = options.max_char;
+    }
+
+    indent_inner_html = (options.indent_inner_html === undefined) ? false : options.indent_inner_html;
+    indent_body_inner_html = (options.indent_body_inner_html === undefined) ? true : options.indent_body_inner_html;
+    indent_head_inner_html = (options.indent_head_inner_html === undefined) ? true : options.indent_head_inner_html;
+    indent_size = (options.indent_size === undefined) ? 4 : parseInt(options.indent_size, 10);
+    indent_character = (options.indent_char === undefined) ? ' ' : options.indent_char;
+    brace_style = (options.brace_style === undefined) ? 'collapse' : options.brace_style;
+    wrap_line_length = parseInt(options.wrap_line_length, 10) === 0 ? 32786 : parseInt(options.wrap_line_length || 250, 10);
+    unformatted = options.unformatted || [
+        // https://www.w3.org/TR/html5/dom.html#phrasing-content
+        'a', 'abbr', 'area', 'audio', 'b', 'bdi', 'bdo', 'br', 'button', 'canvas', 'cite',
+        'code', 'data', 'datalist', 'del', 'dfn', 'em', 'embed', 'i', 'iframe', 'img',
+        'input', 'ins', 'kbd', 'keygen', 'label', 'map', 'mark', 'math', 'meter', 'noscript',
+        'object', 'output', 'progress', 'q', 'ruby', 's', 'samp', /* 'script', */ 'select', 'small',
+        'span', 'strong', 'sub', 'sup', 'svg', 'template', 'textarea', 'time', 'u', 'var',
+        'video', 'wbr', 'text',
+        // prexisting - not sure of full effect of removing, leaving in
+        'acronym', 'address', 'big', 'dt', 'ins', 'strike', 'tt',
+    ];
+    content_unformatted = options.content_unformatted || [
+        'pre',
+    ];
+    preserve_newlines = (options.preserve_newlines === undefined) ? true : options.preserve_newlines;
+    max_preserve_newlines = preserve_newlines ?
+        (isNaN(parseInt(options.max_preserve_newlines, 10)) ? 32786 : parseInt(options.max_preserve_newlines, 10)) :
+        0;
+    indent_handlebars = (options.indent_handlebars === undefined) ? false : options.indent_handlebars;
+    wrap_attributes = (options.wrap_attributes === undefined) ? 'auto' : options.wrap_attributes;
+    wrap_attributes_indent_size = (isNaN(parseInt(options.wrap_attributes_indent_size, 10))) ? indent_size : parseInt(options.wrap_attributes_indent_size, 10);
+    is_wrap_attributes_force = wrap_attributes.substr(0, 'force'.length) === 'force';
+    is_wrap_attributes_force_expand_multiline = (wrap_attributes === 'force-expand-multiline');
+    is_wrap_attributes_force_aligned = (wrap_attributes === 'force-aligned');
+    end_with_newline = (options.end_with_newline === undefined) ? false : options.end_with_newline;
+    extra_liners = (typeof options.extra_liners === 'object') && options.extra_liners ?
+        options.extra_liners.concat() : (typeof options.extra_liners === 'string') ?
+        options.extra_liners.split(',') : 'head,body,/html'.split(',');
+    eol = options.eol ? options.eol : 'auto';
+
+    if (options.indent_with_tabs) {
+        indent_character = '\t';
+        indent_size = 1;
+    }
+
+    if (eol === 'auto') {
+        eol = '\n';
+        if (html_source && lineBreak.test(html_source || '')) {
+            eol = html_source.match(lineBreak)[0];
+        }
+    }
+
+    eol = eol.replace(/\\r/, '\r').replace(/\\n/, '\n');
+
+    // HACK: newline parsing inconsistent. This brute force normalizes the input.
+    html_source = html_source.replace(allLineBreaks, '\n');
+
+    function Parser() {
+
+        this.pos = 0; //Parser position
+        this.token = '';
+        this.current_mode = 'CONTENT'; //reflects the current Parser mode: TAG/CONTENT
+        this.tags = { //An object to hold tags, their position, and their parent-tags, initiated with default values
+            parent: 'parent1',
+            parentcount: 1,
+            parent1: ''
+        };
+        this.tag_type = '';
+        this.token_text = this.last_token = this.last_text = this.token_type = '';
+        this.newlines = 0;
+        this.indent_content = indent_inner_html;
+        this.indent_body_inner_html = indent_body_inner_html;
+        this.indent_head_inner_html = indent_head_inner_html;
+
+        this.Utils = { //Uilities made available to the various functions
+            whitespace: "\n\r\t ".split(''),
+
+            single_token: options.void_elements || [
+                // HTLM void elements - aka self-closing tags - aka singletons
+                // https://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
+                'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
+                'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr',
+                // NOTE: Optional tags - are not understood.
+                // https://www.w3.org/TR/html5/syntax.html#optional-tags
+                // The rules for optional tags are too complex for a simple list
+                // Also, the content of these tags should still be indented in many cases.
+                // 'li' is a good exmple.
+
+                // Doctype and xml elements
+                '!doctype', '?xml',
+                // ?php tag
+                '?php',
+                // other tags that were in this list, keeping just in case
+                'basefont', 'isindex'
+            ],
+            extra_liners: extra_liners, //for tags that need a line of whitespace before them
+            in_array: function(what, arr) {
+                for (var i = 0; i < arr.length; i++) {
+                    if (what === arr[i]) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+        };
+
+        // Return true if the given text is composed entirely of whitespace.
+        this.is_whitespace = function(text) {
+            for (var n = 0; n < text.length; n++) {
+                if (!this.Utils.in_array(text.charAt(n), this.Utils.whitespace)) {
+                    return false;
+                }
+            }
+            return true;
+        };
+
+        this.traverse_whitespace = function() {
+            var input_char = '';
+
+            input_char = this.input.charAt(this.pos);
+            if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+                this.newlines = 0;
+                while (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+                    if (preserve_newlines && input_char === '\n' && this.newlines <= max_preserve_newlines) {
+                        this.newlines += 1;
+                    }
+
+                    this.pos++;
+                    input_char = this.input.charAt(this.pos);
+                }
+                return true;
+            }
+            return false;
+        };
+
+        // Append a space to the given content (string array) or, if we are
+        // at the wrap_line_length, append a newline/indentation.
+        // return true if a newline was added, false if a space was added
+        this.space_or_wrap = function(content) {
+            if (this.line_char_count >= this.wrap_line_length) { //insert a line when the wrap_line_length is reached
+                this.print_newline(false, content);
+                this.print_indentation(content);
+                return true;
+            } else {
+                this.line_char_count++;
+                content.push(' ');
+                return false;
+            }
+        };
+
+        this.get_content = function() { //function to capture regular content between tags
+            var input_char = '',
+                content = [],
+                handlebarsStarted = 0;
+
+            while (this.input.charAt(this.pos) !== '<' || handlebarsStarted === 2) {
+                if (this.pos >= this.input.length) {
+                    return content.length ? content.join('') : ['', 'TK_EOF'];
+                }
+
+                if (handlebarsStarted < 2 && this.traverse_whitespace()) {
+                    this.space_or_wrap(content);
+                    continue;
+                }
+
+                input_char = this.input.charAt(this.pos);
+
+                if (indent_handlebars) {
+                    if (input_char === '{') {
+                        handlebarsStarted += 1;
+                    } else if (handlebarsStarted < 2) {
+                        handlebarsStarted = 0;
+                    }
+
+                    if (input_char === '}' && handlebarsStarted > 0) {
+                        if (handlebarsStarted-- === 0) {
+                            break;
+                        }
+                    }
+                    // Handlebars parsing is complicated.
+                    // {{#foo}} and {{/foo}} are formatted tags.
+                    // {{something}} should get treated as content, except:
+                    // {{else}} specifically behaves like {{#if}} and {{/if}}
+                    var peek3 = this.input.substr(this.pos, 3);
+                    if (peek3 === '{{#' || peek3 === '{{/') {
+                        // These are tags and not content.
+                        break;
+                    } else if (peek3 === '{{!') {
+                        return [this.get_tag(), 'TK_TAG_HANDLEBARS_COMMENT'];
+                    } else if (this.input.substr(this.pos, 2) === '{{') {
+                        if (this.get_tag(true) === '{{else}}') {
+                            break;
+                        }
+                    }
+                }
+
+                this.pos++;
+                this.line_char_count++;
+                content.push(input_char); //letter at-a-time (or string) inserted to an array
+            }
+            return content.length ? content.join('') : '';
+        };
+
+        this.get_contents_to = function(name) { //get the full content of a script or style to pass to js_beautify
+            if (this.pos === this.input.length) {
+                return ['', 'TK_EOF'];
+            }
+            var content = '';
+            var reg_match = new RegExp('</' + name + '\\s*>', 'igm');
+            reg_match.lastIndex = this.pos;
+            var reg_array = reg_match.exec(this.input);
+            var end_script = reg_array ? reg_array.index : this.input.length; //absolute end of script
+            if (this.pos < end_script) { //get everything in between the script tags
+                content = this.input.substring(this.pos, end_script);
+                this.pos = end_script;
+            }
+            return content;
+        };
+
+        this.record_tag = function(tag) { //function to record a tag and its parent in this.tags Object
+            if (this.tags[tag + 'count']) { //check for the existence of this tag type
+                this.tags[tag + 'count']++;
+                this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+            } else { //otherwise initialize this tag type
+                this.tags[tag + 'count'] = 1;
+                this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+            }
+            this.tags[tag + this.tags[tag + 'count'] + 'parent'] = this.tags.parent; //set the parent (i.e. in the case of a div this.tags.div1parent)
+            this.tags.parent = tag + this.tags[tag + 'count']; //and make this the current parent (i.e. in the case of a div 'div1')
+        };
+
+        this.retrieve_tag = function(tag) { //function to retrieve the opening tag to the corresponding closer
+            if (this.tags[tag + 'count']) { //if the openener is not in the Object we ignore it
+                var temp_parent = this.tags.parent; //check to see if it's a closable tag.
+                while (temp_parent) { //till we reach '' (the initial value);
+                    if (tag + this.tags[tag + 'count'] === temp_parent) { //if this is it use it
+                        break;
+                    }
+                    temp_parent = this.tags[temp_parent + 'parent']; //otherwise keep on climbing up the DOM Tree
+                }
+                if (temp_parent) { //if we caught something
+                    this.indent_level = this.tags[tag + this.tags[tag + 'count']]; //set the indent_level accordingly
+                    this.tags.parent = this.tags[temp_parent + 'parent']; //and set the current parent
+                }
+                delete this.tags[tag + this.tags[tag + 'count'] + 'parent']; //delete the closed tags parent reference...
+                delete this.tags[tag + this.tags[tag + 'count']]; //...and the tag itself
+                if (this.tags[tag + 'count'] === 1) {
+                    delete this.tags[tag + 'count'];
+                } else {
+                    this.tags[tag + 'count']--;
+                }
+            }
+        };
+
+        this.indent_to_tag = function(tag) {
+            // Match the indentation level to the last use of this tag, but don't remove it.
+            if (!this.tags[tag + 'count']) {
+                return;
+            }
+            var temp_parent = this.tags.parent;
+            while (temp_parent) {
+                if (tag + this.tags[tag + 'count'] === temp_parent) {
+                    break;
+                }
+                temp_parent = this.tags[temp_parent + 'parent'];
+            }
+            if (temp_parent) {
+                this.indent_level = this.tags[tag + this.tags[tag + 'count']];
+            }
+        };
+
+        this.get_tag = function(peek) { //function to get a full tag and parse its type
+            var input_char = '',
+                content = [],
+                comment = '',
+                space = false,
+                first_attr = true,
+                has_wrapped_attrs = false,
+                tag_start, tag_end,
+                tag_start_char,
+                orig_pos = this.pos,
+                orig_line_char_count = this.line_char_count,
+                is_tag_closed = false,
+                tail;
+
+            peek = peek !== undefined ? peek : false;
+
+            do {
+                if (this.pos >= this.input.length) {
+                    if (peek) {
+                        this.pos = orig_pos;
+                        this.line_char_count = orig_line_char_count;
+                    }
+                    return content.length ? content.join('') : ['', 'TK_EOF'];
+                }
+
+                input_char = this.input.charAt(this.pos);
+                this.pos++;
+
+                if (this.Utils.in_array(input_char, this.Utils.whitespace)) { //don't want to insert unnecessary space
+                    space = true;
+                    continue;
+                }
+
+                if (input_char === "'" || input_char === '"') {
+                    input_char += this.get_unformatted(input_char);
+                    space = true;
+                }
+
+                if (input_char === '=') { //no space before =
+                    space = false;
+                }
+                tail = this.input.substr(this.pos - 1);
+                if (is_wrap_attributes_force_expand_multiline && has_wrapped_attrs && !is_tag_closed && (input_char === '>' || input_char === '/')) {
+                    if (tail.match(/^\/?\s*>/)) {
+                        space = false;
+                        is_tag_closed = true;
+                        this.print_newline(false, content);
+                        this.print_indentation(content);
+                    }
+                }
+                if (content.length && content[content.length - 1] !== '=' && input_char !== '>' && space) {
+                    //no space after = or before >
+                    var wrapped = this.space_or_wrap(content);
+                    var indentAttrs = wrapped && input_char !== '/' && !is_wrap_attributes_force;
+                    space = false;
+
+                    if (is_wrap_attributes_force && input_char !== '/') {
+                        var force_first_attr_wrap = false;
+                        if (is_wrap_attributes_force_expand_multiline && first_attr) {
+                            var is_only_attribute = tail.match(/^\S*(="([^"]|\\")*")?\s*\/?\s*>/) !== null;
+                            force_first_attr_wrap = !is_only_attribute;
+                        }
+                        if (!first_attr || force_first_attr_wrap) {
+                            this.print_newline(false, content);
+                            this.print_indentation(content);
+                            indentAttrs = true;
+                        }
+                    }
+                    if (indentAttrs) {
+                        has_wrapped_attrs = true;
+
+                        //indent attributes an auto, forced, or forced-align line-wrap
+                        var alignment_size = wrap_attributes_indent_size;
+                        if (is_wrap_attributes_force_aligned) {
+                            alignment_size = content.indexOf(' ') + 1;
+                        }
+
+                        for (var count = 0; count < alignment_size; count++) {
+                            // only ever further indent with spaces since we're trying to align characters
+                            content.push(' ');
+                        }
+                    }
+                    if (first_attr) {
+                        for (var i = 0; i < content.length; i++) {
+                            if (content[i] === ' ') {
+                                first_attr = false;
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                if (indent_handlebars && tag_start_char === '<') {
+                    // When inside an angle-bracket tag, put spaces around
+                    // handlebars not inside of strings.
+                    if ((input_char + this.input.charAt(this.pos)) === '{{') {
+                        input_char += this.get_unformatted('}}');
+                        if (content.length && content[content.length - 1] !== ' ' && content[content.length - 1] !== '<') {
+                            input_char = ' ' + input_char;
+                        }
+                        space = true;
+                    }
+                }
+
+                if (input_char === '<' && !tag_start_char) {
+                    tag_start = this.pos - 1;
+                    tag_start_char = '<';
+                }
+
+                if (indent_handlebars && !tag_start_char) {
+                    if (content.length >= 2 && content[content.length - 1] === '{' && content[content.length - 2] === '{') {
+                        if (input_char === '#' || input_char === '/' || input_char === '!') {
+                            tag_start = this.pos - 3;
+                        } else {
+                            tag_start = this.pos - 2;
+                        }
+                        tag_start_char = '{';
+                    }
+                }
+
+                this.line_char_count++;
+                content.push(input_char); //inserts character at-a-time (or string)
+
+                if (content[1] && (content[1] === '!' || content[1] === '?' || content[1] === '%')) { //if we're in a comment, do something special
+                    // We treat all comments as literals, even more than preformatted tags
+                    // we just look for the appropriate close tag
+                    content = [this.get_comment(tag_start)];
+                    break;
+                }
+
+                if (indent_handlebars && content[1] && content[1] === '{' && content[2] && content[2] === '!') { //if we're in a comment, do something special
+                    // We treat all comments as literals, even more than preformatted tags
+                    // we just look for the appropriate close tag
+                    content = [this.get_comment(tag_start)];
+                    break;
+                }
+
+                if (indent_handlebars && tag_start_char === '{' && content.length > 2 && content[content.length - 2] === '}' && content[content.length - 1] === '}') {
+                    break;
+                }
+            } while (input_char !== '>');
+
+            var tag_complete = content.join('');
+            var tag_index;
+            var tag_offset;
+
+            // must check for space first otherwise the tag could have the first attribute included, and
+            // then not un-indent correctly
+            if (tag_complete.indexOf(' ') !== -1) { //if there's whitespace, thats where the tag name ends
+                tag_index = tag_complete.indexOf(' ');
+            } else if (tag_complete.indexOf('\n') !== -1) { //if there's a line break, thats where the tag name ends
+                tag_index = tag_complete.indexOf('\n');
+            } else if (tag_complete.charAt(0) === '{') {
+                tag_index = tag_complete.indexOf('}');
+            } else { //otherwise go with the tag ending
+                tag_index = tag_complete.indexOf('>');
+            }
+            if (tag_complete.charAt(0) === '<' || !indent_handlebars) {
+                tag_offset = 1;
+            } else {
+                tag_offset = tag_complete.charAt(2) === '#' ? 3 : 2;
+            }
+            var tag_check = tag_complete.substring(tag_offset, tag_index).toLowerCase();
+            if (tag_complete.charAt(tag_complete.length - 2) === '/' ||
+                this.Utils.in_array(tag_check, this.Utils.single_token)) { //if this tag name is a single tag type (either in the list or has a closing /)
+                if (!peek) {
+                    this.tag_type = 'SINGLE';
+                }
+            } else if (indent_handlebars && tag_complete.charAt(0) === '{' && tag_check === 'else') {
+                if (!peek) {
+                    this.indent_to_tag('if');
+                    this.tag_type = 'HANDLEBARS_ELSE';
+                    this.indent_content = true;
+                    this.traverse_whitespace();
+                }
+            } else if (this.is_unformatted(tag_check, unformatted) ||
+                this.is_unformatted(tag_check, content_unformatted)) {
+                // do not reformat the "unformatted" or "content_unformatted" tags
+                comment = this.get_unformatted('</' + tag_check + '>', tag_complete); //...delegate to get_unformatted function
+                content.push(comment);
+                tag_end = this.pos - 1;
+                this.tag_type = 'SINGLE';
+            } else if (tag_check === 'script' &&
+                (tag_complete.search('type') === -1 ||
+                    (tag_complete.search('type') > -1 &&
+                        tag_complete.search(/\b(text|application|dojo)\/(x-)?(javascript|ecmascript|jscript|livescript|(ld\+)?json|method|aspect)/) > -1))) {
+                if (!peek) {
+                    this.record_tag(tag_check);
+                    this.tag_type = 'SCRIPT';
+                }
+            } else if (tag_check === 'style' &&
+                (tag_complete.search('type') === -1 ||
+                    (tag_complete.search('type') > -1 && tag_complete.search('text/css') > -1))) {
+                if (!peek) {
+                    this.record_tag(tag_check);
+                    this.tag_type = 'STYLE';
+                }
+            } else if (tag_check.charAt(0) === '!') { //peek for <! comment
+                // for comments content is already correct.
+                if (!peek) {
+                    this.tag_type = 'SINGLE';
+                    this.traverse_whitespace();
+                }
+            } else if (!peek) {
+                if (tag_check.charAt(0) === '/') { //this tag is a double tag so check for tag-ending
+                    this.retrieve_tag(tag_check.substring(1)); //remove it and all ancestors
+                    this.tag_type = 'END';
+                } else { //otherwise it's a start-tag
+                    this.record_tag(tag_check); //push it on the tag stack
+                    if (tag_check.toLowerCase() !== 'html') {
+                        this.indent_content = true;
+                    }
+                    this.tag_type = 'START';
+                }
+
+                // Allow preserving of newlines after a start or end tag
+                if (this.traverse_whitespace()) {
+                    this.space_or_wrap(content);
+                }
+
+                if (this.Utils.in_array(tag_check, this.Utils.extra_liners)) { //check if this double needs an extra line
+                    this.print_newline(false, this.output);
+                    if (this.output.length && this.output[this.output.length - 2] !== '\n') {
+                        this.print_newline(true, this.output);
+                    }
+                }
+            }
+
+            if (peek) {
+                this.pos = orig_pos;
+                this.line_char_count = orig_line_char_count;
+            }
+
+            return content.join(''); //returns fully formatted tag
+        };
+
+        this.get_comment = function(start_pos) { //function to return comment content in its entirety
+            // this is will have very poor perf, but will work for now.
+            var comment = '',
+                delimiter = '>',
+                matched = false;
+
+            this.pos = start_pos;
+            var input_char = this.input.charAt(this.pos);
+            this.pos++;
+
+            while (this.pos <= this.input.length) {
+                comment += input_char;
+
+                // only need to check for the delimiter if the last chars match
+                if (comment.charAt(comment.length - 1) === delimiter.charAt(delimiter.length - 1) &&
+                    comment.indexOf(delimiter) !== -1) {
+                    break;
+                }
+
+                // only need to search for custom delimiter for the first few characters
+                if (!matched && comment.length < 10) {
+                    if (comment.indexOf('<![if') === 0) { //peek for <![if conditional comment
+                        delimiter = '<![endif]>';
+                        matched = true;
+                    } else if (comment.indexOf('<![cdata[') === 0) { //if it's a <[cdata[ comment...
+                        delimiter = ']]>';
+                        matched = true;
+                    } else if (comment.indexOf('<![') === 0) { // some other ![ comment? ...
+                        delimiter = ']>';
+                        matched = true;
+                    } else if (comment.indexOf('<!--') === 0) { // <!-- comment ...
+                        delimiter = '-->';
+                        matched = true;
+                    } else if (comment.indexOf('{{!--') === 0) { // {{!-- handlebars comment
+                        delimiter = '--}}';
+                        matched = true;
+                    } else if (comment.indexOf('{{!') === 0) { // {{! handlebars comment
+                        if (comment.length === 5 && comment.indexOf('{{!--') === -1) {
+                            delimiter = '}}';
+                            matched = true;
+                        }
+                    } else if (comment.indexOf('<?') === 0) { // {{! handlebars comment
+                        delimiter = '?>';
+                        matched = true;
+                    } else if (comment.indexOf('<%') === 0) { // {{! handlebars comment
+                        delimiter = '%>';
+                        matched = true;
+                    }
+                }
+
+                input_char = this.input.charAt(this.pos);
+                this.pos++;
+            }
+
+            return comment;
+        };
+
+        function tokenMatcher(delimiter) {
+            var token = '';
+
+            var add = function(str) {
+                var newToken = token + str.toLowerCase();
+                token = newToken.length <= delimiter.length ? newToken : newToken.substr(newToken.length - delimiter.length, delimiter.length);
+            };
+
+            var doesNotMatch = function() {
+                return token.indexOf(delimiter) === -1;
+            };
+
+            return {
+                add: add,
+                doesNotMatch: doesNotMatch
+            };
+        }
+
+        this.get_unformatted = function(delimiter, orig_tag) { //function to return unformatted content in its entirety
+            if (orig_tag && orig_tag.toLowerCase().indexOf(delimiter) !== -1) {
+                return '';
+            }
+            var input_char = '';
+            var content = '';
+            var space = true;
+
+            var delimiterMatcher = tokenMatcher(delimiter);
+
+            do {
+
+                if (this.pos >= this.input.length) {
+                    return content;
+                }
+
+                input_char = this.input.charAt(this.pos);
+                this.pos++;
+
+                if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+                    if (!space) {
+                        this.line_char_count--;
+                        continue;
+                    }
+                    if (input_char === '\n' || input_char === '\r') {
+                        content += '\n';
+                        /*  Don't change tab indention for unformatted blocks.  If using code for html editing, this will greatly affect <pre> tags if they are specified in the 'unformatted array'
+            for (var i=0; i<this.indent_level; i++) {
+              content += this.indent_string;
+            }
+            space = false; //...and make sure other indentation is erased
+            */
+                        this.line_char_count = 0;
+                        continue;
+                    }
+                }
+                content += input_char;
+                delimiterMatcher.add(input_char);
+                this.line_char_count++;
+                space = true;
+
+                if (indent_handlebars && input_char === '{' && content.length && content.charAt(content.length - 2) === '{') {
+                    // Handlebars expressions in strings should also be unformatted.
+                    content += this.get_unformatted('}}');
+                    // Don't consider when stopping for delimiters.
+                }
+            } while (delimiterMatcher.doesNotMatch());
+
+            return content;
+        };
+
+        this.get_token = function() { //initial handler for token-retrieval
+            var token;
+
+            if (this.last_token === 'TK_TAG_SCRIPT' || this.last_token === 'TK_TAG_STYLE') { //check if we need to format javascript
+                var type = this.last_token.substr(7);
+                token = this.get_contents_to(type);
+                if (typeof token !== 'string') {
+                    return token;
+                }
+                return [token, 'TK_' + type];
+            }
+            if (this.current_mode === 'CONTENT') {
+                token = this.get_content();
+                if (typeof token !== 'string') {
+                    return token;
+                } else {
+                    return [token, 'TK_CONTENT'];
+                }
+            }
+
+            if (this.current_mode === 'TAG') {
+                token = this.get_tag();
+                if (typeof token !== 'string') {
+                    return token;
+                } else {
+                    var tag_name_type = 'TK_TAG_' + this.tag_type;
+                    return [token, tag_name_type];
+                }
+            }
+        };
+
+        this.get_full_indent = function(level) {
+            level = this.indent_level + level || 0;
+            if (level < 1) {
+                return '';
+            }
+
+            return Array(level + 1).join(this.indent_string);
+        };
+
+        this.is_unformatted = function(tag_check, unformatted) {
+            //is this an HTML5 block-level link?
+            if (!this.Utils.in_array(tag_check, unformatted)) {
+                return false;
+            }
+
+            if (tag_check.toLowerCase() !== 'a' || !this.Utils.in_array('a', unformatted)) {
+                return true;
+            }
+
+            //at this point we have an  tag; is its first child something we want to remain
+            //unformatted?
+            var next_tag = this.get_tag(true /* peek. */ );
+
+            // test next_tag to see if it is just html tag (no external content)
+            var tag = (next_tag || "").match(/^\s*<\s*\/?([a-z]*)\s*[^>]*>\s*$/);
+
+            // if next_tag comes back but is not an isolated tag, then
+            // let's treat the 'a' tag as having content
+            // and respect the unformatted option
+            if (!tag || this.Utils.in_array(tag[1], unformatted)) {
+                return true;
+            } else {
+                return false;
+            }
+        };
+
+        this.printer = function(js_source, indent_character, indent_size, wrap_line_length, brace_style) { //handles input/output and some other printing functions
+
+            this.input = js_source || ''; //gets the input for the Parser
+
+            // HACK: newline parsing inconsistent. This brute force normalizes the input.
+            this.input = this.input.replace(/\r\n|[\r\u2028\u2029]/g, '\n');
+
+            this.output = [];
+            this.indent_character = indent_character;
+            this.indent_string = '';
+            this.indent_size = indent_size;
+            this.brace_style = brace_style;
+            this.indent_level = 0;
+            this.wrap_line_length = wrap_line_length;
+            this.line_char_count = 0; //count to see if wrap_line_length was exceeded
+
+            for (var i = 0; i < this.indent_size; i++) {
+                this.indent_string += this.indent_character;
+            }
+
+            this.print_newline = function(force, arr) {
+                this.line_char_count = 0;
+                if (!arr || !arr.length) {
+                    return;
+                }
+                if (force || (arr[arr.length - 1] !== '\n')) { //we might want the extra line
+                    if ((arr[arr.length - 1] !== '\n')) {
+                        arr[arr.length - 1] = rtrim(arr[arr.length - 1]);
+                    }
+                    arr.push('\n');
+                }
+            };
+
+            this.print_indentation = function(arr) {
+                for (var i = 0; i < this.indent_level; i++) {
+                    arr.push(this.indent_string);
+     &nb