Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: error from executing url regex #559

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions __tests__/ExpensiMark-test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable max-len */
import ExpensiMark from '../lib/ExpensiMark';
import Str from '../lib/str';
import _ from 'underscore';

const parser = new ExpensiMark();

Expand All @@ -14,3 +16,20 @@ test('Test text is unescaped', () => {
const resultString = '& &amp; &lt; <';
expect(parser.htmlToText(htmlString)).toBe(resultString);
});

test('Test with regex Maximum regex stack depth reached error', () => {
const testString = '<h1>heading</h1> asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdicdjejajasjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfisjksksjsjssskssjskskssksksksksskdkddkddkdksskskdkdkdksskskskdkdkdkdkekeekdkddenejeodxkdndekkdjddkeemdjxkdenendkdjddekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdicdjejajasjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdi.cdjd';
const parser = new ExpensiMark();
// Mock method modifyTextForUrlLinks to let it throw an error to test try/catch of method ExpensiMark.replace
const modifyTextForUrlLinksMock = jest.fn((a, b, c) => {throw new Error('Maximum regex stack depth reached')});
parser.modifyTextForUrlLinks = modifyTextForUrlLinksMock;
expect(parser.replace(testString)).toBe(_.escape(testString));
expect(modifyTextForUrlLinksMock).toHaveBeenCalledTimes(1);

// Mock method extractLinksInMarkdownComment to let it return undefined to test try/catch of method ExpensiMark.extractLinksInMarkdownComment
const extractLinksInMarkdownCommentMock = jest.fn((a) => undefined);
parser.extractLinksInMarkdownComment = extractLinksInMarkdownCommentMock;
expect(parser.extractLinksInMarkdownComment(testString)).toBe(undefined);
expect(parser.getRemovedMarkdownLinks(testString, 'google.com')).toStrictEqual([]);
expect(extractLinksInMarkdownCommentMock).toHaveBeenCalledTimes(3);
});
2 changes: 1 addition & 1 deletion lib/ExpensiMark.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default class ExpensiMark {
* @param textToCheck - Text to check
*/
containsNonPairTag(textToCheck: string): boolean;
extractLinksInMarkdownComment(comment: string): string[];
extractLinksInMarkdownComment(comment: string): string[] | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of undefined, lets return [] as it was doing previously?

Copy link
Contributor Author

@eh2077 eh2077 Jul 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allroundexperts I tend to stick with return undefined when exception occurs for the following reason. This method currently is only used by method getRemovedMarkdownLinks which needs to call this method twice to do diff to get the removed links. My point here is to let method getRemovedMarkdownLinks return empty array [] if any of the two links array is undefined, which means don't remove links if any exception occurs. Does it make sense to you?

Let's say, there's a new comment with multiple links but one of them causes regex execution error, in this case, we return undefined to skip removing any links. If we return [] for the new comment, then method getRemovedMarkdownLinks will remove all links.

New comment
asjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdicdjejajasjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfisjksksjsjssskssjskskssksksksksskdkddkddkdksskskdkdkdksskskskdkdkdkdkekeekdkddenejeodxkdndekkdjddkeemdjxkdenendkdjddekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdicdjejajasjjssjdjdjdjdjdjjeiwiwiwowkdjdjdieikdjfidekjcjdkekejdcjdkeekcjcdidjjcdkekdiccjdkejdjcjxisdjjdkedncicdjejejcckdsijcjdsodjcicdkejdi.cdjd
www.google.com
www.expensify.com
Old comment
www.google.com
www.expensify.com

/**
* Compares two markdown comments and returns a list of the links removed in a new comment.
*
Expand Down
60 changes: 37 additions & 23 deletions lib/ExpensiMark.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import _ from 'underscore';
import Log from './Log';
import Str from './str';
import {MARKDOWN_URL_REGEX, LOOSE_URL_REGEX, URL_REGEX} from './Url';
import {CONST} from './CONST';
Expand Down Expand Up @@ -391,23 +392,31 @@ export default class ExpensiMark {
let replacedText = shouldEscapeText ? _.escape(text) : text;

const rules = _.isEmpty(filterRules) ? this.rules : _.filter(this.rules, rule => _.contains(filterRules, rule.name));
rules.forEach((rule) => {
// Pre-process text before applying regex
if (rule.pre) {
replacedText = rule.pre(replacedText);
}

if (rule.process) {
replacedText = rule.process(replacedText, rule.replacement);
} else {
replacedText = replacedText.replace(rule.regex, rule.replacement);
}
try {
rules.forEach((rule) => {
// Pre-process text before applying regex
if (rule.pre) {
replacedText = rule.pre(replacedText);
}

// Post-process text after applying regex
if (rule.post) {
replacedText = rule.post(replacedText);
}
});
if (rule.process) {
replacedText = rule.process(replacedText, rule.replacement);
} else {
replacedText = replacedText.replace(rule.regex, rule.replacement);
}

// Post-process text after applying regex
if (rule.post) {
replacedText = rule.post(replacedText);
}
});
} catch (e) {
Log.warn('Error replacing text with html in ExpensiMark.replace', {error: e});

// We want to return text without applying rules if exception occurs during replacing
return shouldEscapeText ? _.escape(text) : text;
}

return replacedText;
}
Expand Down Expand Up @@ -731,15 +740,20 @@ export default class ExpensiMark {

/**
* @param {String} comment
* @returns {Array}
* @returns {Array} or undefined if exception occurs when executing regex matching
*/
extractLinksInMarkdownComment(comment) {
const escapedComment = _.escape(comment);
const matches = [...escapedComment.matchAll(MARKDOWN_LINK_REGEX)];

// Element 1 from match is the regex group if it exists which contains the link URLs
const links = _.map(matches, match => Str.sanitizeURL(match[2]));
return links;
try {
const escapedComment = _.escape(comment);
const matches = [...escapedComment.matchAll(MARKDOWN_LINK_REGEX)];

// Element 1 from match is the regex group if it exists which contains the link URLs
const links = _.map(matches, match => Str.sanitizeURL(match[2]));
return links;
} catch (e) {
Log.warn('Error parsing url in ExpensiMark.extractLinksInMarkdownComment', {error: e});
return undefined;
}
}

/**
Expand All @@ -752,6 +766,6 @@ export default class ExpensiMark {
getRemovedMarkdownLinks(oldComment, newComment) {
const linksInOld = this.extractLinksInMarkdownComment(oldComment);
const linksInNew = this.extractLinksInMarkdownComment(newComment);
return _.difference(linksInOld, linksInNew);
return linksInOld === undefined || linksInNew === undefined ? [] : _.difference(linksInOld, linksInNew);
}
}
Loading