Index: tools/dom/templates/html/impl/impl_Element.darttemplate |
diff --git a/tools/dom/templates/html/impl/impl_Element.darttemplate b/tools/dom/templates/html/impl/impl_Element.darttemplate |
index 1698edcd7f7dec3834be8dd2f1275440bd9d5f09..a18094f0b0f6a27f0768c763005cb2b19e74ee0a 100644 |
--- a/tools/dom/templates/html/impl/impl_Element.darttemplate |
+++ b/tools/dom/templates/html/impl/impl_Element.darttemplate |
@@ -309,20 +309,34 @@ $(ANNOTATIONS)abstract class $CLASSNAME$EXTENDS$IMPLEMENTS$NATIVESPEC { |
/** |
* Creates an HTML element from a valid fragment of HTML. |
* |
- * The [html] fragment must represent valid HTML with a single element root, |
- * which will be parsed and returned. |
+ * var element = new Element.html('<div class="foo">content</div>'); |
* |
- * Important: the contents of [html] should not contain any user-supplied |
- * data. Without strict data validation it is impossible to prevent script |
- * injection exploits. |
+ * The HTML fragment should contain only one single root element, any |
+ * leading or trailing text nodes will be removed. |
* |
- * It is instead recommended that elements be constructed via [Element.tag] |
- * and text be added via [text]. |
+ * The HTML fragment is parsed as if it occurred within the context of a |
+ * `<body>` tag, this means that special elements such as `<caption>` which |
+ * must be parsed within the scope of a `<table>` element will be dropped. Use |
+ * [createFragment] to parse contextual HTML fragments. |
+ * |
+ * Unless a validator is provided this will perform the default validation |
+ * and remove all scriptable elements and attributes. |
+ * |
+ * See also: |
+ * |
+ * * [NodeValidator] |
* |
- * var element = new Element.html('<div class="foo">content</div>'); |
*/ |
- factory $CLASSNAME.html(String html) => |
- _$(CLASSNAME)FactoryProvider.createElement_html(html); |
+ factory Element.html(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
+ var fragment = document.body.createFragment(html, validator: validator, |
+ treeSanitizer: treeSanitizer); |
+ |
+ if (fragment._firstElementChild != fragment._lastElementChild) { |
+ throw new StateError("More than one element root"); |
+ } |
+ return fragment._firstElementChild; |
+ } |
/** |
* Creates the HTML element specified by the tag name. |
@@ -1171,136 +1185,135 @@ $endif |
return new Point(p.x + current.offsetLeft, p.y + current.offsetTop); |
} |
-$if DART2JS |
- @JSName('innerHTML') |
- @DomName('HTMLElement.innerHTML') |
- String get innerHtml => JS('String', '#.innerHTML', this); |
- |
- void set innerHtml(String value) { |
- JS('', '#.innerHTML = #', this, value); |
- // Polyfill relies on mutation observers for upgrading, but we want it |
- // immediate. |
- Platform.upgradeCustomElements(this); |
- } |
-$endif |
- |
-$!MEMBERS |
-} |
+ static HtmlDocument _parseDocument; |
+ static NodeValidatorBuilder _defaultValidator; |
+ static _ValidatingTreeSanitizer _defaultSanitizer; |
-final _START_TAG_REGEXP = new RegExp('<(\\w+)'); |
-class _ElementFactoryProvider { |
- static const _CUSTOM_PARENT_TAG_MAP = const { |
- 'body' : 'html', |
- 'head' : 'html', |
- 'caption' : 'table', |
- 'td': 'tr', |
- 'th': 'tr', |
- 'colgroup': 'table', |
- 'col' : 'colgroup', |
- 'tr' : 'tbody', |
- 'tbody' : 'table', |
- 'tfoot' : 'table', |
- 'thead' : 'table', |
- 'track' : 'audio', |
- }; |
- |
- @DomName('Document.createElement') |
- static Element createElement_html(String html) { |
- // TODO(jacobr): this method can be made more robust and performant. |
- // 1) Cache the dummy parent elements required to use innerHTML rather than |
- // creating them every call. |
- // 2) Verify that the html does not contain leading or trailing text nodes. |
- // 3) Verify that the html does not contain both <head> and <body> tags. |
- // 4) Detatch the created element from its dummy parent. |
- String parentTag = 'div'; |
- String tag; |
- final match = _START_TAG_REGEXP.firstMatch(html); |
- if (match != null) { |
- tag = match.group(1).toLowerCase(); |
- if (Device.isIE && Element._TABLE_TAGS.containsKey(tag)) { |
- return _createTableForIE(html, tag); |
+ /** |
+ * Create a DocumentFragment from the HTML fragment and ensure that it follows |
+ * the sanitization rules specified by the validator or treeSanitizer. |
+ * |
+ * If the default validation behavior is too restrictive then a new |
+ * NodeValidator should be created, either extending or wrapping a default |
+ * validator and overriding the validation APIs. |
+ * |
+ * The treeSanitizer is used to walk the generated node tree and sanitize it. |
+ * A custom treeSanitizer can also be provided to perform special validation |
+ * rules but since the API is more complex to implement this is discouraged. |
+ * |
+ * The returned tree is guaranteed to only contain nodes and attributes which |
+ * are allowed by the provided validator. |
+ * |
+ * See also: |
+ * |
+ * * [NodeValidator] |
+ * * [NodeTreeSanitizer] |
+ */ |
+ DocumentFragment createFragment(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
+ if (treeSanitizer == null) { |
+ if (validator == null) { |
+ if (_defaultValidator == null) { |
+ _defaultValidator = new NodeValidatorBuilder.common(); |
+ } |
+ validator = _defaultValidator; |
+ } |
+ if (_defaultSanitizer == null) { |
+ _defaultSanitizer = new _ValidatingTreeSanitizer(validator); |
+ } else { |
+ _defaultSanitizer.validator = validator; |
} |
- parentTag = _CUSTOM_PARENT_TAG_MAP[tag]; |
- if (parentTag == null) parentTag = 'div'; |
+ treeSanitizer = _defaultSanitizer; |
+ } else if (validator != null) { |
+ throw new ArgumentError( |
+ 'validator can only be passed if treeSanitizer is null'); |
} |
- final temp = new Element.tag(parentTag); |
- temp.innerHtml = html; |
- |
- Element element; |
- if (temp.children.length == 1) { |
- element = temp.children[0]; |
- } else if (parentTag == 'html' && temp.children.length == 2) { |
- // In html5 the root <html> tag will always have a <body> and a <head>, |
- // even though the inner html only contains one of them. |
- element = temp.children[tag == 'head' ? 0 : 1]; |
+ if (_parseDocument == null) { |
+ _parseDocument = document.implementation.createHtmlDocument(''); |
+ } |
+ var contextElement; |
+ if (this is BodyElement) { |
+ contextElement = _parseDocument.body; |
} else { |
- _singleNode(temp.children); |
+ contextElement = _parseDocument.$dom_createElement(tagName); |
+ _parseDocument.body.append(contextElement); |
} |
- element.remove(); |
- return element; |
+ var fragment; |
+ if (Range.supportsCreateContextualFragment) { |
+ var range = _parseDocument.$dom_createRange(); |
+ range.selectNodeContents(contextElement); |
+ fragment = range.createContextualFragment(html); |
+ } else { |
+ contextElement._innerHtml = html; |
+ |
+ fragment = _parseDocument.createDocumentFragment(); |
+ while (contextElement.firstChild != null) { |
+ fragment.append(contextElement.firstChild); |
+ } |
+ } |
+ if (contextElement != _parseDocument.body) { |
+ contextElement.remove(); |
+ } |
+ |
+ treeSanitizer.sanitizeTree(fragment); |
+ return fragment; |
} |
/** |
- * IE table elements don't support innerHTML (even in standards mode). |
- * Instead we use a div and inject the table element in the innerHtml string. |
- * This technique works on other browsers too, but it's probably slower, |
- * so we only use it when running on IE. |
- * |
- * See also innerHTML: |
- * <http://msdn.microsoft.com/en-us/library/ie/ms533897(v=vs.85).aspx> |
- * and Building Tables Dynamically: |
- * <http://msdn.microsoft.com/en-us/library/ie/ms532998(v=vs.85).aspx>. |
+ * Parses the HTML fragment and sets it as the contents of this element. |
+ * |
+ * This uses the default sanitization behavior to sanitize the HTML fragment, |
+ * use [setInnerHtml] to override the default behavior. |
*/ |
- static Element _createTableForIE(String html, String tag) { |
- var div = new Element.tag('div'); |
- div.innerHtml = '<table>$html</table>'; |
- var table = _singleNode(div.children); |
- Element element; |
- switch (tag) { |
- case 'td': |
- case 'th': |
- TableRowElement row = _singleNode(table.rows); |
- element = _singleNode(row.cells); |
- break; |
- case 'tr': |
- element = _singleNode(table.rows); |
- break; |
- case 'tbody': |
- element = _singleNode(table.tBodies); |
- break; |
- case 'thead': |
- element = table.tHead; |
- break; |
- case 'tfoot': |
- element = table.tFoot; |
- break; |
- case 'caption': |
- element = table.caption; |
- break; |
- case 'colgroup': |
- element = _getColgroup(table); |
- break; |
- case 'col': |
- element = _singleNode(_getColgroup(table).children); |
- break; |
- } |
- element.remove(); |
- return element; |
+ void set innerHtml(String html) { |
+ this.setInnerHtml(html); |
} |
- static TableColElement _getColgroup(TableElement table) { |
- // TODO(jmesserly): is there a better way to do this? |
- return _singleNode(table.children.where((n) => n.tagName == 'COLGROUP') |
- .toList()); |
+ /** |
+ * Parses the HTML fragment and sets it as the contents of this element. |
+ * This ensures that the generated content follows the sanitization rules |
+ * specified by the validator or treeSanitizer. |
+ * |
+ * If the default validation behavior is too restrictive then a new |
+ * NodeValidator should be created, either extending or wrapping a default |
+ * validator and overriding the validation APIs. |
+ * |
+ * The treeSanitizer is used to walk the generated node tree and sanitize it. |
+ * A custom treeSanitizer can also be provided to perform special validation |
+ * rules but since the API is more complex to implement this is discouraged. |
+ * |
+ * The resulting tree is guaranteed to only contain nodes and attributes which |
+ * are allowed by the provided validator. |
+ * |
+ * See also: |
+ * |
+ * * [NodeValidator] |
+ * * [NodeTreeSanitizer] |
+ */ |
+ void setInnerHtml(String html, |
+ {NodeValidator validator, NodeTreeSanitizer treeSanitizer}) { |
+ text = null; |
+ append(createFragment( |
+ html, validator: validator, treeSanitizer: treeSanitizer)); |
} |
+ String get innerHtml => _innerHtml; |
- static Node _singleNode(List<Node> list) { |
- if (list.length == 1) return list[0]; |
- throw new ArgumentError('HTML had ${list.length} ' |
- 'top level elements but 1 expected'); |
+ /** |
+ * For use while transitioning to the safe [innerHtml] or [setInnerHtml]. |
+ * Unsafe because it opens the app to cross-site scripting vulnerabilities. |
+ */ |
+ @deprecated |
+ void set unsafeInnerHtml(String html) { |
+ _innerHtml = html; |
} |
+$!MEMBERS |
+} |
+ |
+ |
+class _ElementFactoryProvider { |
+ |
@DomName('Document.createElement') |
$if DART2JS |
// Optimization to improve performance until the dart2js compiler inlines this |