diff --git a/lib/editor/atto/autosave-ajax.php b/lib/editor/atto/autosave-ajax.php index 6626075a9fb..a31117c02ea 100644 --- a/lib/editor/atto/autosave-ajax.php +++ b/lib/editor/atto/autosave-ajax.php @@ -27,156 +27,195 @@ define('AJAX_SCRIPT', true); require_once(dirname(__FILE__) . '/../../../config.php'); require_once($CFG->libdir . '/filestorage/file_storage.php'); -$contextid = required_param('contextid', PARAM_INT); -$elementid = required_param('elementid', PARAM_ALPHANUMEXT); -$pagehash = required_param('pagehash', PARAM_ALPHANUMEXT); -$pageinstance = required_param('pageinstance', PARAM_ALPHANUMEXT); +// Clean up actions. +$actions = array_map(function($actionparams) { + $action = isset($actionparams['action']) ? $actionparams['action'] : null; + $params = []; + $keys = [ + 'action' => PARAM_ALPHA, + 'contextid' => PARAM_INT, + 'elementid' => PARAM_ALPHANUMEXT, + 'pagehash' => PARAM_ALPHANUMEXT, + 'pageinstance' => PARAM_ALPHANUMEXT + ]; + + if ($action == 'save') { + $keys['drafttext'] = PARAM_RAW; + } else if ($action == 'resume') { + $keys['draftid'] = PARAM_INT; + } + + foreach ($keys as $key => $type) { + // Replicate required_param(). + if (!isset($actionparams[$key])) { + print_error('missingparam', '', '', $key); + } + $params[$key] = clean_param($actionparams[$key], $type); + } + + return $params; +}, isset($_REQUEST['actions']) ? $_REQUEST['actions'] : []); + $now = time(); // This is the oldest time any autosave text will be recovered from. // This is so that there is a good chance the draft files will still exist (there are many variables so // this is impossible to guarantee). $before = $now - 60*60*24*4; -list($context, $course, $cm) = get_context_info_array($contextid); +$context = context_system::instance(); $PAGE->set_url('/lib/editor/atto/autosave-ajax.php'); $PAGE->set_context($context); -require_login($course, false, $cm); -require_sesskey(); - +require_login(); if (isguestuser()) { print_error('accessdenied', 'admin'); } +require_sesskey(); if (!in_array('atto', explode(',', get_config('core', 'texteditors')))) { print_error('accessdenied', 'admin'); } -$action = required_param('action', PARAM_ALPHA); +$responses = array(); +foreach ($actions as $actionparams) { -$response = array(); + $action = $actionparams['action']; + $contextid = $actionparams['contextid']; + $elementid = $actionparams['elementid']; + $pagehash = $actionparams['pagehash']; + $pageinstance = $actionparams['pageinstance']; -if ($action === 'save') { - $drafttext = required_param('drafttext', PARAM_RAW); - $params = array('elementid' => $elementid, - 'userid' => $USER->id, - 'pagehash' => $pagehash, - 'contextid' => $contextid); + if ($action === 'save') { + $drafttext = $actionparams['drafttext']; + $params = array('elementid' => $elementid, + 'userid' => $USER->id, + 'pagehash' => $pagehash, + 'contextid' => $contextid); - $record = $DB->get_record('editor_atto_autosave', $params); - if ($record && $record->pageinstance != $pageinstance) { - print_error('concurrent access from the same user is not supported'); - die(); - } - - if (!$record) { - $record = new stdClass(); - $record->elementid = $elementid; - $record->userid = $USER->id; - $record->pagehash = $pagehash; - $record->contextid = $contextid; - $record->drafttext = $drafttext; - $record->pageinstance = $pageinstance; - $record->timemodified = $now; - - $DB->insert_record('editor_atto_autosave', $record); - - // No response means no error. - die(); - } else { - $record->drafttext = $drafttext; - $record->timemodified = time(); - $DB->update_record('editor_atto_autosave', $record); - - // No response means no error. - die(); - } -} else if ($action == 'resume') { - $params = array('elementid' => $elementid, - 'userid' => $USER->id, - 'pagehash' => $pagehash, - 'contextid' => $contextid); - - $newdraftid = required_param('draftid', PARAM_INT); - - $record = $DB->get_record('editor_atto_autosave', $params); - - if (!$record) { - $record = new stdClass(); - $record->elementid = $elementid; - $record->userid = $USER->id; - $record->pagehash = $pagehash; - $record->contextid = $contextid; - $record->pageinstance = $pageinstance; - $record->pagehash = $pagehash; - $record->draftid = $newdraftid; - $record->timemodified = time(); - $record->drafttext = ''; - - $DB->insert_record('editor_atto_autosave', $record); - - // No response means no error. - die(); - } else { - // Copy all draft files from the old draft area. - $usercontext = context_user::instance($USER->id); - $stale = $record->timemodified < $before; - require_once($CFG->libdir . '/filelib.php'); - - $fs = get_file_storage(); - $files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $newdraftid, '/', true, true); - - $lastfilemodified = 0; - foreach ($files as $file) { - $lastfilemodified = max($lastfilemodified, $file->get_timemodified()); - } - if ($record->timemodified < $lastfilemodified) { - $stale = true; + $record = $DB->get_record('editor_atto_autosave', $params); + if ($record && $record->pageinstance != $pageinstance) { + print_error('concurrent access from the same user is not supported'); + die(); } - if (!$stale) { - // This function copies all the files in one draft area, to another area (in this case it's - // another draft area). It also rewrites the text to @@PLUGINFILE@@ links. - $newdrafttext = file_save_draft_area_files($record->draftid, - $usercontext->id, - 'user', - 'draft', - $newdraftid, - array(), - $record->drafttext); - - // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again). - $newdrafttext = file_rewrite_pluginfile_urls($newdrafttext, - 'draftfile.php', - $usercontext->id, - 'user', - 'draft', - $newdraftid); - $record->drafttext = $newdrafttext; - + if (!$record) { + $record = new stdClass(); + $record->elementid = $elementid; + $record->userid = $USER->id; + $record->pagehash = $pagehash; + $record->contextid = $contextid; + $record->drafttext = $drafttext; $record->pageinstance = $pageinstance; - $record->draftid = $newdraftid; + $record->timemodified = $now; + + $DB->insert_record('editor_atto_autosave', $record); + + // No response means no error. + $responses[] = null; + continue; + } else { + $record->drafttext = $drafttext; $record->timemodified = time(); $DB->update_record('editor_atto_autosave', $record); - // A response means the draft has been restored and here is the auto-saved text. - $response['result'] = $record->drafttext; - echo json_encode($response); - } else { - $DB->delete_records('editor_atto_autosave', array('id' => $record->id)); + // No response means no error. + $responses[] = null; + continue; + } + + } else if ($action == 'resume') { + $params = array('elementid' => $elementid, + 'userid' => $USER->id, + 'pagehash' => $pagehash, + 'contextid' => $contextid); + + $newdraftid = $actionparams['draftid']; + + $record = $DB->get_record('editor_atto_autosave', $params); + + if (!$record) { + $record = new stdClass(); + $record->elementid = $elementid; + $record->userid = $USER->id; + $record->pagehash = $pagehash; + $record->contextid = $contextid; + $record->pageinstance = $pageinstance; + $record->pagehash = $pagehash; + $record->draftid = $newdraftid; + $record->timemodified = time(); + $record->drafttext = ''; + + $DB->insert_record('editor_atto_autosave', $record); // No response means no error. - } - die(); - } -} else if ($action == 'reset') { - $params = array('elementid' => $elementid, - 'userid' => $USER->id, - 'pagehash' => $pagehash, - 'contextid' => $contextid); + $responses[] = null; + continue; - $DB->delete_records('editor_atto_autosave', $params); - die(); + } else { + // Copy all draft files from the old draft area. + $usercontext = context_user::instance($USER->id); + $stale = $record->timemodified < $before; + require_once($CFG->libdir . '/filelib.php'); + + $fs = get_file_storage(); + $files = $fs->get_directory_files($usercontext->id, 'user', 'draft', $newdraftid, '/', true, true); + + $lastfilemodified = 0; + foreach ($files as $file) { + $lastfilemodified = max($lastfilemodified, $file->get_timemodified()); + } + if ($record->timemodified < $lastfilemodified) { + $stale = true; + } + + if (!$stale) { + // This function copies all the files in one draft area, to another area (in this case it's + // another draft area). It also rewrites the text to @@PLUGINFILE@@ links. + $newdrafttext = file_save_draft_area_files($record->draftid, + $usercontext->id, + 'user', + 'draft', + $newdraftid, + array(), + $record->drafttext); + + // Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again). + $newdrafttext = file_rewrite_pluginfile_urls($newdrafttext, + 'draftfile.php', + $usercontext->id, + 'user', + 'draft', + $newdraftid); + $record->drafttext = $newdrafttext; + + $record->pageinstance = $pageinstance; + $record->draftid = $newdraftid; + $record->timemodified = time(); + $DB->update_record('editor_atto_autosave', $record); + + // A response means the draft has been restored and here is the auto-saved text. + $response = ['result' => $record->drafttext]; + $responses[] = $response; + + } else { + $DB->delete_records('editor_atto_autosave', array('id' => $record->id)); + + // No response means no error. + $responses[] = null; + } + continue; + } + + } else if ($action == 'reset') { + $params = array('elementid' => $elementid, + 'userid' => $USER->id, + 'pagehash' => $pagehash, + 'contextid' => $contextid); + + $DB->delete_records('editor_atto_autosave', $params); + $responses[] = null; + continue; + } } -print_error('invalidarguments'); +echo json_encode($responses); diff --git a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js index 09ba0b77189..fbcfd8b9e91 100644 --- a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js +++ b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js @@ -786,19 +786,6 @@ EditorAutosave.ATTRS= { pageHash: { value: '', writeOnce: true - }, - - /** - * The relative path to the ajax script. - * - * @attribute autosaveAjaxScript - * @type String - * @default '/lib/editor/atto/autosave-ajax.php' - * @readOnly - */ - autosaveAjaxScript: { - value: '/lib/editor/atto/autosave-ajax.php', - readOnly: true } }; @@ -839,8 +826,7 @@ EditorAutosave.prototype = { form, optiontype = null, options = this.get('filepickeroptions'), - params, - url; + params; if (!this.get('autosaveEnabled')) { // Autosave disabled for this instance. @@ -856,99 +842,73 @@ EditorAutosave.prototype = { // First see if there are any saved drafts. // Make an ajax request. - url = M.cfg.wwwroot + this.get('autosaveAjaxScript'); params = { - sesskey: M.cfg.sesskey, contextid: this.get('contextid'), action: 'resume', - drafttext: '', draftid: draftid, elementid: this.get('elementid'), pageinstance: this.autosaveInstance, pagehash: this.get('pageHash') }; - Y.io(url, { - method: 'POST', - data: params, - context: this, - on: { - success: function(id,o) { - var response_json; - if (typeof o.responseText !== "undefined" && o.responseText !== "") { - response_json = JSON.parse(o.responseText); + this.autosaveIo(params, this, { + success: function(response) { + if (response === null) { + // This can happen when there is nothing to resume from. + return; + } else if (!response) { + Y.log('Invalid response received.', 'debug', LOGNAME_AUTOSAVE); + return; + } - // Revert untouched editor contents to an empty string. - // Check for FF and Chrome. - if (response_json.result === '

' || response_json.result === '


' || - response_json.result === '
') { - response_json.result = ''; - } + // Revert untouched editor contents to an empty string. + // Check for FF and Chrome. + if (response.result === '

' || response.result === '


' || + response.result === '
') { + response.result = ''; + } - // Check for IE 9 and 10. - if (response_json.result === '

 

' || response_json.result === '


 

') { - response_json.result = ''; - } + // Check for IE 9 and 10. + if (response.result === '

 

' || response.result === '


 

') { + response.result = ''; + } - if (response_json.error || typeof response_json.result === 'undefined') { - Y.log('Error occurred recovering draft text: ' + response_json.error, 'debug', LOGNAME_AUTOSAVE); - this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'), - NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT); - } else if (response_json.result !== this.textarea.get('value') && - response_json.result !== '') { - Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE); - this.recoverText(response_json.result); - } - this._fireSelectionChanged(); - } - }, - failure: function() { + if (response.error || typeof response.result === 'undefined') { + Y.log('Error occurred recovering draft text: ' + response.error, 'debug', LOGNAME_AUTOSAVE); this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'), NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT); + } else if (response.result !== this.textarea.get('value') && + response.result !== '') { + Y.log('Autosave text found - recover it.', 'debug', LOGNAME_AUTOSAVE); + this.recoverText(response.result); } + this._fireSelectionChanged(); + + }, + failure: function() { + this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'), + NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT); } }); // Now setup the timer for periodic saves. - var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000; this.autosaveTimer = Y.later(delay, this, this.saveDraft, false, true); // Now setup the listener for form submission. form = this.textarea.ancestor('form'); if (form) { - form.on('submit', this.resetAutosave, this); + this.autosaveIoOnSubmit(form, { + action: 'reset', + contextid: this.get('contextid'), + elementid: this.get('elementid'), + pageinstance: this.autosaveInstance, + pagehash: this.get('pageHash') + }); } return this; }, - /** - * Clear the autosave text because the form was submitted normally. - * - * @method resetAutosave - * @chainable - */ - resetAutosave: function() { - // Make an ajax request to reset the autosaved text. - var url = M.cfg.wwwroot + this.get('autosaveAjaxScript'); - var params = { - sesskey: M.cfg.sesskey, - contextid: this.get('contextid'), - action: 'reset', - elementid: this.get('elementid'), - pageinstance: this.autosaveInstance, - pagehash: this.get('pageHash') - }; - - Y.io(url, { - method: 'POST', - data: params, - sync: true - }); - return this; - }, - - /** * Recover a previous version of this text and show a message. * @@ -1004,29 +964,23 @@ EditorAutosave.prototype = { }; // Reusable error handler - must be passed the correct context. - var ajaxErrorFunction = function(code, response) { + var ajaxErrorFunction = function(response) { var errorDuration = parseInt(this.get('autosaveFrequency'), 10) * 1000; - Y.log('Error while autosaving text:' + code, 'warn', LOGNAME_AUTOSAVE); + Y.log('Error while autosaving text', 'warn', LOGNAME_AUTOSAVE); Y.log(response, 'warn', LOGNAME_AUTOSAVE); this.showMessage(M.util.get_string('autosavefailed', 'editor_atto'), NOTIFY_WARNING, errorDuration); }; - Y.io(url, { - method: 'POST', - data: params, - context: this, - on: { - error: ajaxErrorFunction, - failure: ajaxErrorFunction, - success: function(code, response) { - if (response.responseText !== "") { - Y.soon(Y.bind(ajaxErrorFunction, this, [code, response])); - } else { - // All working. - this.lastText = newText; - this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'), - NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT); - } + this.autosaveIo(params, this, { + failure: ajaxErrorFunction, + success: function(response) { + if (response && response.error) { + Y.soon(Y.bind(ajaxErrorFunction, this, [response])); + } else { + // All working. + this.lastText = newText; + this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'), + NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT); } } }); @@ -1051,6 +1005,250 @@ Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosave]); // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * A autosave function for the Atto editor. + * + * @module moodle-editor_atto-autosave-io + * @submodule autosave-io + * @package editor_atto + * @copyright 2016 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +var EditorAutosaveIoDispatcherInstance = null; + +function EditorAutosaveIoDispatcher() { + EditorAutosaveIoDispatcher.superclass.constructor.apply(this, arguments); + this._submitEvents = {}; + this._queue = []; + this._throttle = null; +} +EditorAutosaveIoDispatcher.NAME = 'EditorAutosaveIoDispatcher'; +EditorAutosaveIoDispatcher.ATTRS = { + + /** + * The relative path to the ajax script. + * + * @attribute autosaveAjaxScript + * @type String + * @default '/lib/editor/atto/autosave-ajax.php' + * @readOnly + */ + autosaveAjaxScript: { + value: '/lib/editor/atto/autosave-ajax.php', + readOnly: true + }, + + /** + * The time buffer for the throttled requested. + * + * @attribute delay + * @type Number + * @default 50 + * @readOnly + */ + delay: { + value: 50, + readOnly: true + } + +}; +Y.extend(EditorAutosaveIoDispatcher, Y.Base, { + + /** + * Dispatch an IO request. + * + * This method will put the requests in a queue in order to attempt to bulk them. + * + * @param {Object} params The parameters of the request. + * @param {Object} context The context in which the callbacks are called. + * @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as + * optional keys defining the callbacks to call. Success and Complete + * functions will receive the response as parameter. Success and Complete + * may receive an object containing the error key, use this to confirm + * that no errors occured. + * @return {Void} + */ + dispatch: function(params, context, callbacks) { + if (this._throttle) { + this._throttle.cancel(); + } + + this._throttle = Y.later(this.get('delay'), this, this._processDispatchQueue); + this._queue.push([params, context, callbacks]); + }, + + /** + * Dispatches the requests in the queue. + * + * @return {Void} + */ + _processDispatchQueue: function() { + var queue = this._queue, + data = {}; + + this._queue = []; + if (queue.length < 1) { + return; + } + + Y.Array.each(queue, function(item, index) { + data[index] = item[0]; + }); + + Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), { + method: 'POST', + data: Y.QueryString.stringify({ + actions: data, + sesskey: M.cfg.sesskey + }), + on: { + start: this._makeIoEventCallback('start', queue), + complete: this._makeIoEventCallback('complete', queue), + failure: this._makeIoEventCallback('failure', queue), + end: this._makeIoEventCallback('end', queue), + success: this._makeIoEventCallback('success', queue) + } + }); + }, + + /** + * Creates a function that dispatches an IO response to callbacks. + * + * @param {String} event The type of event. + * @param {Array} queue The queue. + * @return {Function} + */ + _makeIoEventCallback: function(event, queue) { + var noop = function() {}; + return function() { + var response = arguments[1], + parsed = {}; + + if ((event == 'complete' || event == 'success') && (typeof response !== 'undefined' + && typeof response.responseText !== 'undefined' && response.responseText !== '')) { + + // Success and complete events need to parse the response. + parsed = JSON.parse(response.responseText) || {}; + } + + Y.Array.each(queue, function(item, index) { + var context = item[1], + cb = (item[2] && item[2][event]) || noop, + arg; + + if (parsed && parsed.error) { + // The response is an error, we send it to everyone. + arg = parsed; + } else if (parsed) { + // The response was parsed, we only communicate the relevant portion of the response. + arg = parsed[index]; + } + + cb.apply(context, [arg]); + }); + }; + }, + + /** + * Form submit handler. + * + * @param {EventFacade} e The event. + * @return {Void} + */ + _onSubmit: function(e) { + var data = {}, + id = e.currentTarget.generateID(), + params = this._submitEvents[id]; + + if (!params || params.ios.length < 1) { + return; + } + + Y.Array.each(params.ios, function(param, index) { + data[index] = param; + }); + + Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), { + method: 'POST', + data: Y.QueryString.stringify({ + actions: data, + sesskey: M.cfg.sesskey + }), + sync: true + }); + }, + + /** + * Registers a request to be made on form submission. + * + * @param {Node} node The forum node we will listen to. + * @param {Object} params Parameters for the IO request. + * @return {Void} + */ + whenSubmit: function(node, params) { + if (typeof this._submitEvents[node.generateID()] === 'undefined') { + this._submitEvents[node.generateID()] = { + event: node.on('submit', this._onSubmit, this), + ios: [] + }; + } + this._submitEvents[node.get('id')].ios.push([params]); + } + +}); +EditorAutosaveIoDispatcherInstance = new EditorAutosaveIoDispatcher(); + + +function EditorAutosaveIo() {} +EditorAutosaveIo.prototype = { + + /** + * Dispatch an IO request. + * + * This method will put the requests in a queue in order to attempt to bulk them. + * + * @param {Object} params The parameters of the request. + * @param {Object} context The context in which the callbacks are called. + * @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as + * optional keys defining the callbacks to call. Success and Complete + * functions will receive the response as parameter. Success and Complete + * may receive an object containing the error key, use this to confirm + * that no errors occured. + * @return {Void} + */ + autosaveIo: function(params, context, callbacks) { + EditorAutosaveIoDispatcherInstance.dispatch(params, context, callbacks); + }, + + /** + * Registers a request to be made on form submission. + * + * @param {Node} form The forum node we will listen to. + * @param {Object} params Parameters for the IO request. + * @return {Void} + */ + autosaveIoOnSubmit: function(form, params) { + EditorAutosaveIoDispatcherInstance.whenSubmit(form, params); + } + +}; +Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosaveIo]); +// 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 . + /** * @module moodle-editor_atto-editor * @submodule clean @@ -2606,6 +2804,7 @@ Y.Base.mix(Y.M.editor_atto.Editor, [EditorFilepicker]); "moodle-core-notification-confirm", "moodle-editor_atto-rangy", "handlebars", - "timers" + "timers", + "querystring-stringify" ] }); diff --git a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js index 912122eba5c..c8557d0e7cc 100644 --- a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js +++ b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js @@ -1,4 +1,4 @@ -YUI.add("moodle-editor_atto-editor",function(e,t){function s(){s.superclass.constructor.apply(this,arguments)}function f(){}function l(){}function d(){}function v(){}function m(){}function g(){}function y(){}function b(){}function w(){}function E(){}var n="moodle-editor_atto-editor",r={CONTENT:"editor_atto_content",CONTENTWRAPPER:"editor_atto_content_wrap",TOOLBAR:"editor_atto_toolbar",WRAPPER:"editor_atto",HIGHLIGHT:"highlight"},i=window.rangy;e.extend(s,e.Base,{BLOCK_TAGS:["address","article","aside","audio","blockquote","canvas","dd","div","dl","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","hr","noscript","ol","output","p","pre","section","table","tfoot","ul","video"],PLACEHOLDER_CLASS:"atto-tmp-class",ALL_NODES_SELECTOR:"[style],font[face]",FONT_FAMILY:"fontFamily",_wrapper:null,editor:null,textarea:null,textareaLabel:null,plugins:null,_eventHandles:null,initializer:function(){var t;this.textarea=e.one(document.getElementById(this.get("elementid")));if(!this.textarea)return;this._eventHandles=[],this._wrapper=e.Node.create('
'),t=e.Handlebars.compile('
'),this.editor=e.Node.create(t({elementid:this.get("elementid"),CSS:r})),this.textareaLabel=e.one('[for="'+this.get("elementid")+'"]'),this.textareaLabel&&(this.textareaLabel.generateID(),this.editor.setAttribute("aria-labelledby",this.textareaLabel.get("id"))),this.setupToolbar();var n=e.Node.create('
');n.appendChild(this.editor),this._wrapper.appendChild(n),this.editor.setStyle("minHeight",20*this.textarea.getAttribute("rows")+8+"px"),e.UA.ie===0&&this.editor.setStyle("height",20*this.textarea.getAttribute("rows")+8+"px"),this.disableCssStyling(),document.queryCommandSupported("DefaultParagraphSeparator")&&document.execCommand("DefaultParagraphSeparator",!1,"p"),this.textarea.get("parentNode").insert(this._wrapper,this.textarea).setAttribute("class","editor_atto_wrap"),this.textarea.hide(),this.updateFromTextArea(),this.publishEvents(),this.setupSelectionWatchers(),this.setupAutomaticPolling(),this.setupPlugins(),this.setupAutosave(),this.setupNotifications()},focus:function(){return this.editor.focus(),this},publishEvents:function(){return this.publish("change",{broadcast:!0,preventable:!0}),this.publish("pluginsloaded",{fireOnce:!0}),this.publish("atto:selectionchanged",{prefix:"atto"}),this},setupAutomaticPolling:function(){return this._registerEventHandle(this.editor.on(["keyup","cut"],this.updateOriginal,this)),this._registerEventHandle(this.editor.on("paste",this.pasteCleanup,this)),this._registerEventHandle(this.editor.on("drop",this.updateOriginalDelayed,this)),this},updateOriginalDelayed:function(){return e.soon(e.bind(this.updateOriginal,this)),this},setupPlugins:function(){this.plugins={};var t=this.get("plugins"),n,r,i,s,o;for(n in t){r=t[n];if(!r.plugins)continue;for(i in r.plugins){s=r.plugins[i],o=e.mix({name:s.name,group:r.group,editor:this.editor,toolbar:this.toolbar,host:this},s);if(typeof e.M["atto_"+s.name]=="undefined")continue;this.plugins[s.name]=new e.M["atto_"+s.name].Button(o)}}return this.fire("pluginsloaded"),this},enablePlugins:function(e){this._setPluginState(!0,e)},disablePlugins:function(e){this._setPluginState(!1,e)},_setPluginState:function(t,n){var r="disableButtons";t&&(r="enableButtons"),n?this.plugins[n][r]():e.Object.each(this.plugins,function(e){e[r]()},this)},_registerEventHandle:function(e){this._eventHandles.push(e)}},{NS:"editor_atto",ATTRS:{elementid:{value:null,writeOnce:!0},contextid:{value:null,writeOnce:!0},plugins:{value:{},writeOnce:!0}}}),e.augment(s,e.EventTarget),e.namespace("M.editor_atto").Editor=s,e.namespace("M.editor_atto.Editor").init=function(t){return new e.M.editor_atto.Editor(t)};var o="moodle-editor_atto-editor-notify",u="info",a="warning";f.ATTRS={},f.prototype={messageOverlay:null,hideTimer:null,setupNotifications:function(){var e=new Image,t=new Image;return e.src=M.util.image_url("i/warning","moodle"),t.src=M.util.image_url("i/info","moodle"),this},showMessage:function(t,n,r){var i="",s,o;return this.messageOverlay===null&&(this.messageOverlay=e.Node.create('
'),this.messageOverlay.hide(!0),this.textarea.get("parentNode").append(this.messageOverlay),this.messageOverlay.on("click",function(){this.messageOverlay.hide(!0)},this)),this.hideTimer!==null&&this.hideTimer.cancel(),n===a?i=''+M.util.get_string(':n===u&&(i=''+M.util.get_string('),s=parseInt(r,10),s<=0&&(s=6e4),n="atto_"+n,o=e.Node.create('"),this.messageOverlay.empty(),this.messageOverlay.append(o),this.messageOverlay.show(!0),this.hideTimer=e.later(s,this,function(){this.hideTimer=null,this.messageOverlay.hide(!0)}),this}},e.Base.mix(e.M.editor_atto.Editor,[f]),l.ATTRS={},l.prototype={_getEmptyContent:function(){return e.UA.ie&&e.UA.ie<10?"

":"


"},updateFromTextArea:function(){return this.editor.setHTML(""),this.editor.append(this._cleanHTML(this.textarea.get("value"))),this.editor.getHTML()===""&&this.editor.setHTML(this._getEmptyContent()),this},updateOriginal:function(){var e=this.textarea.get("value"),t=this.getCleanHTML();return t===""&&this.isActive()&&(t=this._getEmptyContent()),e!==t&&(this.textarea.set("value",t),this.textarea.simulate("change"),this.fire("change")),this}},e.Base.mix(e.M.editor_atto.Editor,[l]);var c=5e3,h=6e4,p="moodle-editor_atto-editor-autosave";d.ATTRS={autosaveEnabled:{value:!0,writeOnce:!0},autosaveFrequency:{value:60,writeOnce:!0},pageHash:{value:"",writeOnce:!0},autosaveAjaxScript:{value:"/lib/editor/atto/autosave-ajax.php",readOnly:!0}} -,d.prototype={lastText:"",autosaveInstance:null,autosaveTimer:null,setupAutosave:function(){var t=-1,n,r=null,i=this.get("filepickeroptions"),s,o;if(!this.get("autosaveEnabled"))return;this.autosaveInstance=e.stamp(this);for(r in i)typeof i[r].itemid!="undefined"&&(t=i[r].itemid);o=M.cfg.wwwroot+this.get("autosaveAjaxScript"),s={sesskey:M.cfg.sesskey,contextid:this.get("contextid"),action:"resume",drafttext:"",draftid:t,elementid:this.get("elementid"),pageinstance:this.autosaveInstance,pagehash:this.get("pageHash")},e.io(o,{method:"POST",data:s,context:this,on:{success:function(e,t){var n;if(typeof t.responseText!="undefined"&&t.responseText!==""){n=JSON.parse(t.responseText);if(n.result==="

"||n.result==="


"||n.result==="
")n.result="";if(n.result==="

 

"||n.result==="


 

")n.result="";n.error||typeof n.result=="undefined"?this.showMessage(M.util.get_string("errortextrecovery","editor_atto"),a,h):n.result!==this.textarea.get("value")&&n.result!==""&&this.recoverText(n.result),this._fireSelectionChanged()}},failure:function(){this.showMessage(M.util.get_string("errortextrecovery","editor_atto"),a,h)}}});var u=parseInt(this.get("autosaveFrequency"),10)*1e3;return this.autosaveTimer=e.later(u,this,this.saveDraft,!1,!0),n=this.textarea.ancestor("form"),n&&n.on("submit",this.resetAutosave,this),this},resetAutosave:function(){var t=M.cfg.wwwroot+this.get("autosaveAjaxScript"),n={sesskey:M.cfg.sesskey,contextid:this.get("contextid"),action:"reset",elementid:this.get("elementid"),pageinstance:this.autosaveInstance,pagehash:this.get("pageHash")};return e.io(t,{method:"POST",data:n,sync:!0}),this},recoverText:function(e){return this.editor.setHTML(e),this.saveSelection(),this.updateOriginal(),this.lastText=e,this.showMessage(M.util.get_string("textrecovered","editor_atto"),u,h),this},saveDraft:function(){var t,n;if(!this.editor.getDOMNode()){this.autosaveTimer.cancel();return}this.editor.get("hidden")||this.updateOriginal();var r=this.textarea.get("value");if(r!==this.lastText){t=M.cfg.wwwroot+this.get("autosaveAjaxScript"),n={sesskey:M.cfg.sesskey,contextid:this.get("contextid"),action:"save",drafttext:r,elementid:this.get("elementid"),pagehash:this.get("pageHash"),pageinstance:this.autosaveInstance};var i=function(e,t){var n=parseInt(this.get("autosaveFrequency"),10)*1e3;this.showMessage(M.util.get_string("autosavefailed","editor_atto"),a,n)};e.io(t,{method:"POST",data:n,context:this,on:{error:i,failure:i,success:function(t,n){n.responseText!==""?e.soon(e.bind(i,this,[t,n])):(this.lastText=r,this.showMessage(M.util.get_string("autosavesucceeded","editor_atto"),u,c))}}})}return this}},e.Base.mix(e.M.editor_atto.Editor,[d]),v.ATTRS={},v.prototype={getCleanHTML:function(){var t=this.editor.cloneNode(!0),n;return e.each(t.all('[id^="yui"]'),function(e){e.removeAttribute("id")}),t.all(".atto_control").remove(!0),n=t.get("innerHTML"),n==="

"||n==="


"?"":this._cleanHTML(n)},cleanEditorHTML:function(){var e=this.editor.get("innerHTML");return this.editor.set("innerHTML",this._cleanHTML(e)),this},_cleanHTML:function(e){var t=[{regex:/]*>[\s\S]*?<\/style>/gi,replace:""},{regex:/)/gi,replace:""},{regex:/<\/?(?:title|meta|style|st\d|head|font|html|body|link)[^>]*?>/gi,replace:""}];return this._filterContentWithRules(e,t)},_filterContentWithRules:function(e,t){var n=0;for(n=0;n-1);if(r){var i;try{i=t.clipboardData.getData("text/html")}catch(s){return this.fallbackPasteCleanupDelayed(),!0}e.preventDefault(),i=this._cleanPasteHTML(i);var o=window.rangy.saveSelection();return this.insertContentAtFocusPoint(i),window.rangy.restoreSelection(o),window.rangy.getSelection().collapseToEnd(),this.updateOriginal(),!1}return this.fallbackPasteCleanupDelayed(),!0}return this.fallbackPasteCleanupDelayed(),!0}return this.updateOriginalDelayed(),!0},fallbackPasteCleanup:function(){var e=window.rangy.saveSelection(),t=this.editor.get("innerHTML");return this.editor.set("innerHTML",this._cleanPasteHTML(t)),this.updateOriginal(),window.rangy.restoreSelection(e),this},fallbackPasteCleanupDelayed:function(){return e.soon(e.bind(this.fallbackPasteCleanup,this)),this},_cleanPasteHTML:function(e){if(!e||e.length===0)return"";var t=[{regex:/<\s*\/html\s*>([\s\S]+)$/gi,replace:""},{regex://gi,replace:""},{regex://gi,replace:""},{regex:/]*>[\s\S]*?<\/xml>/gi,replace:""},{regex:/<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi,replace:""},{regex:/<\/?\w+:[^>]*>/gi,replace:""}];e=this._filterContentWithRules(e,t),e=this._cleanHTML(e);if(e.length===0||!e.match(/\S/))return e;var n=document.createElement("div");return n.innerHTML=e,e=n.innerHTML,n.innerHTML="",t=[{regex:/(<[^>]*?style\s*?=\s*?")([^>"]*)(")/gi,replace:function(e,t,n,r){return n=n.replace(/(?:^|;)[\s]*MSO[-:](?:&[\w]*;|[^;"])*/gi,""),n=n.replace(/background-color:.*?;/gi,""),t+n+r}},{regex:/(<[^>]*?class\s*?=\s*?")([^>"]*)(")/gi,replace:function(e,t,n,r){return n=n.replace(/(?:^|[\s])[\s]*MSO[_a-zA-Z0-9\-]*/gi,""),n=n.replace(/(?:^|[\s])[\s]*Apple-[_a-zA-Z0-9\-]*/gi,""),t+n+r}},{regex:/]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi,replace:""}],e=this._filterContentWithRules(e,t),e=this._cleanHTML(e),e=this._cleanSpans(e),e},_cleanSpans:function(e){if(!e||e.length===0)return"";if(e.length===0||!e.match(/\S/))return e;var t=[{regex:/(<[^>]*?)(?:[\s]*(?:class|style|id)\s*?=\s*?"\s*?")+/gi,replace:"$1"}];e=this._filterContentWithRules(e,t);var n=document.createElement("div");n.innerHTML=e;var r=n.getElementsByTagName("span"),i=Array.prototype.slice.call(r,0);return i.forEach(function(e){if(!e.hasAttributes()){while( -e.firstChild)e.parentNode.insertBefore(e.firstChild,e);e.parentNode.removeChild(e)}}),n.innerHTML}},e.Base.mix(e.M.editor_atto.Editor,[v]),m.ATTRS={},m.prototype={applyFormat:function(t,n,r,i){function s(t,n,r,i,s,o){e.soon(e.bind(function(e,t,n,r,i,s){var o=window.rangy.getSelection(),u=o.getRangeAt(0);u.setStart(i,s),o.setSingleRange(u),t.apply(n,[e,r]),o.collapseToEnd(),this.saveSelection(),this.updateOriginal()},this,t,n,r,i,s,o))}r=r||this;var o=window.rangy.getSelection();if(o.isCollapsed){var u=this.editor.once("input",s,this,n,r,i,o.anchorNode,o.anchorOffset);this.editor.onceAfter(["click","selectstart"],u.detach,u);return}n.apply(r,[t,i]),this.saveSelection(),this.updateOriginal()},replaceTags:function(t,n){t.setAttribute("data-iterate",!0);var r=this.editor.one('[data-iterate="true"]');while(r){var i=e.Node.create("<"+n+" />").setAttrs(r.getAttrs()).removeAttribute("data-iterate");r.getAttribute("style")&&i.setAttribute("style",r.getAttribute("style")),r.getAttribute("class")&&i.setAttribute("class",r.getAttribute("class"));var s=r.getDOMNode().childNodes,o;o=s[0];while(typeof o!="undefined")i.append(o),o=s[0];r.replace(i),r=this.editor.one('[data-iterate="true"]')}},changeToCSS:function(e,t){var n=window.rangy.saveSelection();this.editor.all(".rangySelectionBoundary").setStyle("display",null),this.editor.all(e).addClass(t),this.replaceTags(this.editor.all("."+t),"span"),window.rangy.restoreSelection(n)},changeToTags:function(e,t){var n=window.rangy.saveSelection();this.editor.all(".rangySelectionBoundary").setStyle("display",null),this.replaceTags(this.editor.all('span[class="'+e+'"]'),t),this.editor.all(t+'[class="'+e+'"]').removeAttribute("class"),this.editor.all("."+e).each(function(n){n.wrap("<"+t+"/>"),n.removeClass(e)}),this.editor.all('[class="'+e+'"]').removeAttribute("class"),this.editor.all(t).removeClass(e),window.rangy.restoreSelection(n)}},e.Base.mix(e.M.editor_atto.Editor,[m]),g.ATTRS={},g.prototype={toolbar:null,openMenus:null,setupToolbar:function(){return this.toolbar=e.Node.create('