diff --git a/changelog.txt b/changelog.txt index 2a17763e..adc07202 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +Version 4.9.10 (2020-04-23) + Fixed an issue where the editor selection could end up inside a short ended element (eg br) #TINY-3999 + Fixed a security issue related to CDATA sanitization during parsing #TINY-4669 + Fixed `media` embed content not processing safely in some cases #TINY-4857 Version 4.9.9 (2020-03-25) Fixed the table selection not functioning correctly in Microsoft Edge 44 or higher #TINY-3862 Fixed the table resize handles not functioning correctly in Microsoft Edge 44 or higher #TINY-4160 diff --git a/composer.json b/composer.json index de631426..d1d2cc79 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "tinymce/tinymce", - "version": "4.9.9", + "version": "4.9.10", "description": "Web based JavaScript HTML WYSIWYG editor control.", "license": [ "LGPL-2.1-only" diff --git a/package.json b/package.json index 2f8bdb60..056dd5ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tinymce", - "version": "4.9.9", + "version": "4.9.10", "repository": { "type": "git", "url": "https://github.com/tinymce/tinymce-dist.git" diff --git a/plugins/media/plugin.js b/plugins/media/plugin.js index c75d4454..dffa0fe7 100644 --- a/plugins/media/plugin.js +++ b/plugins/media/plugin.js @@ -47,9 +47,154 @@ var media = (function () { hasDimensions: hasDimensions }; - var global$3 = tinymce.util.Tools.resolve('tinymce.html.SaxParser'); + var Cell = function (initial) { + var value = initial; + var get = function () { + return value; + }; + var set = function (v) { + value = v; + }; + var clone = function () { + return Cell(get()); + }; + return { + get: get, + set: set, + clone: clone + }; + }; - var global$4 = tinymce.util.Tools.resolve('tinymce.dom.DOMUtils'); + var noop = function () { + }; + var constant = function (value) { + return function () { + return value; + }; + }; + var never = constant(false); + var always = constant(true); + + var none = function () { + return NONE; + }; + var NONE = function () { + var eq = function (o) { + return o.isNone(); + }; + var call = function (thunk) { + return thunk(); + }; + var id = function (n) { + return n; + }; + var me = { + fold: function (n, s) { + return n(); + }, + is: never, + isSome: never, + isNone: always, + getOr: id, + getOrThunk: call, + getOrDie: function (msg) { + throw new Error(msg || 'error: getOrDie called on none.'); + }, + getOrNull: constant(null), + getOrUndefined: constant(undefined), + or: id, + orThunk: call, + map: none, + each: noop, + bind: none, + exists: never, + forall: always, + filter: none, + equals: eq, + equals_: eq, + toArray: function () { + return []; + }, + toString: constant('none()') + }; + if (Object.freeze) { + Object.freeze(me); + } + return me; + }(); + var some = function (a) { + var constant_a = constant(a); + var self = function () { + return me; + }; + var bind = function (f) { + return f(a); + }; + var me = { + fold: function (n, s) { + return s(a); + }, + is: function (v) { + return a === v; + }, + isSome: always, + isNone: never, + getOr: constant_a, + getOrThunk: constant_a, + getOrDie: constant_a, + getOrNull: constant_a, + getOrUndefined: constant_a, + or: self, + orThunk: self, + map: function (f) { + return some(f(a)); + }, + each: function (f) { + f(a); + }, + bind: bind, + exists: bind, + forall: bind, + filter: function (f) { + return f(a) ? me : NONE; + }, + toArray: function () { + return [a]; + }, + toString: function () { + return 'some(' + a + ')'; + }, + equals: function (o) { + return o.is(a); + }, + equals_: function (o, elementEq) { + return o.fold(never, function (b) { + return elementEq(a, b); + }); + } + }; + return me; + }; + var from = function (value) { + return value === null || value === undefined ? NONE : some(value); + }; + var Option = { + some: some, + none: none, + from: from + }; + + var hasOwnProperty = Object.hasOwnProperty; + var get = function (obj, key) { + return has(obj, key) ? Option.from(obj[key]) : Option.none(); + }; + var has = function (obj, key) { + return hasOwnProperty.call(obj, key); + }; + + var global$3 = tinymce.util.Tools.resolve('tinymce.dom.DOMUtils'); + + var global$4 = tinymce.util.Tools.resolve('tinymce.html.SaxParser'); var getVideoScriptMatch = function (prefixes, src) { if (prefixes) { @@ -62,76 +207,65 @@ var media = (function () { }; var VideoScript = { getVideoScriptMatch: getVideoScriptMatch }; + var DOM = global$3.DOM; var trimPx = function (value) { return value.replace(/px$/, ''); }; - var addPx = function (value) { - return /^[0-9.]+$/.test(value) ? value + 'px' : value; - }; - var getSize = function (name) { - return function (elm) { - return elm ? trimPx(elm.style[name]) : ''; - }; - }; - var setSize = function (name) { - return function (elm, value) { - if (elm) { - elm.style[name] = addPx(value); - } + var getEphoxEmbedData = function (attrs) { + var style = attrs.map.style; + var styles = style ? DOM.parseStyle(style) : {}; + return { + type: 'ephox-embed-iri', + source1: attrs.map['data-ephox-embed-iri'], + source2: '', + poster: '', + width: get(styles, 'max-width').map(trimPx).getOr(''), + height: get(styles, 'max-height').map(trimPx).getOr('') }; }; - var Size = { - getMaxWidth: getSize('maxWidth'), - getMaxHeight: getSize('maxHeight'), - setMaxWidth: setSize('maxWidth'), - setMaxHeight: setSize('maxHeight') - }; - - var DOM = global$4.DOM; - var getEphoxEmbedIri = function (elm) { - return DOM.getAttrib(elm, 'data-ephox-embed-iri'); - }; - var isEphoxEmbed = function (html) { - var fragment = DOM.createFragment(html); - return getEphoxEmbedIri(fragment.firstChild) !== ''; - }; - var htmlToDataSax = function (prefixes, html) { + var htmlToData = function (prefixes, html) { + var isEphoxEmbed = Cell(false); var data = {}; - global$3({ + global$4({ validate: false, allow_conditional_comments: true, special: 'script,noscript', start: function (name, attrs) { - if (!data.source1 && name === 'param') { - data.source1 = attrs.map.movie; - } - if (name === 'iframe' || name === 'object' || name === 'embed' || name === 'video' || name === 'audio') { - if (!data.type) { - data.type = name; + if (isEphoxEmbed.get()) ; else if (has(attrs.map, 'data-ephox-embed-iri')) { + isEphoxEmbed.set(true); + data = getEphoxEmbedData(attrs); + } else { + if (!data.source1 && name === 'param') { + data.source1 = attrs.map.movie; } - data = global$2.extend(attrs.map, data); - } - if (name === 'script') { - var videoScript = VideoScript.getVideoScriptMatch(prefixes, attrs.map.src); - if (!videoScript) { - return; + if (name === 'iframe' || name === 'object' || name === 'embed' || name === 'video' || name === 'audio') { + if (!data.type) { + data.type = name; + } + data = global$2.extend(attrs.map, data); } - data = { - type: 'script', - source1: attrs.map.src, - width: videoScript.width, - height: videoScript.height - }; - } - if (name === 'source') { - if (!data.source1) { - data.source1 = attrs.map.src; - } else if (!data.source2) { - data.source2 = attrs.map.src; + if (name === 'script') { + var videoScript = VideoScript.getVideoScriptMatch(prefixes, attrs.map.src); + if (!videoScript) { + return; + } + data = { + type: 'script', + source1: attrs.map.src, + width: videoScript.width, + height: videoScript.height + }; + } + if (name === 'source') { + if (!data.source1) { + data.source1 = attrs.map.src; + } else if (!data.source2) { + data.source2 = attrs.map.src; + } + } + if (name === 'img' && !data.poster) { + data.poster = attrs.map.src; } - } - if (name === 'img' && !data.poster) { - data.poster = attrs.map.src; } } }).parse(html); @@ -140,21 +274,6 @@ var media = (function () { data.poster = data.poster || ''; return data; }; - var ephoxEmbedHtmlToData = function (html) { - var fragment = DOM.createFragment(html); - var div = fragment.firstChild; - return { - type: 'ephox-embed-iri', - source1: getEphoxEmbedIri(div), - source2: '', - poster: '', - width: Size.getMaxWidth(div), - height: Size.getMaxHeight(div) - }; - }; - var htmlToData = function (prefixes, html) { - return isEphoxEmbed(html) ? ephoxEmbedHtmlToData(html) : htmlToDataSax(prefixes, html); - }; var HtmlToData = { htmlToData: htmlToData }; var global$5 = tinymce.util.Tools.resolve('tinymce.util.Promise'); @@ -174,22 +293,21 @@ var media = (function () { }; var Mime = { guess: guess }; - var global$6 = tinymce.util.Tools.resolve('tinymce.html.Writer'); + var global$6 = tinymce.util.Tools.resolve('tinymce.html.Schema'); - var global$7 = tinymce.util.Tools.resolve('tinymce.html.Schema'); + var global$7 = tinymce.util.Tools.resolve('tinymce.html.Writer'); - var DOM$1 = global$4.DOM; + var DOM$1 = global$3.DOM; + var addPx = function (value) { + return /^[0-9.]+$/.test(value) ? value + 'px' : value; + }; var setAttributes = function (attrs, updatedAttrs) { - var name; - var i; - var value; - var attr; - for (name in updatedAttrs) { - value = '' + updatedAttrs[name]; + for (var name in updatedAttrs) { + var value = '' + updatedAttrs[name]; if (attrs.map[name]) { - i = attrs.length; + var i = attrs.length; while (i--) { - attr = attrs[i]; + var attr = attrs[i]; if (attr.name === name) { if (value) { attrs.map[name] = value; @@ -209,17 +327,19 @@ var media = (function () { } } }; - var normalizeHtml = function (html) { - var writer = global$6(); - var parser = global$3(writer); - parser.parse(html); - return writer.getContent(); + var updateEphoxEmbed = function (data, attrs) { + var style = attrs.map.style; + var styleMap = style ? DOM$1.parseStyle(style) : {}; + styleMap['max-width'] = addPx(data.width); + styleMap['max-height'] = addPx(data.height); + setAttributes(attrs, { style: DOM$1.serializeStyle(styleMap) }); }; - var updateHtmlSax = function (html, data, updateAll) { - var writer = global$6(); + var updateHtml = function (html, data, updateAll) { + var writer = global$7(); + var isEphoxEmbed = Cell(false); var sourceCount = 0; var hasImage; - global$3({ + global$4({ validate: false, allow_conditional_comments: true, special: 'script,noscript', @@ -233,101 +353,94 @@ var media = (function () { writer.text(text, raw); }, start: function (name, attrs, empty) { - switch (name) { - case 'video': - case 'object': - case 'embed': - case 'img': - case 'iframe': - if (data.height !== undefined && data.width !== undefined) { - setAttributes(attrs, { - width: data.width, - height: data.height - }); - } - break; - } - if (updateAll) { + if (isEphoxEmbed.get()) ; else if (has(attrs.map, 'data-ephox-embed-iri')) { + isEphoxEmbed.set(true); + updateEphoxEmbed(data, attrs); + } else { switch (name) { case 'video': - setAttributes(attrs, { - poster: data.poster, - src: '' - }); - if (data.source2) { - setAttributes(attrs, { src: '' }); - } - break; + case 'object': + case 'embed': + case 'img': case 'iframe': - setAttributes(attrs, { src: data.source1 }); + if (data.height !== undefined && data.width !== undefined) { + setAttributes(attrs, { + width: data.width, + height: data.height + }); + } break; - case 'source': - sourceCount++; - if (sourceCount <= 2) { + } + if (updateAll) { + switch (name) { + case 'video': setAttributes(attrs, { - src: data['source' + sourceCount], - type: data['source' + sourceCount + 'mime'] + poster: data.poster, + src: '' }); - if (!data['source' + sourceCount]) { + if (data.source2) { + setAttributes(attrs, { src: '' }); + } + break; + case 'iframe': + setAttributes(attrs, { src: data.source1 }); + break; + case 'source': + sourceCount++; + if (sourceCount <= 2) { + setAttributes(attrs, { + src: data['source' + sourceCount], + type: data['source' + sourceCount + 'mime'] + }); + if (!data['source' + sourceCount]) { + return; + } + } + break; + case 'img': + if (!data.poster) { return; } + hasImage = true; + break; } - break; - case 'img': - if (!data.poster) { - return; - } - hasImage = true; - break; } } writer.start(name, attrs, empty); }, end: function (name) { - if (name === 'video' && updateAll) { - for (var index = 1; index <= 2; index++) { - if (data['source' + index]) { - var attrs = []; - attrs.map = {}; - if (sourceCount < index) { - setAttributes(attrs, { - src: data['source' + index], - type: data['source' + index + 'mime'] - }); - writer.start('source', attrs, true); + if (!isEphoxEmbed.get()) { + if (name === 'video' && updateAll) { + for (var index = 1; index <= 2; index++) { + if (data['source' + index]) { + var attrs = []; + attrs.map = {}; + if (sourceCount < index) { + setAttributes(attrs, { + src: data['source' + index], + type: data['source' + index + 'mime'] + }); + writer.start('source', attrs, true); + } } } } - } - if (data.poster && name === 'object' && updateAll && !hasImage) { - var imgAttrs = []; - imgAttrs.map = {}; - setAttributes(imgAttrs, { - src: data.poster, - width: data.width, - height: data.height - }); - writer.start('img', imgAttrs, true); + if (data.poster && name === 'object' && updateAll && !hasImage) { + var imgAttrs = []; + imgAttrs.map = {}; + setAttributes(imgAttrs, { + src: data.poster, + width: data.width, + height: data.height + }); + writer.start('img', imgAttrs, true); + } } writer.end(name); } - }, global$7({})).parse(html); + }, global$6({})).parse(html); return writer.getContent(); }; - var isEphoxEmbed$1 = function (html) { - var fragment = DOM$1.createFragment(html); - return DOM$1.getAttrib(fragment.firstChild, 'data-ephox-embed-iri') !== ''; - }; - var updateEphoxEmbed = function (html, data) { - var fragment = DOM$1.createFragment(html); - var div = fragment.firstChild; - Size.setMaxWidth(div, data.width); - Size.setMaxHeight(div, data.height); - return normalizeHtml(div.outerHTML); - }; - var updateHtml = function (html, data, updateAll) { - return isEphoxEmbed$1(html) ? updateEphoxEmbed(html, data) : updateHtmlSax(html, data, updateAll); - }; var UpdateHtml = { updateHtml: updateHtml }; var urlPatterns = [ @@ -551,6 +664,31 @@ var media = (function () { isCached: isCached }; + var trimPx$1 = function (value) { + return value.replace(/px$/, ''); + }; + var addPx$1 = function (value) { + return /^[0-9.]+$/.test(value) ? value + 'px' : value; + }; + var getSize = function (name) { + return function (elm) { + return elm ? trimPx$1(elm.style[name]) : ''; + }; + }; + var setSize = function (name) { + return function (elm, value) { + if (elm) { + elm.style[name] = addPx$1(value); + } + }; + }; + var Size = { + getMaxWidth: getSize('maxWidth'), + getMaxHeight: getSize('maxHeight'), + setMaxWidth: setSize('maxWidth'), + setMaxHeight: setSize('maxHeight') + }; + var doSyncSize = function (widthCtrl, heightCtrl) { widthCtrl.state.set('oldVal', widthCtrl.value()); heightCtrl.state.set('oldVal', heightCtrl.value()); @@ -825,13 +963,13 @@ var media = (function () { }; var Dialog = { showDialog: showDialog }; - var get = function (editor) { + var get$1 = function (editor) { var showDialog = function () { Dialog.showDialog(editor); }; return { showDialog: showDialog }; }; - var Api = { get: get }; + var Api = { get: get$1 }; var register = function (editor) { var showDialog = function () { @@ -847,9 +985,9 @@ var media = (function () { if (Settings.shouldFilterHtml(editor) === false) { return html; } - var writer = global$6(); + var writer = global$7(); var blocked; - global$3({ + global$4({ validate: false, allow_conditional_comments: false, special: 'script,noscript', @@ -864,14 +1002,16 @@ var media = (function () { }, start: function (name, attrs, empty) { blocked = true; - if (name === 'script' || name === 'noscript') { + if (name === 'script' || name === 'noscript' || name === 'svg') { return; } - for (var i = 0; i < attrs.length; i++) { - if (attrs[i].name.indexOf('on') === 0) { - return; + for (var i = attrs.length - 1; i >= 0; i--) { + var attrName = attrs[i].name; + if (attrName.indexOf('on') === 0) { + delete attrs.map[attrName]; + attrs.splice(i, 1); } - if (attrs[i].name === 'style') { + if (attrName === 'style') { attrs[i].value = editor.dom.serializeStyle(editor.dom.parseStyle(attrs[i].value), name); } } @@ -884,7 +1024,7 @@ var media = (function () { } writer.end(name); } - }, global$7({})).parse(html); + }, global$6({})).parse(html); return writer.getContent(); }; var Sanitize = { sanitize: sanitize }; diff --git a/plugins/media/plugin.min.js b/plugins/media/plugin.min.js index 0dd06ba6..e78d8efc 100644 --- a/plugins/media/plugin.min.js +++ b/plugins/media/plugin.min.js @@ -1 +1 @@ -!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),o=tinymce.util.Tools.resolve("tinymce.Env"),v=tinymce.util.Tools.resolve("tinymce.util.Tools"),w=function(e){return e.getParam("media_scripts")},b=function(e){return e.getParam("audio_template_callback")},y=function(e){return e.getParam("video_template_callback")},n=function(e){return e.getParam("media_live_embeds",!0)},t=function(e){return e.getParam("media_filter_html",!0)},s=function(e){return e.getParam("media_url_resolver")},m=function(e){return e.getParam("media_alt_source",!0)},d=function(e){return e.getParam("media_poster",!0)},h=function(e){return e.getParam("media_dimensions",!0)},p=tinymce.util.Tools.resolve("tinymce.html.SaxParser"),r=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),x=function(e,t){if(e)for(var r=0;r"):"application/x-shockwave-flash"===i.source1mime?(d='',m.poster&&(d+=''),d+=""):-1!==i.source1mime.indexOf("audio")?(u=i,(l=f)?l(u):'"):"script"===i.type?'