bastion 3.2.2 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  }