From a7fdadc94cfe97faf95ecbfb0be7a9651546bbe0 Mon Sep 17 00:00:00 2001
From: Eric Merrill
Date: Wed, 18 Mar 2015 00:08:34 -0400
Subject: [PATCH] MDL-47002 atto: Improve paste behaviour to ensure propper
cleanup
Old code failed to clean the editor div which resulted in
undesirable code being being left in the div for editing. To properly
handel incoming code, we should try to intercept and clean before the
paste, or clean the entire editable div.
---
.../moodle-editor_atto-editor-debug.js | 134 +++++++++++++++++-
.../moodle-editor_atto-editor-min.js | 7 +-
.../moodle-editor_atto-editor.js | 133 ++++++++++++++++-
lib/editor/atto/yui/src/editor/js/clean.js | 131 ++++++++++++++++-
lib/editor/atto/yui/src/editor/js/editor.js | 3 +-
5 files changed, 396 insertions(+), 12 deletions(-)
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 52d04bf2afb..4cf531c6078 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
@@ -298,7 +298,8 @@ Y.extend(Editor, Y.Base, {
* @chainable
*/
setupAutomaticPolling: function() {
- this._registerEventHandle(this.editor.on(['keyup', 'paste', 'cut'], this.updateOriginal, this));
+ this._registerEventHandle(this.editor.on(['keyup', 'cut'], this.updateOriginal, this));
+ this._registerEventHandle(this.editor.on('paste', this.pasteCleanup, this));
// Call this.updateOriginal after dropped content has been processed.
this._registerEventHandle(this.editor.on('drop', this.updateOriginalDelayed, this));
@@ -1129,10 +1130,12 @@ EditorClean.prototype = {
{regex: /]*>( |\s)*<\/span>/gi, replace: ""},
// Remove class="Msoblah"
{regex: /class="Mso[^"]*"/gi, replace: ""},
+ // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout.
+ {regex: /)/gi, replace: ""},
// Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"
- // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body.
- {regex: /<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|!\[)[^>]*?>/gi, replace: ""},
+ // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link.
+ {regex: /<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|\/?link|!\[)[^>]*?>/gi, replace: ""},
// Source: "http://www.tim-jarrett.com/labs_javascript_scrub_word.php"
// Replace extended chars with simple text.
@@ -1155,6 +1158,131 @@ EditorClean.prototype = {
}
return content;
+ },
+
+ /**
+ * Intercept and clean html paste events.
+ *
+ * @method pasteCleanup
+ * @param {Object} sourceEvent The YUI EventFacade object
+ * @return {Boolean} True if the passed event should continue, false if not.
+ */
+ pasteCleanup: function(sourceEvent) {
+ // We only expect paste events, but we will check anyways.
+ if (sourceEvent.type === 'paste') {
+ // The YUI event wrapper doesn't provide paste event info, so we need the underlying event.
+ var event = sourceEvent._event;
+ // Check if we have a valid clipboardData object in the event.
+ // IE has a clipboard object at window.clipboardData, but as of IE 11, it does not provide HTML content access.
+ if (event && event.clipboardData && event.clipboardData.getData) {
+ // Check if there is HTML type to be pasted, this is all we care about.
+ var types = event.clipboardData.types;
+ var isHTML = false;
+ // Different browsers use different things to hold the types, so test various functions.
+ if (!types) {
+ isHTML = false;
+ } else if (typeof types.contains === 'function') {
+ isHTML = types.contains('text/html');
+ } else if (typeof types.indexOf === 'function') {
+ isHTML = (types.indexOf('text/html') > -1);
+ if (!isHTML) {
+ if ((types.indexOf('com.apple.webarchive') > -1) || (types.indexOf('com.apple.iWork.TSPNativeData') > -1)) {
+ // This is going to be a specialized Apple paste paste. We cannot capture this, so clean everything.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+ }
+ } else {
+ // We don't know how to handle the clipboard info, so wait for the clipboard event to finish then fallback.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+
+ if (isHTML) {
+ // Get the clipboard content.
+ var content;
+ try {
+ content = event.clipboardData.getData('text/html');
+ } catch (error) {
+ // Something went wrong. Fallback.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+
+ // Stop the original paste.
+ sourceEvent.preventDefault();
+
+ // Scrub the paste content.
+ content = this._cleanHTML(content);
+
+ // Save the current selection.
+ // Using saveSelection as it produces a more consistent experience.
+ var selection = window.rangy.saveSelection();
+
+ // Insert the content.
+ this.insertContentAtFocusPoint(content);
+
+ // Restore the selection, and collapse to end.
+ window.rangy.restoreSelection(selection);
+ window.rangy.getSelection().collapseToEnd();
+
+ // Update the text area.
+ this.updateOriginal();
+ return false;
+ } else {
+ // This is a non-html paste event, we can just let this continue on and call updateOriginalDelayed.
+ this.updateOriginalDelayed();
+ return true;
+ }
+ } else {
+ // If we reached a here, this probably means the browser has limited (or no) clipboard support.
+ // Wait for the clipboard event to finish then fallback.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+ }
+
+ // We should never get here - we must have received a non-paste event for some reason.
+ // Um, just call updateOriginalDelayed() - it's safe.
+ this.updateOriginalDelayed();
+ return true;
+ },
+
+ /**
+ * Cleanup code after a paste event if we couldn't intercept the paste content.
+ *
+ * @method fallbackPasteCleanup
+ * @chainable
+ */
+ fallbackPasteCleanup: function() {
+ Y.log('Using fallbackPasteCleanup for atto cleanup', 'debug', LOGNAME);
+
+ // Save the current selection (cursor position).
+ var selection = window.rangy.saveSelection();
+
+ // Get, clean, and replace the content in the editable.
+ var content = this.editor.get('innerHTML');
+ this.editor.set('innerHTML', this._cleanHTML(content));
+
+ // Update the textarea.
+ this.updateOriginal();
+
+ // Restore the selection (cursor position).
+ window.rangy.restoreSelection(selection);
+
+ return this;
+ },
+
+ /**
+ * Calls fallbackPasteCleanup on a short timer to allow the paste event handlers to complete.
+ *
+ * @method fallbackPasteCleanupDelayed
+ * @chainable
+ */
+ fallbackPasteCleanupDelayed: function() {
+ Y.soon(Y.bind(this.fallbackPasteCleanup, this));
+
+ return this;
}
};
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 6a50ded964a..ee5d8bed30d 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,3 +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(){}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","paste","cut"],this.updateOriginal,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='
':n===u&&(i='
'),s=parseInt(r,10),s<=0&&(s=6e4),n="atto_"+n,o=e.Node.create(''+i+" "+e.Escape.html(t)+"
"),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(){this.editor.setHTML(""),this.editor.append(this.textarea.get("value")),this.cleanEditorHTML(),this.editor.getHTML()===""&&this.editor.setHTML(this._getEmptyContent())},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,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 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;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://gi,replace:""},{regex:/<\\?\?xml[^>]*>/gi,replace:""},{regex:/<\/?\w+:[^>]*>/gi,replace:""},{regex:/\s*MSO[-:][^;"']*;?/gi,replace:""},{regex:/]*>( |\s)*<\/span>/gi,replace:""},{regex:/class="Mso[^"]*"/gi,replace:""},{regex:/<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|!\[)[^>]*?>/gi,replace:""},{regex:new RegExp(String.fromCharCode(8220),"gi"),replace:'"'},{regex:new RegExp(String.fromCharCode(8216),"gi"),replace:"'"},{regex:new RegExp(String.fromCharCode(8217),"gi"),replace:"'"},{regex:new RegExp(String.fromCharCode(8211),"gi"),replace:"-"},{regex:new RegExp(String.fromCharCode(8212),"gi"),replace:"--"},{regex:new RegExp(String.fromCharCode(189),"gi"),replace:"1/2"},{regex:new RegExp(String.fromCharCode(188),"gi"),replace:"1/4"},{regex:new RegExp(String.fromCharCode(190),"gi"),replace:"3/4"},{regex:new RegExp(String.fromCharCode(169),"gi"),replace:"(c)"},{regex:new RegExp(String.fromCharCode(174),"gi"),replace:"(r)"},{regex:new RegExp(String.fromCharCode(8230),"gi"),replace:"..."}],n=0;for(n=0;n'),this.openMenus=[],this._wrapper.appendChild(this.toolbar),this.textareaLabel&&this.toolbar.setAttribute("aria-labelledby",this.textareaLabel.get("id")),this.setupToolbarNavigation(),this}},e.Base.mix(e.M.editor_atto.Editor,[m]),g.ATTRS={},g.prototype={_tabFocus:null,setupToolbarNavigation:function(){return this._wrapper.delegate("key",this.toolbarKeyboardNavigation,"down:37,39","."+r.TOOLBAR,this),this._wrapper.delegate("focus",function(e){this._setTabFocus(e.currentTarget)},"."+r.TOOLBAR+" button",this),this},toolbarKeyboardNavigation:function(e){e.preventDefault();var t=this.toolbar.all("button"),n=1,r,i=e.target.ancestor("button",!0);e.keyCode===37&&(n=-1),r=this._findFirstFocusable(t,i,n),r&&(r.focus(),this._setTabFocus(r))},_findFirstFocusable:function(e,t,n){var r=0,i,s,o,u;u=e.indexOf(t),u<-1&&(u=0);while(r=e.size()&&(u=0),s=e.item(u),r++;if(s.hasAttribute("hidden")||s.hasAttribute("disabled"))continue;i=s.ancestor(".atto_group");if(i.hasAttribute("hidden"))continue;o=s;break}return o},checkTabFocus:function(){if(this._tabFocus)if(this._tabFocus.hasAttribute("disabled")||this._tabFocus.hasAttribute("hidden")||this._tabFocus.ancestor(".atto_group").hasAttribute("hidden")){var e=this._findFirstFocusable(this.toolbar.all("button"),this._tabFocus,-1);e&&(this._tabFocus.compareTo(document.activeElement)&&e.focus(),this._setTabFocus(e))}return this},_setTabFocus:function(e){return this._tabFocus&&this._tabFocus.setAttribute("tabindex","-1"),this._tabFocus=e,this._tabFocus.setAttribute("tabindex",0),this.toolbar.setAttribute("aria-activedescendant",this._tabFocus.generateID()),this}},e.Base.mix(e.M.editor_atto.Editor,[g]),y.ATTRS={},y.prototype={_selections
-:null,_lastSelection:null,_focusFromClick:!1,setupSelectionWatchers:function(){return this.on("atto:selectionchanged",this.saveSelection,this),this.editor.on("focus",this.restoreSelection,this),this.editor.on("mousedown",function(){this._focusFromClick=!0},this),this.editor.on("blur",function(){this._focusFromClick=!1,this.updateOriginal()},this),this.editor.on(["keyup","focus"],function(t){e.soon(e.bind(this._hasSelectionChanged,this,t))},this),this.editor.on("gesturemoveend",function(t){e.soon(e.bind(this._hasSelectionChanged,this,t))},{standAlone:!0},this),this},isActive:function(){var e=i.createRange(),t=i.getSelection();return t.rangeCount?!document.activeElement||!this.editor.compareTo(document.activeElement)&&!this.editor.contains(document.activeElement)?!1:(e.selectNode(this.editor.getDOMNode()),e.intersectsRange(t.getRangeAt(0))):!1},getSelectionFromNode:function(e){var t=i.createRange();return t.selectNode(e.getDOMNode()),[t]},saveSelection:function(){this.isActive()&&(this._selections=this.getSelection())},restoreSelection:function(){this._focusFromClick||this._selections&&this.setSelection(this._selections),this._focusFromClick=!1},getSelection:function(){return i.getSelection().getAllRanges()},selectionContainsNode:function(e){return i.getSelection().containsNode(e.getDOMNode(),!0)},selectionFilterMatches:function(e,t,n){typeof n=="undefined"&&(n=!0),t||(t=this.getSelectedNodes());var r=t.size()>0,i=!1,s=this.editor,o=function(e){return e===s};return s.one(e)?(t.each(function(t){if(n){if(!r||!t.ancestor(e,!0,o))r=!1}else!i&&t.ancestor(e,!0,o)&&(i=!0)},this),n?r:i):!1},getSelectedNodes:function(){var t=new e.NodeList,n,r,s,o,u;r=i.getSelection(),r.rangeCount?s=r.getRangeAt(0):s=i.createRange(),s.collapsed&&s.commonAncestorContainer!==this.editor.getDOMNode()&&s.commonAncestorContainer!==e.config.doc&&(s=s.cloneRange(),s.selectNode(s.commonAncestorContainer)),n=s.getNodes();for(u=0;u
"),i.get("childNodes").each(function(e){u.append(e.remove())}),i.append(u),o=u),t&&t!==""&&(f=e.Node.create("<"+t+">"+t+">"),f.setAttrs(o.getAttrs()),o.get("childNodes").each(function(e){e.remove(),f.append(e)}),o.replace(f),o=f),n&&o.setAttrs(n);var l=this.getSelectionFromNode(o);return this.setSelection(l),o}},e.Base.mix(e.M.editor_atto.Editor,[b]),w.ATTRS={filepickeroptions:{value:{}}},w.prototype={canShowFilepicker:function(e){return typeof this.get("filepickeroptions")[e]!="undefined"},showFilepicker:function(t,n,r){var i=this;e.use("core_filepicker",function(e){var s=e.clone(i.get("filepickeroptions")[t],!0);s.formcallback=n,r&&(s.magicscope=r),M.core_filepicker.show(e,s)})}},e.Base.mix(e.M.editor_atto.Editor,[w])},"@VERSION@",{requires:["node","transition","io","overlay","escape","event","event-simulate","event-custom","yui-throttle","moodle-core-notification-dialogue","moodle-core-notification-confirm","moodle-editor_atto-rangy","handlebars","timers"]});
+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(){}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='
':n===u&&(i='
'),s=parseInt(r,10),s<=0&&(s=6e4),n="atto_"+n,o=e.Node.create(''+i+" "+e.Escape.html(t)+"
"),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(){this.editor.setHTML(""),this.editor.append(this.textarea.get("value")),this.cleanEditorHTML(),this.editor.getHTML()===""&&this.editor.setHTML(this._getEmptyContent())},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,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 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;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://gi,replace:""},{regex:/<\\?\?xml[^>]*>/gi,replace:""},{regex:/<\/?\w+:[^>]*>/gi,replace:""},{regex:/\s*MSO[-:][^;"']*;?/gi,replace:""},{regex:/]*>( |\s)*<\/span>/gi,replace:""},{regex:/class="Mso[^"]*"/gi,replace:""},{regex:/)/gi,replace:""},{regex:/<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|\/?link|!\[)[^>]*?>/gi,replace:""},{regex:new RegExp(String.fromCharCode(8220),"gi"),replace:'"'},{regex:new RegExp(String.fromCharCode(8216),"gi"),replace:"'"},{regex:new RegExp(String.fromCharCode(8217),"gi"),replace:"'"},{regex:new RegExp(String.fromCharCode(8211),"gi"),replace:"-"},{regex:new RegExp(String.fromCharCode(8212),"gi"),replace:"--"},{regex:new RegExp(String.fromCharCode(189),"gi"),replace:"1/2"},{regex:new RegExp(String.fromCharCode(188),"gi"),replace:"1/4"},{regex:new RegExp(String.fromCharCode(190),"gi"),replace:"3/4"},{regex:new RegExp(String.fromCharCode(169),"gi"),replace:"(c)"},{regex:new RegExp(String.fromCharCode(174),"gi"),replace:"(r)"},{regex:new RegExp(String.fromCharCode(8230),"gi"),replace:"..."}],n=0;for(n=0;n-1;if(!r)if(n.indexOf("com.apple.webarchive")>-1||n.indexOf("com.apple.iWork.TSPNativeData")>-1)return this.fallbackPasteCleanupDelayed(),!0}if(r){var i;try{i=t.clipboardData.getData("text/html")}catch(s){return this.fallbackPasteCleanupDelayed(),!0}e.preventDefault(),i=this._cleanHTML(i);var o=window.rangy.saveSelection();return this.insertContentAtFocusPoint(i),window.rangy.restoreSelection(o),window.rangy.getSelection().collapseToEnd(),this.updateOriginal(),!1}return this.updateOriginalDelayed(),!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._cleanHTML(t)),this.updateOriginal(),window.rangy.restoreSelection(e),this},fallbackPasteCleanupDelayed:function(){return e.soon(e.bind(this.fallbackPasteCleanup,this)),this}},e.Base.mix(e.M.editor_atto.Editor,[v]),m.ATTRS={},m.prototype={toolbar:null,openMenus:null,setupToolbar:function(){return this.toolbar=e.Node.create(''),this.openMenus=[],this._wrapper.appendChild(this.toolbar),this.textareaLabel&&this.toolbar.setAttribute("aria-labelledby",this.textareaLabel.get("id")),this.setupToolbarNavigation(),this}},e.Base.mix(e.M.editor_atto.Editor,[m]),g.ATTRS={},g.prototype={_tabFocus:null,setupToolbarNavigation:function(){return this._wrapper.delegate("key",this.toolbarKeyboardNavigation,"down:37,39"
+,"."+r.TOOLBAR,this),this._wrapper.delegate("focus",function(e){this._setTabFocus(e.currentTarget)},"."+r.TOOLBAR+" button",this),this},toolbarKeyboardNavigation:function(e){e.preventDefault();var t=this.toolbar.all("button"),n=1,r,i=e.target.ancestor("button",!0);e.keyCode===37&&(n=-1),r=this._findFirstFocusable(t,i,n),r&&(r.focus(),this._setTabFocus(r))},_findFirstFocusable:function(e,t,n){var r=0,i,s,o,u;u=e.indexOf(t),u<-1&&(u=0);while(r=e.size()&&(u=0),s=e.item(u),r++;if(s.hasAttribute("hidden")||s.hasAttribute("disabled"))continue;i=s.ancestor(".atto_group");if(i.hasAttribute("hidden"))continue;o=s;break}return o},checkTabFocus:function(){if(this._tabFocus)if(this._tabFocus.hasAttribute("disabled")||this._tabFocus.hasAttribute("hidden")||this._tabFocus.ancestor(".atto_group").hasAttribute("hidden")){var e=this._findFirstFocusable(this.toolbar.all("button"),this._tabFocus,-1);e&&(this._tabFocus.compareTo(document.activeElement)&&e.focus(),this._setTabFocus(e))}return this},_setTabFocus:function(e){return this._tabFocus&&this._tabFocus.setAttribute("tabindex","-1"),this._tabFocus=e,this._tabFocus.setAttribute("tabindex",0),this.toolbar.setAttribute("aria-activedescendant",this._tabFocus.generateID()),this}},e.Base.mix(e.M.editor_atto.Editor,[g]),y.ATTRS={},y.prototype={_selections:null,_lastSelection:null,_focusFromClick:!1,setupSelectionWatchers:function(){return this.on("atto:selectionchanged",this.saveSelection,this),this.editor.on("focus",this.restoreSelection,this),this.editor.on("mousedown",function(){this._focusFromClick=!0},this),this.editor.on("blur",function(){this._focusFromClick=!1,this.updateOriginal()},this),this.editor.on(["keyup","focus"],function(t){e.soon(e.bind(this._hasSelectionChanged,this,t))},this),this.editor.on("gesturemoveend",function(t){e.soon(e.bind(this._hasSelectionChanged,this,t))},{standAlone:!0},this),this},isActive:function(){var e=i.createRange(),t=i.getSelection();return t.rangeCount?!document.activeElement||!this.editor.compareTo(document.activeElement)&&!this.editor.contains(document.activeElement)?!1:(e.selectNode(this.editor.getDOMNode()),e.intersectsRange(t.getRangeAt(0))):!1},getSelectionFromNode:function(e){var t=i.createRange();return t.selectNode(e.getDOMNode()),[t]},saveSelection:function(){this.isActive()&&(this._selections=this.getSelection())},restoreSelection:function(){this._focusFromClick||this._selections&&this.setSelection(this._selections),this._focusFromClick=!1},getSelection:function(){return i.getSelection().getAllRanges()},selectionContainsNode:function(e){return i.getSelection().containsNode(e.getDOMNode(),!0)},selectionFilterMatches:function(e,t,n){typeof n=="undefined"&&(n=!0),t||(t=this.getSelectedNodes());var r=t.size()>0,i=!1,s=this.editor,o=function(e){return e===s};return s.one(e)?(t.each(function(t){if(n){if(!r||!t.ancestor(e,!0,o))r=!1}else!i&&t.ancestor(e,!0,o)&&(i=!0)},this),n?r:i):!1},getSelectedNodes:function(){var t=new e.NodeList,n,r,s,o,u;r=i.getSelection(),r.rangeCount?s=r.getRangeAt(0):s=i.createRange(),s.collapsed&&s.commonAncestorContainer!==this.editor.getDOMNode()&&s.commonAncestorContainer!==e.config.doc&&(s=s.cloneRange(),s.selectNode(s.commonAncestorContainer)),n=s.getNodes();for(u=0;u"),i.get("childNodes").each(function(e){u.append(e.remove())}),i.append(u),o=u),t&&t!==""&&(f=e.Node.create("<"+t+">"+t+">"),f.setAttrs(o.getAttrs()),o.get("childNodes").each(function(e){e.remove(),f.append(e)}),o.replace(f),o=f),n&&o.setAttrs(n);var l=this.getSelectionFromNode(o);return this.setSelection(l),o}},e.Base.mix(e.M.editor_atto.Editor,[b]),w.ATTRS={filepickeroptions:{value:{}}},w.prototype={canShowFilepicker:function(e){return typeof this.get("filepickeroptions")[e]!="undefined"},showFilepicker:function(t,n,r){var i=this;e.use("core_filepicker",function(e){var s=e.clone(i.get("filepickeroptions")[t],!0);s.formcallback=n,r&&(s.magicscope=r),M.core_filepicker.show(e,s)})}},e.Base.mix
+(e.M.editor_atto.Editor,[w])},"@VERSION@",{requires:["node","transition","io","overlay","escape","event","event-simulate","event-custom","yui-throttle","moodle-core-notification-dialogue","moodle-core-notification-confirm","moodle-editor_atto-rangy","handlebars","timers"]});
diff --git a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
index f01c730276f..51da6969a10 100644
--- a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
+++ b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
@@ -296,7 +296,8 @@ Y.extend(Editor, Y.Base, {
* @chainable
*/
setupAutomaticPolling: function() {
- this._registerEventHandle(this.editor.on(['keyup', 'paste', 'cut'], this.updateOriginal, this));
+ this._registerEventHandle(this.editor.on(['keyup', 'cut'], this.updateOriginal, this));
+ this._registerEventHandle(this.editor.on('paste', this.pasteCleanup, this));
// Call this.updateOriginal after dropped content has been processed.
this._registerEventHandle(this.editor.on('drop', this.updateOriginalDelayed, this));
@@ -1119,10 +1120,12 @@ EditorClean.prototype = {
{regex: /]*>( |\s)*<\/span>/gi, replace: ""},
// Remove class="Msoblah"
{regex: /class="Mso[^"]*"/gi, replace: ""},
+ // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout.
+ {regex: /)/gi, replace: ""},
// Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"
- // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body.
- {regex: /<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|!\[)[^>]*?>/gi, replace: ""},
+ // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link.
+ {regex: /<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|\/?link|!\[)[^>]*?>/gi, replace: ""},
// Source: "http://www.tim-jarrett.com/labs_javascript_scrub_word.php"
// Replace extended chars with simple text.
@@ -1145,6 +1148,130 @@ EditorClean.prototype = {
}
return content;
+ },
+
+ /**
+ * Intercept and clean html paste events.
+ *
+ * @method pasteCleanup
+ * @param {Object} sourceEvent The YUI EventFacade object
+ * @return {Boolean} True if the passed event should continue, false if not.
+ */
+ pasteCleanup: function(sourceEvent) {
+ // We only expect paste events, but we will check anyways.
+ if (sourceEvent.type === 'paste') {
+ // The YUI event wrapper doesn't provide paste event info, so we need the underlying event.
+ var event = sourceEvent._event;
+ // Check if we have a valid clipboardData object in the event.
+ // IE has a clipboard object at window.clipboardData, but as of IE 11, it does not provide HTML content access.
+ if (event && event.clipboardData && event.clipboardData.getData) {
+ // Check if there is HTML type to be pasted, this is all we care about.
+ var types = event.clipboardData.types;
+ var isHTML = false;
+ // Different browsers use different things to hold the types, so test various functions.
+ if (!types) {
+ isHTML = false;
+ } else if (typeof types.contains === 'function') {
+ isHTML = types.contains('text/html');
+ } else if (typeof types.indexOf === 'function') {
+ isHTML = (types.indexOf('text/html') > -1);
+ if (!isHTML) {
+ if ((types.indexOf('com.apple.webarchive') > -1) || (types.indexOf('com.apple.iWork.TSPNativeData') > -1)) {
+ // This is going to be a specialized Apple paste paste. We cannot capture this, so clean everything.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+ }
+ } else {
+ // We don't know how to handle the clipboard info, so wait for the clipboard event to finish then fallback.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+
+ if (isHTML) {
+ // Get the clipboard content.
+ var content;
+ try {
+ content = event.clipboardData.getData('text/html');
+ } catch (error) {
+ // Something went wrong. Fallback.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+
+ // Stop the original paste.
+ sourceEvent.preventDefault();
+
+ // Scrub the paste content.
+ content = this._cleanHTML(content);
+
+ // Save the current selection.
+ // Using saveSelection as it produces a more consistent experience.
+ var selection = window.rangy.saveSelection();
+
+ // Insert the content.
+ this.insertContentAtFocusPoint(content);
+
+ // Restore the selection, and collapse to end.
+ window.rangy.restoreSelection(selection);
+ window.rangy.getSelection().collapseToEnd();
+
+ // Update the text area.
+ this.updateOriginal();
+ return false;
+ } else {
+ // This is a non-html paste event, we can just let this continue on and call updateOriginalDelayed.
+ this.updateOriginalDelayed();
+ return true;
+ }
+ } else {
+ // If we reached a here, this probably means the browser has limited (or no) clipboard support.
+ // Wait for the clipboard event to finish then fallback.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+ }
+
+ // We should never get here - we must have received a non-paste event for some reason.
+ // Um, just call updateOriginalDelayed() - it's safe.
+ this.updateOriginalDelayed();
+ return true;
+ },
+
+ /**
+ * Cleanup code after a paste event if we couldn't intercept the paste content.
+ *
+ * @method fallbackPasteCleanup
+ * @chainable
+ */
+ fallbackPasteCleanup: function() {
+
+ // Save the current selection (cursor position).
+ var selection = window.rangy.saveSelection();
+
+ // Get, clean, and replace the content in the editable.
+ var content = this.editor.get('innerHTML');
+ this.editor.set('innerHTML', this._cleanHTML(content));
+
+ // Update the textarea.
+ this.updateOriginal();
+
+ // Restore the selection (cursor position).
+ window.rangy.restoreSelection(selection);
+
+ return this;
+ },
+
+ /**
+ * Calls fallbackPasteCleanup on a short timer to allow the paste event handlers to complete.
+ *
+ * @method fallbackPasteCleanupDelayed
+ * @chainable
+ */
+ fallbackPasteCleanupDelayed: function() {
+ Y.soon(Y.bind(this.fallbackPasteCleanup, this));
+
+ return this;
}
};
diff --git a/lib/editor/atto/yui/src/editor/js/clean.js b/lib/editor/atto/yui/src/editor/js/clean.js
index 89ccac7447c..e801fe12d2a 100644
--- a/lib/editor/atto/yui/src/editor/js/clean.js
+++ b/lib/editor/atto/yui/src/editor/js/clean.js
@@ -105,10 +105,12 @@ EditorClean.prototype = {
{regex: /]*>( |\s)*<\/span>/gi, replace: ""},
// Remove class="Msoblah"
{regex: /class="Mso[^"]*"/gi, replace: ""},
+ // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout.
+ {regex: /)/gi, replace: ""},
// Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"
- // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body.
- {regex: /<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|!\[)[^>]*?>/gi, replace: ""},
+ // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link.
+ {regex: /<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|\/?link|!\[)[^>]*?>/gi, replace: ""},
// Source: "http://www.tim-jarrett.com/labs_javascript_scrub_word.php"
// Replace extended chars with simple text.
@@ -131,6 +133,131 @@ EditorClean.prototype = {
}
return content;
+ },
+
+ /**
+ * Intercept and clean html paste events.
+ *
+ * @method pasteCleanup
+ * @param {Object} sourceEvent The YUI EventFacade object
+ * @return {Boolean} True if the passed event should continue, false if not.
+ */
+ pasteCleanup: function(sourceEvent) {
+ // We only expect paste events, but we will check anyways.
+ if (sourceEvent.type === 'paste') {
+ // The YUI event wrapper doesn't provide paste event info, so we need the underlying event.
+ var event = sourceEvent._event;
+ // Check if we have a valid clipboardData object in the event.
+ // IE has a clipboard object at window.clipboardData, but as of IE 11, it does not provide HTML content access.
+ if (event && event.clipboardData && event.clipboardData.getData) {
+ // Check if there is HTML type to be pasted, this is all we care about.
+ var types = event.clipboardData.types;
+ var isHTML = false;
+ // Different browsers use different things to hold the types, so test various functions.
+ if (!types) {
+ isHTML = false;
+ } else if (typeof types.contains === 'function') {
+ isHTML = types.contains('text/html');
+ } else if (typeof types.indexOf === 'function') {
+ isHTML = (types.indexOf('text/html') > -1);
+ if (!isHTML) {
+ if ((types.indexOf('com.apple.webarchive') > -1) || (types.indexOf('com.apple.iWork.TSPNativeData') > -1)) {
+ // This is going to be a specialized Apple paste paste. We cannot capture this, so clean everything.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+ }
+ } else {
+ // We don't know how to handle the clipboard info, so wait for the clipboard event to finish then fallback.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+
+ if (isHTML) {
+ // Get the clipboard content.
+ var content;
+ try {
+ content = event.clipboardData.getData('text/html');
+ } catch (error) {
+ // Something went wrong. Fallback.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+
+ // Stop the original paste.
+ sourceEvent.preventDefault();
+
+ // Scrub the paste content.
+ content = this._cleanHTML(content);
+
+ // Save the current selection.
+ // Using saveSelection as it produces a more consistent experience.
+ var selection = window.rangy.saveSelection();
+
+ // Insert the content.
+ this.insertContentAtFocusPoint(content);
+
+ // Restore the selection, and collapse to end.
+ window.rangy.restoreSelection(selection);
+ window.rangy.getSelection().collapseToEnd();
+
+ // Update the text area.
+ this.updateOriginal();
+ return false;
+ } else {
+ // This is a non-html paste event, we can just let this continue on and call updateOriginalDelayed.
+ this.updateOriginalDelayed();
+ return true;
+ }
+ } else {
+ // If we reached a here, this probably means the browser has limited (or no) clipboard support.
+ // Wait for the clipboard event to finish then fallback.
+ this.fallbackPasteCleanupDelayed();
+ return true;
+ }
+ }
+
+ // We should never get here - we must have received a non-paste event for some reason.
+ // Um, just call updateOriginalDelayed() - it's safe.
+ this.updateOriginalDelayed();
+ return true;
+ },
+
+ /**
+ * Cleanup code after a paste event if we couldn't intercept the paste content.
+ *
+ * @method fallbackPasteCleanup
+ * @chainable
+ */
+ fallbackPasteCleanup: function() {
+ Y.log('Using fallbackPasteCleanup for atto cleanup', 'debug', LOGNAME);
+
+ // Save the current selection (cursor position).
+ var selection = window.rangy.saveSelection();
+
+ // Get, clean, and replace the content in the editable.
+ var content = this.editor.get('innerHTML');
+ this.editor.set('innerHTML', this._cleanHTML(content));
+
+ // Update the textarea.
+ this.updateOriginal();
+
+ // Restore the selection (cursor position).
+ window.rangy.restoreSelection(selection);
+
+ return this;
+ },
+
+ /**
+ * Calls fallbackPasteCleanup on a short timer to allow the paste event handlers to complete.
+ *
+ * @method fallbackPasteCleanupDelayed
+ * @chainable
+ */
+ fallbackPasteCleanupDelayed: function() {
+ Y.soon(Y.bind(this.fallbackPasteCleanup, this));
+
+ return this;
}
};
diff --git a/lib/editor/atto/yui/src/editor/js/editor.js b/lib/editor/atto/yui/src/editor/js/editor.js
index 93e4ede4a32..8089a1185af 100644
--- a/lib/editor/atto/yui/src/editor/js/editor.js
+++ b/lib/editor/atto/yui/src/editor/js/editor.js
@@ -296,7 +296,8 @@ Y.extend(Editor, Y.Base, {
* @chainable
*/
setupAutomaticPolling: function() {
- this._registerEventHandle(this.editor.on(['keyup', 'paste', 'cut'], this.updateOriginal, this));
+ this._registerEventHandle(this.editor.on(['keyup', 'cut'], this.updateOriginal, this));
+ this._registerEventHandle(this.editor.on('paste', this.pasteCleanup, this));
// Call this.updateOriginal after dropped content has been processed.
this._registerEventHandle(this.editor.on('drop', this.updateOriginalDelayed, this));