vendor/assets/javascripts/angular-sanitize.js in angularjs-rails-1.4.8 vs vendor/assets/javascripts/angular-sanitize.js in angularjs-rails-1.5.0
- old
+ new
@@ -1,8 +1,8 @@
/**
- * @license AngularJS v1.4.8
- * (c) 2010-2015 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.5.0
+ * (c) 2010-2016 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
@@ -31,41 +31,28 @@
* <div doc-module-components="ngSanitize"></div>
*
* See {@link ngSanitize.$sanitize `$sanitize`} for usage.
*/
-/*
- * HTML Parser By Misko Hevery (misko@hevery.com)
- * based on: HTML Parser By John Resig (ejohn.org)
- * Original code by Erik Arvidsson, Mozilla Public License
- * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
- *
- * // Use like so:
- * htmlParser(htmlString, {
- * start: function(tag, attrs, unary) {},
- * end: function(tag) {},
- * chars: function(text) {},
- * comment: function(text) {}
- * });
- *
- */
-
-
/**
* @ngdoc service
* @name $sanitize
* @kind function
*
* @description
+ * Sanitizes an html string by stripping all potentially dangerous tokens.
+ *
* The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
* then serialized back to properly escaped html string. This means that no unsafe input can make
- * it into the returned string, however, since our parser is more strict than a typical browser
- * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
- * browser, won't make it through the sanitizer. The input may also contain SVG markup.
- * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
- * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
+ * it into the returned string.
*
+ * The whitelist for URL sanitization of attribute values is configured using the functions
+ * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
+ * `$compileProvider`}.
+ *
+ * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
+ *
* @param {string} html HTML input.
* @returns {string} Sanitized HTML.
*
* @example
<example module="sanitizeExample" deps="angular-sanitize.js">
@@ -146,20 +133,74 @@
"new <b onclick=\"alert(1)\">text</b>");
});
</file>
</example>
*/
+
+
+/**
+ * @ngdoc provider
+ * @name $sanitizeProvider
+ *
+ * @description
+ * Creates and configures {@link $sanitize} instance.
+ */
function $SanitizeProvider() {
+ var svgEnabled = false;
+
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
+ if (svgEnabled) {
+ angular.extend(validElements, svgElements);
+ }
return function(html) {
var buf = [];
htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
- return !/^unsafe/.test($$sanitizeUri(uri, isImage));
+ return !/^unsafe:/.test($$sanitizeUri(uri, isImage));
}));
return buf.join('');
};
}];
+
+
+ /**
+ * @ngdoc method
+ * @name $sanitizeProvider#enableSvg
+ * @kind function
+ *
+ * @description
+ * Enables a subset of svg to be supported by the sanitizer.
+ *
+ * <div class="alert alert-warning">
+ * <p>By enabling this setting without taking other precautions, you might expose your
+ * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
+ * outside of the containing element and be rendered over other elements on the page (e.g. a login
+ * link). Such behavior can then result in phishing incidents.</p>
+ *
+ * <p>To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
+ * tags within the sanitized content:</p>
+ *
+ * <br>
+ *
+ * <pre><code>
+ * .rootOfTheIncludedContent svg {
+ * overflow: hidden !important;
+ * }
+ * </code></pre>
+ * </div>
+ *
+ * @param {boolean=} regexp New regexp to whitelist urls with.
+ * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called
+ * without an argument or self for chaining otherwise.
+ */
+ this.enableSvg = function(enableSvg) {
+ if (angular.isDefined(enableSvg)) {
+ svgEnabled = enableSvg;
+ return this;
+ } else {
+ return svgEnabled;
+ }
+ };
}
function sanitizeText(chars) {
var buf = [];
var writer = htmlSanitizeWriter(buf, angular.noop);
@@ -167,80 +208,70 @@
return buf.join('');
}
// Regular Expressions for parsing tags and attributes
-var START_TAG_REGEXP =
- /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
- END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
- ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
- BEGIN_TAG_REGEXP = /^</,
- BEGING_END_TAGE_REGEXP = /^<\//,
- COMMENT_REGEXP = /<!--(.*?)-->/g,
- DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
- CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
- SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
+var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
// Match everything outside of normal chars and " (quote character)
- NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
+ NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g;
// Good source of info about elements and attributes
// http://dev.w3.org/html5/spec/Overview.html#semantics
// http://simon.html5.org/html-elements
// Safe Void Elements - HTML5
// http://dev.w3.org/html5/spec/Overview.html#void-elements
-var voidElements = makeMap("area,br,col,hr,img,wbr");
+var voidElements = toMap("area,br,col,hr,img,wbr");
// Elements that you can, intentionally, leave open (and which close themselves)
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
-var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
- optionalEndTagInlineElements = makeMap("rp,rt"),
+var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
+ optionalEndTagInlineElements = toMap("rp,rt"),
optionalEndTagElements = angular.extend({},
optionalEndTagInlineElements,
optionalEndTagBlockElements);
// Safe Block Elements - HTML5
-var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
+var blockElements = angular.extend({}, optionalEndTagBlockElements, toMap("address,article," +
"aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
- "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
+ "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul"));
// Inline Elements - HTML5
-var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
+var inlineElements = angular.extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," +
"bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
"samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
// SVG Elements
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
// They can potentially allow for arbitrary javascript to be executed. See #11290
-var svgElements = makeMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
+var svgElements = toMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
"hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," +
- "radialGradient,rect,stop,svg,switch,text,title,tspan,use");
+ "radialGradient,rect,stop,svg,switch,text,title,tspan");
-// Special Elements (can contain anything)
-var specialElements = makeMap("script,style");
+// Blocked Elements (will be stripped)
+var blockedElements = toMap("script,style");
var validElements = angular.extend({},
voidElements,
blockElements,
inlineElements,
- optionalEndTagElements,
- svgElements);
+ optionalEndTagElements);
//Attributes that have href and hence need to be sanitized
-var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href");
+var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href");
-var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
+var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
'valign,value,vspace,width');
// SVG attributes (without "id" and "name" attributes)
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
-var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
+var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
@@ -257,196 +288,122 @@
var validAttrs = angular.extend({},
uriAttrs,
svgAttrs,
htmlAttrs);
-function makeMap(str, lowercaseKeys) {
+function toMap(str, lowercaseKeys) {
var obj = {}, items = str.split(','), i;
for (i = 0; i < items.length; i++) {
obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true;
}
return obj;
}
+var inertBodyElement;
+(function(window) {
+ var doc;
+ if (window.document && window.document.implementation) {
+ doc = window.document.implementation.createHTMLDocument("inert");
+ } else {
+ throw $sanitizeMinErr('noinert', "Can't create an inert html document");
+ }
+ var docElement = doc.documentElement || doc.getDocumentElement();
+ var bodyElements = docElement.getElementsByTagName('body');
+ // usually there should be only one body element in the document, but IE doesn't have any, so we need to create one
+ if (bodyElements.length === 1) {
+ inertBodyElement = bodyElements[0];
+ } else {
+ var html = doc.createElement('html');
+ inertBodyElement = doc.createElement('body');
+ html.appendChild(inertBodyElement);
+ doc.appendChild(html);
+ }
+})(window);
+
/**
* @example
* htmlParser(htmlString, {
- * start: function(tag, attrs, unary) {},
+ * start: function(tag, attrs) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
* @param {string} html string
* @param {object} handler
*/
function htmlParser(html, handler) {
- if (typeof html !== 'string') {
- if (html === null || typeof html === 'undefined') {
- html = '';
- } else {
- html = '' + html;
- }
+ if (html === null || html === undefined) {
+ html = '';
+ } else if (typeof html !== 'string') {
+ html = '' + html;
}
- var index, chars, match, stack = [], last = html, text;
- stack.last = function() { return stack[stack.length - 1]; };
+ inertBodyElement.innerHTML = html;
- while (html) {
- text = '';
- chars = true;
+ //mXSS protection
+ var mXSSAttempts = 5;
+ do {
+ if (mXSSAttempts === 0) {
+ throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable");
+ }
+ mXSSAttempts--;
- // Make sure we're not in a script or style element
- if (!stack.last() || !specialElements[stack.last()]) {
+ // strip custom-namespaced attributes on IE<=11
+ if (document.documentMode <= 11) {
+ stripCustomNsAttrs(inertBodyElement);
+ }
+ html = inertBodyElement.innerHTML; //trigger mXSS
+ inertBodyElement.innerHTML = html;
+ } while (html !== inertBodyElement.innerHTML);
- // Comment
- if (html.indexOf("<!--") === 0) {
- // comments containing -- are not allowed unless they terminate the comment
- index = html.indexOf("--", 4);
+ var node = inertBodyElement.firstChild;
+ while (node) {
+ switch (node.nodeType) {
+ case 1: // ELEMENT_NODE
+ handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes));
+ break;
+ case 3: // TEXT NODE
+ handler.chars(node.textContent);
+ break;
+ }
- if (index >= 0 && html.lastIndexOf("-->", index) === index) {
- if (handler.comment) handler.comment(html.substring(4, index));
- html = html.substring(index + 3);
- chars = false;
- }
- // DOCTYPE
- } else if (DOCTYPE_REGEXP.test(html)) {
- match = html.match(DOCTYPE_REGEXP);
-
- if (match) {
- html = html.replace(match[0], '');
- chars = false;
- }
- // end tag
- } else if (BEGING_END_TAGE_REGEXP.test(html)) {
- match = html.match(END_TAG_REGEXP);
-
- if (match) {
- html = html.substring(match[0].length);
- match[0].replace(END_TAG_REGEXP, parseEndTag);
- chars = false;
- }
-
- // start tag
- } else if (BEGIN_TAG_REGEXP.test(html)) {
- match = html.match(START_TAG_REGEXP);
-
- if (match) {
- // We only have a valid start-tag if there is a '>'.
- if (match[4]) {
- html = html.substring(match[0].length);
- match[0].replace(START_TAG_REGEXP, parseStartTag);
+ var nextNode;
+ if (!(nextNode = node.firstChild)) {
+ if (node.nodeType == 1) {
+ handler.end(node.nodeName.toLowerCase());
+ }
+ nextNode = node.nextSibling;
+ if (!nextNode) {
+ while (nextNode == null) {
+ node = node.parentNode;
+ if (node === inertBodyElement) break;
+ nextNode = node.nextSibling;
+ if (node.nodeType == 1) {
+ handler.end(node.nodeName.toLowerCase());
}
- chars = false;
- } else {
- // no ending tag found --- this piece should be encoded as an entity.
- text += '<';
- html = html.substring(1);
}
}
-
- if (chars) {
- index = html.indexOf("<");
-
- text += index < 0 ? html : html.substring(0, index);
- html = index < 0 ? "" : html.substring(index);
-
- if (handler.chars) handler.chars(decodeEntities(text));
- }
-
- } else {
- // IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w].
- html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
- function(all, text) {
- text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
-
- if (handler.chars) handler.chars(decodeEntities(text));
-
- return "";
- });
-
- parseEndTag("", stack.last());
}
-
- if (html == last) {
- throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
- "of html: {0}", html);
- }
- last = html;
+ node = nextNode;
}
- // Clean up any remaining tags
- parseEndTag();
-
- function parseStartTag(tag, tagName, rest, unary) {
- tagName = angular.lowercase(tagName);
- if (blockElements[tagName]) {
- while (stack.last() && inlineElements[stack.last()]) {
- parseEndTag("", stack.last());
- }
- }
-
- if (optionalEndTagElements[tagName] && stack.last() == tagName) {
- parseEndTag("", tagName);
- }
-
- unary = voidElements[tagName] || !!unary;
-
- if (!unary) {
- stack.push(tagName);
- }
-
- var attrs = {};
-
- rest.replace(ATTR_REGEXP,
- function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
- var value = doubleQuotedValue
- || singleQuotedValue
- || unquotedValue
- || '';
-
- attrs[name] = decodeEntities(value);
- });
- if (handler.start) handler.start(tagName, attrs, unary);
+ while (node = inertBodyElement.firstChild) {
+ inertBodyElement.removeChild(node);
}
+}
- function parseEndTag(tag, tagName) {
- var pos = 0, i;
- tagName = angular.lowercase(tagName);
- if (tagName) {
- // Find the closest opened tag of the same type
- for (pos = stack.length - 1; pos >= 0; pos--) {
- if (stack[pos] == tagName) break;
- }
- }
-
- if (pos >= 0) {
- // Close all the open elements, up the stack
- for (i = stack.length - 1; i >= pos; i--)
- if (handler.end) handler.end(stack[i]);
-
- // Remove the open elements from the stack
- stack.length = pos;
- }
+function attrToMap(attrs) {
+ var map = {};
+ for (var i = 0, ii = attrs.length; i < ii; i++) {
+ var attr = attrs[i];
+ map[attr.name] = attr.value;
}
+ return map;
}
-var hiddenPre=document.createElement("pre");
-/**
- * decodes all entities into regular string
- * @param value
- * @returns {string} A string with decoded entities.
- */
-function decodeEntities(value) {
- if (!value) { return ''; }
- hiddenPre.innerHTML = value.replace(/</g,"<");
- // innerText depends on styling as it doesn't display hidden elements.
- // Therefore, it's better to use textContent not to cause unnecessary reflows.
- return hiddenPre.textContent;
-}
-
/**
* Escapes all potentially dangerous characters, so that the
* resulting string can be safely inserted into attribute or
* element text.
* @param value
@@ -467,28 +424,28 @@
replace(/>/g, '>');
}
/**
* create an HTML/XML writer which writes to buffer
- * @param {Array} buf use buf.jain('') to get out sanitized html string
+ * @param {Array} buf use buf.join('') to get out sanitized html string
* @returns {object} in the form of {
- * start: function(tag, attrs, unary) {},
+ * start: function(tag, attrs) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* }
*/
function htmlSanitizeWriter(buf, uriValidator) {
- var ignore = false;
+ var ignoreCurrentElement = false;
var out = angular.bind(buf, buf.push);
return {
- start: function(tag, attrs, unary) {
+ start: function(tag, attrs) {
tag = angular.lowercase(tag);
- if (!ignore && specialElements[tag]) {
- ignore = tag;
+ if (!ignoreCurrentElement && blockedElements[tag]) {
+ ignoreCurrentElement = tag;
}
- if (!ignore && validElements[tag] === true) {
+ if (!ignoreCurrentElement && validElements[tag] === true) {
out('<');
out(tag);
angular.forEach(attrs, function(value, key) {
var lkey=angular.lowercase(key);
var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
@@ -499,33 +456,67 @@
out('="');
out(encodeEntities(value));
out('"');
}
});
- out(unary ? '/>' : '>');
+ out('>');
}
},
end: function(tag) {
- tag = angular.lowercase(tag);
- if (!ignore && validElements[tag] === true) {
- out('</');
- out(tag);
- out('>');
- }
- if (tag == ignore) {
- ignore = false;
- }
- },
+ tag = angular.lowercase(tag);
+ if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) {
+ out('</');
+ out(tag);
+ out('>');
+ }
+ if (tag == ignoreCurrentElement) {
+ ignoreCurrentElement = false;
+ }
+ },
chars: function(chars) {
- if (!ignore) {
- out(encodeEntities(chars));
- }
+ if (!ignoreCurrentElement) {
+ out(encodeEntities(chars));
}
+ }
};
}
+/**
+ * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare
+ * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want
+ * to allow any of these custom attributes. This method strips them all.
+ *
+ * @param node Root element to process
+ */
+function stripCustomNsAttrs(node) {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ var attrs = node.attributes;
+ for (var i = 0, l = attrs.length; i < l; i++) {
+ var attrNode = attrs[i];
+ var attrName = attrNode.name.toLowerCase();
+ if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
+ node.removeAttributeNode(attrNode);
+ i--;
+ l--;
+ }
+ }
+ }
+
+ var nextNode = node.firstChild;
+ if (nextNode) {
+ stripCustomNsAttrs(nextNode);
+ }
+
+ nextNode = node.nextSibling;
+ if (nextNode) {
+ stripCustomNsAttrs(nextNode);
+ }
+}
+
+
+
// define ngSanitize module and register $sanitize service
angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
/* global sanitizeText: false */
@@ -533,44 +524,43 @@
* @ngdoc filter
* @name linky
* @kind function
*
* @description
- * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
+ * Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and
* plain email address links.
*
* Requires the {@link ngSanitize `ngSanitize`} module to be installed.
*
* @param {string} text Input text.
- * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
- * @returns {string} Html-linkified text.
+ * @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in.
+ * @param {object|function(url)} [attributes] Add custom attributes to the link element.
*
+ * Can be one of:
+ *
+ * - `object`: A map of attributes
+ * - `function`: Takes the url as a parameter and returns a map of attributes
+ *
+ * If the map of attributes contains a value for `target`, it overrides the value of
+ * the target parameter.
+ *
+ *
+ * @returns {string} Html-linkified and {@link $sanitize sanitized} text.
+ *
* @usage
<span ng-bind-html="linky_expression | linky"></span>
*
* @example
<example module="linkyExample" deps="angular-sanitize.js">
<file name="index.html">
- <script>
- angular.module('linkyExample', ['ngSanitize'])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.snippet =
- 'Pretty text with some links:\n'+
- 'http://angularjs.org/,\n'+
- 'mailto:us@somewhere.org,\n'+
- 'another@somewhere.org,\n'+
- 'and one more: ftp://127.0.0.1/.';
- $scope.snippetWithTarget = 'http://angularjs.org/';
- }]);
- </script>
<div ng-controller="ExampleController">
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
- <td>Filter</td>
- <td>Source</td>
- <td>Rendered</td>
+ <th>Filter</th>
+ <th>Source</th>
+ <th>Rendered</th>
</tr>
<tr id="linky-filter">
<td>linky filter</td>
<td>
<pre><div ng-bind-html="snippet | linky"><br></div></pre>
@@ -580,23 +570,44 @@
</td>
</tr>
<tr id="linky-target">
<td>linky target</td>
<td>
- <pre><div ng-bind-html="snippetWithTarget | linky:'_blank'"><br></div></pre>
+ <pre><div ng-bind-html="snippetWithSingleURL | linky:'_blank'"><br></div></pre>
</td>
<td>
- <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
+ <div ng-bind-html="snippetWithSingleURL | linky:'_blank'"></div>
</td>
</tr>
+ <tr id="linky-custom-attributes">
+ <td>linky custom attributes</td>
+ <td>
+ <pre><div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"><br></div></pre>
+ </td>
+ <td>
+ <div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"></div>
+ </td>
+ </tr>
<tr id="escaped-html">
<td>no filter</td>
<td><pre><div ng-bind="snippet"><br></div></pre></td>
<td><div ng-bind="snippet"></div></td>
</tr>
</table>
</file>
+ <file name="script.js">
+ angular.module('linkyExample', ['ngSanitize'])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.snippet =
+ 'Pretty text with some links:\n'+
+ 'http://angularjs.org/,\n'+
+ 'mailto:us@somewhere.org,\n'+
+ 'another@somewhere.org,\n'+
+ 'and one more: ftp://127.0.0.1/.';
+ $scope.snippetWithSingleURL = 'http://angularjs.org/';
+ }]);
+ </file>
<file name="protractor.js" type="protractor">
it('should linkify the snippet with urls', function() {
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
@@ -620,24 +631,36 @@
.toBe('new http://link.');
});
it('should work with the target property', function() {
expect(element(by.id('linky-target')).
- element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
+ element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
toBe('http://angularjs.org/');
expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
});
+
+ it('should optionally add custom attributes', function() {
+ expect(element(by.id('linky-custom-attributes')).
+ element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
+ toBe('http://angularjs.org/');
+ expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
+ });
</file>
</example>
*/
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
var LINKY_URL_REGEXP =
/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
MAILTO_REGEXP = /^mailto:/i;
- return function(text, target) {
- if (!text) return text;
+ var linkyMinErr = angular.$$minErr('linky');
+ var isString = angular.isString;
+
+ return function(text, target, attributes) {
+ if (text == null || text === '') return text;
+ if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
+
var match;
var raw = text;
var html = [];
var url;
var i;
@@ -662,11 +685,22 @@
}
html.push(sanitizeText(text));
}
function addLink(url, text) {
+ var key;
html.push('<a ');
- if (angular.isDefined(target)) {
+ if (angular.isFunction(attributes)) {
+ attributes = attributes(url);
+ }
+ if (angular.isObject(attributes)) {
+ for (key in attributes) {
+ html.push(key + '="' + attributes[key] + '" ');
+ }
+ } else {
+ attributes = {};
+ }
+ if (angular.isDefined(target) && !('target' in attributes)) {
html.push('target="',
target,
'" ');
}
html.push('href="',