diff --git a/webclient/js/angular-sanitize.js b/webclient/js/angular-sanitize.js
index d34522ac8d..ec46895f68 100644
--- a/webclient/js/angular-sanitize.js
+++ b/webclient/js/angular-sanitize.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.2.0
- * (c) 2010-2012 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.3.0-rc.1
+ * (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
@@ -8,7 +8,7 @@
var $sanitizeMinErr = angular.$$minErr('$sanitize');
/**
- * @ngdoc overview
+ * @ngdoc module
* @name ngSanitize
* @description
*
@@ -16,7 +16,6 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
*
* The `ngSanitize` module provides functionality to sanitize HTML.
*
- * {@installModule sanitize}
*
* <div doc-module-components="ngSanitize"></div>
*
@@ -42,8 +41,8 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
/**
* @ngdoc service
- * @name ngSanitize.$sanitize
- * @function
+ * @name $sanitize
+ * @kind function
*
* @description
* The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
@@ -51,25 +50,28 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
* 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 whitelist is configured using the functions `aHrefSanitizationWhitelist` and
+ * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
*
* @param {string} html Html input.
* @returns {string} Sanitized html.
*
* @example
- <doc:example module="ngSanitize">
- <doc:source>
+ <example module="sanitizeExample" deps="angular-sanitize.js">
+ <file name="index.html">
<script>
- function Ctrl($scope, $sce) {
- $scope.snippet =
- '<p style="color:blue">an html\n' +
- '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
- 'snippet</p>';
- $scope.deliberatelyTrustDangerousSnippet = function() {
- return $sce.trustAsHtml($scope.snippet);
- };
- }
+ angular.module('sanitizeExample', ['ngSanitize'])
+ .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
+ $scope.snippet =
+ '<p style="color:blue">an html\n' +
+ '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
+ 'snippet</p>';
+ $scope.deliberatelyTrustDangerousSnippet = function() {
+ return $sce.trustAsHtml($scope.snippet);
+ };
+ }]);
</script>
- <div ng-controller="Ctrl">
+ <div ng-controller="ExampleController">
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
@@ -101,56 +103,71 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
</tr>
</table>
</div>
- </doc:source>
- <doc:scenario>
+ </file>
+ <file name="protractor.js" type="protractor">
it('should sanitize the html snippet by default', function() {
- expect(using('#bind-html-with-sanitize').element('div').html()).
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
});
it('should inline raw snippet if bound to a trusted value', function() {
- expect(using('#bind-html-with-trust').element("div").html()).
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
toBe("<p style=\"color:blue\">an html\n" +
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
"snippet</p>");
});
it('should escape snippet without any filter', function() {
- expect(using('#bind-default').element('div').html()).
+ expect(element(by.css('#bind-default div')).getInnerHtml()).
toBe("<p style=\"color:blue\">an html\n" +
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
"snippet</p>");
});
it('should update', function() {
- input('snippet').enter('new <b onclick="alert(1)">text</b>');
- expect(using('#bind-html-with-sanitize').element('div').html()).toBe('new <b>text</b>');
- expect(using('#bind-html-with-trust').element('div').html()).toBe(
+ element(by.model('snippet')).clear();
+ element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
+ expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
+ toBe('new <b>text</b>');
+ expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
'new <b onclick="alert(1)">text</b>');
- expect(using('#bind-default').element('div').html()).toBe(
+ expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
"new <b onclick=\"alert(1)\">text</b>");
});
- </doc:scenario>
- </doc:example>
+ </file>
+ </example>
*/
-var $sanitize = function(html) {
+function $SanitizeProvider() {
+ this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
+ return function(html) {
+ var buf = [];
+ htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
+ return !/^unsafe/.test($$sanitizeUri(uri, isImage));
+ }));
+ return buf.join('');
+ };
+ }];
+}
+
+function sanitizeText(chars) {
var buf = [];
- htmlParser(html, htmlSanitizeWriter(buf));
- return buf.join('');
-};
+ var writer = htmlSanitizeWriter(buf, angular.noop);
+ writer.chars(chars);
+ return buf.join('');
+}
// Regular Expressions for parsing tags and attributes
var START_TAG_REGEXP =
- /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,
- END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/,
+ /^<((?:[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 = /^<\s*\//,
+ BEGING_END_TAGE_REGEXP = /^<\//,
COMMENT_REGEXP = /<!--(.*?)-->/g,
DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
- URI_REGEXP = /^((ftp|https?):\/\/|mailto:|tel:|#)/i,
+ SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
// Match everything outside of normal chars and " (quote character)
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
@@ -197,7 +214,7 @@ var validAttrs = angular.extend({}, uriAttrs, makeMap(
'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,span,start,summary,target,title,type,'+
+ 'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
'valign,value,vspace,width'));
function makeMap(str) {
@@ -220,10 +237,18 @@ function makeMap(str) {
* @param {object} handler
*/
function htmlParser( html, handler ) {
- var index, chars, match, stack = [], last = html;
+ if (typeof html !== 'string') {
+ if (html === null || typeof html === 'undefined') {
+ html = '';
+ } else {
+ html = '' + html;
+ }
+ }
+ var index, chars, match, stack = [], last = html, text;
stack.last = function() { return stack[ stack.length - 1 ]; };
while ( html ) {
+ text = '';
chars = true;
// Make sure we're not in a script or style element
@@ -244,7 +269,7 @@ function htmlParser( html, handler ) {
match = html.match( DOCTYPE_REGEXP );
if ( match ) {
- html = html.replace( match[0] , '');
+ html = html.replace( match[0], '');
chars = false;
}
// end tag
@@ -262,16 +287,23 @@ function htmlParser( html, handler ) {
match = html.match( START_TAG_REGEXP );
if ( match ) {
- html = html.substring( match[0].length );
- match[0].replace( START_TAG_REGEXP, parseStartTag );
+ // 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 );
+ }
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("<");
- var text = index < 0 ? html : html.substring( 0, index );
+ text += index < 0 ? html : html.substring( 0, index );
html = index < 0 ? "" : html.substring( index );
if (handler.chars) handler.chars( decodeEntities(text) );
@@ -351,15 +383,32 @@ function htmlParser( html, handler ) {
}
}
+var hiddenPre=document.createElement("pre");
+var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
/**
* decodes all entities into regular string
* @param value
* @returns {string} A string with decoded entities.
*/
-var hiddenPre=document.createElement("pre");
function decodeEntities(value) {
- hiddenPre.innerHTML=value.replace(/</g,"<");
- return hiddenPre.innerText || hiddenPre.textContent || '';
+ if (!value) { return ''; }
+
+ // Note: IE8 does not preserve spaces at the start/end of innerHTML
+ // so we must capture them and reattach them afterward
+ var parts = spaceRe.exec(value);
+ var spaceBefore = parts[1];
+ var spaceAfter = parts[3];
+ var content = parts[2];
+ if (content) {
+ hiddenPre.innerHTML=content.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. However, IE<9 don't support textContent so the innerText
+ // fallback is necessary.
+ content = 'textContent' in hiddenPre ?
+ hiddenPre.textContent : hiddenPre.innerText;
+ }
+ return spaceBefore + content + spaceAfter;
}
/**
@@ -367,11 +416,16 @@ function decodeEntities(value) {
* resulting string can be safely inserted into attribute or
* element text.
* @param value
- * @returns escaped text
+ * @returns {string} escaped text
*/
function encodeEntities(value) {
return value.
replace(/&/g, '&').
+ replace(SURROGATE_PAIR_REGEXP, function (value) {
+ var hi = value.charCodeAt(0);
+ var low = value.charCodeAt(1);
+ return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
+ }).
replace(NON_ALPHANUMERIC_REGEXP, function(value){
return '&#' + value.charCodeAt(0) + ';';
}).
@@ -389,7 +443,7 @@ function encodeEntities(value) {
* comment: function(text) {}
* }
*/
-function htmlSanitizeWriter(buf){
+function htmlSanitizeWriter(buf, uriValidator){
var ignore = false;
var out = angular.bind(buf, buf.push);
return {
@@ -403,7 +457,9 @@ function htmlSanitizeWriter(buf){
out(tag);
angular.forEach(attrs, function(value, key){
var lkey=angular.lowercase(key);
- if (validAttrs[lkey]===true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) {
+ var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
+ if (validAttrs[lkey] === true &&
+ (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
out(' ');
out(key);
out('="');
@@ -435,14 +491,14 @@ function htmlSanitizeWriter(buf){
// define ngSanitize module and register $sanitize service
-angular.module('ngSanitize', []).value('$sanitize', $sanitize);
+angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
-/* global htmlSanitizeWriter: false */
+/* global sanitizeText: false */
/**
* @ngdoc filter
- * @name ngSanitize.filter:linky
- * @function
+ * @name linky
+ * @kind function
*
* @description
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
@@ -458,20 +514,21 @@ angular.module('ngSanitize', []).value('$sanitize', $sanitize);
<span ng-bind-html="linky_expression | linky"></span>
*
* @example
- <doc:example module="ngSanitize">
- <doc:source>
+ <example module="linkyExample" deps="angular-sanitize.js">
+ <file name="index.html">
<script>
- function Ctrl($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/';
- }
+ 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="Ctrl">
+ <div ng-controller="ExampleController">
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
@@ -503,43 +560,44 @@ angular.module('ngSanitize', []).value('$sanitize', $sanitize);
<td><div ng-bind="snippet"></div></td>
</tr>
</table>
- </doc:source>
- <doc:scenario>
+ </file>
+ <file name="protractor.js" type="protractor">
it('should linkify the snippet with urls', function() {
- expect(using('#linky-filter').binding('snippet | linky')).
- toBe('Pretty text with some links: ' +
- '<a href="http://angularjs.org/">http://angularjs.org/</a>, ' +
- '<a href="mailto:us@somewhere.org">us@somewhere.org</a>, ' +
- '<a href="mailto:another@somewhere.org">another@somewhere.org</a>, ' +
- 'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.');
+ 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/.');
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
});
- it ('should not linkify snippet without the linky filter', function() {
- expect(using('#escaped-html').binding('snippet')).
- toBe("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/.");
+ it('should not linkify snippet without the linky filter', function() {
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
+ toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
+ 'another@somewhere.org, and one more: ftp://127.0.0.1/.');
+ expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
});
it('should update', function() {
- input('snippet').enter('new http://link.');
- expect(using('#linky-filter').binding('snippet | linky')).
- toBe('new <a href="http://link">http://link</a>.');
- expect(using('#escaped-html').binding('snippet')).toBe('new http://link.');
+ element(by.model('snippet')).clear();
+ element(by.model('snippet')).sendKeys('new http://link.');
+ expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
+ toBe('new http://link.');
+ expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
+ expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
+ .toBe('new http://link.');
});
it('should work with the target property', function() {
- expect(using('#linky-target').binding("snippetWithTarget | linky:'_blank'")).
- toBe('<a target="_blank" href="http://angularjs.org/">http://angularjs.org/</a>');
+ expect(element(by.id('linky-target')).
+ element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
+ toBe('http://angularjs.org/');
+ expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
});
- </doc:scenario>
- </doc:example>
+ </file>
+ </example>
*/
-angular.module('ngSanitize').filter('linky', function() {
+angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
var LINKY_URL_REGEXP =
- /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/,
+ /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,
MAILTO_REGEXP = /^mailto:/;
return function(text, target) {
@@ -547,31 +605,43 @@ angular.module('ngSanitize').filter('linky', function() {
var match;
var raw = text;
var html = [];
- // TODO(vojta): use $sanitize instead
- var writer = htmlSanitizeWriter(html);
var url;
var i;
- var properties = {};
- if (angular.isDefined(target)) {
- properties.target = target;
- }
while ((match = raw.match(LINKY_URL_REGEXP))) {
// We can not end in these as they are sometimes found at the end of the sentence
url = match[0];
// if we did not match ftp/http/mailto then assume mailto
if (match[2] == match[3]) url = 'mailto:' + url;
i = match.index;
- writer.chars(raw.substr(0, i));
- properties.href = url;
- writer.start('a', properties);
- writer.chars(match[0].replace(MAILTO_REGEXP, ''));
- writer.end('a');
+ addText(raw.substr(0, i));
+ addLink(url, match[0].replace(MAILTO_REGEXP, ''));
raw = raw.substring(i + match[0].length);
}
- writer.chars(raw);
- return html.join('');
+ addText(raw);
+ return $sanitize(html.join(''));
+
+ function addText(text) {
+ if (!text) {
+ return;
+ }
+ html.push(sanitizeText(text));
+ }
+
+ function addLink(url, text) {
+ html.push('<a ');
+ if (angular.isDefined(target)) {
+ html.push('target="');
+ html.push(target);
+ html.push('" ');
+ }
+ html.push('href="');
+ html.push(url);
+ html.push('">');
+ addText(text);
+ html.push('</a>');
+ }
};
-});
+}]);
})(window, window.angular);
|