diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dd5f59b6..730de26e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,7 +41,7 @@ jobs: key: ${{ runner.os }}-${{ env.extensions-cache-key }} - name: Cache extensions - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.extcache.outputs.dir }} key: ${{ steps.extcache.outputs.key }} @@ -76,7 +76,7 @@ jobs: shell: bash - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ steps.get-date.outputs.date }}-${{ hashFiles('**/composer.json') }} diff --git a/CHANGELOG.md b/CHANGELOG.md index a282c8f3..9a817590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * ADDED: Translations for Romanian * 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) diff --git a/composer.lock b/composer.lock index bf8fa3fd..5f8d2963 100644 --- a/composer.lock +++ b/composer.lock @@ -316,16 +316,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.17.1", + "version": "v4.18.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", - "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", + "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", "shasum": "" }, "require": { @@ -366,9 +366,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" }, - "time": "2023-08-13T19:53:39+00:00" + "time": "2023-12-10T21:03:43+00:00" }, { "name": "phar-io/manifest", @@ -483,23 +483,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.29", + "version": "9.2.30", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76" + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6a3a87ac2bbe33b25042753df8195ba4aa534c76", - "reference": "6a3a87ac2bbe33b25042753df8195ba4aa534c76", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", + "reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.15", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -549,7 +549,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.29" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" }, "funding": [ { @@ -557,7 +557,7 @@ "type": "github" } ], - "time": "2023-09-19T04:57:46+00:00" + "time": "2023-12-22T06:47:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -802,16 +802,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.15", + "version": "9.6.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1" + "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/05017b80304e0eb3f31d90194a563fd53a6021f1", - "reference": "05017b80304e0eb3f31d90194a563fd53a6021f1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3767b2c56ce02d01e3491046f33466a1ae60a37f", + "reference": "3767b2c56ce02d01e3491046f33466a1ae60a37f", "shasum": "" }, "require": { @@ -885,7 +885,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.15" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.16" }, "funding": [ { @@ -901,7 +901,7 @@ "type": "tidelift" } ], - "time": "2023-12-01T16:55:19+00:00" + "time": "2024-01-19T07:03:14+00:00" }, { "name": "sebastian/cli-parser", @@ -1146,20 +1146,20 @@ }, { "name": "sebastian/complexity", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.7", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -1191,7 +1191,7 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" }, "funding": [ { @@ -1199,7 +1199,7 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", @@ -1473,20 +1473,20 @@ }, { "name": "sebastian/lines-of-code", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", "shasum": "" }, "require": { - "nikic/php-parser": "^4.6", + "nikic/php-parser": "^4.18 || ^5.0", "php": ">=7.3" }, "require-dev": { @@ -1518,7 +1518,7 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" }, "funding": [ { @@ -1526,7 +1526,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", diff --git a/js/common.js b/js/common.js index 7e406acb..d3953d36 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'), @@ -113,8 +113,8 @@ exports.jscBase64String = function() { }; // provides a random URL schema supported by the whatwg-url library -exports.jscSchemas = function() { - return jsc.elements(schemas); +exports.jscSchemas = function(withFtp = true) { + return jsc.elements(withFtp ? schemas : schemas.slice(1)); }; // provides a random supported language string @@ -131,3 +131,24 @@ exports.jscMimeTypes = function() { exports.jscFormats = function() { return jsc.elements(formats); }; + +// provides random URLs +exports.jscUrl = function(withFragment = true, withQuery = true) { + let url = { + schema: exports.jscSchemas(), + address: jsc.nearray(exports.jscA2zString()), + }; + if (withFragment) { + url.fragment = jsc.string; + } + if(withQuery) { + url.query = jsc.array(exports.jscQueryString()); + } + return jsc.record(url); +}; + +exports.urlToString = function (url) { + return url.schema + '://' + url.address.join('') + '/' + (url.query ? '?' + + encodeURI(url.query.join('').replace(/^&+|&+$/gm,'')) : '') + + (url.fragment ? '#' + encodeURI(url.fragment) : ''); +}; \ No newline at end of file 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 30354ccd..518cf32a 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -2037,29 +2037,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?:\/\/(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) { - 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); @@ -2125,6 +2103,50 @@ 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; // JSON API will have URL in quotes, XML in tags + 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) { + return false; + } + }).sort(function(a, b) { + return a.length - b.length; // shortest first + })[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/Helper.js b/js/test/Helper.js index 2cd5202f..95ae5709 100644 --- a/js/test/Helper.js +++ b/js/test/Helper.js @@ -96,36 +96,34 @@ describe('Helper', function () { jsc.property( 'replaces URLs with anchors', 'string', - jsc.elements(['http', 'https', 'ftp']), - jsc.nearray(common.jscA2zString()), - jsc.array(common.jscQueryString()), + common.jscUrl(), jsc.array(common.jscHashString()), 'string', - function (prefix, schema, address, query, fragment, postfix) { - query = query.join(''); - fragment = fragment.join(''); + function (prefix, url, fragment, postfix) { prefix = prefix.replace(/\r|\f/g, '\n').replace(/\u0000/g, '').replace(/\u000b/g, ''); postfix = ' ' + postfix.replace(/\r/g, '\n').replace(/\u0000/g, ''); - let url = schema + '://' + address.join('') + '/?' + query + '#' + fragment, + url.fragment = fragment.join(''); + let urlString = common.urlToString(url), clean = jsdom(); $('body').html('
'); let e = $('#foo'); // special cases: When the query string and fragment imply the beginning of an HTML entity, eg. � or &#x if ( - query.slice(-1) === '&' && - (parseInt(fragment.substring(0, 1), 10) >= 0 || fragment.charAt(0) === 'x' ) - ) - { - url = schema + '://' + address.join('') + '/?' + query.substring(0, query.length - 1); + url.query[-1] === '&' && + (parseInt(url.fragment.charAt(0), 10) >= 0 || url.fragment.charAt(0) === 'x') + ) { + url.query.pop(); + urlString = common.urlToString(url); postfix = ''; } - e.text(prefix + url + postfix); + e.text(prefix + urlString + postfix); $.PrivateBin.Helper.urls2links(e); let result = e.html(); clean(); - url = $('
').text(url).html(); - return $('
').text(prefix).html() + '' + url + '' + $('
').text(postfix).html() === result; + urlString = $('
').text(urlString).html(); + const expected = $('
').text(prefix).html() + '' + urlString + '' + $('
').text(postfix).html(); + return $('
').text(prefix).html() + '' + urlString + '' + $('
').text(postfix).html() === result; } ); jsc.property( @@ -261,16 +259,16 @@ describe('Helper', function () { this.timeout(30000); jsc.property( 'returns the URL without query & fragment', - jsc.elements(['http', 'https']), - jsc.nearray(common.jscA2zString()), - jsc.array(common.jscA2zString()), - jsc.array(common.jscQueryString()), - 'string', - function (schema, address, path, query, fragment) { + common.jscSchemas(false), + common.jscUrl(), + function (schema, url) { + url.schema = schema; + const fullUrl = common.urlToString(url); + delete(url.query); + delete(url.fragment); $.PrivateBin.Helper.reset(); - var path = path.join('') + (path.length > 0 ? '/' : ''), - expected = schema + '://' + address.join('') + '/' + path, - clean = jsdom('', {url: expected + '?' + query.join('') + '#' + fragment}), + const expected = common.urlToString(url), + clean = jsdom('', {url: fullUrl}), result = $.PrivateBin.Helper.baseUri(); clean(); return expected === result; diff --git a/js/test/Model.js b/js/test/Model.js index 9ebc5472..db2198ba 100644 --- a/js/test/Model.js +++ b/js/test/Model.js @@ -80,23 +80,22 @@ describe('Model', function () { jsc.property( 'returns the query string without separator, if any', - jsc.nearray(common.jscA2zString()), - jsc.nearray(common.jscA2zString()), + common.jscUrl(true, false), jsc.tuple(new Array(16).fill(common.jscHexString)), jsc.array(common.jscQueryString()), jsc.array(common.jscQueryString()), - 'string', - function (schema, address, pasteId, queryStart, queryEnd, fragment) { - var pasteIdString = pasteId.join(''), - queryStartString = queryStart.join('') + (queryStart.length > 0 ? '&' : ''), - queryEndString = (queryEnd.length > 0 ? '&' : '') + queryEnd.join(''), - queryString = queryStartString + pasteIdString + queryEndString, - clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + - '/?' + queryString + '#' + fragment - }); - global.URL = require('jsdom-url').URL; - var result = $.PrivateBin.Model.getPasteId(); + function (url, pasteId, queryStart, queryEnd) { + if (queryStart.length > 0) { + queryStart.push('&'); + } + if (queryEnd.length > 0) { + queryEnd.unshift('&'); + } + url.query = queryStart.concat(pasteId, queryEnd); + const pasteIdString = pasteId.join(''), + clean = jsdom('', {url: common.urlToString(url)}); + global.URL = require('jsdom-url').URL; + const result = $.PrivateBin.Model.getPasteId(); $.PrivateBin.Model.reset(); clean(); return pasteIdString === result; @@ -104,14 +103,9 @@ describe('Model', function () { ); jsc.property( 'throws exception on empty query string', - jsc.nearray(common.jscA2zString()), - jsc.nearray(common.jscA2zString()), - 'string', - function (schema, address, fragment) { - var clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + - '/#' + fragment - }), + common.jscUrl(true, false), + function (url) { + let clean = jsdom('', {url: common.urlToString(url)}), result = false; global.URL = require('jsdom-url').URL; try { @@ -135,35 +129,24 @@ describe('Model', function () { jsc.property( 'returns the fragment of a v1 URL', - jsc.nearray(common.jscA2zString()), - jsc.nearray(common.jscA2zString()), - jsc.array(common.jscQueryString()), - 'nestring', - function (schema, address, query, fragment) { - const fragmentString = common.btoa(fragment.padStart(32, '\u0000')); - let clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + - '/?' + query.join('') + '#' + fragmentString - }), + common.jscUrl(), + function (url) { + url.fragment = common.btoa(url.fragment.padStart(32, '\u0000')); + const clean = jsdom('', {url: common.urlToString(url)}), result = $.PrivateBin.Model.getPasteKey(); $.PrivateBin.Model.reset(); clean(); - return fragmentString === result; + return url.fragment === result; } ); jsc.property( 'returns the v1 fragment stripped of trailing query parts', - jsc.nearray(common.jscA2zString()), - jsc.nearray(common.jscA2zString()), - jsc.array(common.jscQueryString()), - 'nestring', + common.jscUrl(), jsc.array(common.jscHashString()), - function (schema, address, query, fragment, trail) { - const fragmentString = common.btoa(fragment.padStart(32, '\u0000')); - let clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + '/?' + - query.join('') + '#' + fragmentString + '&' + trail.join('') - }), + function (url, trail) { + const fragmentString = common.btoa(url.fragment.padStart(32, '\u0000')); + url.fragment = fragmentString + '&' + trail.join(''); + const clean = jsdom('', {url: common.urlToString(url)}), result = $.PrivateBin.Model.getPasteKey(); $.PrivateBin.Model.reset(); clean(); @@ -172,18 +155,12 @@ describe('Model', function () { ); jsc.property( 'returns the fragment of a v2 URL', - jsc.nearray(common.jscA2zString()), - jsc.nearray(common.jscA2zString()), - jsc.array(common.jscQueryString()), - 'nestring', - function (schema, address, query, fragment) { + common.jscUrl(), + function (url) { // base58 strips leading NULL bytes, so the string is padded with these if not found - fragment = fragment.padStart(32, '\u0000'); - let fragmentString = $.PrivateBin.CryptTool.base58encode(fragment), - clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + - '/?' + query.join('') + '#' + fragmentString - }), + const fragment = url.fragment.padStart(32, '\u0000'); + url.fragment = $.PrivateBin.CryptTool.base58encode(fragment); + const clean = jsdom('', {url: common.urlToString(url)}), result = $.PrivateBin.Model.getPasteKey(); $.PrivateBin.Model.reset(); clean(); @@ -192,19 +169,13 @@ describe('Model', function () { ); jsc.property( 'returns the v2 fragment stripped of trailing query parts', - jsc.nearray(common.jscA2zString()), - jsc.nearray(common.jscA2zString()), - jsc.array(common.jscQueryString()), - 'nestring', + common.jscUrl(), jsc.array(common.jscHashString()), - function (schema, address, query, fragment, trail) { + function (url, trail) { // base58 strips leading NULL bytes, so the string is padded with these if not found - fragment = fragment.padStart(32, '\u0000'); - let fragmentString = $.PrivateBin.CryptTool.base58encode(fragment), - clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + '/?' + - query.join('') + '#' + fragmentString + '&' + trail.join('') - }), + const fragment = url.fragment.padStart(32, '\u0000'); + url.fragment = $.PrivateBin.CryptTool.base58encode(fragment) + '&' + trail.join(''); + const clean = jsdom('', {url: common.urlToString(url)}), result = $.PrivateBin.Model.getPasteKey(); $.PrivateBin.Model.reset(); clean(); @@ -213,14 +184,9 @@ describe('Model', function () { ); jsc.property( 'throws exception on empty fragment of the URL', - jsc.nearray(common.jscA2zString()), - jsc.nearray(common.jscA2zString()), - jsc.array(common.jscQueryString()), - function (schema, address, query) { - var clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + - '/?' + query.join('') - }), + common.jscUrl(false), + function (url) { + let clean = jsdom('', {url: common.urlToString(url)}), result = false; try { $.PrivateBin.Model.getPasteKey(); diff --git a/js/test/PasteStatus.js b/js/test/PasteStatus.js index cf1f7e0c..a233bb48 100644 --- a/js/test/PasteStatus.js +++ b/js/test/PasteStatus.js @@ -1,32 +1,39 @@ 'use strict'; var common = require('../common'); +function urlStrings(schema, longUrl, shortUrl) { + longUrl.schema = schema; + shortUrl.schema = schema; + let longUrlString = common.urlToString(longUrl), + shortUrlString = common.urlToString(shortUrl); + // ensure the two random URLs actually are sorted as expected + if (longUrlString.length <= shortUrlString.length) { + if (longUrlString.length === shortUrlString.length) { + longUrl.address.unshift('a'); + longUrlString = common.urlToString(longUrl); + } else { + [longUrlString, shortUrlString] = [shortUrlString, longUrlString]; + } + } + return [longUrlString, shortUrlString]; +} + describe('PasteStatus', function () { describe('createPasteNotification', function () { this.timeout(30000); jsc.property( 'creates a notification after a successfull paste upload', - common.jscSchemas(), - jsc.nearray(common.jscA2zString()), - jsc.array(common.jscQueryString()), - 'string', - common.jscSchemas(), - jsc.nearray(common.jscA2zString()), - jsc.array(common.jscQueryString()), - function ( - schema1, address1, query1, fragment1, - schema2, address2, query2 - ) { - var expected1 = schema1 + '://' + address1.join('') + '/?' + - encodeURI(query1.join('').replace(/^&+|&+$/gm,'') + '#' + fragment1), - expected2 = schema2 + '://' + address2.join('') + '/?' + - encodeURI(query2.join('').replace(/^&+|&+$/gm,'')), + common.jscUrl(), + common.jscUrl(false), + function (url1, url2) { + const expected1 = common.urlToString(url1).replace(/&(gt|lt)$/, '&$1a'), + expected2 = common.urlToString(url2).replace(/&(gt|lt)$/, '&$1a'), clean = jsdom(); $('body').html('
'); $.PrivateBin.PasteStatus.init(); $.PrivateBin.PasteStatus.createPasteNotification(expected1, expected2); - var result1 = $('#pasteurl')[0].href, + const result1 = $('#pasteurl')[0].href, result2 = $('#deletelink a')[0].href; clean(); return result1 === expected1 && result2 === expected2; @@ -34,6 +41,138 @@ describe('PasteStatus', function () { ); }); + describe('extractUrl', function () { + this.timeout(30000); + + jsc.property( + 'extracts and updates IDN URLs found in given response', + common.jscSchemas(false), + 'nestring', + common.jscUrl(), + function (schema, domain, url) { + domain = domain.replace(/\P{Letter}|[\u00AA-\u00BA]/gu, '').toLowerCase(); + if (domain.length === 0) { + domain = 'a'; + } + url.schema = schema; + url.address.unshift('.'); + url.address = domain.split('').concat(url.address); + const urlString = common.urlToString(url), + expected = urlString.substring((schema + '://' + domain).length), + clean = jsdom(); + + $('body').html('
'); + $.PrivateBin.PasteStatus.init(); + $.PrivateBin.PasteStatus.createPasteNotification('', ''); + $.PrivateBin.PasteStatus.extractUrl(urlString); + + const result = $('#pasteurl')[0].href; + clean(); + + return result.endsWith(expected) && ( + result.startsWith(schema + '://xn--') || + result.startsWith(schema + '://' + domain) + ); + } + ); + + // YOURLS API samples from: https://yourls.org/readme.html#API;apireturn + jsc.property( + 'extracts and updates URLs found in YOURLS API JSON response', + common.jscSchemas(false), + common.jscUrl(), + common.jscUrl(false), + function (schema, longUrl, shortUrl) { + const [longUrlString, shortUrlString] = urlStrings(schema, longUrl, shortUrl), + yourlsResponse = { + url: { + keyword: longUrl.address.join(''), + url: longUrlString, + title: "example title", + date: "2014-10-24 16:01:39", + ip: "127.0.0.1" + }, + status: "success", + message: longUrlString + " added to database", + title: "example title", + shorturl: shortUrlString, + statusCode: 200 + }, + clean = jsdom(); + + $('body').html('
'); + $.PrivateBin.PasteStatus.init(); + $.PrivateBin.PasteStatus.createPasteNotification('', ''); + $.PrivateBin.PasteStatus.extractUrl(JSON.stringify(yourlsResponse, undefined, 4)); + + const result = $('#pasteurl')[0].href; + clean(); + + return result === shortUrlString; + } + ); + jsc.property( + 'extracts and updates URLs found in YOURLS API XML response', + common.jscSchemas(false), + common.jscUrl(), + common.jscUrl(false), + function (schema, longUrl, shortUrl) { + const [longUrlString, shortUrlString] = urlStrings(schema, longUrl, shortUrl), + yourlsResponse = '\n' + + ' ' + longUrl.address.join('') + '\n' + + ' ' + shortUrlString + '\n' + + ' ' + longUrlString + '\n' + + ' success\n' + + ' 200\n' + + '', + clean = jsdom(); + + $('body').html('
'); + $.PrivateBin.PasteStatus.init(); + $.PrivateBin.PasteStatus.createPasteNotification('', ''); + $.PrivateBin.PasteStatus.extractUrl(yourlsResponse); + + const result = $('#pasteurl')[0].href; + clean(); + + return result === shortUrlString; + } + ); + jsc.property( + 'extracts and updates URLs found in YOURLS proxy HTML response', + common.jscSchemas(false), + common.jscUrl(), + common.jscUrl(false), + function (schema, longUrl, shortUrl) { + const [longUrlString, shortUrlString] = urlStrings(schema, longUrl, shortUrl), + yourlsResponse = '\n' + + '\n' + + '\t\n' + + '\t\t\n' + + '\t\t\n' + + '\t\t\n' + + '\t\t\n' + + '\t\tPrivateBin\n' + + '\t\n' + + '\t\n' + + '\t\t

Your paste is ' + shortUrlString + ' (Hit [Ctrl]+[c] to copy)

\n' + + '\t\n' + + '', + clean = jsdom(); + + $('body').html('
'); + $.PrivateBin.PasteStatus.init(); + $.PrivateBin.PasteStatus.createPasteNotification('', ''); + $.PrivateBin.PasteStatus.extractUrl(yourlsResponse); + + const result = $('#pasteurl')[0].href; + clean(); + + return result === shortUrlString; + } + ); + }); + describe('showRemainingTime', function () { this.timeout(30000); @@ -41,18 +180,9 @@ describe('PasteStatus', function () { 'shows burn after reading message or remaining time v1', 'bool', 'nat', - jsc.nearray(common.jscA2zString()), - jsc.nearray(common.jscA2zString()), - jsc.nearray(common.jscQueryString()), - 'string', - function ( - burnafterreading, remainingTime, - schema, address, query, fragment - ) { - var clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + - '/?' + query.join('') + '#' + fragment - }), + common.jscUrl(), + function (burnafterreading, remainingTime, url) { + let clean = jsdom('', {url: common.urlToString(url)}), result; $('body').html(''); $.PrivateBin.PasteStatus.init(); @@ -79,18 +209,9 @@ describe('PasteStatus', function () { 'shows burn after reading message or remaining time v2', 'bool', 'nat', - jsc.nearray(common.jscA2zString()), - jsc.nearray(common.jscA2zString()), - jsc.nearray(common.jscQueryString()), - 'string', - function ( - burnafterreading, remainingTime, - schema, address, query, fragment - ) { - var clean = jsdom('', { - url: schema.join('') + '://' + address.join('') + - '/?' + query.join('') + '#' + fragment - }), + common.jscUrl(), + function (burnafterreading, remainingTime, url) { + let clean = jsdom('', {url: common.urlToString(url)}), result; $('body').html(''); $.PrivateBin.PasteStatus.init(); diff --git a/js/test/UiHelper.js b/js/test/UiHelper.js index 817345c6..63968fdb 100644 --- a/js/test/UiHelper.js +++ b/js/test/UiHelper.js @@ -13,10 +13,9 @@ describe('UiHelper', function () { jsc.property( 'redirects to home, when the state is null', - common.jscSchemas(), - jsc.nearray(common.jscA2zString()), - function (schema, address) { - var expected = schema + '://' + address.join('') + '/', + common.jscUrl(false, false), + function (url) { + const expected = common.urlToString(url), clean = jsdom('', {url: expected}); // make window.location.href writable @@ -34,13 +33,11 @@ describe('UiHelper', function () { jsc.property( 'does not redirect to home, when a new paste is created', - common.jscSchemas(), - jsc.nearray(common.jscA2zString()), - jsc.array(common.jscQueryString()), + common.jscUrl(false), jsc.nearray(common.jscBase64String()), - function (schema, address, query, fragment) { - var expected = schema + '://' + address.join('') + '/?' + - query.join('') + '#' + fragment.join(''), + function (url, fragment) { + url.fragment = fragment.join(''); + const expected = common.urlToString(url), clean = jsdom('', {url: expected}); // make window.location.href writable @@ -67,15 +64,12 @@ describe('UiHelper', function () { jsc.property( 'redirects to home', - common.jscSchemas(), - jsc.nearray(common.jscA2zString()), - jsc.array(common.jscQueryString()), - jsc.nearray(common.jscBase64String()), - function (schema, address, query, fragment) { - var expected = schema + '://' + address.join('') + '/', - clean = jsdom('', { - url: expected + '?' + query.join('') + '#' + fragment.join('') - }); + common.jscUrl(), + function (url) { + const clean = jsdom('', {url: common.urlToString(url)}); + delete(url.query); + delete(url.fragment); + const expected = common.urlToString(url); // make window.location.href writable Object.defineProperty(window.location, 'href', { diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index 2e800974..6b6efa05 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 930e8eac..7acf2bb5 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -51,7 +51,7 @@ endif; ?> - +