From 7cb1f8ca67c1422e0609f6912007f57557a2c7b4 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Thu, 4 Jan 2024 06:48:34 +0100 Subject: [PATCH 1/6] relax URL regex to support finding IDN domains, filter using built in function, removing non-URLs --- js/privatebin.js | 4 ++-- tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index c5ee7fb0..db85eef7 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -2041,8 +2041,8 @@ jQuery.PrivateBin = (function($, RawDeflate) { responseString = JSON.stringify(responseString); } if (typeof responseString === 'string' && responseString.length > 0) { - const shortUrlMatcher = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; - const shortUrl = (responseString.match(shortUrlMatcher) || []).sort(function(a, b) { + const shortUrlMatcher = /https?:\/\/[^\s]+/g; + const shortUrl = (responseString.match(shortUrlMatcher) || []).filter(URL.canParse).sort(function(a, b) { return a.length - b.length; })[0]; if (typeof shortUrl === 'string' && shortUrl.length > 0) { diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index a307c4e7..386705fa 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -73,7 +73,7 @@ endif; ?> - + diff --git a/tpl/page.php b/tpl/page.php index fa70097a..026b4ae1 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -51,7 +51,7 @@ endif; ?> - + From 8427c1136c7d9c82322004901e75bd18f5963361 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Thu, 4 Jan 2024 06:52:27 +0100 Subject: [PATCH 2/6] document change --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36830d6a..3d39cd30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.6.3 (not yet released) * ADDED: Detect and report on damaged pastes (#1218) * CHANGED: Upgrading libraries to: zlib 1.3 +* FIXED: Support more types of valid URLs for shorteners, incl. IDN ones (#1224) ## 1.6.2 (2023-12-15) * FIXED: English not selectable when `languageselection` enabled (#1208) From a80bd4e4ea1dd97889998b1dca08b511245c06a5 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Thu, 4 Jan 2024 23:08:17 +0100 Subject: [PATCH 3/6] fix url filter, IDN URL unit test --- js/common.js | 2 +- js/package-lock.json | 4 +-- js/privatebin.js | 60 ++++++++++++++++++++++++++---------------- js/test/PasteStatus.js | 45 +++++++++++++++++++++++++++++++ tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 6 files changed, 87 insertions(+), 28 deletions(-) diff --git a/js/common.js b/js/common.js index 7e406acb..295fd090 100644 --- a/js/common.js +++ b/js/common.js @@ -37,7 +37,7 @@ var a2zString = ['a','b','c','d','e','f','g','h','i','j','k','l','m', }) ), schemas = ['ftp','http','https'], - supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'], + supportedLanguages = ['ar', 'bg', 'ca', 'co', 'cs', 'de', 'el', 'es', 'et', 'fi', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'jbo', 'lt', 'no', 'nl', 'pl', 'pt', 'oc', 'ru', 'sk', 'sl', 'th', 'tr', 'uk', 'zh'], mimeTypes = ['image/png', 'application/octet-stream'], formats = ['plaintext', 'markdown', 'syntaxhighlighting'], mimeFile = fs.createReadStream('/etc/mime.types'), diff --git a/js/package-lock.json b/js/package-lock.json index b7b29ab7..ed57b711 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1,12 +1,12 @@ { "name": "privatebin", - "version": "1.5.2", + "version": "1.6.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "privatebin", - "version": "1.5.2", + "version": "1.6.2", "license": "zlib-acknowledgement", "devDependencies": { "@peculiar/webcrypto": "^1.1.1", diff --git a/js/privatebin.js b/js/privatebin.js index db85eef7..e63ab5fb 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -2035,29 +2035,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { xhrFields: { withCredentials: false }, - success: function(response) { - let responseString = response; - if (typeof responseString === 'object') { - responseString = JSON.stringify(responseString); - } - if (typeof responseString === 'string' && responseString.length > 0) { - const shortUrlMatcher = /https?:\/\/[^\s]+/g; - const shortUrl = (responseString.match(shortUrlMatcher) || []).filter(URL.canParse).sort(function(a, b) { - return a.length - b.length; - })[0]; - if (typeof shortUrl === 'string' && shortUrl.length > 0) { - // we disable the button to avoid calling shortener again - $shortenButton.addClass('buttondisabled'); - // update link - $pasteUrl.text(shortUrl); - $pasteUrl.prop('href', shortUrl); - // we pre-select the link so that the user only has to [Ctrl]+[c] the link - Helper.selectText($pasteUrl[0]); - return; - } - } - Alert.showError('Cannot parse response from URL shortener.'); - } + success: PasteStatus.extractUrl }) .fail(function(data, textStatus, errorThrown) { console.error(textStatus, errorThrown); @@ -2123,6 +2101,42 @@ jQuery.PrivateBin = (function($, RawDeflate) { Helper.selectText($pasteUrl[0]); }; + /** + * extracts URLs from given string + * + * if at least one is found, it disables the shortener button and + * replaces the paste URL + * + * @name PasteStatus.extractUrl + * @function + * @param {string} response + */ + me.extractUrl = function(response) + { + if (typeof response === 'object') { + response = JSON.stringify(response); + } + if (typeof response === 'string' && response.length > 0) { + const shortUrlMatcher = /https?:\/\/[^\s]+/g; + const shortUrl = (response.match(shortUrlMatcher) || []).filter(function(a) { + return URL.canParse(a); + }).sort(function(a, b) { + return a.length - b.length; + })[0]; + if (typeof shortUrl === 'string' && shortUrl.length > 0) { + // we disable the button to avoid calling shortener again + $shortenButton.addClass('buttondisabled'); + // update link + $pasteUrl.text(shortUrl); + $pasteUrl.prop('href', shortUrl); + // we pre-select the link so that the user only has to [Ctrl]+[c] the link + Helper.selectText($pasteUrl[0]); + return; + } + } + Alert.showError('Cannot parse response from URL shortener.'); + }; + /** * shows the remaining time * diff --git a/js/test/PasteStatus.js b/js/test/PasteStatus.js index cf1f7e0c..9a5c60d0 100644 --- a/js/test/PasteStatus.js +++ b/js/test/PasteStatus.js @@ -34,6 +34,51 @@ describe('PasteStatus', function () { ); }); + describe('extractUrl', function () { + this.timeout(30000); + + jsc.property( + 'extracts and updates URLs found in given response', + jsc.elements(['http','https']), + 'nestring', + jsc.nearray(common.jscA2zString()), + jsc.array(common.jscQueryString()), + jsc.array(common.jscAlnumString()), + 'string', + function (schema, domain, tld, query, shortid, fragment) { + domain = domain.replace(/\P{Letter}|[\u00AA-\u00BA]/gu,'').toLowerCase(); + if (domain.length === 0) { + domain = 'a'; + } + const expected = '.' + tld.join('') + '/' + (query.length > 0 ? + ('?' + encodeURI(query.join('').replace(/^&+|&+$/gm,'')) + + shortid.join('')) : '') + (fragment.length > 0 ? + ('#' + encodeURI(fragment)) : ''), + clean = jsdom(); + + // not available in node before v19.9.0, v18.17.0 + if (typeof URL.canParse !== 'function') { + URL.canParse = function(a) { + return true; + } + } + + $('body').html('
'); + $.PrivateBin.PasteStatus.init(); + $.PrivateBin.PasteStatus.createPasteNotification('', ''); + $.PrivateBin.PasteStatus.extractUrl(schema + '://' + domain + expected); + + const result = $('#pasteurl')[0].href; + clean(); + + return result.endsWith(expected) && ( + result.startsWith(schema + '://xn--') || + result.startsWith(schema + '://' + domain) + ); + } + ); + }); + describe('showRemainingTime', function () { this.timeout(30000); diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 386705fa..3c72c00b 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -73,7 +73,7 @@ endif; ?> - + diff --git a/tpl/page.php b/tpl/page.php index 026b4ae1..c66c2ee5 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -51,7 +51,7 @@ endif; ?> - + From cc0b6e387ab6e4924d8539800a8a98ebff4ca318 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Thu, 4 Jan 2024 23:23:47 +0100 Subject: [PATCH 4/6] avoid use of bleeding edge function only supported in Firefox & Chrome >= 120 & node >= 19.9.0 & 18.17.0 --- js/privatebin.js | 6 +++++- js/test/PasteStatus.js | 7 ------- tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index e63ab5fb..930a1315 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -2119,7 +2119,11 @@ jQuery.PrivateBin = (function($, RawDeflate) { if (typeof response === 'string' && response.length > 0) { const shortUrlMatcher = /https?:\/\/[^\s]+/g; const shortUrl = (response.match(shortUrlMatcher) || []).filter(function(a) { - return URL.canParse(a); + try { + return !!new URL(a); + } catch (error) { + return false; + } }).sort(function(a, b) { return a.length - b.length; })[0]; diff --git a/js/test/PasteStatus.js b/js/test/PasteStatus.js index 9a5c60d0..baa6ab33 100644 --- a/js/test/PasteStatus.js +++ b/js/test/PasteStatus.js @@ -56,13 +56,6 @@ describe('PasteStatus', function () { ('#' + encodeURI(fragment)) : ''), clean = jsdom(); - // not available in node before v19.9.0, v18.17.0 - if (typeof URL.canParse !== 'function') { - URL.canParse = function(a) { - return true; - } - } - $('body').html('
'); $.PrivateBin.PasteStatus.init(); $.PrivateBin.PasteStatus.createPasteNotification('', ''); diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 3c72c00b..927891ac 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -73,7 +73,7 @@ endif; ?> - + diff --git a/tpl/page.php b/tpl/page.php index c66c2ee5..ba08d2ef 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -51,7 +51,7 @@ endif; ?> - + From c3331070cb1c83328e49b68f443a6abf27d3ce62 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 5 Jan 2024 06:28:19 +0100 Subject: [PATCH 5/6] codestyle, let's use readable variable names Co-authored-by: rugk --- js/privatebin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index 930a1315..b10f2d0d 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -2118,9 +2118,9 @@ jQuery.PrivateBin = (function($, RawDeflate) { } if (typeof response === 'string' && response.length > 0) { const shortUrlMatcher = /https?:\/\/[^\s]+/g; - const shortUrl = (response.match(shortUrlMatcher) || []).filter(function(a) { + const shortUrl = (response.match(shortUrlMatcher) || []).filter(function(urlRegExMatch) { try { - return !!new URL(a); + return !!new URL(urlRegExMatch); } catch (error) { return false; } From ba17e94c5e5763aa42c1fbe49a2bd81e7bd419b2 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Fri, 5 Jan 2024 06:40:12 +0100 Subject: [PATCH 6/6] use the newer function, if possible --- js/privatebin.js | 4 ++++ tpl/bootstrap.php | 2 +- tpl/page.php | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/js/privatebin.js b/js/privatebin.js index b10f2d0d..4e370e91 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -2119,6 +2119,10 @@ jQuery.PrivateBin = (function($, RawDeflate) { if (typeof response === 'string' && response.length > 0) { const shortUrlMatcher = /https?:\/\/[^\s]+/g; const shortUrl = (response.match(shortUrlMatcher) || []).filter(function(urlRegExMatch) { + if (typeof URL.canParse === 'function') { + return URL.canParse(urlRegExMatch); + } + // polyfill for older browsers (< 120) & node (< 19.9 & < 18.17) try { return !!new URL(urlRegExMatch); } catch (error) { diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 927891ac..d1a54fcb 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -73,7 +73,7 @@ endif; ?> - + diff --git a/tpl/page.php b/tpl/page.php index ba08d2ef..b0b9cc1a 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -51,7 +51,7 @@ endif; ?> - +