update jsdom dependency, fix tests

This commit is contained in:
El RIDO 2025-02-27 08:16:30 +01:00
parent 64b0e33574
commit 4e518b3fce
No known key found for this signature in database
GPG Key ID: 0F5C940A6BD81F92
8 changed files with 678 additions and 888 deletions

View File

@ -5,7 +5,6 @@ global.assert = require('assert');
global.jsc = require('jsverify'); global.jsc = require('jsverify');
global.jsdom = require('jsdom-global'); global.jsdom = require('jsdom-global');
global.cleanup = global.jsdom(); global.cleanup = global.jsdom();
global.URL = require('jsdom-url').URL;
global.fs = require('fs'); global.fs = require('fs');
global.WebCrypto = require('@peculiar/webcrypto').Crypto; global.WebCrypto = require('@peculiar/webcrypto').Crypto;
@ -79,8 +78,16 @@ function parseMime(line) {
} }
// common testing helper functions // common testing helper functions
exports.atob = atob; // as of jsDOM 22 the base64 functions provided in the DOM are more restrictive
exports.btoa = btoa; // than browser implementation and throw when being passed invalid unicode
// codepoints - as we use these in the encryption with binary data, we need
// these to be character encoding agnostic
exports.atob = function(encoded) {
return Buffer.from(encoded, 'base64').toString('binary');
}
exports.btoa = function(text) {
return Buffer.from(text, 'binary').toString('base64');
}
// provides random lowercase characters from a to z // provides random lowercase characters from a to z
exports.jscA2zString = function() { exports.jscA2zString = function() {

1059
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,9 +8,8 @@
}, },
"devDependencies": { "devDependencies": {
"@peculiar/webcrypto": "^1.5.0", "@peculiar/webcrypto": "^1.5.0",
"jsdom": "^20.0.3", "jsdom": "^26.0.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"jsdom-url": "^3.0.1",
"jsverify": "^0.8.3" "jsverify": "^0.8.3"
}, },
"scripts": { "scripts": {

View File

@ -1,5 +1,5 @@
'use strict'; 'use strict';
var common = require('../common'); const common = require('../common');
describe('AttachmentViewer', function () { describe('AttachmentViewer', function () {
describe('setAttachment, showAttachment, removeAttachment, hideAttachment, hideAttachmentPreview, hasAttachment, getAttachment & moveAttachmentTo', function () { describe('setAttachment, showAttachment, removeAttachment, hideAttachment, hideAttachmentPreview, hasAttachment, getAttachment & moveAttachmentTo', function () {
@ -14,11 +14,12 @@ describe('AttachmentViewer', function () {
'string', 'string',
function (mimeType, rawdata, filename, prefix, postfix) { function (mimeType, rawdata, filename, prefix, postfix) {
let clean = jsdom(), let clean = jsdom(),
data = 'data:' + mimeType + ';base64,' + btoa(rawdata), data = 'data:' + mimeType + ';base64,' + common.btoa(rawdata),
mimePrefix = mimeType.substring(0, 6),
previewSupported = ( previewSupported = (
mimeType.substring(0, 6) === 'image/' || mimePrefix === 'image/' ||
mimeType.substring(0, 6) === 'audio/' || mimePrefix === 'audio/' ||
mimeType.substring(0, 6) === 'video/' || mimePrefix === 'video/' ||
mimeType.match(/\/pdf/i) mimeType.match(/\/pdf/i)
), ),
results = [], results = [],
@ -48,6 +49,7 @@ describe('AttachmentViewer', function () {
$('#attachment').hasClass('hidden') && $('#attachment').hasClass('hidden') &&
$('#attachmentPreview').hasClass('hidden') $('#attachmentPreview').hasClass('hidden')
); );
global.atob = common.atob;
if (filename.length) { if (filename.length) {
$.PrivateBin.AttachmentViewer.setAttachment(data, filename); $.PrivateBin.AttachmentViewer.setAttachment(data, filename);
} else { } else {

View File

@ -4,8 +4,11 @@ const common = require('../common');
describe('CopyToClipboard', function() { describe('CopyToClipboard', function() {
this.timeout(30000); this.timeout(30000);
describe ('Copy paste co clipboard', function () { describe ('Copy paste to clipboard', function () {
jsc.property('Copy with button click', common.jscFormats(), 'nestring', async function (format, text) { jsc.property('Copy with button click',
common.jscFormats(),
'nestring',
async function (format, text) {
var clean = jsdom(); var clean = jsdom();
common.enableClipboard(); common.enableClipboard();
@ -32,13 +35,17 @@ describe('CopyToClipboard', function() {
clean(); clean();
return text === savedToClipboardText; return text === savedToClipboardText;
}); }
);
/** /**
* Unfortunately in JSVerify impossible to check if copy with shortcut when user selected some text on the page * Unfortunately in JSVerify impossible to check if copy with shortcut when user selected some text on the page
* (the copy paste to clipboard should not work in this case) due to lucking window.getSelection() in jsdom. * (the copy paste to clipboard should not work in this case) due to lacking window.getSelection() in jsdom.
*/ */
jsc.property('Copy with keyboard shortcut', common.jscFormats(), 'nestring', async function (format, text) { jsc.property('Copy with keyboard shortcut',
common.jscFormats(),
'nestring',
async function (format, text) {
var clean = jsdom(); var clean = jsdom();
common.enableClipboard(); common.enableClipboard();
@ -65,11 +72,14 @@ describe('CopyToClipboard', function() {
clean(); clean();
return copiedTextWithoutSelectedText === text; return copiedTextWithoutSelectedText === text;
}); }
);
}); });
jsc.property('Copy link to clipboard', 'nestring', async function (text) { jsc.property('Copy link to clipboard',
'nestring',
async function (text) {
var clean = jsdom(); var clean = jsdom();
common.enableClipboard(); common.enableClipboard();
@ -85,11 +95,14 @@ describe('CopyToClipboard', function() {
clean(); clean();
return text === copiedText; return text === copiedText;
}); }
);
describe('Keyboard shortcut hint', function () { describe('Keyboard shortcut hint', function () {
jsc.property('Show hint', 'nestring', function (text) { jsc.property('Show hint',
'nestring',
function (text) {
var clean = jsdom(); var clean = jsdom();
$('body').html('<small id="copyShortcutHintText"></small>'); $('body').html('<small id="copyShortcutHintText"></small>');
@ -102,9 +115,12 @@ describe('CopyToClipboard', function() {
clean(); clean();
return keyboardShortcutHint.length > 0; return keyboardShortcutHint.length > 0;
}); }
);
jsc.property('Hide hint', 'nestring', function (text) { jsc.property('Hide hint',
'nestring',
function (text) {
var clean = jsdom(); var clean = jsdom();
$('body').html('<small id="copyShortcutHintText">' + text + '</small>'); $('body').html('<small id="copyShortcutHintText">' + text + '</small>');
@ -117,7 +133,7 @@ describe('CopyToClipboard', function() {
clean(); clean();
return keyboardShortcutHint.length === 0; return keyboardShortcutHint.length === 0;
}
);
}); });
});
}); });

View File

@ -1,5 +1,5 @@
'use strict'; 'use strict';
require('../common'); const common = require('../common');
describe('CryptTool', function () { describe('CryptTool', function () {
describe('cipher & decipher', function () { describe('cipher & decipher', function () {
@ -15,24 +15,26 @@ describe('CryptTool', function () {
'string', 'string',
'string', 'string',
async function (key, password, message) { async function (key, password, message) {
// pause to let async functions conclude const clean = jsdom();
await new Promise(resolve => setTimeout(resolve, 300));
let clean = jsdom();
// ensure zlib is getting loaded // ensure zlib is getting loaded
$.PrivateBin.Controller.initZ(); $.PrivateBin.Controller.initZ();
Object.defineProperty(window, 'crypto', { Object.defineProperty(window, 'crypto', {
value: new WebCrypto(), value: new WebCrypto(),
writeable: false, writeable: false,
}); });
global.atob = common.atob;
global.btoa = common.btoa;
message = message.trim(); message = message.trim();
let cipherMessage = await $.PrivateBin.CryptTool.cipher( const cipherMessage = await $.PrivateBin.CryptTool.cipher(
key, password, message, [] key, password, message, []
), ),
plaintext = await $.PrivateBin.CryptTool.decipher( plaintext = await $.PrivateBin.CryptTool.decipher(
key, password, cipherMessage key, password, cipherMessage
); );
clean(); clean();
return message === plaintext; const result = (message === plaintext);
if (!result) console.log(plaintext, cipherMessage);
return result;
} }
), ),
{tests: 3}); {tests: 3});
@ -41,18 +43,19 @@ describe('CryptTool', function () {
// The below static unit tests are included to ensure deciphering of "classic" // The below static unit tests are included to ensure deciphering of "classic"
// SJCL based pastes still works // SJCL based pastes still works
it( it(
'supports PrivateBin v1 ciphertext (SJCL & browser atob)', 'supports PrivateBin v1 ciphertext 1 (SJCL & browser atob)',
function () { async function () {
delete global.Base64; delete global.Base64;
let clean = jsdom(); const clean = jsdom();
Object.defineProperty(window, 'crypto', { Object.defineProperty(window, 'crypto', {
value: new WebCrypto(), value: new WebCrypto(),
writeable: false, writeable: false,
}); });
global.atob = common.atob;
// Of course you can easily decipher the following texts, if you like. // Of course you can easily decipher the following texts, if you like.
// Bonus points for finding their sources and hidden meanings. // Bonus points for finding their sources and hidden meanings.
return $.PrivateBin.CryptTool.decipher( const paste = await $.PrivateBin.CryptTool.decipher(
'6t2qsmLyfXIokNCL+3/yl15rfTUBQvm5SOnFPvNE7Q8=', '6t2qsmLyfXIokNCL+3/yl15rfTUBQvm5SOnFPvNE7Q8=',
// -- "That's amazing. I've got the same combination on my luggage." // -- "That's amazing. I've got the same combination on my luggage."
Array.apply(0, Array(6)).map((_,b) => b + 1).join(''), Array.apply(0, Array(6)).map((_,b) => b + 1).join(''),
@ -83,8 +86,30 @@ describe('CryptTool', function () {
'QUxMXI5htsn2rf0HxCFu7Po8DNYLxTS+67hYjDIYWYaEIc8LXWMLyDm9' + 'QUxMXI5htsn2rf0HxCFu7Po8DNYLxTS+67hYjDIYWYaEIc8LXWMLyDm9' +
'C5fARPJ4F2BIWgzgzkNj+dVjusft2XnziamWdbS5u3kuRlVuz5LQj+R5' + 'C5fARPJ4F2BIWgzgzkNj+dVjusft2XnziamWdbS5u3kuRlVuz5LQj+R5' +
'imnqQAincdZTkTT1nYx+DatlOLllCYIHffpI="}' 'imnqQAincdZTkTT1nYx+DatlOLllCYIHffpI="}'
).then(function (paste1) { );
$.PrivateBin.CryptTool.decipher( clean();
const result = typeof paste === 'string' && paste.includes('securely packed in iron');
if (!result) console.log(paste);
assert.ok(result);
}
);
it(
'supports PrivateBin v1 ciphertext 2 (SJCL & browser atob)',
async function () {
delete global.Base64;
const clean = jsdom();
// ensure zlib is getting loaded
$.PrivateBin.Controller.initZ();
Object.defineProperty(window, 'crypto', {
value: new WebCrypto(),
writeable: false,
});
global.atob = common.atob;
// Of course you can easily decipher the following texts, if you like.
// Bonus points for finding their sources and hidden meanings.
const paste = await $.PrivateBin.CryptTool.decipher(
's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=', 's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=',
'', // no password '', // no password
'{"iv":"WA42mdxIVXUwBqZu7JYNiw==","v":1,"iter":10000,"ks"' + '{"iv":"WA42mdxIVXUwBqZu7JYNiw==","v":1,"iter":10000,"ks"' +
@ -109,30 +134,28 @@ describe('CryptTool', function () {
'XhHvixZLcSjX2KQuHmEoWzmJcr3DavdoXZmAurGWLKjzEdJc5dSD/eNr' + 'XhHvixZLcSjX2KQuHmEoWzmJcr3DavdoXZmAurGWLKjzEdJc5dSD/eNr' +
'99gjHX7wphJ6umKMM+fn6PcbYJkhDh2GlJL5COXjXfm/5aj/vuyaRRWZ' + '99gjHX7wphJ6umKMM+fn6PcbYJkhDh2GlJL5COXjXfm/5aj/vuyaRRWZ' +
'MZtmnYpGAtAPg7AUG"}' 'MZtmnYpGAtAPg7AUG"}'
).then(function (paste2) {
clean();
assert.ok(
paste1.includes('securely packed in iron') &&
paste2.includes('Sol is right')
); );
}); clean();
}); const result = typeof paste === 'string' && paste.includes('Sol is right');
if (!result) console.log(paste);
assert.ok(result);
} }
); );
it( it(
'supports ZeroBin ciphertext (SJCL & Base64 1.7)', 'supports ZeroBin ciphertext 1 (SJCL & Base64 1.7)',
function () { async function () {
global.Base64 = require('../base64-1.7').Base64; global.Base64 = require('../base64-1.7').Base64;
var clean = jsdom(); const clean = jsdom();
Object.defineProperty(window, 'crypto', { Object.defineProperty(window, 'crypto', {
value: new WebCrypto(), value: new WebCrypto(),
writeable: false, writeable: false,
}); });
global.atob = common.atob;
// Of course you can easily decipher the following texts, if you like. // Of course you can easily decipher the following texts, if you like.
// Bonus points for finding their sources and hidden meanings. // Bonus points for finding their sources and hidden meanings.
return $.PrivateBin.CryptTool.decipher( const paste = await $.PrivateBin.CryptTool.decipher(
'6t2qsmLyfXIokNCL+3/yl15rfTUBQvm5SOnFPvNE7Q8=', '6t2qsmLyfXIokNCL+3/yl15rfTUBQvm5SOnFPvNE7Q8=',
// -- "That's amazing. I've got the same combination on my luggage." // -- "That's amazing. I've got the same combination on my luggage."
Array.apply(0, Array(6)).map((_,b) => b + 1).join(''), Array.apply(0, Array(6)).map((_,b) => b + 1).join(''),
@ -155,8 +178,27 @@ describe('CryptTool', function () {
'7mNNo7xba/YT9KoPDaniqnYqb+q2pX1WNWE7dLS2wfroMAS3kh8P22DA' + '7mNNo7xba/YT9KoPDaniqnYqb+q2pX1WNWE7dLS2wfroMAS3kh8P22DA' +
'V37AeiNoD2PcI6ZcHbRdPa+XRrRcJhSPPW7UQ0z4OvBfjdu/w390QxAx' + 'V37AeiNoD2PcI6ZcHbRdPa+XRrRcJhSPPW7UQ0z4OvBfjdu/w390QxAx' +
'SxvZewoh49fKKB6hTsRnZb4tpHkjlww=="}' 'SxvZewoh49fKKB6hTsRnZb4tpHkjlww=="}'
).then(function (paste1) { );
$.PrivateBin.CryptTool.decipher( clean();
delete global.Base64;
const result = typeof paste === 'string' && paste.includes('securely packed in iron');
if (!result) console.log(paste);
assert.ok(result);
}
);
it(
'supports ZeroBin ciphertext 2 (SJCL & Base64 1.7)',
async function () {
global.Base64 = require('../base64-1.7').Base64;
const clean = jsdom();
Object.defineProperty(window, 'crypto', {
value: new WebCrypto(),
writeable: false,
});
global.atob = common.atob;
const paste = await $.PrivateBin.CryptTool.decipher(
's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=', 's9pmKZKOBN7EVvHpTA8jjLFH3Xlz/0l8lB4+ONPACrM=',
'', // no password '', // no password
'{"iv":"Z7lAZQbkrqGMvruxoSm6Pw==","v":1,"iter":10000,"ks"' + '{"iv":"Z7lAZQbkrqGMvruxoSm6Pw==","v":1,"iter":10000,"ks"' +
@ -174,20 +216,17 @@ describe('CryptTool', function () {
'7tmfcF73w9dufDFI3LNca2KxzBnWNPYvIZKBwWbq8ncxkb191dP6mjEi' + '7tmfcF73w9dufDFI3LNca2KxzBnWNPYvIZKBwWbq8ncxkb191dP6mjEi' +
'7NnhqVk5A6vIBbu4AC5PZf76l6yep4xsoy/QtdDxCMocCXeAML9MQ9uP' + '7NnhqVk5A6vIBbu4AC5PZf76l6yep4xsoy/QtdDxCMocCXeAML9MQ9uP' +
'QbuspOKrBvMfN5igA1kBqasnxI472KBNXsdZnaDddSVUuvhTcETM="}' 'QbuspOKrBvMfN5igA1kBqasnxI472KBNXsdZnaDddSVUuvhTcETM="}'
).then(function (paste2) { );
clean(); clean();
delete global.Base64; delete global.Base64;
assert.ok( const result = typeof paste === 'string' && paste.includes('Sol is right');
paste1.includes('securely packed in iron') && if (!result) console.log(paste);
paste2.includes('Sol is right') assert.ok(result);
);
});
});
} }
); );
it('does not truncate messages', async function () { it('does not truncate messages', async function () {
let message = fs.readFileSync('test/compression-sample.txt', 'utf8'), const message = fs.readFileSync('test/compression-sample.txt', 'ascii').trim(),
clean = jsdom(); clean = jsdom();
Object.defineProperty(window, 'crypto', { Object.defineProperty(window, 'crypto', {
value: new WebCrypto(), value: new WebCrypto(),
@ -195,17 +234,18 @@ describe('CryptTool', function () {
}); });
// ensure zlib is getting loaded // ensure zlib is getting loaded
$.PrivateBin.Controller.initZ(); $.PrivateBin.Controller.initZ();
let cipherMessage = await $.PrivateBin.CryptTool.cipher( global.atob = common.atob;
global.btoa = common.btoa;
const cipherMessage = await $.PrivateBin.CryptTool.cipher(
'foo', 'bar', message, [] 'foo', 'bar', message, []
), ),
plaintext = await $.PrivateBin.CryptTool.decipher( plaintext = await $.PrivateBin.CryptTool.decipher(
'foo', 'bar', cipherMessage 'foo', 'bar', cipherMessage
); );
clean(); clean();
assert.strictEqual( const result = (message === plaintext);
message, if (!result) console.log(plaintext, cipherMessage);
plaintext assert.ok(result);
);
}); });
it('can en- and decrypt a particular message (#260)', function () { it('can en- and decrypt a particular message (#260)', function () {
@ -213,8 +253,6 @@ describe('CryptTool', function () {
'string', 'string',
'string', 'string',
async function (key, password) { async function (key, password) {
// pause to let async functions conclude
await new Promise(resolve => setTimeout(resolve, 300));
const message = ` const message = `
1 subgoal 1 subgoal
@ -237,21 +275,23 @@ isWhile : interp (while expr sBody) (MemElem mem) =
======================== ( 1 / 1 ) ======================== ( 1 / 1 )
conseq_or_bottom inv (interp (nth_iterate sBody n) (MemElem mem)) conseq_or_bottom inv (interp (nth_iterate sBody n) (MemElem mem))
`; `;
let clean = jsdom(); const clean = jsdom();
// ensure zlib is getting loaded // ensure zlib is getting loaded
$.PrivateBin.Controller.initZ(); $.PrivateBin.Controller.initZ();
Object.defineProperty(window, 'crypto', { Object.defineProperty(window, 'crypto', {
value: new WebCrypto(), value: new WebCrypto(),
writeable: false, writeable: false,
}); });
let cipherMessage = await $.PrivateBin.CryptTool.cipher( const cipherMessage = await $.PrivateBin.CryptTool.cipher(
key, password, message, [] key, password, message, []
), ),
plaintext = await $.PrivateBin.CryptTool.decipher( plaintext = await $.PrivateBin.CryptTool.decipher(
key, password, cipherMessage key, password, cipherMessage
); );
clean(); clean();
return message === plaintext; const result = (message === plaintext);
if (!result) console.log(plaintext, cipherMessage);
return result;
} }
), ),
{tests: 3}); {tests: 3});
@ -259,25 +299,27 @@ conseq_or_bottom inv (interp (nth_iterate sBody n) (MemElem mem))
}); });
describe('getSymmetricKey', function () { describe('getSymmetricKey', function () {
this.timeout(30000); this.timeout(10000);
var keys = []; let keys = [];
// the parameter is used to ensure the test is run more then one time // the parameter is used to ensure the test is run more then one time
jsc.property( it('returns random, non-empty keys', function () {
'returns random, non-empty keys', jsc.assert(jsc.forall(
'integer', 'integer',
function(counter) { function(counter) {
var clean = jsdom(); const clean = jsdom();
Object.defineProperty(window, 'crypto', { Object.defineProperty(window, 'crypto', {
value: new WebCrypto(), value: new WebCrypto(),
writeable: false, writeable: false,
}); });
var key = $.PrivateBin.CryptTool.getSymmetricKey(), const key = $.PrivateBin.CryptTool.getSymmetricKey(),
result = (key !== '' && keys.indexOf(key) === -1); result = (key !== '' && keys.indexOf(key) === -1);
keys.push(key); keys.push(key);
clean(); clean();
return result; return result;
} }
); ),
{tests: 10});
});
}); });
}); });

View File

@ -94,7 +94,6 @@ describe('Model', function () {
url.query = queryStart.concat(pasteId, queryEnd); url.query = queryStart.concat(pasteId, queryEnd);
const pasteIdString = pasteId.join(''), const pasteIdString = pasteId.join(''),
clean = jsdom('', {url: common.urlToString(url)}); clean = jsdom('', {url: common.urlToString(url)});
global.URL = require('jsdom-url').URL;
const result = $.PrivateBin.Model.getPasteId(); const result = $.PrivateBin.Model.getPasteId();
$.PrivateBin.Model.reset(); $.PrivateBin.Model.reset();
clean(); clean();
@ -107,7 +106,6 @@ describe('Model', function () {
function (url) { function (url) {
let clean = jsdom('', {url: common.urlToString(url)}), let clean = jsdom('', {url: common.urlToString(url)}),
result = false; result = false;
global.URL = require('jsdom-url').URL;
try { try {
$.PrivateBin.Model.getPasteId(); $.PrivateBin.Model.getPasteId();
} }
@ -131,7 +129,7 @@ describe('Model', function () {
'returns the fragment of a v1 URL', 'returns the fragment of a v1 URL',
common.jscUrl(), common.jscUrl(),
function (url) { function (url) {
url.fragment = common.btoa(url.fragment.padStart(32, '\u0000')); url.fragment = '0OIl'; // any non-base58 string
const clean = jsdom('', {url: common.urlToString(url)}), const clean = jsdom('', {url: common.urlToString(url)}),
result = $.PrivateBin.Model.getPasteKey(); result = $.PrivateBin.Model.getPasteKey();
$.PrivateBin.Model.reset(); $.PrivateBin.Model.reset();
@ -140,17 +138,17 @@ describe('Model', function () {
} }
); );
jsc.property( jsc.property(
'returns the v1 fragment stripped of trailing query parts', 'returns the fragment stripped of trailing query parts',
common.jscUrl(), common.jscUrl(),
jsc.array(common.jscHashString()), jsc.array(common.jscHashString()),
function (url, trail) { function (url, trail) {
const fragmentString = common.btoa(url.fragment.padStart(32, '\u0000')); const fragment = url.fragment.padStart(32, '\u0000');
url.fragment = fragmentString + '&' + trail.join(''); url.fragment = $.PrivateBin.CryptTool.base58encode(fragment) + '&' + trail.join('');
const clean = jsdom('', {url: common.urlToString(url)}), const clean = jsdom('', {url: common.urlToString(url)}),
result = $.PrivateBin.Model.getPasteKey(); result = $.PrivateBin.Model.getPasteKey();
$.PrivateBin.Model.reset(); $.PrivateBin.Model.reset();
clean(); clean();
return fragmentString === result; return fragment === result;
} }
); );
jsc.property( jsc.property(

View File

@ -732,7 +732,6 @@ describe('TopNav', function () {
'displays raw text view correctly', 'displays raw text view correctly',
function () { function () {
const clean = jsdom('', {url: 'https://privatebin.net/?0123456789abcdef#0'}); const clean = jsdom('', {url: 'https://privatebin.net/?0123456789abcdef#0'});
//global.URL = require('jsdom-url').URL;
$('body').html('<button id="rawtextbutton"></button>'); $('body').html('<button id="rawtextbutton"></button>');
const sample = 'example'; const sample = 'example';
$.PrivateBin.PasteViewer.setText(sample); $.PrivateBin.PasteViewer.setText(sample);