Merge branch 'master' into crowdin-translation
This commit is contained in:
commit
239f6da73c
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@ -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') }}
|
||||
|
@ -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)
|
||||
|
66
composer.lock
generated
66
composer.lock
generated
@ -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",
|
||||
|
27
js/common.js
27
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) : '');
|
||||
};
|
4
js/package-lock.json
generated
4
js/package-lock.json
generated
@ -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",
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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. � 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;
|
||||
|
112
js/test/Model.js
112
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();
|
||||
|
@ -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;
|
||||
@ -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('<div><div id="pastelink"></div></div>');
|
||||
$.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('<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 () {
|
||||
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('<div id="remainingtime" class="hidden"></div>');
|
||||
$.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('<div id="remainingtime" class="hidden"></div>');
|
||||
$.PrivateBin.PasteStatus.init();
|
||||
|
@ -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', {
|
||||
|
@ -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-Zy/z0iR0xK8/2xkYJOr7JD7GS46Z2EoWFKcnSWK2FkCKf3VxFKoL3l7Tc+ru2mqZfxgk6H/j8+5JLeDKcafqxw==" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-cfRk8a/8RpvMb4g9Su9kcKNcs7+PyGioUxH6z6k9e4vcdZmtz+gM2HwIP/Gd/1r6h3qoxrkO4jodn2E7gtZ7EA==" 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" />
|
||||
|
@ -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-Zy/z0iR0xK8/2xkYJOr7JD7GS46Z2EoWFKcnSWK2FkCKf3VxFKoL3l7Tc+ru2mqZfxgk6H/j8+5JLeDKcafqxw==" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" data-cfasync="false" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-cfRk8a/8RpvMb4g9Su9kcKNcs7+PyGioUxH6z6k9e4vcdZmtz+gM2HwIP/Gd/1r6h3qoxrkO4jodn2E7gtZ7EA==" 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" />
|
||||
|
Loading…
Reference in New Issue
Block a user