Snippet:
Filter |
Source |
Rendered |
linky filter |
<div ng-bind-html="snippet | linky"> </div>
|
|
linky target |
<div ng-bind-html="snippetWithSingleURL | linky:'_blank'"> </div>
|
|
linky custom attributes |
<div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"> </div>
|
|
no filter |
<div ng-bind="snippet"> </div> |
|
angular.module('linkyExample', ['ngSanitize'])
.controller('ExampleController', ['$scope', function($scope) {
$scope.snippet =
'Pretty text with some links:\n' +
'http://angularjs.org/,\n' +
'mailto:us@somewhere.org,\n' +
'another@somewhere.org,\n' +
'and one more: ftp://127.0.0.1/.';
$scope.snippetWithSingleURL = 'http://angularjs.org/';
}]);
it('should linkify the snippet with urls', function() {
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
});
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() {
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(element(by.id('linky-target')).
element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()).
toBe('http://angularjs.org/');
expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
});
it('should optionally add custom attributes', function() {
expect(element(by.id('linky-custom-attributes')).
element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()).
toBe('http://angularjs.org/');
expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow');
});
*/
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
var LINKY_URL_REGEXP =
/((s?ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,
MAILTO_REGEXP = /^mailto:/i;
var linkyMinErr = angular.$$minErr('linky');
var isDefined = angular.isDefined;
var isFunction = angular.isFunction;
var isObject = angular.isObject;
var isString = angular.isString;
return function(text, target, attributes) {
if (text == null || text === '') return text;
if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text);
var attributesFn =
isFunction(attributes) ? attributes :
isObject(attributes) ? function getAttributesObject() {return attributes;} :
function getEmptyAttributesObject() {return {};};
var match;
var raw = text;
var html = [];
var url;
var i;
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/www/mailto then assume mailto
if (!match[2] && !match[4]) {
url = (match[3] ? 'http://' : 'mailto:') + url;
}
i = match.index;
addText(raw.substr(0, i));
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
raw = raw.substring(i + match[0].length);
}
addText(raw);
return $sanitize(html.join(''));
function addText(text) {
if (!text) {
return;
}
html.push(sanitizeText(text));
}
function addLink(url, text) {
var key, linkAttributes = attributesFn(url);
html.push('
');
addText(text);
html.push('');
}
};
}]);
describe('ngBindHtml', function() {
beforeEach(module('ngSanitize'));
it('should set html', inject(function($rootScope, $compile) {
var element = $compile('
')($rootScope);
$rootScope.html = '
hello
';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('
hello
');
}));
it('should reset html when value is null or undefined', inject(function($compile, $rootScope) {
var element = $compile('
')($rootScope);
angular.forEach([null, undefined, ''], function(val) {
$rootScope.html = 'some val';
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('some val');
$rootScope.html = val;
$rootScope.$digest();
expect(lowercase(element.html())).toEqual('');
});
}));
});
describe('linky', function() {
var linky;
beforeEach(module('ngSanitize'));
beforeEach(inject(function($filter) {
linky = $filter('linky');
}));
it('should do basic filter', function() {
expect(linky('http://ab/ (http://a/)
http://1.2/v:~-123. c “http://example.com” ‘http://me.com’')).
toEqual('
http://ab/ ' +
'(
http://a/) ' +
'<
http://a/> ' +
'
http://1.2/v:~-123. c ' +
'“
http://example.com” ' +
'‘
http://me.com’');
expect(linky(undefined)).not.toBeDefined();
});
it('should return `undefined`/`null`/`""` values unchanged', function() {
expect(linky(undefined)).toBeUndefined();
expect(linky(null)).toBe(null);
expect(linky('')).toBe('');
});
it('should throw an error when used with a non-string value (other than `undefined`/`null`)',
function() {
expect(function() { linky(false); }).
toThrowMinErr('linky', 'notstring', 'Expected string but received: false');
expect(function() { linky(true); }).
toThrowMinErr('linky', 'notstring', 'Expected string but received: true');
expect(function() { linky(0); }).
toThrowMinErr('linky', 'notstring', 'Expected string but received: 0');
expect(function() { linky(42); }).
toThrowMinErr('linky', 'notstring', 'Expected string but received: 42');
expect(function() { linky({}); }).
toThrowMinErr('linky', 'notstring', 'Expected string but received: {}');
expect(function() { linky([]); }).
toThrowMinErr('linky', 'notstring', 'Expected string but received: []');
expect(function() { linky(noop); }).
toThrowMinErr('linky', 'notstring', 'Expected string but received: function noop()');
}
);
it('should be case-insensitive', function() {
expect(linky('WWW.example.com')).toEqual('
WWW.example.com');
expect(linky('WWW.EXAMPLE.COM')).toEqual('
WWW.EXAMPLE.COM');
expect(linky('HTTP://www.example.com')).toEqual('
HTTP://www.example.com');
expect(linky('HTTP://example.com')).toEqual('
HTTP://example.com');
expect(linky('HTTPS://www.example.com')).toEqual('
HTTPS://www.example.com');
expect(linky('HTTPS://example.com')).toEqual('
HTTPS://example.com');
expect(linky('FTP://www.example.com')).toEqual('
FTP://www.example.com');
expect(linky('FTP://example.com')).toEqual('
FTP://example.com');
expect(linky('SFTP://www.example.com')).toEqual('
SFTP://www.example.com');
expect(linky('SFTP://example.com')).toEqual('
SFTP://example.com');
});
it('should handle www.', function() {
expect(linky('www.example.com')).toEqual('
www.example.com');
});
it('should handle mailto:', function() {
expect(linky('mailto:me@example.com')).
toEqual('
me@example.com');
expect(linky('me@example.com')).
toEqual('
me@example.com');
expect(linky('send email to me@example.com, but')).
toEqual('send email to
me@example.com, but');
expect(linky('my email is "me@example.com"')).
toEqual('my email is "
me@example.com"');
});
it('should handle quotes in the email', function() {
expect(linky('foo@"bar".com')).toEqual('
foo@"bar".com');
});
it('should handle target:', function() {
expect(linky('http://example.com', '_blank')).
toBeOneOf('
http://example.com',
'
http://example.com');
expect(linky('http://example.com', 'someNamedIFrame')).
toBeOneOf('
http://example.com',
'
http://example.com');
});
describe('custom attributes', function() {
it('should optionally add custom attributes', function() {
expect(linky('http://example.com', '_self', {rel: 'nofollow'})).
toBeOneOf('
http://example.com',
'
http://example.com');
});
it('should override target parameter with custom attributes', function() {
expect(linky('http://example.com', '_self', {target: '_blank'})).
toBeOneOf('
http://example.com',
'
http://example.com');
});
it('should optionally add custom attributes from function', function() {
expect(linky('http://example.com', '_self', function(url) {return {'class': 'blue'};})).
toBeOneOf('
http://example.com',
'
http://example.com',
'
http://example.com');
});
it('should pass url as parameter to custom attribute function', function() {
var linkParameters = jasmine.createSpy('linkParameters').and.returnValue({'class': 'blue'});
linky('http://example.com', '_self', linkParameters);
expect(linkParameters).toHaveBeenCalledWith('http://example.com');
});
it('should call the attribute function for all links in the input', function() {
var attributeFn = jasmine.createSpy('attributeFn').and.returnValue({});
linky('http://example.com and http://google.com', '_self', attributeFn);
expect(attributeFn.calls.allArgs()).toEqual([['http://example.com'], ['http://google.com']]);
});
it('should strip unsafe attributes', function() {
expect(linky('http://example.com', '_self', {'class': 'blue', 'onclick': 'alert(\'Hi\')'})).
toBeOneOf('
http://example.com',
'
http://example.com',
'
http://example.com');
});
});
});
describe('HTML', function() {
var ua = window.navigator.userAgent;
var isChrome = /Chrome/.test(ua) && !/Edge/.test(ua);
var expectHTML;
beforeEach(module('ngSanitize'));
beforeEach(function() {
expectHTML = function(html) {
var sanitize;
inject(function($sanitize) {
sanitize = $sanitize;
});
return expect(sanitize(html));
};
});
describe('htmlParser', function() {
/* global htmlParser */
var handler, start, text, comment;
beforeEach(function() {
text = '';
start = null;
handler = {
start: function(tag, attrs) {
start = {
tag: tag,
attrs: attrs
};
// Since different browsers handle newlines differently we trim
// so that it is easier to write tests.
for (var i = 0, ii = attrs.length; i < ii; i++) {
var keyValue = attrs[i];
var key = keyValue.key;
var value = keyValue.value;
attrs[key] = value.replace(/^\s*/, '').replace(/\s*$/, '');
}
},
chars: function(text_) {
text += text_;
},
end:function(tag) {
expect(tag).toEqual(start.tag);
},
comment:function(comment_) {
comment = comment_;
}
};
// Trigger the $sanitizer provider to execute, which initializes the `htmlParser` function.
inject(function($sanitize) {});
});
it('should not parse comments', function() {
htmlParser('', handler);
expect(comment).not.toBeDefined();
});
it('should parse basic format', function() {
htmlParser('
text', handler);
expect(start).toEqual({tag:'tag', attrs:{attr:'value'}});
expect(text).toEqual('text');
});
it('should not treat "<" followed by a non-/ or non-letter as a tag', function() {
expectHTML('<- text1 text2 <1 text1 text2 <{', handler).
toBe('<- text1 text2 <1 text1 text2 <{');
});
it('should accept tag delimiters such as "<" inside real tags', function() {
// Assert that the < is part of the text node content, and not part of a tag name.
htmlParser('
10 < 100
', handler);
expect(text).toEqual(' 10 < 100 ');
});
it('should parse newlines in tags', function() {
htmlParser('
text\ntag\n>', handler);
expect(start).toEqual({tag:'tag', attrs:{attr:'value'}});
expect(text).toEqual('text');
});
it('should parse newlines in attributes', function() {
htmlParser('text', handler);
expect(start).toEqual({tag:'tag', attrs:{attr:'\nvalue\n'}});
expect(text).toEqual('text');
});
it('should parse namespace', function() {
htmlParser('text', handler);
expect(start).toEqual({tag:'ns:t-a-g', attrs:{'ns:a-t-t-r':'\nvalue\n'}});
expect(text).toEqual('text');
});
it('should parse empty value attribute of node', function() {
htmlParser('abc', handler);
expect(start).toEqual({tag:'test-foo', attrs:{selected:'', value:''}});
expect(text).toEqual('abc');
});
});
// THESE TESTS ARE EXECUTED WITH COMPILED ANGULAR
it('should echo html', function() {
expectHTML('helloworld.').
toBeOneOf('helloworld.',
'helloworld.');
});
it('should remove script', function() {
expectHTML('ac.').toEqual('ac.');
});
it('should remove script that has newline characters', function() {
expectHTML('ac.').toEqual('ac.');
});
it('should remove DOCTYPE header', function() {
expectHTML('').toEqual('');
expectHTML('').toEqual('');
expectHTML('ac.').toEqual('ac.');
expectHTML('ac.').toEqual('ac.');
});
it('should escape non-start tags', function() {
expectHTML('a< SCRIPT >A< SCRIPT >evil< / scrIpt >B< / scrIpt >c.').
toBe('a< SCRIPT >A< SCRIPT >evil< / scrIpt >B< / scrIpt >c.');
});
it('should remove attrs', function() {
expectHTML('ab
c').toEqual('ab
c');
});
it('should handle large datasets', function() {
// Large is non-trivial to quantify, but handling ~100,000 should be sufficient for most purposes.
var largeNumber = 17; // 2^17 = 131,072
var result = 'b
';
// Ideally we would use repeat, but that isn't supported in IE.
for (var i = 0; i < largeNumber; i++) {
result += result;
}
expectHTML('a' + result + 'c').toEqual('a' + result + 'c');
});
it('should remove style', function() {
expectHTML('ac.').toEqual('ac.');
});
it('should remove style that has newline characters', function() {
expectHTML('ac.').toEqual('ac.');
});
it('should remove script and style', function() {
expectHTML('ac.').toEqual('ac.');
});
it('should remove double nested script', function() {
expectHTML('ailc.').toEqual('ailc.');
});
it('should remove unknown names', function() {
expectHTML('abc').toEqual('abc');
});
it('should remove unsafe value', function() {
expectHTML('').toEqual('');
expectHTML('').toEqual('');
});
it('should handle self closed elements', function() {
expectHTML('a
c').toEqual('a
c');
});
it('should handle namespace', function() {
expectHTML('abc').toEqual('abc');
});
it('should handle entities', function() {
var everything = '' +
'!@#$%^&*()_+-={}[]:";\'<>?,./`~ ħ
';
expectHTML(everything).toEqual(everything);
});
it('should mangle improper html', function() {
// This text is encoded more than a real HTML parser would, but it should render the same.
expectHTML('< div rel=" " alt=abc dir=\'"\' >text< /div>').
toBe('< div rel="" alt=abc dir=\'"\' >text< /div>');
});
it('should mangle improper html2', function() {
// A proper HTML parser would clobber this more in most cases, but it looks reasonable.
expectHTML('< div rel="" / >').
toBe('< div rel="" / >');
});
it('should ignore back slash as escape', function() {
expectHTML('