diff --git a/.gitattributes b/.gitattributes index daef8b1c..3c395463 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,8 +2,6 @@ doc/ export-ignore tst/ export-ignore js/.istanbul.yml export-ignore js/test.js export-ignore -js/mocha-3.2.0.js export-ignore -css/mocha-3.2.0.css export-ignore .codeclimate.yml export-ignore .csslintrc export-ignore .dockerignore export-ignore diff --git a/.styleci.yml b/.styleci.yml index 0c7ba38a..8a62bd56 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -3,10 +3,11 @@ preset: recommended risky: false enabled: - - no_empty_comment - align_equals - - long_array_syntax - concat_with_spaces + - long_array_syntax + - no_empty_comment + - pre_increment disabled: - blank_line_after_opening_tag @@ -23,6 +24,7 @@ disabled: - phpdoc_separation - phpdoc_single_line_var_spacing - phpdoc_summary + - post_increment - short_array_syntax - single_line_after_imports - unalign_equals diff --git a/js/privatebin.js b/js/privatebin.js index 0d34603a..20396b2d 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -124,7 +124,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { range = document.body.createTextRange(); range.moveToElementText(element); range.select(); - } else if (window.getSelection){ + } else if (window.getSelection) { selection = window.getSelection(); range = document.createRange(); range.selectNodeContents(element); @@ -302,8 +302,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * internationalization module * * @name I18n - * @param {object} window - * @param {object} document * @class */ var I18n = (function () { @@ -414,7 +412,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { var orgArguments = arguments; $(document).on(languageLoadedEvent, function () { // log to show that the previous error could be mitigated - console.log('Fix missing translation of \'' + messageId + '\' with now loaded language ' + language); + console.warn('Fix missing translation of \'' + messageId + '\' with now loaded language ' + language); // re-execute this function me.translate.apply(this, orgArguments); }); @@ -845,11 +843,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * everything directly UI-related, which fits nowhere else * * @name UiHelper - * @param {object} window - * @param {object} document * @class */ - var UiHelper = (function (window, document) { + var UiHelper = (function () { var me = {}; /** @@ -866,7 +862,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { var currentLocation = Helper.baseUri(); if (event.originalEvent.state === null && // no state object passed - event.originalEvent.target.location.href === currentLocation && // target location is home page + event.target.location.href === currentLocation && // target location is home page window.location.href === currentLocation // and we are not already on the home page ) { // redirect to home page @@ -958,6 +954,23 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }); } + /** + * trigger a history (pop) state change + * + * used to test the UiHelper.historyChange private function + * + * @name UiHelper.mockHistoryChange + * @function + * @param {string} state (optional) state to mock + */ + me.mockHistoryChange = function(state) + { + if (typeof state === 'undefined') { + state = null; + } + historyChange($.Event('popstate', {originalEvent: new PopStateEvent('popstate', {state: state}), target: window})); + } + /** * initialize * @@ -973,7 +986,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return me; - })(window, document); + })(); /** * Alert/error manager @@ -989,12 +1002,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $statusMessage, $remainingTime; - var currentIcon = [ - 'glyphicon-time', // loading icon - 'glyphicon-info-sign', // status icon - '', // resevered for warning, not used yet - 'glyphicon-alert' // error icon - ]; + var currentIcon; var alertType = [ 'loading', // not in bootstrap, but using a good value here @@ -1090,7 +1098,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.showStatus = function(message, icon, dismissable, autoclose) { - console.log('status shown: ', message); + console.info('status shown: ', message); // @TODO: implement dismissable // @TODO: implement autoclose @@ -1133,7 +1141,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.showRemaining = function(message) { - console.log('remaining message shown: ', message); + console.info('remaining message shown: ', message); handleNotification(1, $remainingTime, message); } @@ -1151,7 +1159,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showLoading = function(message, percentage, icon) { if (typeof message !== 'undefined' && message !== null) { - console.log('status changed: ', message); + console.info('status changed: ', message); } // default message text @@ -1238,6 +1246,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { $loadingIndicator = $('#loadingindicator'); $statusMessage = $('#status'); $remainingTime = $('#remainingtime'); + + currentIcon = [ + 'glyphicon-time', // loading icon + 'glyphicon-info-sign', // status icon + '', // reserved for warning, not used yet + 'glyphicon-alert' // error icon + ]; } return me; @@ -1247,10 +1262,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * handles paste status/result * * @name PasteStatus - * @param {object} window * @class */ - var PasteStatus = (function (window) { + var PasteStatus = (function () { var me = {}; var $pasteSuccess, @@ -1351,7 +1365,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { ]; Alert.showRemaining([expirationLabel, expiration[0]]); - $remainingTime.removeClass('foryoureyesonly') + $remainingTime.removeClass('foryoureyesonly'); } else { // never expires return; @@ -1383,7 +1397,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { */ me.init = function() { - $pasteSuccess = $('#pasteSuccess'); + $pasteSuccess = $('#pastesuccess'); // $pasteUrl is saved in me.createPasteNotification() after creation $remainingTime = $('#remainingtime'); $shortenButton = $('#shortenbutton'); @@ -1393,7 +1407,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } return me; - })(window); + })(); /** * password prompt @@ -1454,7 +1468,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { throw 'password prompt canceled'; } if (password.length === 0) { - // recursive… + // recurse… return me.requestPassword(); } @@ -1462,7 +1476,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } /** - * getthe cached password + * get the cached password * * If you do not get a password with this function * (returns an empty string), use requestPassword. @@ -1827,7 +1841,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * @name PasteViewer.setFormat * @function - * @param {string} newFormat the the new format + * @param {string} newFormat the new format */ me.setFormat = function(newFormat) { @@ -1836,7 +1850,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return; } - // needs to update display too, if from or to Markdown is switched + // needs to update display too, if we switch from or to Markdown if (format === 'markdown' || newFormat === 'markdown') { isDisplayed = false; } @@ -1965,6 +1979,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // get default option from template/HTML or fall back to set value format = Model.getFormatDefault() || format; + text = ''; + isDisplayed = false; + isChanged = true; } return me; @@ -2641,7 +2658,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showViewButtons = function() { if (viewButtonsDisplayed) { - console.log('showViewButtons: view buttons are already displayed'); + console.warn('showViewButtons: view buttons are already displayed'); return; } @@ -2661,7 +2678,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hideViewButtons = function() { if (!viewButtonsDisplayed) { - console.log('hideViewButtons: view buttons are already hidden'); + console.warn('hideViewButtons: view buttons are already hidden'); return; } @@ -2693,7 +2710,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.showCreateButtons = function() { if (createButtonsDisplayed) { - console.log('showCreateButtons: create buttons are already displayed'); + console.warn('showCreateButtons: create buttons are already displayed'); return; } @@ -2718,7 +2735,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { me.hideCreateButtons = function() { if (!createButtonsDisplayed) { - console.log('hideCreateButtons: create buttons are already hidden'); + console.warn('hideCreateButtons: create buttons are already hidden'); return; } @@ -3970,7 +3987,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { Uploader.setUnencryptedData('deletetoken', deleteToken); Uploader.setFailure(function () { - Controller.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.')); + Alert.showError(I18n._('Could not delete the paste, it was not stored in burn after reading mode.')); }) Uploader.run(); } diff --git a/js/test.js b/js/test.js index 807d29dd..4b689718 100644 --- a/js/test.js +++ b/js/test.js @@ -22,11 +22,17 @@ global.sjcl = require('./sjcl-1.0.6'); global.Base64 = require('./base64-2.1.9').Base64; global.RawDeflate = require('./rawdeflate-0.5').RawDeflate; global.RawDeflate.inflate = require('./rawinflate-0.3').RawDeflate.inflate; +require('./prettify'); +global.prettyPrint = window.PR.prettyPrint; +global.prettyPrintOne = window.PR.prettyPrintOne; +global.showdown = require('./showdown-1.6.1'); +global.DOMPurify = require('./purify.min'); +require('./bootstrap-3.3.7'); require('./privatebin'); // redirect console messages to log file -console.warn = console.error = function (msg) { - logFile.write(msg + '\n'); +console.info = console.warn = console.error = function () { + logFile.write(Array.prototype.slice.call(arguments).join('') + '\n'); } describe('Helper', function () { @@ -182,7 +188,7 @@ describe('Helper', function () { jsc.array(jsc.elements(queryString)), 'string', function (prefix, query, postfix) { - var url = 'magnet:?' + query.join(''), + var url = 'magnet:?' + query.join('').replace(/^&+|&+$/gm,''), prefix = $.PrivateBin.Helper.htmlEntities(prefix), postfix = $.PrivateBin.Helper.htmlEntities(postfix), element = $('
' + prefix + url + ' ' + postfix + '
'); @@ -599,6 +605,82 @@ describe('Model', function () { ); }); + describe('getFormatDefault', function () { + before(function () { + $.PrivateBin.Model.reset(); + cleanup(); + }); + + jsc.property( + 'returns the contents of the element with id "pasteFormatter"', + 'array asciinestring', + 'string', + 'small nat', + function (keys, value, key) { + keys = keys.map($.PrivateBin.Helper.htmlEntities); + value = $.PrivateBin.Helper.htmlEntities(value); + var content = keys.length > key ? keys[key] : (keys.length > 0 ? keys[0] : 'null'), + contents = ''; + $('body').html(contents); + var result = $.PrivateBin.Helper.htmlEntities( + $.PrivateBin.Model.getFormatDefault() + ); + $.PrivateBin.Model.reset(); + return content === result; + } + ); + }); + + describe('hasCipherData', function () { + before(function () { + $.PrivateBin.Model.reset(); + cleanup(); + }); + + jsc.property( + 'checks if the element with id "cipherdata" contains any data', + 'asciistring', + function (value) { + value = $.PrivateBin.Helper.htmlEntities(value).trim(); + $('body').html('
' + value + '
'); + $.PrivateBin.Model.init(); + var result = $.PrivateBin.Model.hasCipherData(); + $.PrivateBin.Model.reset(); + return (value.length > 0) === result; + } + ); + }); + + describe('getCipherData', function () { + before(function () { + $.PrivateBin.Model.reset(); + cleanup(); + }); + + jsc.property( + 'returns the contents of the element with id "cipherdata"', + 'asciistring', + function (value) { + value = $.PrivateBin.Helper.htmlEntities(value).trim(); + $('body').html('
' + value + '
'); + $.PrivateBin.Model.init(); + var result = $.PrivateBin.Helper.htmlEntities( + $.PrivateBin.Model.getCipherData() + ); + $.PrivateBin.Model.reset(); + return value === result; + } + ); + }); + describe('getPasteId', function () { this.timeout(30000); before(function () { @@ -710,4 +792,701 @@ describe('Model', function () { } ); }); + + describe('getTemplate', function () { + before(function () { + $.PrivateBin.Model.reset(); + cleanup(); + }); + + jsc.property( + 'returns the contents of the element with id "[name]template"', + jsc.nearray(jsc.elements(alnumString)), + jsc.nearray(jsc.elements(a2zString)), + jsc.nearray(jsc.elements(alnumString)), + function (id, element, value) { + id = id.join(''); + element = element.join(''); + value = value.join('').trim(); + + //
,
, and tags can't contain strings, + // table tags can't be alone, so test with a

instead + if (['br', 'col', 'hr', 'img', 'tr', 'td', 'th', 'wbr'].indexOf(element) >= 0) { + element = 'p'; + } + + $('body').html( + '

<' + element + ' id="' + id + + 'template">' + value + '
' + ); + $.PrivateBin.Model.init(); + var template = '<' + element + ' id="' + id + '">' + value + + '', + result = $.PrivateBin.Model.getTemplate(id).wrap('

').parent().html(); + $.PrivateBin.Model.reset(); + return template === result; + } + ); + }); }); + +describe('UiHelper', function () { + // TODO: As per https://github.com/tmpvar/jsdom/issues/1565 there is no navigation support in jsdom, yet. + // for now we use a mock function to trigger the event + describe('historyChange', function () { + this.timeout(30000); + before(function () { + $.PrivateBin.Helper.reset(); + }); + + jsc.property( + 'redirects to home, when the state is null', + jsc.elements(schemas), + jsc.nearray(jsc.elements(a2zString)), + function (schema, address) { + var expected = schema + '://' + address.join('') + '/', + clean = jsdom('', {url: expected}); + + // make window.location.href writable + Object.defineProperty(window.location, 'href', { + writable: true, + value: window.location.href + }); + $.PrivateBin.UiHelper.mockHistoryChange(); + $.PrivateBin.Helper.reset(); + var result = window.location.href; + clean(); + return expected === result; + } + ); + + jsc.property( + 'does not redirect to home, when a new paste is created', + jsc.elements(schemas), + jsc.nearray(jsc.elements(a2zString)), + jsc.array(jsc.elements(queryString)), + jsc.nearray(jsc.elements(base64String)), + function (schema, address, query, fragment) { + var expected = schema + '://' + address.join('') + '/' + '?' + + query.join('') + '#' + fragment.join(''), + clean = jsdom('', {url: expected}); + + // make window.location.href writable + Object.defineProperty(window.location, 'href', { + writable: true, + value: window.location.href + }); + $.PrivateBin.UiHelper.mockHistoryChange([ + {type: 'newpaste'}, '', expected + ]); + $.PrivateBin.Helper.reset(); + var result = window.location.href; + clean(); + return expected === result; + } + ); + }); + + describe('reloadHome', function () { + this.timeout(30000); + before(function () { + $.PrivateBin.Helper.reset(); + }); + + jsc.property( + 'redirects to home', + jsc.elements(schemas), + jsc.nearray(jsc.elements(a2zString)), + jsc.array(jsc.elements(queryString)), + jsc.nearray(jsc.elements(base64String)), + function (schema, address, query, fragment) { + var expected = schema + '://' + address.join('') + '/', + clean = jsdom('', { + url: expected + '?' + query.join('') + '#' + fragment.join('') + }); + + // make window.location.href writable + Object.defineProperty(window.location, 'href', { + writable: true, + value: window.location.href + }); + $.PrivateBin.UiHelper.reloadHome(); + $.PrivateBin.Helper.reset(); + var result = window.location.href; + clean(); + return expected === result; + } + ); + }); + + describe('isVisible', function () { + // TODO As per https://github.com/tmpvar/jsdom/issues/1048 there is no layout support in jsdom, yet. + // once it is supported or a workaround is found, uncomment the section below + /* + before(function () { + $.PrivateBin.Helper.reset(); + }); + + jsc.property( + 'detect visible elements', + jsc.nearray(jsc.elements(alnumString)), + jsc.nearray(jsc.elements(a2zString)), + function (id, element) { + id = id.join(''); + element = element.join(''); + var clean = jsdom( + '<' + element + ' id="' + id + '">' + ); + var result = $.PrivateBin.UiHelper.isVisible($('#' + id)); + clean(); + return result; + } + ); + */ + }); + + describe('scrollTo', function () { + // TODO Did not find a way to test that, see isVisible test above + }); +}); + +describe('Alert', function () { + describe('showStatus', function () { + before(function () { + cleanup(); + }); + + jsc.property( + 'shows a status message', + jsc.array(jsc.elements(alnumString)), + jsc.array(jsc.elements(alnumString)), + function (icon, message) { + icon = icon.join(''); + message = message.join(''); + var expected = '

'; + $('body').html( + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.showStatus(message, icon); + var result = $('body').html(); + return expected === result; + } + ); + }); + + describe('showError', function () { + before(function () { + cleanup(); + }); + + jsc.property( + 'shows an error message', + jsc.array(jsc.elements(alnumString)), + jsc.array(jsc.elements(alnumString)), + function (icon, message) { + icon = icon.join(''); + message = message.join(''); + var expected = ''; + $('body').html( + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.showError(message, icon); + var result = $('body').html(); + return expected === result; + } + ); + }); + + describe('showRemaining', function () { + before(function () { + cleanup(); + }); + + jsc.property( + 'shows remaining time', + jsc.array(jsc.elements(alnumString)), + jsc.array(jsc.elements(alnumString)), + 'integer', + function (message, string, number) { + message = message.join(''); + string = string.join(''); + var expected = ''; + $('body').html( + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.showRemaining(['%s' + message + '%d', string, number]); + var result = $('body').html(); + return expected === result; + } + ); + }); + + describe('showLoading', function () { + before(function () { + cleanup(); + }); + + jsc.property( + 'shows a loading message', + jsc.array(jsc.elements(alnumString)), + jsc.array(jsc.elements(alnumString)), + 'integer', + function (icon, message, number) { + icon = icon.join(''); + message = message.join(''); + var default_message = 'Loading…'; + if (message.length == 0) { + message = default_message; + } + var expected = ''; + $('body').html( + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.showLoading(message, number, icon); + var result = $('body').html(); + return expected === result; + } + ); + }); + + describe('hideLoading', function () { + before(function () { + cleanup(); + }); + + it( + 'hides the loading message', + function() { + $('body').html( + '' + ); + $('body').addClass('loading'); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.hideLoading(); + return !$('body').hasClass('loading') && + $('#loadingindicator').hasClass('hidden'); + } + ); + }); + + describe('hideMessages', function () { + before(function () { + cleanup(); + }); + + it( + 'hides all messages', + function() { + $('body').html( + '' + + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.hideMessages(); + return $('#statusmessage').hasClass('hidden') && + $('#errormessage').hasClass('hidden'); + } + ); + }); + + describe('setCustomHandler', function () { + before(function () { + cleanup(); + }); + + jsc.property( + 'calls a given handler function', + 'nat 3', + jsc.array(jsc.elements(alnumString)), + function (trigger, message) { + message = message.join(''); + var handlerCalled = false, + default_message = 'Loading…', + functions = [ + $.PrivateBin.Alert.showStatus, + $.PrivateBin.Alert.showError, + $.PrivateBin.Alert.showRemaining, + $.PrivateBin.Alert.showLoading + ]; + if (message.length == 0) { + message = default_message; + } + $('body').html( + '' + + '' + + '' + + '' + ); + $.PrivateBin.Alert.init(); + $.PrivateBin.Alert.setCustomHandler(function(id, $element) { + handlerCalled = true; + return jsc.random(0, 1) ? true : $element; + }); + functions[trigger](message); + return handlerCalled; + } + ); + }); +}); + +describe('PasteStatus', function () { + describe('createPasteNotification', function () { + this.timeout(30000); + before(function () { + cleanup(); + }); + + jsc.property( + 'creates a notification after a successfull paste upload', + jsc.elements(schemas), + jsc.nearray(jsc.elements(a2zString)), + jsc.array(jsc.elements(queryString)), + 'string', + jsc.elements(schemas), + jsc.nearray(jsc.elements(a2zString)), + jsc.array(jsc.elements(queryString)), + 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('')), + clean = jsdom(); + $('body').html('
'); + $.PrivateBin.PasteStatus.init(); + $.PrivateBin.PasteStatus.createPasteNotification(expected1, expected2); + var result1 = $('#pasteurl')[0].href, + result2 = $('#deletelink a')[0].href; + clean(); + return result1 == expected1 && result2 == expected2; + } + ); + }); + + describe('showRemainingTime', function () { + this.timeout(30000); + before(function () { + cleanup(); + }); + + jsc.property( + 'shows burn after reading message or remaining time', + 'bool', + 'nat', + jsc.nearray(jsc.elements(a2zString)), + jsc.nearray(jsc.elements(a2zString)), + jsc.array(jsc.elements(queryString)), + 'string', + function ( + burnafterreading, remaining_time, + schema, address, query, fragment + ) { + var clean = jsdom('', { + url: schema.join('') + '://' + address.join('') + + '/?' + queryString + '#' + fragment + }); + $('body').html(''); + $.PrivateBin.PasteStatus.init(); + $.PrivateBin.PasteStatus.showRemainingTime({ + 'burnafterreading': burnafterreading, + 'remaining_time': remaining_time, + 'expire_date': remaining_time ? ((new Date()).getTime() / 1000) + remaining_time : 0 + }); + if (burnafterreading) { + var result = $('#remainingtime').hasClass('foryoureyesonly') && + !$('#remainingtime').hasClass('hidden'); + } else if (remaining_time) { + var result =!$('#remainingtime').hasClass('foryoureyesonly') && + !$('#remainingtime').hasClass('hidden'); + } else { + var result = $('#remainingtime').hasClass('hidden') && + !$('#remainingtime').hasClass('foryoureyesonly'); + } + clean(); + return result; + } + ); + }); + + describe('hideMessages', function () { + before(function () { + cleanup(); + }); + + it( + 'hides all messages', + function() { + $('body').html( + '
' + ); + $.PrivateBin.PasteStatus.init(); + $.PrivateBin.PasteStatus.hideMessages(); + return $('#remainingtime').hasClass('hidden') && + $('#pastesuccess').hasClass('hidden'); + } + ); + }); +}); + +describe('Prompt', function () { + // TODO: this does not test the prompt() fallback, since that isn't available + // in nodejs -> replace the prompt in the "page" template with a modal + describe('requestPassword & getPassword', function () { + this.timeout(30000); + before(function () { + cleanup(); + }); + + jsc.property( + 'returns the password fed into the dialog', + 'string', + function (password) { + password = password.replace(/\r+/g, ''); + var clean = jsdom('', {url: 'ftp://example.com/#0'}); + $('body').html( + '
{}
' + ); + $.PrivateBin.Model.init(); + $.PrivateBin.Prompt.init(); + $.PrivateBin.Prompt.requestPassword(); + $('#passworddecrypt').val(password); + $('#passwordform').submit(); + var result = $.PrivateBin.Prompt.getPassword(); + clean(); + return result == password; + } + ); + }); +}); + +describe('Editor', function () { + describe('show, hide, getText, setText & isPreview', function () { + this.timeout(30000); + before(function () { + cleanup(); + }); + + jsc.property( + 'returns text fed into the textarea, handles editor tabs', + 'string', + function (text) { + var clean = jsdom(), + results = []; + $('body').html( + '' + + '

' + ); + $.PrivateBin.Editor.init(); + results.push( + $('#editorTabs').hasClass('hidden') && + $('#message').hasClass('hidden') + ); + $.PrivateBin.Editor.show(); + results.push( + !$('#editorTabs').hasClass('hidden') && + !$('#message').hasClass('hidden') + ); + $.PrivateBin.Editor.hide(); + results.push( + $('#editorTabs').hasClass('hidden') && + $('#message').hasClass('hidden') + ); + $.PrivateBin.Editor.show(); + $.PrivateBin.Editor.focusInput(); + results.push( + $.PrivateBin.Editor.getText().length == 0 + ); + $.PrivateBin.Editor.setText(text); + results.push( + $.PrivateBin.Editor.getText() == $('#message').val() + ); + $.PrivateBin.Editor.setText(); + results.push( + !$.PrivateBin.Editor.isPreview() && + !$('#message').hasClass('hidden') + ); + $('#messagepreview').click(); + results.push( + $.PrivateBin.Editor.isPreview() && + $('#message').hasClass('hidden') + ); + $('#messageedit').click(); + results.push( + !$.PrivateBin.Editor.isPreview() && + !$('#message').hasClass('hidden') + ); + clean(); + return results.every(element => element); + } + ); + }); +}); + +describe('PasteViewer', function () { + describe('run, hide, getText, setText, getFormat, setFormat & isPrettyPrinted', function () { + this.timeout(30000); + before(function () { + cleanup(); + }); + + jsc.property( + 'displays text according to format', + jsc.elements(['plaintext', 'markdown', 'syntaxhighlighting']), + 'nestring', + function (format, text) { + var clean = jsdom(), + results = []; + $('body').html( + '' + ); + $.PrivateBin.PasteViewer.init(); + $.PrivateBin.PasteViewer.setFormat(format); + $.PrivateBin.PasteViewer.setText(''); + results.push( + $('#placeholder').hasClass('hidden') && + $('#prettymessage').hasClass('hidden') && + $('#plaintext').hasClass('hidden') && + $.PrivateBin.PasteViewer.getFormat() == format && + $.PrivateBin.PasteViewer.getText() == '' + ); + $.PrivateBin.PasteViewer.run(); + results.push( + !$('#placeholder').hasClass('hidden') && + $('#prettymessage').hasClass('hidden') && + $('#plaintext').hasClass('hidden') + ); + $.PrivateBin.PasteViewer.hide(); + results.push( + $('#placeholder').hasClass('hidden') && + $('#prettymessage').hasClass('hidden') && + $('#plaintext').hasClass('hidden') + ); + $.PrivateBin.PasteViewer.setText(text); + $.PrivateBin.PasteViewer.run(); + results.push( + $('#placeholder').hasClass('hidden') && + !$.PrivateBin.PasteViewer.isPrettyPrinted() && + $.PrivateBin.PasteViewer.getText() == text + ); + if (format == 'markdown') { + results.push( + $('#prettymessage').hasClass('hidden') && + !$('#plaintext').hasClass('hidden') + ); + } else { + results.push( + !$('#prettymessage').hasClass('hidden') && + $('#plaintext').hasClass('hidden') + ); + } + clean(); + return results.every(element => element); + } + ); + + jsc.property( + 'sanitizes XSS', + jsc.elements(['plaintext', 'markdown', 'syntaxhighlighting']), + 'string', + // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet + jsc.elements([ + '', + '\';alert(String.fromCharCode(88,83,83))//\';alert(String.fromCharCode(88,83,83))//";', + 'alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//--', + '></SCRIPT>">\'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>', + '\'\';!--"<XSS>=&{()}', + '<SCRIPT SRC=http://example.com/xss.js></SCRIPT>', + '\'">><marquee><img src=x onerror=confirm(1)></marquee>"></plaintext\\></|\\><plaintext/onmouseover=prompt(1)><script>prompt(1)</script>@gmail.com<isindex formaction=javascript:alert(/XSS/) type=submit>\'-->"></script><script>alert(document.cookie)</script>"><img/id="confirm&lpar;1)"/alt="/"src="/"onerror=eval(id)>\'">', + '<IMG SRC="javascript:alert(\'XSS\');">', + '<IMG SRC=javascript:alert(\'XSS\')>', + '<IMG SRC=JaVaScRiPt:alert(\'XSS\')>', + '<IMG SRC=javascript:alert(&quot;XSS&quot;)>', + '<IMG SRC=`javascript:alert("RSnake says, \'XSS\'")`>', + '<a onmouseover="alert(document.cookie)">xxs link</a>', + '<a onmouseover=alert(document.cookie)>xxs link</a>', + '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">', + '<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>' + // the list goes on… + ]), + 'string', + function (format, prefix, xss, suffix) { + var clean = jsdom(), + text = prefix + xss + suffix; + $('body').html( + '<div id="placeholder" class="hidden">+++ no paste text ' + + '+++</div><div id="prettymessage" class="hidden"><pre ' + + 'id="prettyprint" class="prettyprint linenums:1"></pre>' + + '</div><div id="plaintext" class="hidden"></div>' + ); + $.PrivateBin.PasteViewer.init(); + $.PrivateBin.PasteViewer.setFormat(format); + $.PrivateBin.PasteViewer.setText(text); + $.PrivateBin.PasteViewer.run(); + var result = $('body').html().indexOf(xss) !== -1; + clean(); + return result; + } + ); + }); +}); + diff --git a/lib/Filter.php b/lib/Filter.php index 4c0a22e9..302c84c8 100644 --- a/lib/Filter.php +++ b/lib/Filter.php @@ -64,7 +64,7 @@ class Filter $i = 0; while (($size / 1024) >= 1) { $size = $size / 1024; - $i++; + ++$i; } return number_format($size, ($i ? 2 : 0), '.', ' ') . ' ' . I18n::_($iec[$i]); } @@ -82,7 +82,7 @@ class Filter public static function slowEquals($a, $b) { $diff = strlen($a) ^ strlen($b); - for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) { + for ($i = 0; $i < strlen($a) && $i < strlen($b); ++$i) { $diff |= ord($a[$i]) ^ ord($b[$i]); } return $diff === 0; diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index def90dfb..208ab7d0 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -70,7 +70,7 @@ if ($MARKDOWN): <?php endif; ?> - <script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-CbxrV468ako77cl5jNqoqohA9EphJI54ha7/3Zv0K7lXW/0fC7l1L+SXpTq94FpQP4vSIZFmQnOkrmPxkgNbag==" crossorigin="anonymous"></script> + <script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-zbiQgBDsSGOdieFoYhNqu7fnj+GpeuQRXQcUSg1U9lP3HlHUY6Lp1w/Hl/LK2y/RGjkoV3MRcjU/BQVGoZxKlw==" crossorigin="anonymous"></script> <!--[if lt IE 10]> <style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style> <![endif]--> @@ -231,6 +231,17 @@ if ($isCpct): ?> </ul> <select id="pasteFormatter" name="pasteFormatter" class="hidden"> +<?php + foreach ($FORMATTER as $key => $value): +?> + <option value="<?php echo $key; ?>"<?php + if ($key == $FORMATTERDEFAULT): +?> selected="selected"<?php + endif; +?>><?php echo $value; ?></option> +<?php + endforeach; +?> </select> </li> <?php @@ -424,7 +435,7 @@ endif; <a href="https://www.opera.com/">Opera</a>, <a href="https://www.google.com/chrome">Chrome</a>… </div> - <div id="pasteSuccess" role="alert" class="hidden alert alert-success"> + <div id="pastesuccess" role="alert" class="hidden alert alert-success"> <span class="glyphicon glyphicon-ok" aria-hidden="true"></span> <div id="deletelink"></div> <div id="pastelink"> diff --git a/tpl/page.php b/tpl/page.php index 9c1db28d..2ecbb697 100644 --- a/tpl/page.php +++ b/tpl/page.php @@ -48,7 +48,7 @@ if ($MARKDOWN): <?php endif; ?> - <script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-CbxrV468ako77cl5jNqoqohA9EphJI54ha7/3Zv0K7lXW/0fC7l1L+SXpTq94FpQP4vSIZFmQnOkrmPxkgNbag==" crossorigin="anonymous"></script> + <script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-zbiQgBDsSGOdieFoYhNqu7fnj+GpeuQRXQcUSg1U9lP3HlHUY6Lp1w/Hl/LK2y/RGjkoV3MRcjU/BQVGoZxKlw==" crossorigin="anonymous"></script> <!--[if lt IE 10]> <style type="text/css">body {padding-left:60px;padding-right:60px;} #ienotice {display:block;} #oldienotice {display:block;}</style> <![endif]--> @@ -185,7 +185,7 @@ if (strlen($LANGUAGESELECTION)): endif; ?> </div> - <div id="pasteresult" class="hidden"> + <div id="pastesuccess" class="hidden"> <div id="deletelink"></div> <div id="pastelink"> <?php diff --git a/tst/ConfigurationTest.php b/tst/ConfigurationTest.php index 66acecea..15b7fd2b 100644 --- a/tst/ConfigurationTest.php +++ b/tst/ConfigurationTest.php @@ -142,7 +142,7 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase public function testHandleConfigFileRename() { - $options = $this->_options; + $options = $this->_options; Helper::createIniFile(PATH . 'cfg' . DIRECTORY_SEPARATOR . 'conf.ini.sample', $options); $options['main']['opendiscussion'] = true; diff --git a/tst/I18nTest.php b/tst/I18nTest.php index c7ded0ee..de163490 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -150,8 +150,8 @@ class I18nTest extends PHPUnit_Framework_TestCase $dir = dir(PATH . 'i18n'); while (false !== ($file = $dir->read())) { if (strlen($file) === 7) { - $language = substr($file, 0, 2); - $languageMessageIds = array_keys( + $language = substr($file, 0, 2); + $languageMessageIds = array_keys( json_decode( file_get_contents(PATH . 'i18n' . DIRECTORY_SEPARATOR . $file), true diff --git a/tst/PrivateBinWithDbTest.php b/tst/PrivateBinWithDbTest.php index a438d4c8..25e6082b 100644 --- a/tst/PrivateBinWithDbTest.php +++ b/tst/PrivateBinWithDbTest.php @@ -35,7 +35,7 @@ class PrivateBinWithDbTest extends PrivateBinTest $options['model'] = array( 'class' => 'Database', ); - $options['model_options'] = $this->_options; + $options['model_options'] = $this->_options; Helper::createIniFile(CONF, $options); } }