bastion 3.2.2 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,25 @@
1
1
  /**
2
- * @license AngularJS v1.2.9
3
- * (c) 2010-2014 Google, Inc. http://angularjs.org
2
+ * @license AngularJS v1.5.5
3
+ * (c) 2010-2016 Google, Inc. http://angularjs.org
4
4
  * License: MIT
5
5
  */
6
- (function(window, angular, undefined) {'use strict';
6
+ (function(window, angular) {'use strict';
7
+
8
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
9
+ * Any commits to this file should be reviewed with security in mind. *
10
+ * Changes to this file can potentially create security vulnerabilities. *
11
+ * An approval from 2 Core members with history of modifying *
12
+ * this file is required. *
13
+ * *
14
+ * Does the change somehow allow for arbitrary javascript to be executed? *
15
+ * Or allows for someone to change the prototype of built-in objects? *
16
+ * Or gives undesired access to variables likes document or window? *
17
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
7
18
 
8
19
  var $sanitizeMinErr = angular.$$minErr('$sanitize');
9
20
 
10
21
  /**
11
- * @ngdoc overview
22
+ * @ngdoc module
12
23
  * @name ngSanitize
13
24
  * @description
14
25
  *
@@ -16,62 +27,49 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
16
27
  *
17
28
  * The `ngSanitize` module provides functionality to sanitize HTML.
18
29
  *
19
- * {@installModule sanitize}
20
30
  *
21
31
  * <div doc-module-components="ngSanitize"></div>
22
32
  *
23
33
  * See {@link ngSanitize.$sanitize `$sanitize`} for usage.
24
34
  */
25
35
 
26
- /*
27
- * HTML Parser By Misko Hevery (misko@hevery.com)
28
- * based on: HTML Parser By John Resig (ejohn.org)
29
- * Original code by Erik Arvidsson, Mozilla Public License
30
- * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
31
- *
32
- * // Use like so:
33
- * htmlParser(htmlString, {
34
- * start: function(tag, attrs, unary) {},
35
- * end: function(tag) {},
36
- * chars: function(text) {},
37
- * comment: function(text) {}
38
- * });
39
- *
40
- */
41
-
42
-
43
36
  /**
44
37
  * @ngdoc service
45
- * @name ngSanitize.$sanitize
46
- * @function
38
+ * @name $sanitize
39
+ * @kind function
47
40
  *
48
41
  * @description
49
- * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
42
+ * Sanitizes an html string by stripping all potentially dangerous tokens.
43
+ *
44
+ * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
50
45
  * then serialized back to properly escaped html string. This means that no unsafe input can make
51
- * it into the returned string, however, since our parser is more strict than a typical browser
52
- * parser, it's possible that some obscure input, which would be recognized as valid HTML by a
53
- * browser, won't make it through the sanitizer.
54
- * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
55
- * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
46
+ * it into the returned string.
47
+ *
48
+ * The whitelist for URL sanitization of attribute values is configured using the functions
49
+ * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider
50
+ * `$compileProvider`}.
56
51
  *
57
- * @param {string} html Html input.
58
- * @returns {string} Sanitized html.
52
+ * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}.
53
+ *
54
+ * @param {string} html HTML input.
55
+ * @returns {string} Sanitized HTML.
59
56
  *
60
57
  * @example
61
- <doc:example module="ngSanitize">
62
- <doc:source>
58
+ <example module="sanitizeExample" deps="angular-sanitize.js">
59
+ <file name="index.html">
63
60
  <script>
64
- function Ctrl($scope, $sce) {
65
- $scope.snippet =
66
- '<p style="color:blue">an html\n' +
67
- '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
68
- 'snippet</p>';
69
- $scope.deliberatelyTrustDangerousSnippet = function() {
70
- return $sce.trustAsHtml($scope.snippet);
71
- };
72
- }
61
+ angular.module('sanitizeExample', ['ngSanitize'])
62
+ .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
63
+ $scope.snippet =
64
+ '<p style="color:blue">an html\n' +
65
+ '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
66
+ 'snippet</p>';
67
+ $scope.deliberatelyTrustDangerousSnippet = function() {
68
+ return $sce.trustAsHtml($scope.snippet);
69
+ };
70
+ }]);
73
71
  </script>
74
- <div ng-controller="Ctrl">
72
+ <div ng-controller="ExampleController">
75
73
  Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
76
74
  <table>
77
75
  <tr>
@@ -103,48 +101,104 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
103
101
  </tr>
104
102
  </table>
105
103
  </div>
106
- </doc:source>
107
- <doc:scenario>
104
+ </file>
105
+ <file name="protractor.js" type="protractor">
108
106
  it('should sanitize the html snippet by default', function() {
109
- expect(using('#bind-html-with-sanitize').element('div').html()).
107
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
110
108
  toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
111
109
  });
112
110
 
113
111
  it('should inline raw snippet if bound to a trusted value', function() {
114
- expect(using('#bind-html-with-trust').element("div").html()).
112
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
115
113
  toBe("<p style=\"color:blue\">an html\n" +
116
114
  "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
117
115
  "snippet</p>");
118
116
  });
119
117
 
120
118
  it('should escape snippet without any filter', function() {
121
- expect(using('#bind-default').element('div').html()).
119
+ expect(element(by.css('#bind-default div')).getInnerHtml()).
122
120
  toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
123
121
  "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
124
122
  "snippet&lt;/p&gt;");
125
123
  });
126
124
 
127
125
  it('should update', function() {
128
- input('snippet').enter('new <b onclick="alert(1)">text</b>');
129
- expect(using('#bind-html-with-sanitize').element('div').html()).toBe('new <b>text</b>');
130
- expect(using('#bind-html-with-trust').element('div').html()).toBe(
126
+ element(by.model('snippet')).clear();
127
+ element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
128
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
129
+ toBe('new <b>text</b>');
130
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
131
131
  'new <b onclick="alert(1)">text</b>');
132
- expect(using('#bind-default').element('div').html()).toBe(
132
+ expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
133
133
  "new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
134
134
  });
135
- </doc:scenario>
136
- </doc:example>
135
+ </file>
136
+ </example>
137
+ */
138
+
139
+
140
+ /**
141
+ * @ngdoc provider
142
+ * @name $sanitizeProvider
143
+ *
144
+ * @description
145
+ * Creates and configures {@link $sanitize} instance.
137
146
  */
138
147
  function $SanitizeProvider() {
148
+ var svgEnabled = false;
149
+
139
150
  this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
151
+ if (svgEnabled) {
152
+ angular.extend(validElements, svgElements);
153
+ }
140
154
  return function(html) {
141
155
  var buf = [];
142
156
  htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
143
- return !/^unsafe/.test($$sanitizeUri(uri, isImage));
157
+ return !/^unsafe:/.test($$sanitizeUri(uri, isImage));
144
158
  }));
145
159
  return buf.join('');
146
160
  };
147
161
  }];
162
+
163
+
164
+ /**
165
+ * @ngdoc method
166
+ * @name $sanitizeProvider#enableSvg
167
+ * @kind function
168
+ *
169
+ * @description
170
+ * Enables a subset of svg to be supported by the sanitizer.
171
+ *
172
+ * <div class="alert alert-warning">
173
+ * <p>By enabling this setting without taking other precautions, you might expose your
174
+ * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
175
+ * outside of the containing element and be rendered over other elements on the page (e.g. a login
176
+ * link). Such behavior can then result in phishing incidents.</p>
177
+ *
178
+ * <p>To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
179
+ * tags within the sanitized content:</p>
180
+ *
181
+ * <br>
182
+ *
183
+ * <pre><code>
184
+ * .rootOfTheIncludedContent svg {
185
+ * overflow: hidden !important;
186
+ * }
187
+ * </code></pre>
188
+ * </div>
189
+ *
190
+ * @param {boolean=} regexp New regexp to whitelist urls with.
191
+ * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called
192
+ * without an argument or self for chaining otherwise.
193
+ */
194
+ this.enableSvg = function(enableSvg) {
195
+ if (angular.isDefined(enableSvg)) {
196
+ svgEnabled = enableSvg;
197
+ return this;
198
+ } else {
199
+ return svgEnabled;
200
+ }
201
+ };
148
202
  }
149
203
 
150
204
  function sanitizeText(chars) {
@@ -156,17 +210,9 @@ function sanitizeText(chars) {
156
210
 
157
211
 
158
212
  // Regular Expressions for parsing tags and attributes
159
- var START_TAG_REGEXP =
160
- /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,
161
- END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/,
162
- ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
163
- BEGIN_TAG_REGEXP = /^</,
164
- BEGING_END_TAGE_REGEXP = /^<\s*\//,
165
- COMMENT_REGEXP = /<!--(.*?)-->/g,
166
- DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
167
- CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
213
+ var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
168
214
  // Match everything outside of normal chars and " (quote character)
169
- NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
215
+ NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g;
170
216
 
171
217
 
172
218
  // Good source of info about elements and attributes
@@ -175,29 +221,36 @@ var START_TAG_REGEXP =
175
221
 
176
222
  // Safe Void Elements - HTML5
177
223
  // http://dev.w3.org/html5/spec/Overview.html#void-elements
178
- var voidElements = makeMap("area,br,col,hr,img,wbr");
224
+ var voidElements = toMap("area,br,col,hr,img,wbr");
179
225
 
180
226
  // Elements that you can, intentionally, leave open (and which close themselves)
181
227
  // http://dev.w3.org/html5/spec/Overview.html#optional-tags
182
- var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
183
- optionalEndTagInlineElements = makeMap("rp,rt"),
228
+ var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
229
+ optionalEndTagInlineElements = toMap("rp,rt"),
184
230
  optionalEndTagElements = angular.extend({},
185
231
  optionalEndTagInlineElements,
186
232
  optionalEndTagBlockElements);
187
233
 
188
234
  // Safe Block Elements - HTML5
189
- var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
235
+ var blockElements = angular.extend({}, optionalEndTagBlockElements, toMap("address,article," +
190
236
  "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
191
- "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
237
+ "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul"));
192
238
 
193
239
  // Inline Elements - HTML5
194
- var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
240
+ var inlineElements = angular.extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," +
195
241
  "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
196
242
  "samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
197
243
 
244
+ // SVG Elements
245
+ // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
246
+ // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
247
+ // They can potentially allow for arbitrary javascript to be executed. See #11290
248
+ var svgElements = toMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," +
249
+ "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," +
250
+ "radialGradient,rect,stop,svg,switch,text,title,tspan");
198
251
 
199
- // Special Elements (can contain anything)
200
- var specialElements = makeMap("script,style");
252
+ // Blocked Elements (will be stripped)
253
+ var blockedElements = toMap("script,style");
201
254
 
202
255
  var validElements = angular.extend({},
203
256
  voidElements,
@@ -206,25 +259,71 @@ var validElements = angular.extend({},
206
259
  optionalEndTagElements);
207
260
 
208
261
  //Attributes that have href and hence need to be sanitized
209
- var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
210
- var validAttrs = angular.extend({}, uriAttrs, makeMap(
211
- 'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
212
- 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
213
- 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
214
- 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
215
- 'valign,value,vspace,width'));
216
-
217
- function makeMap(str) {
262
+ var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href");
263
+
264
+ var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
265
+ 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
266
+ 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
267
+ 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
268
+ 'valign,value,vspace,width');
269
+
270
+ // SVG attributes (without "id" and "name" attributes)
271
+ // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
272
+ var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
273
+ 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
274
+ 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
275
+ 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
276
+ 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' +
277
+ 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' +
278
+ 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' +
279
+ 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' +
280
+ 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' +
281
+ 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' +
282
+ 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' +
283
+ 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' +
284
+ 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' +
285
+ 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' +
286
+ 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true);
287
+
288
+ var validAttrs = angular.extend({},
289
+ uriAttrs,
290
+ svgAttrs,
291
+ htmlAttrs);
292
+
293
+ function toMap(str, lowercaseKeys) {
218
294
  var obj = {}, items = str.split(','), i;
219
- for (i = 0; i < items.length; i++) obj[items[i]] = true;
295
+ for (i = 0; i < items.length; i++) {
296
+ obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true;
297
+ }
220
298
  return obj;
221
299
  }
222
300
 
301
+ var inertBodyElement;
302
+ (function(window) {
303
+ var doc;
304
+ if (window.document && window.document.implementation) {
305
+ doc = window.document.implementation.createHTMLDocument("inert");
306
+ } else {
307
+ throw $sanitizeMinErr('noinert', "Can't create an inert html document");
308
+ }
309
+ var docElement = doc.documentElement || doc.getDocumentElement();
310
+ var bodyElements = docElement.getElementsByTagName('body');
311
+
312
+ // usually there should be only one body element in the document, but IE doesn't have any, so we need to create one
313
+ if (bodyElements.length === 1) {
314
+ inertBodyElement = bodyElements[0];
315
+ } else {
316
+ var html = doc.createElement('html');
317
+ inertBodyElement = doc.createElement('body');
318
+ html.appendChild(inertBodyElement);
319
+ doc.appendChild(html);
320
+ }
321
+ })(window);
223
322
 
224
323
  /**
225
324
  * @example
226
325
  * htmlParser(htmlString, {
227
- * start: function(tag, attrs, unary) {},
326
+ * start: function(tag, attrs) {},
228
327
  * end: function(tag) {},
229
328
  * chars: function(text) {},
230
329
  * comment: function(text) {}
@@ -233,177 +332,92 @@ function makeMap(str) {
233
332
  * @param {string} html string
234
333
  * @param {object} handler
235
334
  */
236
- function htmlParser( html, handler ) {
237
- var index, chars, match, stack = [], last = html;
238
- stack.last = function() { return stack[ stack.length - 1 ]; };
239
-
240
- while ( html ) {
241
- chars = true;
242
-
243
- // Make sure we're not in a script or style element
244
- if ( !stack.last() || !specialElements[ stack.last() ] ) {
245
-
246
- // Comment
247
- if ( html.indexOf("<!--") === 0 ) {
248
- // comments containing -- are not allowed unless they terminate the comment
249
- index = html.indexOf("--", 4);
250
-
251
- if ( index >= 0 && html.lastIndexOf("-->", index) === index) {
252
- if (handler.comment) handler.comment( html.substring( 4, index ) );
253
- html = html.substring( index + 3 );
254
- chars = false;
255
- }
256
- // DOCTYPE
257
- } else if ( DOCTYPE_REGEXP.test(html) ) {
258
- match = html.match( DOCTYPE_REGEXP );
259
-
260
- if ( match ) {
261
- html = html.replace( match[0] , '');
262
- chars = false;
263
- }
264
- // end tag
265
- } else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
266
- match = html.match( END_TAG_REGEXP );
267
-
268
- if ( match ) {
269
- html = html.substring( match[0].length );
270
- match[0].replace( END_TAG_REGEXP, parseEndTag );
271
- chars = false;
272
- }
273
-
274
- // start tag
275
- } else if ( BEGIN_TAG_REGEXP.test(html) ) {
276
- match = html.match( START_TAG_REGEXP );
277
-
278
- if ( match ) {
279
- html = html.substring( match[0].length );
280
- match[0].replace( START_TAG_REGEXP, parseStartTag );
281
- chars = false;
282
- }
283
- }
284
-
285
- if ( chars ) {
286
- index = html.indexOf("<");
287
-
288
- var text = index < 0 ? html : html.substring( 0, index );
289
- html = index < 0 ? "" : html.substring( index );
290
-
291
- if (handler.chars) handler.chars( decodeEntities(text) );
292
- }
293
-
294
- } else {
295
- html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
296
- function(all, text){
297
- text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
298
-
299
- if (handler.chars) handler.chars( decodeEntities(text) );
300
-
301
- return "";
302
- });
335
+ function htmlParser(html, handler) {
336
+ if (html === null || html === undefined) {
337
+ html = '';
338
+ } else if (typeof html !== 'string') {
339
+ html = '' + html;
340
+ }
341
+ inertBodyElement.innerHTML = html;
303
342
 
304
- parseEndTag( "", stack.last() );
343
+ //mXSS protection
344
+ var mXSSAttempts = 5;
345
+ do {
346
+ if (mXSSAttempts === 0) {
347
+ throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable");
305
348
  }
349
+ mXSSAttempts--;
306
350
 
307
- if ( html == last ) {
308
- throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
309
- "of html: {0}", html);
351
+ // strip custom-namespaced attributes on IE<=11
352
+ if (window.document.documentMode) {
353
+ stripCustomNsAttrs(inertBodyElement);
310
354
  }
311
- last = html;
312
- }
313
-
314
- // Clean up any remaining tags
315
- parseEndTag();
316
-
317
- function parseStartTag( tag, tagName, rest, unary ) {
318
- tagName = angular.lowercase(tagName);
319
- if ( blockElements[ tagName ] ) {
320
- while ( stack.last() && inlineElements[ stack.last() ] ) {
321
- parseEndTag( "", stack.last() );
322
- }
355
+ html = inertBodyElement.innerHTML; //trigger mXSS
356
+ inertBodyElement.innerHTML = html;
357
+ } while (html !== inertBodyElement.innerHTML);
358
+
359
+ var node = inertBodyElement.firstChild;
360
+ while (node) {
361
+ switch (node.nodeType) {
362
+ case 1: // ELEMENT_NODE
363
+ handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes));
364
+ break;
365
+ case 3: // TEXT NODE
366
+ handler.chars(node.textContent);
367
+ break;
323
368
  }
324
369
 
325
- if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) {
326
- parseEndTag( "", tagName );
370
+ var nextNode;
371
+ if (!(nextNode = node.firstChild)) {
372
+ if (node.nodeType == 1) {
373
+ handler.end(node.nodeName.toLowerCase());
374
+ }
375
+ nextNode = node.nextSibling;
376
+ if (!nextNode) {
377
+ while (nextNode == null) {
378
+ node = node.parentNode;
379
+ if (node === inertBodyElement) break;
380
+ nextNode = node.nextSibling;
381
+ if (node.nodeType == 1) {
382
+ handler.end(node.nodeName.toLowerCase());
383
+ }
384
+ }
385
+ }
327
386
  }
328
-
329
- unary = voidElements[ tagName ] || !!unary;
330
-
331
- if ( !unary )
332
- stack.push( tagName );
333
-
334
- var attrs = {};
335
-
336
- rest.replace(ATTR_REGEXP,
337
- function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
338
- var value = doubleQuotedValue
339
- || singleQuotedValue
340
- || unquotedValue
341
- || '';
342
-
343
- attrs[name] = decodeEntities(value);
344
- });
345
- if (handler.start) handler.start( tagName, attrs, unary );
387
+ node = nextNode;
346
388
  }
347
389
 
348
- function parseEndTag( tag, tagName ) {
349
- var pos = 0, i;
350
- tagName = angular.lowercase(tagName);
351
- if ( tagName )
352
- // Find the closest opened tag of the same type
353
- for ( pos = stack.length - 1; pos >= 0; pos-- )
354
- if ( stack[ pos ] == tagName )
355
- break;
356
-
357
- if ( pos >= 0 ) {
358
- // Close all the open elements, up the stack
359
- for ( i = stack.length - 1; i >= pos; i-- )
360
- if (handler.end) handler.end( stack[ i ] );
361
-
362
- // Remove the open elements from the stack
363
- stack.length = pos;
364
- }
390
+ while (node = inertBodyElement.firstChild) {
391
+ inertBodyElement.removeChild(node);
365
392
  }
366
393
  }
367
394
 
368
- var hiddenPre=document.createElement("pre");
369
- var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
370
- /**
371
- * decodes all entities into regular string
372
- * @param value
373
- * @returns {string} A string with decoded entities.
374
- */
375
- function decodeEntities(value) {
376
- if (!value) { return ''; }
377
-
378
- // Note: IE8 does not preserve spaces at the start/end of innerHTML
379
- // so we must capture them and reattach them afterward
380
- var parts = spaceRe.exec(value);
381
- var spaceBefore = parts[1];
382
- var spaceAfter = parts[3];
383
- var content = parts[2];
384
- if (content) {
385
- hiddenPre.innerHTML=content.replace(/</g,"&lt;");
386
- // innerText depends on styling as it doesn't display hidden elements.
387
- // Therefore, it's better to use textContent not to cause unnecessary
388
- // reflows. However, IE<9 don't support textContent so the innerText
389
- // fallback is necessary.
390
- content = 'textContent' in hiddenPre ?
391
- hiddenPre.textContent : hiddenPre.innerText;
395
+ function attrToMap(attrs) {
396
+ var map = {};
397
+ for (var i = 0, ii = attrs.length; i < ii; i++) {
398
+ var attr = attrs[i];
399
+ map[attr.name] = attr.value;
392
400
  }
393
- return spaceBefore + content + spaceAfter;
401
+ return map;
394
402
  }
395
403
 
404
+
396
405
  /**
397
406
  * Escapes all potentially dangerous characters, so that the
398
407
  * resulting string can be safely inserted into attribute or
399
408
  * element text.
400
409
  * @param value
401
- * @returns escaped text
410
+ * @returns {string} escaped text
402
411
  */
403
412
  function encodeEntities(value) {
404
413
  return value.
405
414
  replace(/&/g, '&amp;').
406
- replace(NON_ALPHANUMERIC_REGEXP, function(value){
415
+ replace(SURROGATE_PAIR_REGEXP, function(value) {
416
+ var hi = value.charCodeAt(0);
417
+ var low = value.charCodeAt(1);
418
+ return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
419
+ }).
420
+ replace(NON_ALPHANUMERIC_REGEXP, function(value) {
407
421
  return '&#' + value.charCodeAt(0) + ';';
408
422
  }).
409
423
  replace(/</g, '&lt;').
@@ -412,27 +426,27 @@ function encodeEntities(value) {
412
426
 
413
427
  /**
414
428
  * create an HTML/XML writer which writes to buffer
415
- * @param {Array} buf use buf.jain('') to get out sanitized html string
429
+ * @param {Array} buf use buf.join('') to get out sanitized html string
416
430
  * @returns {object} in the form of {
417
- * start: function(tag, attrs, unary) {},
431
+ * start: function(tag, attrs) {},
418
432
  * end: function(tag) {},
419
433
  * chars: function(text) {},
420
434
  * comment: function(text) {}
421
435
  * }
422
436
  */
423
- function htmlSanitizeWriter(buf, uriValidator){
424
- var ignore = false;
437
+ function htmlSanitizeWriter(buf, uriValidator) {
438
+ var ignoreCurrentElement = false;
425
439
  var out = angular.bind(buf, buf.push);
426
440
  return {
427
- start: function(tag, attrs, unary){
441
+ start: function(tag, attrs) {
428
442
  tag = angular.lowercase(tag);
429
- if (!ignore && specialElements[tag]) {
430
- ignore = tag;
443
+ if (!ignoreCurrentElement && blockedElements[tag]) {
444
+ ignoreCurrentElement = tag;
431
445
  }
432
- if (!ignore && validElements[tag] === true) {
446
+ if (!ignoreCurrentElement && validElements[tag] === true) {
433
447
  out('<');
434
448
  out(tag);
435
- angular.forEach(attrs, function(value, key){
449
+ angular.forEach(attrs, function(value, key) {
436
450
  var lkey=angular.lowercase(key);
437
451
  var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
438
452
  if (validAttrs[lkey] === true &&
@@ -444,29 +458,63 @@ function htmlSanitizeWriter(buf, uriValidator){
444
458
  out('"');
445
459
  }
446
460
  });
447
- out(unary ? '/>' : '>');
461
+ out('>');
448
462
  }
449
463
  },
450
- end: function(tag){
451
- tag = angular.lowercase(tag);
452
- if (!ignore && validElements[tag] === true) {
453
- out('</');
454
- out(tag);
455
- out('>');
456
- }
457
- if (tag == ignore) {
458
- ignore = false;
459
- }
460
- },
461
- chars: function(chars){
462
- if (!ignore) {
463
- out(encodeEntities(chars));
464
- }
464
+ end: function(tag) {
465
+ tag = angular.lowercase(tag);
466
+ if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) {
467
+ out('</');
468
+ out(tag);
469
+ out('>');
465
470
  }
471
+ if (tag == ignoreCurrentElement) {
472
+ ignoreCurrentElement = false;
473
+ }
474
+ },
475
+ chars: function(chars) {
476
+ if (!ignoreCurrentElement) {
477
+ out(encodeEntities(chars));
478
+ }
479
+ }
466
480
  };
467
481
  }
468
482
 
469
483
 
484
+ /**
485
+ * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare
486
+ * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want
487
+ * to allow any of these custom attributes. This method strips them all.
488
+ *
489
+ * @param node Root element to process
490
+ */
491
+ function stripCustomNsAttrs(node) {
492
+ if (node.nodeType === window.Node.ELEMENT_NODE) {
493
+ var attrs = node.attributes;
494
+ for (var i = 0, l = attrs.length; i < l; i++) {
495
+ var attrNode = attrs[i];
496
+ var attrName = attrNode.name.toLowerCase();
497
+ if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
498
+ node.removeAttributeNode(attrNode);
499
+ i--;
500
+ l--;
501
+ }
502
+ }
503
+ }
504
+
505
+ var nextNode = node.firstChild;
506
+ if (nextNode) {
507
+ stripCustomNsAttrs(nextNode);
508
+ }
509
+
510
+ nextNode = node.nextSibling;
511
+ if (nextNode) {
512
+ stripCustomNsAttrs(nextNode);
513
+ }
514
+ }
515
+
516
+
517
+
470
518
  // define ngSanitize module and register $sanitize service
471
519
  angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
472
520
 
@@ -474,43 +522,43 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
474
522
 
475
523
  /**
476
524
  * @ngdoc filter
477
- * @name ngSanitize.filter:linky
478
- * @function
525
+ * @name linky
526
+ * @kind function
479
527
  *
480
528
  * @description
481
- * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
529
+ * Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and
482
530
  * plain email address links.
483
531
  *
484
532
  * Requires the {@link ngSanitize `ngSanitize`} module to be installed.
485
533
  *
486
534
  * @param {string} text Input text.
487
- * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
488
- * @returns {string} Html-linkified text.
535
+ * @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in.
536
+ * @param {object|function(url)} [attributes] Add custom attributes to the link element.
537
+ *
538
+ * Can be one of:
539
+ *
540
+ * - `object`: A map of attributes
541
+ * - `function`: Takes the url as a parameter and returns a map of attributes
542
+ *
543
+ * If the map of attributes contains a value for `target`, it overrides the value of
544
+ * the target parameter.
545
+ *
546
+ *
547
+ * @returns {string} Html-linkified and {@link $sanitize sanitized} text.
489
548
  *
490
549
  * @usage
491
550
  <span ng-bind-html="linky_expression | linky"></span>
492
551
  *
493
552
  * @example
494
- <doc:example module="ngSanitize">
495
- <doc:source>
496
- <script>
497
- function Ctrl($scope) {
498
- $scope.snippet =
499
- 'Pretty text with some links:\n'+
500
- 'http://angularjs.org/,\n'+
501
- 'mailto:us@somewhere.org,\n'+
502
- 'another@somewhere.org,\n'+
503
- 'and one more: ftp://127.0.0.1/.';
504
- $scope.snippetWithTarget = 'http://angularjs.org/';
505
- }
506
- </script>
507
- <div ng-controller="Ctrl">
553
+ <example module="linkyExample" deps="angular-sanitize.js">
554
+ <file name="index.html">
555
+ <div ng-controller="ExampleController">
508
556
  Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
509
557
  <table>
510
558
  <tr>
511
- <td>Filter</td>
512
- <td>Source</td>
513
- <td>Rendered</td>
559
+ <th>Filter</th>
560
+ <th>Source</th>
561
+ <th>Rendered</th>
514
562
  </tr>
515
563
  <tr id="linky-filter">
516
564
  <td>linky filter</td>
@@ -524,10 +572,19 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
524
572
  <tr id="linky-target">
525
573
  <td>linky target</td>
526
574
  <td>
527
- <pre>&lt;div ng-bind-html="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
575
+ <pre>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
528
576
  </td>
529
577
  <td>
530
- <div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
578
+ <div ng-bind-html="snippetWithSingleURL | linky:'_blank'"></div>
579
+ </td>
580
+ </tr>
581
+ <tr id="linky-custom-attributes">
582
+ <td>linky custom attributes</td>
583
+ <td>
584
+ <pre>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"&gt;<br>&lt;/div&gt;</pre>
585
+ </td>
586
+ <td>
587
+ <div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"></div>
531
588
  </td>
532
589
  </tr>
533
590
  <tr id="escaped-html">
@@ -536,47 +593,72 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
536
593
  <td><div ng-bind="snippet"></div></td>
537
594
  </tr>
538
595
  </table>
539
- </doc:source>
540
- <doc:scenario>
596
+ </file>
597
+ <file name="script.js">
598
+ angular.module('linkyExample', ['ngSanitize'])
599
+ .controller('ExampleController', ['$scope', function($scope) {
600
+ $scope.snippet =
601
+ 'Pretty text with some links:\n'+
602
+ 'http://angularjs.org/,\n'+
603
+ 'mailto:us@somewhere.org,\n'+
604
+ 'another@somewhere.org,\n'+
605
+ 'and one more: ftp://127.0.0.1/.';
606
+ $scope.snippetWithSingleURL = 'http://angularjs.org/';
607
+ }]);
608
+ </file>
609
+ <file name="protractor.js" type="protractor">
541
610
  it('should linkify the snippet with urls', function() {
542
- expect(using('#linky-filter').binding('snippet | linky')).
543
- toBe('Pretty text with some links:&#10;' +
544
- '<a href="http://angularjs.org/">http://angularjs.org/</a>,&#10;' +
545
- '<a href="mailto:us@somewhere.org">us@somewhere.org</a>,&#10;' +
546
- '<a href="mailto:another@somewhere.org">another@somewhere.org</a>,&#10;' +
547
- 'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.');
611
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
612
+ toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
613
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
614
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
548
615
  });
549
616
 
550
- it ('should not linkify snippet without the linky filter', function() {
551
- expect(using('#escaped-html').binding('snippet')).
552
- toBe("Pretty text with some links:\n" +
553
- "http://angularjs.org/,\n" +
554
- "mailto:us@somewhere.org,\n" +
555
- "another@somewhere.org,\n" +
556
- "and one more: ftp://127.0.0.1/.");
617
+ it('should not linkify snippet without the linky filter', function() {
618
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
619
+ toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
620
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
621
+ expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
557
622
  });
558
623
 
559
624
  it('should update', function() {
560
- input('snippet').enter('new http://link.');
561
- expect(using('#linky-filter').binding('snippet | linky')).
562
- toBe('new <a href="http://link">http://link</a>.');
563
- expect(using('#escaped-html').binding('snippet')).toBe('new http://link.');
625
+ element(by.model('snippet')).clear();
626
+ element(by.model('snippet')).sendKeys('new http://link.');
627
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
628
+ toBe('new http://link.');
629
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
630
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
631
+ .toBe('new http://link.');
564
632
  });
565
633
 
566
634
  it('should work with the target property', function() {
567
- expect(using('#linky-target').binding("snippetWithTarget | linky:'_blank'")).
568
- toBe('<a target="_blank" href="http://angularjs.org/">http://angularjs.org/</a>');
635
+ expect(element(by.id('linky-target')).
636
+ element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
637
+ toBe('http://angularjs.org/');
638
+ expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
569
639
  });
570
- </doc:scenario>
571
- </doc:example>
640
+
641
+ it('should optionally add custom attributes', function() {
642
+ expect(element(by.id('linky-custom-attributes')).
643
+ element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
644
+ toBe('http://angularjs.org/');
645
+ expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
646
+ });
647
+ </file>
648
+ </example>
572
649
  */
573
650
  angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
574
651
  var LINKY_URL_REGEXP =
575
- /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,
576
- MAILTO_REGEXP = /^mailto:/;
652
+ /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
653
+ MAILTO_REGEXP = /^mailto:/i;
654
+
655
+ var linkyMinErr = angular.$$minErr('linky');
656
+ var isString = angular.isString;
657
+
658
+ return function(text, target, attributes) {
659
+ if (text == null || text === '') return text;
660
+ if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
577
661
 
578
- return function(text, target) {
579
- if (!text) return text;
580
662
  var match;
581
663
  var raw = text;
582
664
  var html = [];
@@ -585,8 +667,10 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
585
667
  while ((match = raw.match(LINKY_URL_REGEXP))) {
586
668
  // We can not end in these as they are sometimes found at the end of the sentence
587
669
  url = match[0];
588
- // if we did not match ftp/http/mailto then assume mailto
589
- if (match[2] == match[3]) url = 'mailto:' + url;
670
+ // if we did not match ftp/http/www/mailto then assume mailto
671
+ if (!match[2] && !match[4]) {
672
+ url = (match[3] ? 'http://' : 'mailto:') + url;
673
+ }
590
674
  i = match.index;
591
675
  addText(raw.substr(0, i));
592
676
  addLink(url, match[0].replace(MAILTO_REGEXP, ''));
@@ -603,15 +687,26 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
603
687
  }
604
688
 
605
689
  function addLink(url, text) {
690
+ var key;
606
691
  html.push('<a ');
607
- if (angular.isDefined(target)) {
608
- html.push('target="');
609
- html.push(target);
610
- html.push('" ');
692
+ if (angular.isFunction(attributes)) {
693
+ attributes = attributes(url);
694
+ }
695
+ if (angular.isObject(attributes)) {
696
+ for (key in attributes) {
697
+ html.push(key + '="' + attributes[key] + '" ');
698
+ }
699
+ } else {
700
+ attributes = {};
701
+ }
702
+ if (angular.isDefined(target) && !('target' in attributes)) {
703
+ html.push('target="',
704
+ target,
705
+ '" ');
611
706
  }
612
- html.push('href="');
613
- html.push(url);
614
- html.push('">');
707
+ html.push('href="',
708
+ url.replace(/"/g, '&quot;'),
709
+ '">');
615
710
  addText(text);
616
711
  html.push('</a>');
617
712
  }