Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

Commit

Permalink
fix(icon): large SVG files can cause icon caching to hang
Browse files Browse the repository at this point in the history
use querySelectorAll as much as possible
reduce the use of RegEx on large chunks of DOM

Fixes #11651. Relates to #8689. Relates to #11342. Relates to #11635.
  • Loading branch information
Splaktar committed Feb 25, 2019
1 parent 251cfed commit 46563a5
Showing 1 changed file with 64 additions and 17 deletions.
81 changes: 64 additions & 17 deletions src/components/icon/js/iconService.js
Original file line number Diff line number Diff line change
Expand Up @@ -479,13 +479,18 @@ function MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce) {
function transformClone(cacheElement) {
var clone = cacheElement.clone();
var newUid = $mdUtil.nextUid();
var cacheSuffix, svgElement;
var cacheSuffix, svgUrlQuerySelector, i;
// These are SVG attributes that can reference element ids.
var svgUrlAttributes = [
'clip-path', 'color-profile', 'cursor', 'fill', 'filter', 'href', 'marker-start',
'marker-mid', 'marker-end', 'mask', 'stroke', 'style', 'vector-effect'
];
var isIeSvg = clone.innerHTML === undefined;

// Verify that the newUid only contains a number and not some XSS content.
if (!isFinite(Number(newUid))) {
throw new Error('Unsafe and unexpected non-number result from $mdUtil.nextUid().');
}

cacheSuffix = '_cache' + newUid;

// For each cached icon, we need to modify the id attributes and references.
Expand All @@ -495,27 +500,69 @@ function MdIconService(config, $templateRequest, $q, $log, $mdUtil, $sce) {
clone.id += cacheSuffix;
}

var addCacheSuffixToId = function(match, p1, p2, p3) {
return [p1, p2, cacheSuffix, p3].join('');
};
// Do as much as possible with querySelectorAll as it provides much greater performance
// than RegEx against serialized DOM.
angular.forEach(clone.querySelectorAll('[id]'), function(descendantElem) {
svgUrlQuerySelector = '';
for (i = 0; i < svgUrlAttributes.length; i++) {
svgUrlQuerySelector += '[' + svgUrlAttributes[i] + '="url(#' + descendantElem.id + ')"]';
if (i + 1 < svgUrlAttributes.length) {
svgUrlQuerySelector += ', ';
}
}
// Append the cacheSuffix to references of the element's id within url(#id) calls.
angular.forEach(clone.querySelectorAll(svgUrlQuerySelector), function(refItem) {
updateSvgIdReferences(descendantElem, refItem, isIeSvg, newUid);
});
// Handle usages of url(#id) in the SVG's stylesheets
angular.forEach(clone.querySelectorAll('style'), function(refItem) {
updateSvgIdReferences(descendantElem, refItem, isIeSvg, newUid);
});
descendantElem.id += cacheSuffix;
});
// innerHTML of SVG elements is not supported by IE11
if (clone.innerHTML === undefined) {
svgElement = $mdUtil.getOuterHTML(clone);
svgElement = svgElement.replace(/(.*url\(#)(\w*)(\).*)/g, addCacheSuffixToId);
svgElement = svgElement.replace(/(.*xlink:href="#)(\w*)(".*)/g, addCacheSuffixToId);
clone = angular.element(svgElement)[0];
// Update ids referenced by the deprecated (in SVG v2) xlink:href XML attribute. The now
// preferred href attribute is handled above. However, this non-standard XML namespaced
// attribute cannot be handled in the same way. Explanation of this query selector here:
// https://stackoverflow.com/q/23034283/633107.
angular.forEach(clone.querySelectorAll('[*|href]:not([href])'), function(descendantElem) {
if (descendantElem.getAttribute('xlink:href')) {
descendantElem.setAttribute('xlink:href',
descendantElem.getAttribute('xlink:href') + cacheSuffix);
}
});

return clone;
}

/**
* @param {Element} referencedElement element w/ id that needs to be updated
* @param {Element} referencingElement element that references the original id
* @param {boolean} isIeSvg true if we're dealing with an SVG in IE11, false otherwise
* @param {string} newUid the cache id to add as part of the cache suffix
*/
function updateSvgIdReferences(referencedElement, referencingElement, isIeSvg, newUid) {
var svgElement, cacheSuffix;

// Verify that the newUid only contains a number and not some XSS content.
if (!isFinite(Number(newUid))) {
throw new Error('Unsafe and unexpected non-number result for newUid.');
}
cacheSuffix = '_cache' + newUid;

// outerHTML of SVG elements is not supported by IE11
if (isIeSvg) {
// TODO this needs to be verified in IE11 manually
svgElement = $mdUtil.getOuterHTML(referencingElement);
svgElement = svgElement.replace("url(#" + referencedElement.id + ")",
"url(#" + referencedElement.id + cacheSuffix + ")");
referencingElement = angular.element(svgElement)[0];
} else {
// Inject the cacheSuffix into all instances of url(id) and xlink:href="#id".
// This use of innerHTML should be safe from XSS attack since we are only injecting the
// This use of outerHTML should be safe from XSS attack since we are only injecting the
// cacheSuffix with content from $mdUtil.nextUid which we verify is a finite number above.
clone.innerHTML = clone.innerHTML.replace(/(.*url\(#)(\w*)(\).*)/g, addCacheSuffixToId);
clone.innerHTML = clone.innerHTML.replace(/(.*xlink:href="#)(\w*)(".*)/g, addCacheSuffixToId);
referencingElement.outerHTML = referencingElement.outerHTML.replace(
"url(#" + referencedElement.id + ")",
"url(#" + referencedElement.id + cacheSuffix + ")");
}

return clone;
}

/**
Expand Down

0 comments on commit 46563a5

Please sign in to comment.