Просмотр исходного кода

Merge branch 'master' into ask-before-burn

El RIDO 2 лет назад
Родитель
Сommit
03d2291ec7
10 измененных файлов с 272 добавлено и 210 удалено
  1. 2 2
      .github/workflows/tests.yml
  2. 33 33
      composer.lock
  3. 23 2
      js/common.js
  4. 2 2
      js/privatebin.js
  5. 22 24
      js/test/Helper.js
  6. 39 73
      js/test/Model.js
  7. 136 53
      js/test/PasteStatus.js
  8. 13 19
      js/test/UiHelper.js
  9. 1 1
      tpl/bootstrap.php
  10. 1 1
      tpl/page.php

+ 2 - 2
.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') }}

+ 33 - 33
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",

+ 23 - 2
js/common.js

@@ -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) : '');
+};

+ 2 - 2
js/privatebin.js

@@ -2128,7 +2128,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
                 response = JSON.stringify(response);
             }
             if (typeof response === 'string' && response.length > 0) {
-                const shortUrlMatcher = /https?:\/\/[^\s]+/g;
+                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);
@@ -2140,7 +2140,7 @@ jQuery.PrivateBin = (function($, RawDeflate) {
                         return false;
                     }
                 }).sort(function(a, b) {
-                    return a.length - b.length;
+                    return a.length - b.length; // shortest first
                 })[0];
                 if (typeof shortUrl === 'string' && shortUrl.length > 0) {
                     // we disable the button to avoid calling shortener again

+ 22 - 24
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('<div id="foo"></div>');
                 let e = $('#foo');
 
                 // special cases: When the query string and fragment imply the beginning of an HTML entity, eg. &#0 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 = $('<div />').text(url).html();
-                return $('<div />').text(prefix).html() + '<a href="' + url + '" target="_blank" rel="nofollow noopener noreferrer">' + url + '</a>' + $('<div />').text(postfix).html() === result;
+                urlString = $('<div />').text(urlString).html();
+                const expected = $('<div />').text(prefix).html() + '<a href="' + urlString + '" target="_blank" rel="nofollow noopener noreferrer">' + urlString + '</a>' + $('<div />').text(postfix).html();
+                return $('<div />').text(prefix).html() + '<a href="' + urlString + '" target="_blank" rel="nofollow noopener noreferrer">' + urlString + '</a>' + $('<div />').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;

+ 39 - 73
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();

+ 136 - 53
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('<div><div id="deletelink"></div><div id="pastelink"></div></div>');
                 $.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;
@@ -38,28 +45,26 @@ describe('PasteStatus', function () {
         this.timeout(30000);
 
         jsc.property(
-            'extracts and updates URLs found in given response',
-            jsc.elements(['http','https']),
+            'extracts and updates IDN URLs found in given response',
+            common.jscSchemas(false),
             '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();
+            common.jscUrl(),
+            function (schema, domain, url) {
+                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)) : ''),
+                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('<div><div id="pastelink"></div></div>');
                 $.PrivateBin.PasteStatus.init();
                 $.PrivateBin.PasteStatus.createPasteNotification('', '');
-                $.PrivateBin.PasteStatus.extractUrl(schema + '://' + domain + expected);
+                $.PrivateBin.PasteStatus.extractUrl(urlString);
 
                 const result = $('#pasteurl')[0].href;
                 clean();
@@ -70,6 +75,102 @@ describe('PasteStatus', function () {
                 );
             }
         );
+
+        // 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('<div><div id="pastelink"></div></div>');
+                $.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 = '<result>\n' +
+                        '    <keyword>' + longUrl.address.join('') + '</keyword>\n' +
+                        '    <shorturl>' + shortUrlString + '</shorturl>\n' +
+                        '    <longurl>' + longUrlString + '</longurl>\n' +
+                        '    <message>success</message>\n' +
+                        '    <statusCode>200</statusCode>\n' +
+                        '</result>',
+                    clean = jsdom();
+
+                $('body').html('<div><div id="pastelink"></div></div>');
+                $.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 = '<!DOCTYPE html>\n' +
+                        '<html lang="en">\n' +
+                        '\t<head>\n' +
+                        '\t\t<meta charset="utf-8" />\n' +
+                        '\t\t<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; base-uri \'self\'; form-action \'none\'; manifest-src \'self\'; connect-src * blob:; script-src \'self\' \'unsafe-eval\'; style-src \'self\'; font-src \'self\'; frame-ancestors \'none\'; img-src \'self\' data: blob:; media-src blob:; object-src blob:; sandbox allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-downloads">\n' +
+                        '\t\t<meta name="robots" content="noindex" />\n' +
+                        '\t\t<meta name="google" content="notranslate">\n' +
+                        '\t\t<title>PrivateBin</title>\n' +
+                        '\t</head>\n' +
+                        '\t<body>\n' +
+                        '\t\t<p>Your paste is <a id="pasteurl" href="' + shortUrlString + '">' + shortUrlString + '</a> <span id="copyhint">(Hit [Ctrl]+[c] to copy)</span></p>\n' +
+                        '\t</body>\n' +
+                        '</html>',
+                    clean = jsdom();
+
+                $('body').html('<div><div id="pastelink"></div></div>');
+                $.PrivateBin.PasteStatus.init();
+                $.PrivateBin.PasteStatus.createPasteNotification('', '');
+                $.PrivateBin.PasteStatus.extractUrl(yourlsResponse);
+
+                const result = $('#pasteurl')[0].href;
+                clean();
+
+                return result === shortUrlString;
+            }
+        );
     });
 
     describe('showRemainingTime', function () {
@@ -79,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('<div id="remainingtime" class="hidden"></div>');
                 $.PrivateBin.PasteStatus.init();
@@ -117,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('<div id="remainingtime" class="hidden"></div>');
                 $.PrivateBin.PasteStatus.init();

+ 13 - 19
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', {

+ 1 - 1
tpl/bootstrap.php

@@ -73,7 +73,7 @@ endif;
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-3.0.6.js" integrity="sha512-N3y6/HOk3pbsw3lFh4O8CKKEVwu1B2CF8kinhjURf8Yqa5OfSUt+/arozxFW+TUPOPw3TsDCRT/0u7BGRTEVUw==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-zHSMNVafjkfbJWyExunMI9NvD/oSdifOiPCUGKdf4GHeCjl/IRi8RYyGoAzZPekqRDhhkl13SlfkUiQYKD0g5Q==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-TwHs7AU0ZWg8bylMgHXtPjnTeGeqSGHknoD3GENdI+xUjJYlup5vNNgL9pg3KSTgPnN49HqNt7h1NdqjHZ1Rfg==" crossorigin="anonymous"></script>
 		<!-- icon -->
 		<link rel="apple-touch-icon" href="<?php echo I18n::encode($BASEPATH); ?>img/apple-touch-icon.png" sizes="180x180" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png" sizes="32x32" />

+ 1 - 1
tpl/page.php

@@ -51,7 +51,7 @@ endif;
 ?>
 		<script type="text/javascript" data-cfasync="false" src="js/purify-3.0.6.js" integrity="sha512-N3y6/HOk3pbsw3lFh4O8CKKEVwu1B2CF8kinhjURf8Yqa5OfSUt+/arozxFW+TUPOPw3TsDCRT/0u7BGRTEVUw==" crossorigin="anonymous"></script>
 		<script type="text/javascript" data-cfasync="false" src="js/legacy.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-LYos+qXHIRqFf5ZPNphvtTB0cgzHUizu2wwcOwcwz/VIpRv9lpcBgPYz4uq6jx0INwCAj6Fbnl5HoKiLufS2jg==" crossorigin="anonymous"></script>
-		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-zHSMNVafjkfbJWyExunMI9NvD/oSdifOiPCUGKdf4GHeCjl/IRi8RYyGoAzZPekqRDhhkl13SlfkUiQYKD0g5Q==" crossorigin="anonymous"></script>
+		<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-TwHs7AU0ZWg8bylMgHXtPjnTeGeqSGHknoD3GENdI+xUjJYlup5vNNgL9pg3KSTgPnN49HqNt7h1NdqjHZ1Rfg==" crossorigin="anonymous"></script>
 		<!-- icon -->
 		<link rel="apple-touch-icon" href="img/apple-touch-icon.png?<?php echo rawurlencode($VERSION); ?>" sizes="180x180" />
 		<link rel="icon" type="image/png" href="img/favicon-32x32.png?<?php echo rawurlencode($VERSION); ?>" sizes="32x32" />