From 35fcaac78b30d72a717ef90ea3e4895d9963e8ee Mon Sep 17 00:00:00 2001 From: Jake Dallimore Date: Thu, 9 Feb 2017 09:51:47 +0800 Subject: [PATCH] MDL-56364 javascript: Prevent bad prototype chains in core dialogue The DIALOGUE object was using Y.clone to copy the input config, messing up the prototype chains for any objects in the config param. Really, this doesn't need to clone the config, so I've refactored to avoid doing so. Also moved some of the other attribute init and setup calls to the appropriate places (initializer and modifyAttrs). --- ...moodle-core-notification-dialogue-debug.js | 117 +++++++++++++----- .../moodle-core-notification-dialogue-min.js | 4 +- .../moodle-core-notification-dialogue.js | 117 +++++++++++++----- lib/yui/src/notification/js/dialogue.js | 117 +++++++++++++----- 4 files changed, 260 insertions(+), 95 deletions(-) diff --git a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js index 58d935976fb..d08322d6cbc 100644 --- a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js +++ b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js @@ -55,10 +55,14 @@ var DIALOGUE_NAME = 'Moodle dialogue', * @class M.core.dialogue * @extends Panel */ -DIALOGUE = function(c) { - var config = Y.clone(c); - config.COUNT = Y.stamp(this); - var id = 'moodle-dialogue-' + config.COUNT; +DIALOGUE = function(config) { + // The code below is a hack to add the custom content node to the DOM, on the fly, per-instantiation and to assign the value + // of 'srcNode' to this newly created node. Normally (see docs: https://yuilibrary.com/yui/docs/widget/widget-extend.html), + // this node would be pre-existing in the DOM, and an id string would simply be passed in as a property of the config object + // during widget instantiation, however, because we're creating it on the fly (and 'config.srcNode' isn't set yet), care must + // be taken to add it to the DOM and to properly set the value of 'config.srcNode' before calling the parent constructor. + // Note: additional classes can be added to this content node by setting the 'additionalBaseClass' config property (a string). + var id = 'moodle-dialogue-' + Y.stamp(this); // Can't use this.get('id') as it's not set at this stage. config.notificationBase = Y.Node.create('
') .append(Y.Node.create('')) .append(Y.Node.create('
'))); Y.one(document.body).append(config.notificationBase); - - if (config.additionalBaseClass) { - config.notificationBase.addClass(config.additionalBaseClass); - } - config.srcNode = '#' + id; - - // closeButton param to keep the stable versions API. - if (config.closeButton === false) { - config.buttons = null; - } else { - config.buttons = [ - { - section: Y.WidgetStdMod.HEADER, - classNames: 'closebutton', - action: function() { - this.hide(); - } - } - ]; - } DIALOGUE.superclass.constructor.apply(this, [config]); - - if (config.closeButton !== false) { - // The buttons constructor does not allow custom attributes - this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle')); - } }; Y.extend(DIALOGUE, Y.Panel, { // Window resize event listener. @@ -130,6 +109,11 @@ Y.extend(DIALOGUE, Y.Panel, { initializer: function() { var bb; + if (this.get('closeButton') !== false) { + // The buttons constructor does not allow custom attributes + this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle')); + } + // Initialise the element cache. this._hiddenSiblings = []; @@ -181,6 +165,13 @@ Y.extend(DIALOGUE, Y.Panel, { } }, this); + // Add any additional classes to the content node if required. + var nBase = this.get('notificationBase'); + var additionalClasses = this.get('additionalBaseClass'); + if (additionalClasses !== '') { + nBase.addClass(additionalClasses); + } + // Remove the dialogue from the DOM when it is destroyed. this.after('destroyedChange', function() { this.get(BASE).remove(true); @@ -536,6 +527,23 @@ Y.extend(DIALOGUE, Y.Panel, { NAME: DIALOGUE_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { + /** + * Any additional classes to add to the base Node. + * + * @attribute additionalBaseClass + * @type String + * @default '' + */ + additionalBaseClass: { + value: '' + }, + + /** + * The Notification base Node. + * + * @attribute notificationBase + * @type Node + */ notificationBase: { }, @@ -616,9 +624,13 @@ Y.extend(DIALOGUE, Y.Panel, { * @attribute COUNT * @type String * @default null + * @writeonce */ COUNT: { - value: null + writeOnce: true, + valueFn: function() { + return Y.stamp(this); + } }, /** @@ -656,7 +668,6 @@ Y.extend(DIALOGUE, Y.Panel, { focusOnShowSelector: { value: null } - } }); @@ -741,6 +752,50 @@ Y.Base.modifyAttrs(DIALOGUE, { */ extraClasses: { value: [] + }, + + /** + * Identifier for the widget. + * + * @attribute id + * @type String + * @default a product of guid(). + * @writeOnce + */ + id: { + writeOnce: true, + valueFn: function() { + var id = 'moodle-dialogue-' + Y.stamp(this); + return id; + } + }, + + /** + * Collection containing the widget's buttons. + * + * @attribute buttons + * @type Object + * @default {} + */ + buttons: { + // Readonly is really important. We don't want to allow users of the plugin to pass in buttons. closeButton handles this. + readOnly: true, + getter: Y.WidgetButtons.prototype._getButtons, + valueFn: function() { + if (this.get('closeButton') === false) { + return null; + } else { + return [ + { + section: Y.WidgetStdMod.HEADER, + classNames: 'closebutton', + action: function() { + this.hide(); + } + } + ]; + } + } } }); diff --git a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js index bdbf8f36a79..2a3ba0e9022 100644 --- a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js +++ b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js @@ -1,2 +1,2 @@ -YUI.add("moodle-core-notification-dialogue",function(e,t){var n,r,i,s,o,u,a;n="moodle-dialogue",r="notificationBase",i="yesLabel",s="noLabel",o="title",u="question",a={BASE:"moodle-dialogue-base",WRAP:"moodle-dialogue-wrap",HEADER:"moodle-dialogue-hd",BODY:"moodle-dialogue-bd",CONTENT:"moodle-dialogue-content",FOOTER:"moodle-dialogue-ft",HIDDEN:"hidden",LIGHTBOX:"moodle-dialogue-lightbox"},M.core=M.core||{};var f="Moodle dialogue",l,c=n+"-fullscreen",h=n+"-hidden",p=" [role=dialog]",d="[role=menubar]",v=".",m="moodle-has-zindex",g='input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]';l=function(t){var n=e.clone(t);n.COUNT=e.stamp(this);var r="moodle-dialogue-"+n.COUNT;n.notificationBase=e.Node.create('
').append(e.Node.create('').append(e.Node.create('
')).append(e.Node.create('
')).append(e.Node.create('
'))),e.one(document.body).append(n.notificationBase),n.additionalBaseClass&&n.notificationBase.addClass(n.additionalBaseClass),n.srcNode="#"+r,n.closeButton===!1?n.buttons=null:n.buttons=[{section:e.WidgetStdMod.HEADER,classNames:"closebutton",action:function(){this.hide()}}],l.superclass.constructor.apply(this,[n]),n.closeButton!==!1&&this.get("buttons").header[0].setAttribute("title",this.get("closeButtonTitle"))},e.extend(l,e.Panel,{_resizeevent:null,_orientationevent:null,_calculatedzindex:!1,_originalPosition:null,_hiddenSiblings:null,initializer:function(){var t;this._hiddenSiblings=[],this.get("render")&&this.render(),this.after("visibleChange",this.visibilityChanged,this),this.get("center")&&this.centerDialogue(),this.get("modal")&&(this.get(r).set("aria-hidden","true"),this.plug(e.M.core.LockScroll)),t=this.get("boundingBox"),t.addClass(m),e.Array.each(this.get("extraClasses"),t.addClass,t),this.get("visible")&&this.applyZIndex(),this.on("maskShow",this.applyZIndex),this.on("maskShow",function(){var t=e.one(e.config.win),n=this.get("boundingBox");this.get("center")||(this._originalPosition=n.getXY()),n.getStyle("position")!=="fixed"&&n.setStyles({top:t.get("scrollTop"),left:t.get("scrollLeft")})},this),this.after("destroyedChange",function(){this.get(r).remove(!0)},this)},applyZIndex:function(){var t=1,n=1,r=this.get("boundingBox"),i=this.get("maskNode"),s=this.get("zIndex");s!==0&&!this._calculatedzindex?r.setStyle("zIndex",s):(e.all(p+", "+d+", "+v+m).each(function(e){var n=this.findZIndex(e);n>t&&(t=n)},this),n=(t+1).toString(),r.setStyle("zIndex",n),this.set("zIndex",n),this.get("modal")&&(i.setStyle("zIndex",n),e.UA.ie&&e.UA.compareVersions(e.UA.ie,9)<0&&setTimeout(function(){i.setStyle("position","static"),setTimeout(function(){i.setStyle("position","fixed")},0)},0)),this._calculatedzindex=!0)},findZIndex:function(e){var t=e.getStyle("zIndex")||e.ancestor().getStyle("zIndex");return t?parseInt(t,10):0},visibilityChanged:function(t){var n,r;t.attrName==="visible"&&(this.get("maskNode").addClass(a.LIGHTBOX),t.prevVal&&!t.newVal&&(r=this.get("boundingBox"),this._resizeevent&&(this._resizeevent.detach(),this._resizeevent=null),this._orientationevent&&(this._orientationevent.detach(),this._orientationevent=null),r.detach("key",this.keyDelegation),this.get("modal")&&this.setAccessibilityHidden()),!t.prevVal&&t.newVal&&(this.applyZIndex(),this.makeResponsive(),this.shouldResizeFullscreen()||this.get("draggable")&&(n="#"+this.get("id")+" ."+a.HEADER,this.plug(e.Plugin.Drag,{handles:[n]}),e.one(n).setStyle("cursor","move")),this.keyDelegation(),this.get("modal")&&this.setAccessibilityVisible()),this.get("center")&&!t.prevVal&&t.newVal&&this.centerDialogue())},makeResponsive:function(){var e=this.get("boundingBox");this.shouldResizeFullscreen()?(e.addClass(c),e.setStyles({left:null,top:null,width:null,height:null,right:null,bottom:null})):this.get("responsive")&&e.removeClass(c).setStyles({width:this.get("width"),height:this.get("height")}),this.lockScroll&&this.lockScroll.updateScrollLock(this.shouldResizeFullscreen())},centerDialogue:function(){var t=this.get("boundingBox"),n=t.hasClass(h),r,i;if(this.shouldResizeFullscreen())return;n&&t.setStyle("top","-1000px").removeClass(h),r=Math.max(Math.round((t.get("winWidth")-t.get("offsetWidth"))/2),15),i=Math.max(Math.round((t.get("winHeight")-t.get("offsetHeight"))/2),15)+e.one(window).get("scrollTop"),t.setStyles({left:r,top:i}),n&&t.addClass(h),this.makeResponsive()},shouldResizeFullscreen:function(){return window===window.parent&&this.get("responsive")&&Math.floor(e.one(document.body).get("winWidth"))').append(e.Node.create('').append(e.Node.create('
')).append(e.Node.create('
')).append(e.Node.create('
'))),e.one(document.body).append(t.notificationBase),t.srcNode="#"+n,l.superclass.constructor.apply(this,[t])},e.extend(l,e.Panel,{_resizeevent:null,_orientationevent:null,_calculatedzindex:!1,_originalPosition:null,_hiddenSiblings:null,initializer:function(){var t;this.get("closeButton")!==!1&&this.get("buttons").header[0].setAttribute("title",this.get("closeButtonTitle")),this._hiddenSiblings=[],this.get("render")&&this.render(),this.after("visibleChange",this.visibilityChanged,this),this.get("center")&&this.centerDialogue(),this.get("modal")&&(this.get(r).set("aria-hidden","true"),this.plug(e.M.core.LockScroll)),t=this.get("boundingBox"),t.addClass(m),e.Array.each(this.get("extraClasses"),t.addClass,t),this.get("visible")&&this.applyZIndex(),this.on("maskShow",this.applyZIndex),this.on("maskShow",function(){var t=e.one(e.config.win),n=this.get("boundingBox");this.get("center")||(this._originalPosition=n.getXY()),n.getStyle("position")!=="fixed"&&n.setStyles({top:t.get("scrollTop"),left:t.get("scrollLeft")})},this);var n=this.get("notificationBase"),i=this.get("additionalBaseClass");i!==""&&n.addClass(i),this.after("destroyedChange",function(){this.get(r).remove(!0)},this)},applyZIndex:function(){var t=1,n=1,r=this.get("boundingBox"),i=this.get("maskNode"),s=this.get("zIndex");s!==0&&!this._calculatedzindex?r.setStyle("zIndex",s):(e.all(p+", "+d+", "+v+m).each(function(e){var n=this.findZIndex(e);n>t&&(t=n)},this),n=(t+1).toString(),r.setStyle("zIndex",n),this.set("zIndex",n),this.get("modal")&&(i.setStyle("zIndex",n),e.UA.ie&&e.UA.compareVersions(e.UA.ie,9)<0&&setTimeout(function(){i.setStyle("position","static"),setTimeout(function(){i.setStyle("position","fixed")},0)},0)),this._calculatedzindex=!0)},findZIndex:function(e){var t=e.getStyle("zIndex")||e.ancestor().getStyle("zIndex");return t?parseInt(t,10):0},visibilityChanged:function(t){var n,r;t.attrName==="visible"&&(this.get("maskNode").addClass(a.LIGHTBOX),t.prevVal&&!t.newVal&&(r=this.get("boundingBox"),this._resizeevent&&(this._resizeevent.detach(),this._resizeevent=null),this._orientationevent&&(this._orientationevent.detach(),this._orientationevent=null),r.detach("key",this.keyDelegation),this.get("modal")&&this.setAccessibilityHidden()),!t.prevVal&&t.newVal&&(this.applyZIndex(),this.makeResponsive(),this.shouldResizeFullscreen()||this.get("draggable")&&(n="#"+this.get("id")+" ."+a.HEADER,this.plug(e.Plugin.Drag,{handles:[n]}),e.one(n).setStyle("cursor","move")),this.keyDelegation(),this.get("modal")&&this.setAccessibilityVisible()),this.get("center")&&!t.prevVal&&t.newVal&&this.centerDialogue())},makeResponsive:function(){var e=this.get("boundingBox");this.shouldResizeFullscreen()?(e.addClass(c),e.setStyles({left:null,top:null,width:null,height:null,right:null,bottom:null})):this.get("responsive")&&e.removeClass(c).setStyles({width:this.get("width"),height:this.get("height")}),this.lockScroll&&this.lockScroll.updateScrollLock(this.shouldResizeFullscreen())},centerDialogue:function(){var t=this.get("boundingBox"),n=t.hasClass(h),r,i;if(this.shouldResizeFullscreen())return;n&&t.setStyle("top","-1000px").removeClass(h),r=Math.max(Math.round((t.get("winWidth")-t.get("offsetWidth"))/2),15),i=Math.max(Math.round((t.get("winHeight")-t.get("offsetHeight"))/2),15)+e.one(window).get("scrollTop"),t.setStyles({left:r,top:i}),n&&t.addClass(h),this.makeResponsive()},shouldResizeFullscreen:function(){return window===window.parent&&this.get("responsive")&&Math.floor(e.one(document.body).get("winWidth"))') .append(Y.Node.create('')) .append(Y.Node.create('
'))); Y.one(document.body).append(config.notificationBase); - - if (config.additionalBaseClass) { - config.notificationBase.addClass(config.additionalBaseClass); - } - config.srcNode = '#' + id; - - // closeButton param to keep the stable versions API. - if (config.closeButton === false) { - config.buttons = null; - } else { - config.buttons = [ - { - section: Y.WidgetStdMod.HEADER, - classNames: 'closebutton', - action: function() { - this.hide(); - } - } - ]; - } DIALOGUE.superclass.constructor.apply(this, [config]); - - if (config.closeButton !== false) { - // The buttons constructor does not allow custom attributes - this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle')); - } }; Y.extend(DIALOGUE, Y.Panel, { // Window resize event listener. @@ -130,6 +109,11 @@ Y.extend(DIALOGUE, Y.Panel, { initializer: function() { var bb; + if (this.get('closeButton') !== false) { + // The buttons constructor does not allow custom attributes + this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle')); + } + // Initialise the element cache. this._hiddenSiblings = []; @@ -181,6 +165,13 @@ Y.extend(DIALOGUE, Y.Panel, { } }, this); + // Add any additional classes to the content node if required. + var nBase = this.get('notificationBase'); + var additionalClasses = this.get('additionalBaseClass'); + if (additionalClasses !== '') { + nBase.addClass(additionalClasses); + } + // Remove the dialogue from the DOM when it is destroyed. this.after('destroyedChange', function() { this.get(BASE).remove(true); @@ -536,6 +527,23 @@ Y.extend(DIALOGUE, Y.Panel, { NAME: DIALOGUE_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { + /** + * Any additional classes to add to the base Node. + * + * @attribute additionalBaseClass + * @type String + * @default '' + */ + additionalBaseClass: { + value: '' + }, + + /** + * The Notification base Node. + * + * @attribute notificationBase + * @type Node + */ notificationBase: { }, @@ -613,9 +621,13 @@ Y.extend(DIALOGUE, Y.Panel, { * @attribute COUNT * @type String * @default null + * @writeonce */ COUNT: { - value: null + writeOnce: true, + valueFn: function() { + return Y.stamp(this); + } }, /** @@ -653,7 +665,6 @@ Y.extend(DIALOGUE, Y.Panel, { focusOnShowSelector: { value: null } - } }); @@ -738,6 +749,50 @@ Y.Base.modifyAttrs(DIALOGUE, { */ extraClasses: { value: [] + }, + + /** + * Identifier for the widget. + * + * @attribute id + * @type String + * @default a product of guid(). + * @writeOnce + */ + id: { + writeOnce: true, + valueFn: function() { + var id = 'moodle-dialogue-' + Y.stamp(this); + return id; + } + }, + + /** + * Collection containing the widget's buttons. + * + * @attribute buttons + * @type Object + * @default {} + */ + buttons: { + // Readonly is really important. We don't want to allow users of the plugin to pass in buttons. closeButton handles this. + readOnly: true, + getter: Y.WidgetButtons.prototype._getButtons, + valueFn: function() { + if (this.get('closeButton') === false) { + return null; + } else { + return [ + { + section: Y.WidgetStdMod.HEADER, + classNames: 'closebutton', + action: function() { + this.hide(); + } + } + ]; + } + } } }); diff --git a/lib/yui/src/notification/js/dialogue.js b/lib/yui/src/notification/js/dialogue.js index a8dda1aea04..b940c11f456 100644 --- a/lib/yui/src/notification/js/dialogue.js +++ b/lib/yui/src/notification/js/dialogue.js @@ -25,10 +25,14 @@ var DIALOGUE_NAME = 'Moodle dialogue', * @class M.core.dialogue * @extends Panel */ -DIALOGUE = function(c) { - var config = Y.clone(c); - config.COUNT = Y.stamp(this); - var id = 'moodle-dialogue-' + config.COUNT; +DIALOGUE = function(config) { + // The code below is a hack to add the custom content node to the DOM, on the fly, per-instantiation and to assign the value + // of 'srcNode' to this newly created node. Normally (see docs: https://yuilibrary.com/yui/docs/widget/widget-extend.html), + // this node would be pre-existing in the DOM, and an id string would simply be passed in as a property of the config object + // during widget instantiation, however, because we're creating it on the fly (and 'config.srcNode' isn't set yet), care must + // be taken to add it to the DOM and to properly set the value of 'config.srcNode' before calling the parent constructor. + // Note: additional classes can be added to this content node by setting the 'additionalBaseClass' config property (a string). + var id = 'moodle-dialogue-' + Y.stamp(this); // Can't use this.get('id') as it's not set at this stage. config.notificationBase = Y.Node.create('
') .append(Y.Node.create('')) .append(Y.Node.create('
'))); Y.one(document.body).append(config.notificationBase); - - if (config.additionalBaseClass) { - config.notificationBase.addClass(config.additionalBaseClass); - } - config.srcNode = '#' + id; - - // closeButton param to keep the stable versions API. - if (config.closeButton === false) { - config.buttons = null; - } else { - config.buttons = [ - { - section: Y.WidgetStdMod.HEADER, - classNames: 'closebutton', - action: function() { - this.hide(); - } - } - ]; - } DIALOGUE.superclass.constructor.apply(this, [config]); - - if (config.closeButton !== false) { - // The buttons constructor does not allow custom attributes - this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle')); - } }; Y.extend(DIALOGUE, Y.Panel, { // Window resize event listener. @@ -100,6 +79,11 @@ Y.extend(DIALOGUE, Y.Panel, { initializer: function() { var bb; + if (this.get('closeButton') !== false) { + // The buttons constructor does not allow custom attributes + this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle')); + } + // Initialise the element cache. this._hiddenSiblings = []; @@ -151,6 +135,13 @@ Y.extend(DIALOGUE, Y.Panel, { } }, this); + // Add any additional classes to the content node if required. + var nBase = this.get('notificationBase'); + var additionalClasses = this.get('additionalBaseClass'); + if (additionalClasses !== '') { + nBase.addClass(additionalClasses); + } + // Remove the dialogue from the DOM when it is destroyed. this.after('destroyedChange', function() { this.get(BASE).remove(true); @@ -506,6 +497,23 @@ Y.extend(DIALOGUE, Y.Panel, { NAME: DIALOGUE_NAME, CSS_PREFIX: DIALOGUE_PREFIX, ATTRS: { + /** + * Any additional classes to add to the base Node. + * + * @attribute additionalBaseClass + * @type String + * @default '' + */ + additionalBaseClass: { + value: '' + }, + + /** + * The Notification base Node. + * + * @attribute notificationBase + * @type Node + */ notificationBase: { }, @@ -586,9 +594,13 @@ Y.extend(DIALOGUE, Y.Panel, { * @attribute COUNT * @type String * @default null + * @writeonce */ COUNT: { - value: null + writeOnce: true, + valueFn: function() { + return Y.stamp(this); + } }, /** @@ -626,7 +638,6 @@ Y.extend(DIALOGUE, Y.Panel, { focusOnShowSelector: { value: null } - } }); @@ -711,6 +722,50 @@ Y.Base.modifyAttrs(DIALOGUE, { */ extraClasses: { value: [] + }, + + /** + * Identifier for the widget. + * + * @attribute id + * @type String + * @default a product of guid(). + * @writeOnce + */ + id: { + writeOnce: true, + valueFn: function() { + var id = 'moodle-dialogue-' + Y.stamp(this); + return id; + } + }, + + /** + * Collection containing the widget's buttons. + * + * @attribute buttons + * @type Object + * @default {} + */ + buttons: { + // Readonly is really important. We don't want to allow users of the plugin to pass in buttons. closeButton handles this. + readOnly: true, + getter: Y.WidgetButtons.prototype._getButtons, + valueFn: function() { + if (this.get('closeButton') === false) { + return null; + } else { + return [ + { + section: Y.WidgetStdMod.HEADER, + classNames: 'closebutton', + action: function() { + this.hide(); + } + } + ]; + } + } } });