Skip to content

Commit 715b412

Browse files
committed
Merge branch 'master' into local-urls
# Conflicts: # dist/Autolinker.min.js # tests/matcher/UrlSpec.js
2 parents 65fa01f + f8c2a72 commit 715b412

File tree

12 files changed

+175
-28
lines changed

12 files changed

+175
-28
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,9 +368,15 @@ The full API docs for Autolinker may be referenced at:
368368
Pull requests definitely welcome.
369369
370370
- Make sure to add tests to cover your new functionality/bugfix.
371-
- Run the `gulp` command to build/test (or alternatively, open the `tests/index.html` file to run the tests).
372-
- When committing, please omit checking in the files in the `dist/` folder after building/testing. These are only committed to the repository for users downloading Autolinker via Bower. I will build these files and assign them a version number when merging your PR.
373-
- Please use tabs for indents! Tabs are better for everybody (individuals can set their editors to different tab sizes based on their visual preferences).
371+
- Run the `gulp test` command to build/test (or alternatively, open the
372+
`tests/index.html` file to run the tests).
373+
- When committing, please omit checking in the files in the `dist/`
374+
folder after building/testing. These are only committed to the
375+
repository for users downloading Autolinker via Bower. I will build
376+
these files and assign them a version number when merging your PR.
377+
- Please use tabs for indents! Tabs are better for everybody
378+
(individuals can set their editors to different tab sizes based on
379+
their visual preferences).
374380
375381
376382
## Changelog

dist/Autolinker.js

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*!
22
* Autolinker.js
3-
* 1.2.1
3+
* 1.3.1
44
*
55
* Copyright(c) 2016 Gregory Jacobs <[email protected]>
66
* MIT License
@@ -240,7 +240,7 @@ Autolinker.parse = function( textOrHtml, options ) {
240240
*
241241
* Ex: 0.25.1
242242
*/
243-
Autolinker.version = '1.2.1';
243+
Autolinker.version = '1.3.1';
244244

245245

246246
Autolinker.prototype = {
@@ -1783,7 +1783,8 @@ Autolinker.htmlParser.HtmlParser = Autolinker.Util.extend( Object, {
17831783
* 2. If it is an end tag, this group will have the '/'.
17841784
* 3. If it is a comment tag, this group will hold the comment text (i.e.
17851785
* the text inside the `&lt;!--` and `--&gt;`.
1786-
* 4. The tag name for all tags (other than the &lt;!DOCTYPE&gt; tag)
1786+
* 4. The tag name for a tag without attributes (other than the &lt;!DOCTYPE&gt; tag)
1787+
* 5. The tag name for a tag with attributes (other than the &lt;!DOCTYPE&gt; tag)
17871788
*/
17881789
htmlRegex : (function() {
17891790
var commentTagRegex = /!--([\s\S]+?)--/,
@@ -1821,19 +1822,36 @@ Autolinker.htmlParser.HtmlParser = Autolinker.Util.extend( Object, {
18211822

18221823
'|',
18231824

1825+
// Handle tag without attributes.
1826+
// Doing this separately from a tag that has attributes
1827+
// to fix a regex time complexity issue seen with the
1828+
// example in https://github.com/gregjacobs/Autolinker.js/issues/172
18241829
'(?:',
1830+
// *** Capturing Group 4 - The tag name for a tag without attributes
1831+
'(' + tagNameRegex.source + ')',
1832+
1833+
'\\s*/?', // any trailing spaces and optional '/' before the closing '>'
1834+
')',
18251835

1826-
// *** Capturing Group 4 - The tag name
1836+
'|',
1837+
1838+
// Handle tag with attributes
1839+
// Doing this separately from a tag with no attributes
1840+
// to fix a regex time complexity issue seen with the
1841+
// example in https://github.com/gregjacobs/Autolinker.js/issues/172
1842+
'(?:',
1843+
// *** Capturing Group 5 - The tag name for a tag with attributes
18271844
'(' + tagNameRegex.source + ')',
18281845

1846+
'\\s+', // must have at least one space after the tag name to prevent ReDoS issue (issue #172)
1847+
18291848
// Zero or more attributes following the tag name
18301849
'(?:',
18311850
'(?:\\s+|\\b)', // any number of whitespace chars before an attribute. NOTE: Using \s* here throws Chrome into an infinite loop for some reason, so using \s+|\b instead
18321851
nameEqualsValueRegex, // attr="value" (with optional ="value" part)
18331852
')*',
18341853

18351854
'\\s*/?', // any trailing spaces and optional '/' before the closing '>'
1836-
18371855
')',
18381856
')',
18391857
'>',
@@ -1869,7 +1887,7 @@ Autolinker.htmlParser.HtmlParser = Autolinker.Util.extend( Object, {
18691887
while( ( currentResult = htmlRegex.exec( html ) ) !== null ) {
18701888
var tagText = currentResult[ 0 ],
18711889
commentText = currentResult[ 3 ], // if we've matched a comment
1872-
tagName = currentResult[ 1 ] || currentResult[ 4 ], // The <!DOCTYPE> tag (ex: "!DOCTYPE"), or another tag (ex: "a" or "img")
1890+
tagName = currentResult[ 1 ] || currentResult[ 4 ] || currentResult[ 5 ], // The <!DOCTYPE> tag (ex: "!DOCTYPE"), or another tag (ex: "a" or "img")
18731891
isClosingTag = !!currentResult[ 2 ],
18741892
offset = currentResult.index,
18751893
inBetweenTagsText = html.substring( lastIndex, offset );
@@ -1897,7 +1915,14 @@ Autolinker.htmlParser.HtmlParser = Autolinker.Util.extend( Object, {
18971915
// Push TextNodes and EntityNodes for any text found between tags
18981916
if( text ) {
18991917
textAndEntityNodes = this.parseTextAndEntityNodes( lastIndex, text );
1900-
nodes.push.apply( nodes, textAndEntityNodes );
1918+
1919+
// Note: the following 3 lines were previously:
1920+
// nodes.push.apply( nodes, textAndEntityNodes );
1921+
// but this was causing a "Maximum Call Stack Size Exceeded"
1922+
// error on inputs with a large number of html entities.
1923+
textAndEntityNodes.forEach( function( node ) {
1924+
nodes.push( node );
1925+
} );
19011926
}
19021927
}
19031928

@@ -3803,8 +3828,8 @@ Autolinker.matcher.UrlMatchValidator = {
38033828
( protocolUrlMatch && !this.isValidUriScheme( protocolUrlMatch ) ) ||
38043829
this.urlMatchDoesNotHaveProtocolOrDot( urlMatch, protocolUrlMatch ) || // At least one period ('.') must exist in the URL match for us to consider it an actual URL, *unless* it was a full protocol match (like 'http://localhost')
38053830
(this.urlMatchDoesNotHaveAtLeastOneWordChar( urlMatch, protocolUrlMatch ) && // At least one letter character must exist in the domain name after a protocol match. Ex: skip over something like "git:1.0"
3806-
!this.isValidIpAddress( urlMatch ) // Except if it's an IP address
3807-
)
3831+
!this.isValidIpAddress( urlMatch )) || // Except if it's an IP address
3832+
this.containsMultipleDots( urlMatch )
38083833
) {
38093834
return false;
38103835
}
@@ -3820,6 +3845,10 @@ Autolinker.matcher.UrlMatchValidator = {
38203845
return uriScheme !== null;
38213846
},
38223847

3848+
containsMultipleDots : function ( urlMatch ) {
3849+
return urlMatch.indexOf("..") > -1;
3850+
},
3851+
38233852
/**
38243853
* Determines if the URI scheme is a valid scheme to be autolinked. Returns
38253854
* `false` if the scheme is 'javascript:' or 'vbscript:'
@@ -3888,6 +3917,7 @@ Autolinker.matcher.UrlMatchValidator = {
38883917
}
38893918

38903919
};
3920+
38913921
/*global Autolinker */
38923922
/**
38933923
* A truncation feature where the ellipsis will be placed at the end of the URL.

examples/live-example/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
<!DOCTYPE html>
12
<html>
23

34
<head>
5+
<meta charset="utf-8">
46
<meta name="viewport" content="width=device-width, initial-scale=1">
57

68
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
@@ -74,4 +76,4 @@ <h4>Code Sample:</h4>
7476

7577
</body>
7678

77-
</html>
79+
</html>

gulpfile.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ const pkg = require( './package.json' ),
2929
minDistFilePath = distFolder + minDistFilename;
3030

3131

32-
gulp.task( 'default', [ 'lint', 'build', 'test' ] );
32+
gulp.task( 'default', [ 'doc', 'test' ] );
3333
gulp.task( 'lint', lintTask );
34-
gulp.task( 'build', buildTask );
34+
gulp.task( 'build', [ 'lint' ], buildTask );
3535
gulp.task( 'test', [ 'build' ], testTask );
3636
gulp.task( 'doc', [ 'build', 'typescript' ], docTask );
3737
gulp.task( 'serve', [ 'typescript', 'doc' ], serveTask );

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "autolinker",
3-
"version": "1.2.1",
3+
"version": "1.3.1",
44
"description": "Utility to automatically link the URLs, email addresses, phone numbers, hashtags, and mentions (Twitter, Instagram) in a given block of text/HTML",
55
"main": "dist/Autolinker.js",
66
"files": [

src/htmlParser/HtmlParser.js

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ Autolinker.htmlParser.HtmlParser = Autolinker.Util.extend( Object, {
2424
* 2. If it is an end tag, this group will have the '/'.
2525
* 3. If it is a comment tag, this group will hold the comment text (i.e.
2626
* the text inside the `&lt;!--` and `--&gt;`.
27-
* 4. The tag name for all tags (other than the &lt;!DOCTYPE&gt; tag)
27+
* 4. The tag name for a tag without attributes (other than the &lt;!DOCTYPE&gt; tag)
28+
* 5. The tag name for a tag with attributes (other than the &lt;!DOCTYPE&gt; tag)
2829
*/
2930
htmlRegex : (function() {
3031
var commentTagRegex = /!--([\s\S]+?)--/,
@@ -62,19 +63,36 @@ Autolinker.htmlParser.HtmlParser = Autolinker.Util.extend( Object, {
6263

6364
'|',
6465

66+
// Handle tag without attributes.
67+
// Doing this separately from a tag that has attributes
68+
// to fix a regex time complexity issue seen with the
69+
// example in https://github.com/gregjacobs/Autolinker.js/issues/172
6570
'(?:',
71+
// *** Capturing Group 4 - The tag name for a tag without attributes
72+
'(' + tagNameRegex.source + ')',
73+
74+
'\\s*/?', // any trailing spaces and optional '/' before the closing '>'
75+
')',
76+
77+
'|',
6678

67-
// *** Capturing Group 4 - The tag name
79+
// Handle tag with attributes
80+
// Doing this separately from a tag with no attributes
81+
// to fix a regex time complexity issue seen with the
82+
// example in https://github.com/gregjacobs/Autolinker.js/issues/172
83+
'(?:',
84+
// *** Capturing Group 5 - The tag name for a tag with attributes
6885
'(' + tagNameRegex.source + ')',
6986

87+
'\\s+', // must have at least one space after the tag name to prevent ReDoS issue (issue #172)
88+
7089
// Zero or more attributes following the tag name
7190
'(?:',
7291
'(?:\\s+|\\b)', // any number of whitespace chars before an attribute. NOTE: Using \s* here throws Chrome into an infinite loop for some reason, so using \s+|\b instead
7392
nameEqualsValueRegex, // attr="value" (with optional ="value" part)
7493
')*',
7594

7695
'\\s*/?', // any trailing spaces and optional '/' before the closing '>'
77-
7896
')',
7997
')',
8098
'>',
@@ -110,7 +128,7 @@ Autolinker.htmlParser.HtmlParser = Autolinker.Util.extend( Object, {
110128
while( ( currentResult = htmlRegex.exec( html ) ) !== null ) {
111129
var tagText = currentResult[ 0 ],
112130
commentText = currentResult[ 3 ], // if we've matched a comment
113-
tagName = currentResult[ 1 ] || currentResult[ 4 ], // The <!DOCTYPE> tag (ex: "!DOCTYPE"), or another tag (ex: "a" or "img")
131+
tagName = currentResult[ 1 ] || currentResult[ 4 ] || currentResult[ 5 ], // The <!DOCTYPE> tag (ex: "!DOCTYPE"), or another tag (ex: "a" or "img")
114132
isClosingTag = !!currentResult[ 2 ],
115133
offset = currentResult.index,
116134
inBetweenTagsText = html.substring( lastIndex, offset );
@@ -138,7 +156,14 @@ Autolinker.htmlParser.HtmlParser = Autolinker.Util.extend( Object, {
138156
// Push TextNodes and EntityNodes for any text found between tags
139157
if( text ) {
140158
textAndEntityNodes = this.parseTextAndEntityNodes( lastIndex, text );
141-
nodes.push.apply( nodes, textAndEntityNodes );
159+
160+
// Note: the following 3 lines were previously:
161+
// nodes.push.apply( nodes, textAndEntityNodes );
162+
// but this was causing a "Maximum Call Stack Size Exceeded"
163+
// error on inputs with a large number of html entities.
164+
textAndEntityNodes.forEach( function( node ) {
165+
nodes.push( node );
166+
} );
142167
}
143168
}
144169

src/matcher/UrlMatchValidator.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ Autolinker.matcher.UrlMatchValidator = {
8080
( protocolUrlMatch && !this.isValidUriScheme( protocolUrlMatch ) ) ||
8181
this.urlMatchDoesNotHaveProtocolOrDot( urlMatch, protocolUrlMatch ) || // At least one period ('.') must exist in the URL match for us to consider it an actual URL, *unless* it was a full protocol match (like 'http://localhost')
8282
(this.urlMatchDoesNotHaveAtLeastOneWordChar( urlMatch, protocolUrlMatch ) && // At least one letter character must exist in the domain name after a protocol match. Ex: skip over something like "git:1.0"
83-
!this.isValidIpAddress( urlMatch ) // Except if it's an IP address
84-
)
83+
!this.isValidIpAddress( urlMatch )) || // Except if it's an IP address
84+
this.containsMultipleDots( urlMatch )
8585
) {
8686
return false;
8787
}
@@ -97,6 +97,10 @@ Autolinker.matcher.UrlMatchValidator = {
9797
return uriScheme !== null;
9898
},
9999

100+
containsMultipleDots : function ( urlMatch ) {
101+
return urlMatch.indexOf("..") > -1;
102+
},
103+
100104
/**
101105
* Determines if the URI scheme is a valid scheme to be autolinked. Returns
102106
* `false` if the scheme is 'javascript:' or 'vbscript:'
@@ -164,4 +168,4 @@ Autolinker.matcher.UrlMatchValidator = {
164168
}
165169
}
166170

167-
};
171+
};

tests/AutolinkerSpec.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ describe( "Autolinker", function() {
295295
expect( result ).toBe( 'Joe went to <a href="https://ru.wikipedia.org/wiki/Кириллица?Кириллица=1#Кириллица">ru.wikipedia.org/wiki/Кириллица?Кириллица=1#Кириллица</a>' );
296296
} );
297297

298+
it( 'should not match an address with multiple dots', function() {
299+
expect( autolinker.link( 'hello:...world' ) ).toBe( 'hello:...world' );
300+
expect( autolinker.link( 'hello:wo.....rld' ) ).toBe( 'hello:wo.....rld' );
301+
});
302+
298303
describe( "protocol linking", function() {
299304

300305
it( "should NOT include preceding ':' introductions without a space", function() {
@@ -1419,6 +1424,7 @@ describe( "Autolinker", function() {
14191424
it( "should not fail with an infinite loop for these given input strings (Issue #160)", function() {
14201425
var inputStrings = [
14211426
'asdf ouefof<33we oeweofjojfew oeijwffejifjew ojiwjoiwefj iowjefojwe iofjw:)<33',
1427+
'<3<3<3<3<3<3<3<3<3<3<3<3<3<3<3<3',
14221428
'<33<33<33<33<33<33<33<33<33<33',
14231429
'<33<33<33<33<33<33<33<33<33<33<33'
14241430
];
@@ -1473,6 +1479,32 @@ describe( "Autolinker", function() {
14731479
} );
14741480

14751481

1482+
it( "should not fail with an infinite loop for an input string with " +
1483+
"a string that looks like HTML (Issue #172)",
1484+
function() {
1485+
var str = '<Office%20days:%20Tue.%20&%20Wed.%20(till%2015:30%20hr),%20Thu.%20(till%2017:30%20hr),%20Fri.%20(till%2012:30%20hr).%3c/a%3e%3cbr%3e%3c/td%3e%3ctd%20style=>',
1486+
result = autolinker.link( str );
1487+
1488+
expect( result ).toBe( str );
1489+
} );
1490+
1491+
1492+
it( "should not fail with a Maximum Call Stack Size Exceeded for an " +
1493+
"input with a large number of html entities (Issue #171)",
1494+
function() {
1495+
var testStr = (function() {
1496+
var t = [];
1497+
for (var i = 0; i < 50000; i++) {
1498+
t.push( ' /&gt;&lt;br' );
1499+
}
1500+
return t.join( '' );
1501+
})();
1502+
1503+
var result = autolinker.link( testStr );
1504+
expect( result ).toBe( testStr );
1505+
} );
1506+
1507+
14761508
it( "should NOT modify the email address with other tags when inside another anchor", function() {
14771509
var input = [
14781510
'<div>First name: Subin</div>',

tests/htmlParser/HtmlParserSpec.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,32 @@ describe( "Autolinker.htmlParser.HtmlParser", function() {
132132
expect( result.join( "" ) ).toBe( inputStr );
133133
} );
134134

135+
136+
it( "should properly handle tags without attributes", function() {
137+
var nodes = htmlParser.parse( 'Test1 <div><span>Test2</span> Test3</div>' );
138+
139+
expect( nodes.length ).toBe( 7 );
140+
expectTextNode ( nodes[ 0 ], 0, 'Test1 ' );
141+
expectElementNode( nodes[ 1 ], 6, '<div>', 'div', false );
142+
expectElementNode( nodes[ 2 ], 11, '<span>', 'span', false );
143+
expectTextNode ( nodes[ 3 ], 17, 'Test2' );
144+
expectElementNode( nodes[ 4 ], 22, '</span>', 'span', true );
145+
expectTextNode ( nodes[ 5 ], 29, ' Test3' );
146+
expectElementNode( nodes[ 6 ], 35, '</div>', 'div', true );
147+
} );
148+
149+
150+
it( "should properly handle a tag where the attributes start on the " +
151+
"next line",
152+
function() {
153+
var nodes = htmlParser.parse( 'Test <div\nclass="myClass"\nstyle="color:red"> Test' );
154+
155+
expect( nodes.length ).toBe( 3 );
156+
expectTextNode ( nodes[ 0 ], 0, 'Test ' );
157+
expectElementNode( nodes[ 1 ], 5, '<div\nclass="myClass"\nstyle="color:red">', 'div', false );
158+
expectTextNode ( nodes[ 2 ], 44, ' Test' );
159+
} );
160+
135161
} );
136162

137163

@@ -274,4 +300,13 @@ describe( "Autolinker.htmlParser.HtmlParser", function() {
274300
expectTextNode( nodes[ 0 ], 0, inputStr );
275301
} );
276302

303+
304+
it( "should not freeze up the regular expression engine when presented with the input string in issue #172", function() {
305+
var inputStr = '<Office%20days:%20Tue.%20&%20Wed.%20(till%2015:30%20hr),%20Thu.%20(till%2017',//:30%20hr),%20Fri.%20(till%2012:30%20hr).%3c/a%3e%3cbr%3e%3c/td%3e%3ctd%20style=>',
306+
nodes = htmlParser.parse( inputStr );
307+
308+
expect( nodes.length ).toBe( 1 );
309+
expectTextNode( nodes[ 0 ], 0, inputStr );
310+
} );
311+
277312
} );

tests/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
1+
<!DOCTYPE html>
22
<html>
33
<head>
4+
<meta charset="utf-8">
45
<title>Autolinker.js Tests</title>
56

67
<!-- Jasmine -->

0 commit comments

Comments
 (0)