diff --git a/i18n/de.json b/i18n/de.json
index 87f55ccb..bab88a52 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -85,7 +85,7 @@
"Could not delete the paste, it was not stored in burn after reading mode.":
"Konnte den Text nicht löschen, er wurde nicht im Einmal-Modus gespeichert.",
"FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.":
- "DIESER TEXT IST NUR FÜR DICH GEDACHT. Schliesse das Fenster nicht, diese Nachricht kann nur einmal geöffnet werden.",
+ "DIESER TEXT IST NUR FÜR DICH GEDACHT. Schließe das Fenster nicht, diese Nachricht kann nur einmal geöffnet werden.",
"Could not decrypt comment; Wrong key?":
"Konnte Kommentar nicht entschlüsseln; Falscher Schlüssel?",
"Reply":
@@ -147,5 +147,7 @@
"Passwort eingeben",
"Loading…": "Lädt…",
"In case this message never disappears please have a look at this FAQ for information to troubleshoot.":
- "Wenn diese Nachricht nicht mehr verschwindet, schau bitte in die FAQ (englisch), um zu sehen, wie der Fehler behoben werden kann."
+ "Wenn diese Nachricht nicht mehr verschwindet, schau bitte in die FAQ (englisch), um zu sehen, wie der Fehler behoben werden kann.",
+ "Nothing to see… Try to enter some text.":
+ "Nichts zu sehen… Versuche etwas Text einzugeben."
}
diff --git a/js/privatebin.js b/js/privatebin.js
index 8d784cb5..118106c2 100644
--- a/js/privatebin.js
+++ b/js/privatebin.js
@@ -24,17 +24,23 @@
// Immediately start random number generator collector.
sjcl.random.startCollectors();
+// main application start, called when DOM is fully loaded
+jQuery(document).ready(function() {
+ // run main controller
+ $.PrivateBin.Controller.init();
+});
+
jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
'use strict';
/**
- * static helper methods
+ * static Helper methods
*
* @param {object} window
* @param {object} document
* @class
*/
- var helper = (function (window, document) {
+ var Helper = (function (window, document) {
var me = {};
/**
@@ -62,12 +68,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
* @private
* @enum {string|null}
*/
- var scriptLocation = null;
+ var baseUri = null;
/**
* converts a duration (in seconds) into human friendly approximation
*
- * @name helper.secondsToHuman
+ * @name Helper.secondsToHuman
* @function
* @param {number} seconds
* @return {Array}
@@ -104,7 +110,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
* text range selection
*
* @see {@link https://stackoverflow.com/questions/985272/jquery-selecting-text-in-an-element-akin-to-highlighting-with-your-mouse}
- * @name helper.selectText
+ * @name Helper.selectText
* @function
* @param {HTMLElement} element
*/
@@ -113,15 +119,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
var range, selection;
// MS
- if (document.body.createTextRange)
- {
+ if (document.body.createTextRange) {
range = document.body.createTextRange();
range.moveToElementText(element);
range.select();
- }
- // all others
- else if (window.getSelection)
- {
+ } else if (window.getSelection){
selection = window.getSelection();
range = document.createRange();
range.selectNodeContents(element);
@@ -133,15 +135,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* set text of a jQuery element (required for IE),
*
- * @name helper.setElementText
+ * @name Helper.setElementText
* @function
* @param {jQuery} $element - a jQuery element
* @param {string} text - the text to enter
*/
me.setElementText = function($element, text)
{
- // @TODO: Can we drop IE 10 support? This function looks crazy and checking oldienotice slows everything down…
- // I cannot really say, whether this IE10 method is XSS-safe…
// For IE<10: Doesn't support white-space:pre-wrap; so we have to do this...
if ($('#oldienotice').is(':visible')) {
var html = me.htmlEntities(text).replace(/\n/ig, '\r\n
');
@@ -163,21 +163,21 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
* http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
*
*
- * @name helper.urls2links
+ * @name Helper.urls2links
* @function
* @param {Object} element - a jQuery DOM element
*/
- me.urls2links = function(element)
+ me.urls2links = function($element)
{
var markup = '$1';
- element.html(
- element.html().replace(
+ $element.html(
+ $element.html().replace(
/((http|https|ftp):\/\/[\w?=&.\/-;#@~%+-]+(?![\w\s?&.\/;#~%"=-]*>))/ig,
markup
)
);
- element.html(
- element.html().replace(
+ $element.html(
+ $element.html().replace(
/((magnet):[\w?=&.\/-;#@~%+-]+)/ig,
markup
)
@@ -188,7 +188,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
* minimal sprintf emulation for %s and %d formats
*
* @see {@link https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format#4795914}
- * @name helper.sprintf
+ * @name Helper.sprintf
* @function
* @param {string} format
* @param {...*} args - one or multiple parameters injected into format string
@@ -196,11 +196,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
*/
me.sprintf = function()
{
- var args = arguments;
- if (typeof arguments[0] === 'object')
- {
- args = arguments[0];
- }
+ var args = Array.prototype.slice.call(arguments);
var format = args[0],
i = 1;
return format.replace(/%((%)|s|d)/g, function (m) {
@@ -232,7 +228,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
* get value of cookie, if it was set, empty string otherwise
*
* @see {@link http://www.w3schools.com/js/js_cookies.asp}
- * @name helper.getCookie
+ * @name Helper.getCookie
* @function
* @param {string} cname
* @return {string}
@@ -255,75 +251,38 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
};
/**
- * get the current script location (without search or hash part of the URL),
+ * get the current location (without search or hash part of the URL),
* eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
*
- * @name helper.scriptLocation
+ * @name Helper.baseUri
* @function
- * @return {string} current script location
+ * @return {string}
*/
- me.scriptLocation = function()
+ me.baseUri = function()
{
// check for cached version
- if (scriptLocation !== null) {
- return scriptLocation;
+ if (baseUri !== null) {
+ return baseUri;
}
- scriptLocation = window.location.href.substring(
- 0,
- window.location.href.length - window.location.search.length - window.location.hash.length
- );
+ // get official base uri string, from base tag in head of HTML
+ baseUri = document.baseURI;
- var hashIndex = scriptLocation.indexOf('?');
-
- if (hashIndex !== -1)
- {
- scriptLocation = scriptLocation.substring(0, hashIndex);
+ // if base uri contains query string (when no base tag is present),
+ // it is unwanted
+ if (baseUri.indexOf('?')) {
+ // so we built our own baseuri
+ baseUri = window.location.origin + window.location.pathname;
}
- return scriptLocation;
- };
-
- /**
- * get the pastes unique identifier from the URL,
- * eg. http://example.com/path/?c05354954c49a487#c05354954c49a487 returns c05354954c49a487
- *
- * @name helper.pasteId
- * @function
- * @return {string} unique identifier
- */
- me.pasteId = function()
- {
- return window.location.search.substring(1);
- };
-
- /**
- * return the deciphering key stored in anchor part of the URL
- *
- * @name helper.pageKey
- * @function
- * @return {string} key
- */
- me.pageKey = function()
- {
- var key = window.location.hash.substring(1),
- i = key.indexOf('&');
-
- // Some web 2.0 services and redirectors add data AFTER the anchor
- // (such as &utm_source=...). We will strip any additional data.
- if (i > -1)
- {
- key = key.substring(0, i);
- }
-
- return key;
+ return baseUri;
};
/**
* convert all applicable characters to HTML entities
*
* @see {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content}
- * @name helper.htmlEntities
+ * @name Helper.htmlEntities
* @function
* @param {string} str
* @return {string} escaped HTML
@@ -339,15 +298,24 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
})(window, document);
/**
- * internationalization methods
+ * internationalization module
*
* @param {object} window
* @param {object} document
* @class
*/
- var i18n = (function (window, document) {
+ var I18n = (function (window, document) {
var me = {};
+ /**
+ * const for string of loaded language
+ *
+ * @private
+ * @prop {string}
+ * @readonly
+ */
+ var languageLoadedEvent = 'languageLoaded';
+
/**
* supported languages, minus the built in 'en'
*
@@ -361,9 +329,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
* built in language
*
* @private
- * @prop {string}
+ * @prop {string|null}
*/
- var language = 'en';
+ var language = null;
/**
* translation cache
@@ -374,83 +342,125 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
var translations = {};
/**
- * translate a string, alias for i18n.translate()
+ * translate a string, alias for I18n.translate()
*
- * @name i18n._
+ * for a full description see me.translate
+ *
+ * @name I18n._
* @function
+ * @param {jQuery} $element - optional
* @param {string} messageId
* @param {...*} args - one or multiple parameters injected into placeholders
* @return {string}
*/
me._ = function()
{
- return me.translate(arguments);
+ return me.translate.apply(this, arguments);
};
/**
* translate a string
*
- * @name i18n.translate
+ * Optionally pass a jQuery element as the first parameter, to automatically
+ * let the text of this element be replaced. In case the (asynchronously
+ * loaded) language is not downloadet yet, this will make sure the string
+ * is replaced when it is actually loaded.
+ * So for easy translations passing the jQuery object to apply it to is
+ * more save, especially when they are loaded in the beginning.
+ *
+ * @name I18n.translate
* @function
+ * @param {jQuery} $element - optional
* @param {string} messageId
* @param {...*} args - one or multiple parameters injected into placeholders
* @return {string}
*/
me.translate = function()
{
- var args = arguments, messageId;
- if (typeof arguments[0] === 'object')
- {
- args = arguments[0];
+ // convert parameters to array
+ var args = Array.prototype.slice.call(arguments),
+ messageId,
+ $element = null;
+
+ // parse arguments
+ if (args[0] instanceof jQuery) {
+ // optional jQuery element as first parameter
+ $element = args[0];
+ args.shift();
}
+
+ // extract messageId from arguments
var usesPlurals = $.isArray(args[0]);
- if (usesPlurals)
- {
+ if (usesPlurals) {
// use the first plural form as messageId, otherwise the singular
messageId = (args[0].length > 1 ? args[0][1] : args[0][0]);
- }
- else
- {
+ } else {
messageId = args[0];
}
- if (messageId.length === 0)
- {
+
+ if (messageId.length === 0) {
return messageId;
}
- if (!translations.hasOwnProperty(messageId))
- {
- if (language !== 'en')
- {
- console.error(
- 'Missing ' + language + ' translation for: ' + messageId
- );
+
+ // if no translation string cannot be found (in translations object)
+ if (!translations.hasOwnProperty(messageId)) {
+ // if language is still loading and we have an elemt assigned
+ if (language === null && $element !== null) {
+ // handle the error by attaching the language loaded event
+ var orgArguments = arguments;
+ $(document).on(languageLoadedEvent, function () {
+ // re-execute this function
+ me.translate.apply(this, orgArguments);
+ // log to show that the previous error could be mitigated
+ console.log('Fixed missing translation of \'' + messageId + '\' with now loaded language ' + language);
+ });
+
+ // and fall back to English for now until the real language
+ // file is loaded
}
+
+ // for all other langauges than English for which thsi behaviour
+ // is expected as it is built-in, log error
+ if (language !== 'en') {
+ console.error('Missing translation for: \'' + messageId + '\' in language ' + language);
+ // fallback to English
+ }
+
+ // save English translation (should be the same on both sides)
translations[messageId] = args[0];
}
- if (usesPlurals && $.isArray(translations[messageId]))
- {
+
+ // lookup plural translation
+ if (usesPlurals && $.isArray(translations[messageId])) {
var n = parseInt(args[1] || 1, 10),
key = me.getPluralForm(n),
maxKey = translations[messageId].length - 1;
- if (key > maxKey)
- {
+ if (key > maxKey) {
key = maxKey;
}
args[0] = translations[messageId][key];
args[1] = n;
- }
- else
- {
+ } else {
+ // lookup singular translation
args[0] = translations[messageId];
}
- return helper.sprintf(args);
+
+ // format string
+ var output = Helper.sprintf.apply(this, args);
+
+ // if $element is given, apply text to element
+ if ($element !== null) {
+ $element.text(output);
+ }
+
+ return output;
};
/**
* per language functions to use to determine the plural form
*
* @see {@link http://localization-guide.readthedocs.org/en/latest/l10n/pluralforms.html}
- * @name i18n.getPluralForm
+ * @name I18n.getPluralForm
* @function
* @param {number} n
* @return {number} array key
@@ -475,37 +485,46 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
};
/**
- * load translations into cache, then trigger controller initialization
+ * load translations into cache
*
- * @name i18n.loadTranslations
+ * @name I18n.loadTranslations
* @function
*/
me.loadTranslations = function()
{
- var newLanguage = helper.getCookie('lang');
+ var newLanguage = Helper.getCookie('lang');
// auto-select language based on browser settings
- if (newLanguage.length === 0)
- {
+ if (newLanguage.length === 0) {
newLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
}
- // if language is already used (e.g, default 'en'), skip update
+ // if language is already used skip update
if (newLanguage === language) {
return;
}
+ // if language is built-in (English) skip update
+ if (newLanguage === 'en') {
+ language = 'en';
+ return;
+ }
+
// if language is not supported, show error
if (supportedLanguages.indexOf(newLanguage) === -1) {
console.error('Language \'%s\' is not supported. Translation failed, fallback to English.', newLanguage);
+ language = 'en';
+ return;
}
- // load strongs from JSON
+ // load strings from JSON
$.getJSON('i18n/' + newLanguage + '.json', function(data) {
language = newLanguage;
translations = data;
+ $(document).triggerHandler(languageLoadedEvent);
}).fail(function (data, textStatus, errorMsg) {
console.error('Language \'%s\' could not be loaded (%s: %s). Translation failed, fallback to English.', newLanguage, textStatus, errorMsg);
+ language = 'en';
});
};
@@ -517,7 +536,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
*
* @class
*/
- var cryptTool = (function () {
+ var CryptTool = (function () {
var me = {};
/**
@@ -551,7 +570,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* compress, then encrypt message with given key and password
*
- * @name cryptTool.cipher
+ * @name CryptTool.cipher
* @function
* @param {string} key
* @param {string} password
@@ -577,7 +596,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* decrypt message with key, then decompress
*
- * @name cryptTool.decipher
+ * @name CryptTool.decipher
* @function
* @param {string} key
* @param {string} password
@@ -610,7 +629,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* checks whether the crypt tool is ready.
*
- * @name cryptTool.isReady
+ * @name CryptTool.isReady
* @function
* @return {bool}
*/
@@ -622,9 +641,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* checks whether the crypt tool is ready.
*
- * @name cryptTool.isReady
+ * @name CryptTool.isReady
* @function
- * @param {function} - the function to add
+ * @param {function} func
*/
me.addEntropySeedListener = function(func)
{
@@ -634,9 +653,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* returns a random symmetric key
*
- * @name cryptTool.getSymmetricKey
+ * @name CryptTool.getSymmetricKey
* @function
- * @return {string}
+ * @return {string} func
*/
me.getSymmetricKey = function(func)
{
@@ -646,16 +665,16 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* initialize crypt tool
*
- * @name cryptTool.init
+ * @name CryptTool.init
* @function
*/
me.init = function()
{
// will fail earlier as sjcl is already passed as a parameter
// if (typeof sjcl !== 'object') {
- // alert.showError(
- // i18n._('The library %s is not available.', 'sjcl') +
- // i18n._('Messages cannot be decrypted or encrypted.')
+ // Alert.showError(
+ // I18n._('The library %s is not available.', 'sjcl') +
+ // I18n._('Messages cannot be decrypted or encrypted.')
// );
// }
};
@@ -664,45 +683,23 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
})();
/**
- * Data source (aka MVC)
+ * (modal) Data source (aka MVC)
*
* @param {object} window
* @param {object} document
* @class
*/
- var modal = (function (window, document) {
+ var Modal = (function (window, document) {
var me = {};
var $cipherData;
- /**
- * check if cipher data was supplied
- *
- * @name modal.getCipherData
- * @function
- * @return boolean
- */
- me.hasCipherData = function()
- {
- return (me.getCipherData().length > 0);
- };
-
- /**
- * returns the cipher data
- *
- * @name modal.getCipherData
- * @function
- * @return string
- */
- me.getCipherData = function()
- {
- return $cipherData.text();
- };
+ var id = null, symmetricKey = null;
/**
* returns the expiration set in the HTML
*
- * @name modal.getExpirationDefault
+ * @name Modal.getExpirationDefault
* @function
* @return string
* @TODO the template can be simplified as #pasteExpiration is no longer modified (only default value)
@@ -715,7 +712,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* returns the format set in the HTML
*
- * @name modal.getFormatDefault
+ * @name Modal.getFormatDefault
* @function
* @return string
* @TODO the template can be simplified as #pasteFormatter is no longer modified (only default value)
@@ -725,12 +722,78 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
return $('#pasteFormatter').val();
};
+ /**
+ * check if cipher data was supplied
+ *
+ * @name Modal.getCipherData
+ * @function
+ * @return boolean
+ */
+ me.hasCipherData = function()
+ {
+ return (me.getCipherData().length > 0);
+ };
+
+ /**
+ * returns the cipher data
+ *
+ * @name Modal.getCipherData
+ * @function
+ * @return string
+ */
+ me.getCipherData = function()
+ {
+ return $cipherData.text();
+ };
+
+ /**
+ * get the pastes unique identifier from the URL,
+ * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
+ *
+ * @name Modal.getPasteId
+ * @function
+ * @return {string} unique identifier
+ */
+ me.getPasteId = function()
+ {
+ if (id === null) {
+ id = window.location.search.substring(1);
+ }
+
+ return id;
+ };
+
+ /**
+ * return the deciphering key stored in anchor part of the URL
+ *
+ * @name Modal.getPasteKey
+ * @function
+ * @return {string} key
+ */
+ me.getPasteKey = function()
+ {
+ if (symmetricKey === null) {
+ symmetricKey = window.location.hash.substring(1);
+
+ // Some web 2.0 services and redirectors add data AFTER the anchor
+ // (such as &utm_source=...). We will strip any additional data.
+ var ampersandPos = symmetricKey.indexOf('&');
+ if (ampersandPos > -1)
+ {
+ symmetricKey = symmetricKey.substring(0, ampersandPos);
+ }
+
+ }
+
+ return symmetricKey;
+ };
+
/**
* init navigation manager
*
* preloads jQuery elements
*
- * @name modal.init
+ * @name Modal.init
* @function
*/
me.init = function()
@@ -742,39 +805,29 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
})(window, document);
/**
- * User interface manager
+ * Helper functions for user interface
+ *
+ * everything directly UI-related, which fits nowhere else
*
* @param {object} window
* @param {object} document
* @class
*/
- var uiMan = (function (window, document) {
+ var UiHelper = (function (window, document) {
var me = {};
- // jQuery pre-loaded objects
- var $clearText,
- $clonedFile,
- $comments,
- $discussion,
- $image,
- $prettyMessage,
- $prettyPrint,
- $editorTabs,
- $remainingTime,
- $replyStatus;
-
/**
* handle history (pop) state changes
*
* currently this does only handle redirects to the home page.
*
- * @name controller.historyChange
+ * @private
* @function
* @param {Event} event
*/
- me.historyChange = function(event)
+ function historyChange(event)
{
- var currentLocation = helper.scriptLocation();
+ var currentLocation = Helper.baseUri();
if (event.originalEvent.state === null && // no state object passed
event.originalEvent.target.location.href === currentLocation && // target location is home page
window.location.href === currentLocation // and we are not already on the home page
@@ -787,62 +840,50 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* reload the page
*
- * This takes the user to the PrivateBin home page.
+ * This takes the user to the PrivateBin homepage.
*
- * @name controller.reloadPage
+ * @name UiHelper.reloadHome
* @function
- * @param {Event} event
*/
- me.reloadPage = function(event)
+ me.reloadHome = function()
{
- window.location.href = helper.scriptLocation();
- event.preventDefault();
+ window.location.href = Helper.baseUri();
};
/**
- * main UI manager
+ * initialize
*
- * @name controller.init
+ * @name UiHelper.init
* @function
*/
me.init = function()
{
- // hide "no javascript" message
- $('#noscript').hide();
+ // update link to home page
+ $('.reloadlink').prop('href', Helper.baseUri());
- // bind events
- $('.reloadlink').click(me.reloadPage);
-
- $(window).on('popstate', me.historyChange);
+ $(window).on('popstate', historyChange);
};
return me;
})(window, document);
/**
- * alert/notification manager
+ * Alert/error manager
*
* @param {object} window
* @param {object} document
* @class
*/
- var alert = (function (window, document) {
+ var Alert = (function (window, document) {
var me = {};
- var $attachment,
- $attachmentLink,
- $errorMessage,
- $clonedFile,
- $fileWrap,
- $status,
- $pasteSuccess,
- $shortenButton,
- $pasteUrl;
+ var $errorMessage,
+ $status;
/**
* display a status message
*
- * @name controller.showStatus
+ * @name Alert.showStatus
* @function
* @param {string} message - text to display
* @param {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false
@@ -853,102 +894,83 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
$status.text(message);
};
- /**
- * display a status message for replying to comments
- *
- * @name controller.showStatus
- * @function
- * @param {string} message - text to display
- * @param {boolean} [spin=false] - (optional) tell if the "spinning" animation should be displayed, defaults to false
- */
- me.showReplyStatus = function(message, spin)
- {
- if (spin || false) {
- $replyalert.find('.spinner').removeClass('hidden')
- }
- $replyalert.text(message);
- };
-
/**
* hides any status messages
*
- * @name controller.hideMessages
+ * @name Alert.hideMessages
* @function
*/
me.hideMessages = function()
{
$status.html(' ');
+ $errorMessage.addClass('hidden');
};
/**
* display an error message
*
- * @name alert.showError
+ * @name Alert.showError
* @function
* @param {string} message - text to display
*/
me.showError = function(message)
{
+ console.error('Error shown: ' + message);
+
$errorMessage.removeClass('hidden');
- $errorMessage.find(':last').text(message);
+ $errorMessage.find(':last').text(' ' + message);
};
/**
- * display an error message
+ * init status manager
*
- * @name alert.showError
- * @function
- * @param {string} message - text to display
- */
- me.showReplyError = function(message)
- {
- $replyalert.addClass('alert-danger');
- $replyalert.addClass($errorMessage.attr('class')); // @TODO ????
-
- $replyalert.text(message);
- };
-
- /**
- * removes the existing attachment
+ * preloads jQuery elements
*
- * @name alert.removeAttachment
+ * @name Alert.init
* @function
*/
- me.removeAttachment = function()
+ me.init = function()
{
- $clonedFile.addClass('hidden');
- // removes the saved decrypted file data
- $attachmentLink.attr('href', '');
- // the only way to deselect the file is to recreate the input // @TODO really?
- $fileWrap.html($fileWrap.html());
- $fileWrap.removeClass('hidden');
+ // hide "no javascript" message
+ $('#noscript').hide();
+
+ $errorMessage = $('#errormessage');
+ $status = $('#status');
+
+ // display status returned by php code, if any (eg. paste was properly deleted)
+ // @TODO remove this by handling errors in a different way
+ if ($status.text().length > 0)
+ {
+ me.showStatus($status.text());
+ return;
+ }
+
+ // keep line height even if content empty
+ $status.html(' '); // @TODO what? remove?
+
+ // display error message from php code
+ if ($errorMessage.text().length > 1) {
+ Alert.showError($errorMessage.text());
+ }
};
- /**
- * checks if there is an attachment
- *
- * @name alert.hasAttachment
- * @function
- */
- me.hasAttachment = function()
- {
- return typeof $attachmentLink.attr('href') !== 'undefined'
- };
+ return me;
+ })(window, document);
- /**
- * return the attachment
- *
- * @name alert.getAttachment
- * @function
- * @returns {array}
- */
- me.getAttachment = function()
- {
- return [
- $attachmentLink.attr('href'),
- $attachmentLink.attr('download')
- ];
- };
+ /**
+ * handles paste status/result
+ *
+ * @param {object} window
+ * @param {object} document
+ * @class
+ */
+ var PasteStatus = (function (window, document) {
+ var me = {};
+
+ var $pasteSuccess,
+ $shortenButton,
+ $pasteUrl,
+ $remainingTime;
/**
* forward to URL shortener
@@ -963,45 +985,13 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
+ encodeURIComponent($pasteUrl.attr('href'));
}
- /**
- * reload the page
- *
- * This takes the user to the PrivateBin home page.
- *
- * @name controller.createPasteNotification
- * @function
- * @param {string} url
- * @param {string} deleteUrl
- */
- me.createPasteNotification = function(url, deleteUrl)
- {
- $('#pastelink').find(':first').html(
- i18n._(
- 'Your paste is %s (Hit [Ctrl]+[c] to copy)',
- url, url
- )
- );
- // save newly created element
- $pasteUrl = $('#pasteurl');
- // and add click event
- $pasteUrl.click(pasteLinkClick);
-
- // shorten button
- $('#deletelink').html('' + i18n._('Delete data') + '');
-
- // show result
- $pasteSuccess.removeClass('hidden');
- // we pre-select the link so that the user only has to [Ctrl]+[c] the link
- helper.selectText($pasteUrl[0]);
- };
-
/**
* Forces opening the paste if the link does not do this automatically.
*
* This is necessary as browsers will not reload the page when it is
* already loaded (which is fake as it is set via history.pushState()).
*
- * @name controller.pasteLinkClick
+ * @name Controller.pasteLinkClick
* @function
* @param {Event} event
*/
@@ -1014,127 +1004,196 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
}
}
+ /**
+ * creates a notification after a successfull paste upload
+ *
+ * @name PasteStatus.createPasteNotification
+ * @function
+ * @param {string} url
+ * @param {string} deleteUrl
+ */
+ me.createPasteNotification = function(url, deleteUrl)
+ {
+ $('#pastelink').find(':first').html(
+ I18n._(
+ 'Your paste is %s (Hit [Ctrl]+[c] to copy)',
+ url, url
+ )
+ );
+ // save newly created element
+ $pasteUrl = $('#pasteurl');
+ // and add click event
+ $pasteUrl.click(pasteLinkClick);
+
+ // shorten button
+ $('#deletelink').html('' + I18n._('Delete data') + '');
+
+ // show result
+ $pasteSuccess.removeClass('hidden');
+ // we pre-select the link so that the user only has to [Ctrl]+[c] the link
+ Helper.selectText($pasteUrl[0]);
+ };
+
+ /**
+ * shows the remaining time
+ *
+ * @function
+ * @param {object} pasteMetaData
+ */
+ me.showRemainingTime = function(pasteMetaData)
+ {
+ if (pasteMetaData.burnafterreading) {
+ // display paste "for your eyes only" if it is deleted
+
+ // actually remove paste, before we claim it is deleted
+ Controller.removePaste(Modal.getPasteId(), 'burnafterreading');
+
+ I18n._($remainingTime.find(':last'), "FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
+ $remainingTime.addClass('foryoureyesonly');
+
+ // discourage cloning (it cannot really be prevented)
+ TopNav.hideCloneButton();
+
+ } else if (pasteMetaData.expire_date) {
+ // display paste expiration
+ var expiration = Helper.secondsToHuman(pasteMetaData.remaining_time),
+ expirationLabel = [
+ 'This document will expire in %d ' + expiration[1] + '.',
+ 'This document will expire in %d ' + expiration[1] + 's.'
+ ];
+
+ I18n._($remainingTime.find(':last'), expirationLabel, expiration[0]);
+ $remainingTime.removeClass('foryoureyesonly')
+ } else {
+ // never expires
+ return;
+ }
+
+ // in the end, display notification
+ $remainingTime.removeClass('hidden');
+ };
+
/**
* init status manager
*
* preloads jQuery elements
*
- * @name alert.init
+ * @name Alert.init
* @function
*/
me.init = function()
{
- // hide "no javascript" message
- $('#noscript').hide();
-
$shortenButton = $('#shortenbutton');
- $attachment = $('#attachment');
- $attachmentLink = $('#attachment a');
- $clonedFile = $('#clonedfile');
- $errorMessage = $('#errormessage');
- $fileWrap = $('#filewrap');
$pasteSuccess = $('#pasteSuccess');
- // $pasteUrl is saved in submitPasteUpload() if/after it is
- // actually created
- $status = $('#status');
- // @TODO $replyStatus …
+ // $pasteUrl is saved in me.createPasteNotification() after creation
+ $remainingTime = $('#remainingtime');
// bind elements
$shortenButton.click(sendToShortener);
-
- // display status returned by php code, if any (eg. paste was properly deleted)
- // @TODO remove this by handling errors in a different way
- if ($status.text().length > 0)
- {
- me.showStatus($status.text());
- return;
- }
-
- // keep line height even if content empty
- $status.html(' '); // @TODO what? remove?
-
- // display error message from php code
- if ($errorMessage.text().length > 1) {
- me.showError($errorMessage.text());
- }
};
return me;
})(window, document);
/**
- * Passwort prompt
+ * password prompt
*
* @param {object} window
* @param {object} document
* @class
*/
- var prompt = (function (window, document) {
+ var Prompt = (function (window, document) {
var me = {};
var $passwordModal,
$passwordForm,
$passwordDecrypt;
+ var password = '',
+ passwordCallback = null;
+
/**
* ask the user for the password and set it
*
- * @name controller.requestPassword
+ * the callback set via setPasswordCallback is executed
+ *
+ * @name Prompt.requestPassword()
* @function
*/
me.requestPassword = function()
{
- if ($passwordModal.length === 0) {
- // old method for page template
- var password = prompt(i18n._('Please enter the password for this paste:'), '');
- if (password === null)
- {
- // @TODO when does this happen?
- throw 'password prompt canceled';
- }
- if (password.length === 0)
- {
- // recursive…
- me.requestPassword();
- } else {
- $passwordInput.val(password);
- me.displayMessages();
- }
- } else {
- // new bootstrap method
- $passwordModal.modal();
- }
+ // show new bootstrap method
+ $passwordModal.modal({
+ backdrop: 'static',
+ keyboard: false
+ });
};
/**
- * decrypt using the password from the modal dialog
+ * get cached password or password from easy Prompt
*
- * @name controller.decryptPasswordModal
+ * If you do not get a password with this function, use
+ * requestPassword
+ *
+ * @name Prompt.getPassword
* @function
*/
me.getPassword = function()
{
- if ($passwordDecrypt.val().length === 0) {
- me.requestPassword();
+ if (password.length !== 0) {
+ return password;
}
- return $passwordDecrypt.val();
- // $passwordInput.val($passwordDecrypt.val());
- // me.displayMessages();
+ if ($passwordModal.length === 0) {
+ // old method for page template
+ var newPassword = Prompt(I18n._('Please enter the password for this paste:'), '');
+ if (newPassword === null) {
+ throw 'password Prompt canceled';
+ }
+ if (password.length === 0) {
+ // recursive…
+ me.getPassword();
+ } else {
+ password = newPassword;
+ }
+ }
+
+ return password;
};
/**
- * submit a password in the modal dialog
+ * setsthe callback called when password is entered
*
- * @name controller.submitPasswordModal
+ * @name Prompt.setPasswordCallback
+ * @function
+ * @param {functions} setPasswordCallback
+ */
+ me.setPasswordCallback = function(callback)
+ {
+ passwordCallback = callback;
+ };
+
+ /**
+ * submit a password in the Modal dialog
+ *
+ * @private
* @function
* @param {Event} event
*/
- me.submitPasswordModal = function(event)
+ function submitPasswordModal(event)
{
- event.preventDefault();
+ // get input
+ password = $passwordDecrypt.val();
+
+ // hide modal
$passwordModal.modal('hide');
- };
+
+ if (passwordCallback !== null) {
+ passwordCallback();
+ }
+
+ event.preventDefault();
+ }
/**
@@ -1142,7 +1201,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
*
* preloads jQuery elements
*
- * @name controller.init
+ * @name Controller.init
* @function
*/
me.init = function()
@@ -1154,25 +1213,26 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
// bind events
// focus password input when it is shown
- $passwordModal.on('shown.bs.modal', function () {
+ $passwordModal.on('shown.bs.Modal', function () {
$passwordDecrypt.focus();
});
- // handle modal password request on decryption
- $passwordModal.on('hidden.bs.modal', me.decryptPasswordModal);
- $passwordForm.submit(me.submitPasswordModal);
+ // handle Modal password submission
+ $passwordForm.submit(submitPasswordModal);
};
return me;
})(window, document);
/**
- * Manage paste/message input
+ * Manage paste/message input, and preview tab
+ *
+ * Note that the actual preview is handled by PasteViewer.
*
* @param {object} window
* @param {object} document
* @class
*/
- var editor = (function (window, document) {
+ var Editor = (function (window, document) {
var me = {};
var $message,
@@ -1185,7 +1245,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* support input of tab character
*
- * @name editor.supportTabs
+ * @name Editor.supportTabs
* @function
* @param {Event} event
* @TODO doc what is @this here?
@@ -1211,9 +1271,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
}
/**
- * view the editor tab
+ * view the Editor tab
*
- * @name editor.viewEditor
+ * @name Editor.viewEditor
* @function
* @param {Event} event - optional
*/
@@ -1223,25 +1283,26 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
$messageEdit.addClass('active');
$messagePreview.removeClass('active');
- pasteViewer.hide();
+ PasteViewer.hide();
// reshow input
$message.removeClass('hidden');
me.focusInput();
- // me.stateNewPaste();
// finish
isPreview = false;
- // if (typeof event === 'undefined') {
- // event.preventDefault();
- // } // @TODO confirm this is not needed
+
+ // prevent jumping of page to top
+ if (typeof event !== 'undefined') {
+ event.preventDefault();
+ }
}
/**
* view the preview tab
*
- * @name editor.viewPreview
+ * @name Editor.viewPreview
* @function
* @param {Event} event
*/
@@ -1256,20 +1317,22 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
// show preview
$('#errormessage').find(':last')
- pasteViewer.setText($message.val());
- pasteViewer.trigger();
+ PasteViewer.setText($message.val());
+ PasteViewer.run();
// finish
isPreview = true;
- // if (typeof event === 'undefined') {
- // event.preventDefault();
- // } // @TODO confirm this is not needed
+
+ // prevent jumping of page to top
+ if (typeof event !== 'undefined') {
+ event.preventDefault();
+ }
}
/**
* get the state of the preview
*
- * @name editor.isPreview
+ * @name Editor.isPreview
* @function
*/
me.isPreview = function()
@@ -1278,9 +1341,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
}
/**
- * reset the editor view
+ * reset the Editor view
*
- * @name editor.resetInput
+ * @name Editor.resetInput
* @function
*/
me.resetInput = function()
@@ -1295,9 +1358,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
};
/**
- * shows the editor
+ * shows the Editor
*
- * @name editor.show
+ * @name Editor.show
* @function
*/
me.show = function()
@@ -1307,9 +1370,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
};
/**
- * hides the editor
+ * hides the Editor
*
- * @name editor.reset
+ * @name Editor.reset
* @function
*/
me.hide = function()
@@ -1321,7 +1384,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* focuses the message input
*
- * @name editor.focusInput
+ * @name Editor.focusInput
* @function
*/
me.focusInput = function()
@@ -1332,7 +1395,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* returns the current text
*
- * @name editor.getText
+ * @name Editor.getText
* @function
* @return {string}
*/
@@ -1346,7 +1409,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
*
* preloads jQuery elements
*
- * @name editor.init
+ * @name Editor.init
* @function
*/
me.init = function()
@@ -1367,23 +1430,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
})(window, document);
/**
- * Parse and show paste.
+ * (view) Parse and show paste.
*
* @param {object} window
* @param {object} document
* @class
*/
- var pasteViewer = (function (window, document) {
+ var PasteViewer = (function (window, document) {
var me = {};
- var $clearText,
- $comments,
- $discussion,
- $image,
+ var $clonedFile,
+ $plainText,
$placeholder,
$prettyMessage,
- $prettyPrint,
- $remainingTime;
+ $prettyPrint;
var text,
format = 'plaintext',
@@ -1393,7 +1453,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* apply the set format on paste and displays it
*
- * @name pasteViewer.parsePaste
* @private
* @function
*/
@@ -1405,8 +1464,8 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
}
// set text
- helper.setElementText($clearText, text);
- helper.setElementText($prettyPrint, text);
+ Helper.setElementText($plainText, text);
+ Helper.setElementText($prettyPrint, text);
switch (format) {
case 'markdown':
@@ -1415,11 +1474,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
tables: true,
tablesHeaderId: true
});
- $clearText.html(
+ $plainText.html(
converter.makeHtml(text)
);
// add table classes from bootstrap css
- $clearText.find('table').addClass('table-condensed table-bordered');
+ $plainText.find('table').addClass('table-condensed table-bordered');
break;
case 'syntaxhighlighting':
// @TODO is this really needed or is "one" enough?
@@ -1430,14 +1489,14 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
$prettyPrint.html(
prettyPrintOne(
- helper.htmlEntities(text), null, true
+ Helper.htmlEntities(text), null, true
)
);
// fall through, as the rest is the same
default: // = 'plaintext'
// convert URLs to clickable links
- helper.urls2links($clearText);
- helper.urls2links($prettyPrint);
+ Helper.urls2links($plainText);
+ Helper.urls2links($prettyPrint);
$prettyPrint.css('white-space', 'pre-wrap');
$prettyPrint.css('word-break', 'normal');
@@ -1448,7 +1507,6 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
/**
* displays the paste
*
- * @name pasteViewer.show
* @private
* @function
*/
@@ -1464,234 +1522,20 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) {
switch (format) {
case 'markdown':
- $clearText.removeClass('hidden');
+ $plainText.removeClass('hidden');
$prettyMessage.addClass('hidden');
break;
default:
- $clearText.addClass('hidden');
+ $plainText.addClass('hidden');
$prettyMessage.removeClass('hidden');
break;
}
}
- /**
- * show decrypted text in the display area, including discussion (if open)
- *
- * @name pasteViewer.displayPaste
- * @function
- * @param {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta'))
- */
- me.displayPaste = function(paste)
- {
- paste = paste || $.parseJSON(modal.getCipherData());
- var key = helper.pageKey(),
- password = $passwordInput.val();
- if (!$prettyPrint.hasClass('prettyprinted')) {
- // Try to decrypt the paste.
- try
- {
- if (paste.attachment)
- {
- var attachment = cryptTool.decipher(key, password, paste.attachment);
- if (attachment.length === 0)
- {
- if (password.length === 0)
- {
- me.requestPassword();
- return;
- }
- attachment = cryptTool.decipher(key, password, paste.attachment);
- }
- if (attachment.length === 0)
- {
- throw 'failed to decipher attachment';
- }
-
- if (paste.attachmentname)
- {
- var attachmentname = cryptTool.decipher(key, password, paste.attachmentname);
- if (attachmentname.length > 0)
- {
- $attachmentLink.attr('download', attachmentname);
- }
- }
- $attachmentLink.attr('href', attachment);
- $attachment.removeClass('hidden');
-
- // if the attachment is an image, display it
- var imagePrefix = 'data:image/';
- if (attachment.substring(0, imagePrefix.length) === imagePrefix)
- {
- $image.html(
- $(document.createElement('img'))
- .attr('src', attachment)
- .attr('class', 'img-thumbnail')
- );
- $image.removeClass('hidden');
- }
- }
- var cleartext = cryptTool.decipher(key, password, paste.data);
- if (cleartext.length === 0 && password.length === 0 && !paste.attachment)
- {
- me.requestPassword();
- return;
- }
- if (cleartext.length === 0 && !paste.attachment)
- {
- throw 'failed to decipher message';
- }
-
- $passwordInput.val(password);
- if (cleartext.length > 0)
- {
- pasteViewer.setFormat(paste.meta.formatter);
- me.formatPaste(paste.meta.formatter, cleartext);
- }
- }
- catch(err)
- {
- me.stateOnlyNewPaste();
- me.showError(i18n._('Could not decrypt data (Wrong key?)'));
- return;
- }
- }
-
- // display paste expiration / for your eyes only
- if (paste.meta.expire_date)
- {
- var expiration = helper.secondsToHuman(paste.meta.remaining_time),
- expirationLabel = [
- 'This document will expire in %d ' + expiration[1] + '.',
- 'This document will expire in %d ' + expiration[1] + 's.'
- ];
- $remainingTime.find(':last').text(i18n._(expirationLabel, expiration[0]));
- $remainingTime.removeClass('foryoureyesonly')
- .removeClass('hidden');
- }
- if (paste.meta.burnafterreading)
- {
- // unfortunately many web servers don't support DELETE (and PUT) out of the box
- $.ajax({
- type: 'POST',
- url: helper.scriptLocation() + '?' + helper.pasteId(),
- data: {deletetoken: 'burnafterreading'},
- dataType: 'json',
- headers: headers
- })
- .fail(function() {
- controller.showError(i18n._('Could not delete the paste, it was not stored in burn after reading mode.'));
- });
- $remainingTime.find(':last').text(i18n._(
- 'FOR YOUR EYES ONLY. Don\'t close this window, this message can\'t be displayed again.'
- ));
- $remainingTime.addClass('foryoureyesonly')
- .removeClass('hidden');
- // discourage cloning (as it can't really be prevented)
- $cloneButton.addClass('hidden');
- }
-
- // if the discussion is opened on this paste, display it
- if (paste.meta.opendiscussion)
- {
- $comments.html('');
-
- var $divComment;
-
- // iterate over comments
- for (var i = 0; i < paste.comments.length; ++i)
- {
- var $place = $comments,
- comment = paste.comments[i],
- commentText = cryptTool.decipher(key, password, comment.data),
- $parentComment = $('#comment_' + comment.parentid);
-
- $divComment = $('
' + helper.htmlEntities(paste) + ''); + newDoc.write('
' + Helper.htmlEntities(paste) + ''); newDoc.close(); event.preventDefault(); } /** - * set the language in a cookie and reload the page + * saves the language in a cookie and reloads the page * - * @name topNav.setLanguage + * @name TopNav.setLanguage * @function * @param {Event} event */ function setLanguage(event) { document.cookie = 'lang=' + $(event.target).data('lang'); - me.reloadPage(event); + UiHelper.reloadHome(); } /** * Shows all elements belonging to viwing an existing pastes * - * @name topNav.hideAllElem + * @name TopNav.hideAllElem * @function */ me.showViewButtons = function() @@ -1981,7 +2053,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * Hides all elements belonging to existing pastes * - * @name topNav.hideAllElem + * @name TopNav.hideAllElem * @function */ me.hideViewButtons = function() @@ -1991,6 +2063,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return; } + $newButton.removeClass('hidden'); $cloneButton.addClass('hidden'); $rawTextButton.addClass('hidden'); @@ -2000,7 +2073,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * shows all elements needed when creating a new paste * - * @name topNav.setLanguage + * @name TopNav.setLanguage * @function */ me.showCreateButtons = function() @@ -2026,7 +2099,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * shows all elements needed when creating a new paste * - * @name topNav.setLanguage + * @name TopNav.setLanguage * @function */ me.hideCreateButtons = function() @@ -2036,12 +2109,12 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return; } + $newButton.addClass('hidden'); $sendButton.addClass('hidden'); $expiration.addClass('hidden'); $formatter.addClass('hidden'); $burnAfterReadingOption.addClass('hidden'); $openDiscussionOption.addClass('hidden'); - $newButton.addClass('hidden'); $password.addClass('hidden'); $attach.addClass('hidden'); // $clonedFile.addClass('hidden'); // @TODO @@ -2052,18 +2125,40 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * only shows the "new paste" button * - * @name topNav.setLanguage + * @name TopNav.setLanguage * @function */ me.showNewPasteButton = function() { - $newButton.addClass('hidden'); + $newButton.removeClass('hidden'); + }; + + /** + * only hides the clone button + * + * @name TopNav.hideCloneButton + * @function + */ + me.hideCloneButton = function() + { + $cloneButton.addClass('hidden'); + }; + + /** + * only hides the raw text button + * + * @name TopNav.hideRawButton + * @function + */ + me.hideRawButton = function() + { + $rawTextButton.addClass('hidden'); }; /** * shows a loading message, optionally with a percentage * - * @name topNav.showLoading + * @name TopNav.showLoading * @function * @param {string} message optional, default: 'Loading…' * @param {int} percentage optional, default: null @@ -2072,10 +2167,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { { // default message text if (typeof message === 'undefined') { - message = i18n._('Loading…'); + message = I18n._('Loading…'); } - console.log($loadingIndicator); // currently percentage parameter is ignored if (message !== null) { $loadingIndicator.find(':last').text(message); @@ -2086,7 +2180,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * hides the loading message * - * @name topNav.hideLoading + * @name TopNav.hideLoading * @function */ me.hideLoading = function() @@ -2097,7 +2191,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * collapses the navigation bar if nedded * - * @name topNav.collapseBar + * @name TopNav.collapseBar * @function */ me.collapseBar = function() @@ -2114,7 +2208,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the currently set expiration time * - * @name topNav.getExpiration + * @name TopNav.getExpiration * @function * @return {int} */ @@ -2126,7 +2220,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the currently selected file(s) * - * @name topNav.getFileList + * @name TopNav.getFileList * @function * @return {FileList|null} */ @@ -2149,7 +2243,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the state of the burn after reading checkbox * - * @name topNav.getExpiration + * @name TopNav.getExpiration * @function * @return {bool} */ @@ -2161,7 +2255,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the state of the discussion checkbox * - * @name topNav.getOpenDiscussion + * @name TopNav.getOpenDiscussion * @function * @return {bool} */ @@ -2173,7 +2267,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * returns the entered password * - * @name topNav.getPassword + * @name TopNav.getPassword * @function * @return {string} */ @@ -2187,7 +2281,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { * * preloads jQuery elements * - * @name topNav.init + * @name TopNav.init * @function */ me.init = function() @@ -2216,9 +2310,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // bind events $burnAfterReading.change(changeBurnAfterReading); $openDiscussionOption.change(changeOpenDiscussion); - $newButton.click(controller.newPaste); - $sendButton.click(controller.submitPaste); - $cloneButton.click(controller.clonePaste); + $newButton.click(Controller.newPaste); + $sendButton.click(PasteEncrypter.submitPaste); + $cloneButton.click(Controller.clonePaste); $rawTextButton.click(rawText); $fileRemoveButton.click(me.removeAttachment); @@ -2231,7 +2325,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { changeOpenDiscussion(); // get default value from template or fall back to set value - pasteExpiration = modal.getExpirationDefault() || pasteExpiration; + pasteExpiration = Modal.getExpirationDefault() || pasteExpiration; }; return me; @@ -2240,21 +2334,24 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * Responsible for AJAX requests, transparently handles encryption… * - * @name state * @class */ - var uploader = (function () { + var Uploader = (function () { var me = {}; var successFunc = null, - failureFunc = null; - - var url = helper.scriptLocation(), - data = {}, + failureFunc = null, + url, + data, randomKey, password; - // public variable ('constant') to prevent magic numbers + /** + * public variable ('constant') for errors to prevent magic numbers + * + * @readonly + * @enum {Object} + */ me.error = { okay: 0, custom: 1, @@ -2292,7 +2389,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * called after a upload failure * - * @name uploader.submitPasteUpload + * @name Uploader.submitPasteUpload * @function * @param {int} status - internal code * @param {int} data - original error code @@ -2307,12 +2404,11 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * actually uploads the data * - * @name uploader.submitPasteUpload + * @name Uploader.run * @function */ - me.trigger = function() + me.run = function() { - console.log(data); $.ajax({ type: 'POST', url: url, @@ -2338,7 +2434,19 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * set success function * - * @name uploader.setSuccess + * @name Uploader.setSuccess + * @function + * @param {function} func + */ + me.setUrl = function(newUrl) + { + url = newUrl; + }; + + /** + * set success function + * + * @name Uploader.setSuccess * @function * @param {function} func */ @@ -2350,7 +2458,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * set failure function * - * @name uploader.setSuccess + * @name Uploader.setSuccess * @function * @param {function} func */ @@ -2362,7 +2470,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * prepares a new upload * - * @name uploader.prepare + * @name Uploader.prepare * @function * @param {string} newPassword * @return {object} @@ -2372,33 +2480,35 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { // set password password = newPassword; - // entropy should already be checked - // @TODO maybe move it here? + // entropy should already be checked! // generate a new random key - randomKey = cryptTool.getSymmetricKey(); + randomKey = CryptTool.getSymmetricKey(); // reset data + successFunc = null; + failureFunc = null; + url = Helper.baseUri() data = {}; }; /** * encrypts and sets the data * - * @name uploader.setData + * @name Uploader.setData * @function * @param {string} index * @param {mixed} element */ me.setData = function(index, element) { - data[index] = cryptTool.cipher(randomKey, password, element); + data[index] = CryptTool.cipher(randomKey, password, element); }; /** * set the additional metadata to send unencrypted * - * @name uploader.setUnencryptedData + * @name Uploader.setUnencryptedData * @function * @param {string} index * @param {mixed} element @@ -2411,7 +2521,7 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { /** * set the additional metadata to send unencrypted passed at once * - * @name uploader.setUnencryptedData + * @name Uploader.setUnencryptedData * @function * @param {object} newData */ @@ -2421,9 +2531,9 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { }; /** - * init uploader + * init Uploader * - * @name uploader.init + * @name Uploader.init * @function */ me.init = function() @@ -2435,52 +2545,124 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { })(); /** - * PrivateBin logic + * (controller) Responsible for encrypting paste and sending it to server. * - * @param {object} window - * @param {object} document - * @name controller + * @name state * @class */ - var controller = (function (window, document) { + var PasteEncrypter = (function () { var me = {}; + var requirementsChecked = false; + + /** + * checks whether there is a suitable amount of entrophy + * + * @private + * @function + * @param {function} retryCallback - the callback to execute to retry the upload + * @return {bool} + */ + function checkRequirements(retryCallback) { + // skip double requirement checks + if (requirementsChecked === true) { + return false; + } + + if (!CryptTool.isEntropyReady()) { + // display a message and wait + Alert.showStatus(I18n._('Please move your mouse for more entropy...')); + + CryptTool.addEntropySeedListener(retryCallback); + return false; + } + + requirementsChecked = true; + + return true; + } + /** * called after successful upload * + * @private * @function * @param {int} status * @param {int} data */ function showCreatedPaste(status, data) { - topNav.hideLoading(); - console.log(data); + TopNav.hideLoading(); - var url = helper.scriptLocation() + '?' + data.id + '#' + data.encryptionKey, - deleteUrl = helper.scriptLocation() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken; + var url = Helper.baseUri() + '?' + data.id + '#' + data.encryptionKey, + deleteUrl = Helper.baseUri() + '?pasteid=' + data.id + '&deletetoken=' + data.deletetoken; - alert.hideMessages(); + Alert.hideMessages(); // show notification - alert.createPasteNotification(url, deleteUrl) + PasteStatus.createPasteNotification(url, deleteUrl) // show new URL in browser bar history.pushState({type: 'newpaste'}, document.title, url); - topNav.showViewButtons(); - editor.hide(); + TopNav.showViewButtons(); + TopNav.hideRawButton(); + Editor.hide(); // parse and show text // (preparation already done in me.submitPaste()) - pasteViewer.trigger(); + PasteViewer.run(); + } + + /** + * adds attachments to the Uploader + * + * @private + * @function + * @param {File|null|undefined} file - optional, falls back to cloned attachment + * @param {function} callback - excuted when action is successful + */ + function encryptAttachments(file, callback) { + if (typeof file !== 'undefined' && file !== null) { + // check file reader requirements for upload + if (typeof FileReader === 'undefined') { + Alert.showError(I18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); + // cancels process as it does not execute callback + return; + } + + var reader = new FileReader(); + + // closure to capture the file information + reader.onload = function(event) { + Uploader.setData('attachment', event.target.result); + Uploader.setData('attachmentname', file.name); + + // run callback + callback(); + }; + + // actually read first file + reader.readAsDataURL(file); + } else if (AttachmentViewer.hasAttachment()) { + // fall back to cloned part + var attachment = AttachmentViewer.getAttachment(); + + Uploader.setData('attachment', attachment[0]); + Uploader.setUnencryptedData('attachmentname', attachment[1]); // @TODO does not encrypt file name??! + callback(); + } else { + // if there are no attachments, this is of course still successful + callback(); + } } /** * send a reply in a discussion * - * @name controller.sendComment + * @name PasteEncrypter.sendComment * @function * @param {Event} event + * @TODO WIP */ me.sendComment = function(event) { @@ -2493,36 +2675,36 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { return; } - me.showStatus(i18n._('Sending comment...'), true); + me.showStatus(I18n._('Sending comment...'), true); var parentid = event.data.parentid, - key = helper.pageKey(), - cipherdata = cryptTool.cipher(key, $passwordInput.val(), replyMessage.val()), + key = Modal.getPasteKey(), + cipherdata = CryptTool.cipher(key, $passwordInput.val(), replyMessage.val()), ciphernickname = '', nick = $('#nickname').val(); if (nick.length > 0) { - ciphernickname = cryptTool.cipher(key, $passwordInput.val(), nick); + ciphernickname = CryptTool.cipher(key, $passwordInput.val(), nick); } var dataToSend = { data: cipherdata, parentid: parentid, - pasteid: helper.pasteId(), + pasteid: Modal.getPasteId(), nickname: ciphernickname }; $.ajax({ type: 'POST', - url: helper.scriptLocation(), + url: Helper.baseUri(), data: dataToSend, dataType: 'json', headers: ajaxHeaders, success: function(data) { if (data.status === 0) { - controller.showStatus(i18n._('Comment posted.')); + status.showStatus(I18n._('Comment posted.')); $.ajax({ type: 'GET', - url: helper.scriptLocation() + '?' + helper.pasteId(), + url: Helper.baseUri() + '?' + Modal.getPasteId(), dataType: 'json', headers: ajaxHeaders, success: function(data) { @@ -2532,157 +2714,396 @@ jQuery.PrivateBin = function($, sjcl, Base64, RawDeflate) { } else if (data.status === 1) { - alert.showError(i18n._('Could not refresh display: %s', data.message)); + Alert.showError(I18n._('Could not refresh display: %s', data.message)); } else { - alert.showError(i18n._('Could not refresh display: %s', i18n._('unknown status'))); + Alert.showError(I18n._('Could not refresh display: %s', I18n._('unknown status'))); } } }) .fail(function() { - controller.showError(i18n._('Could not refresh display: %s', i18n._('server error or not responding'))); + Alert.showError(I18n._('Could not refresh display: %s', I18n._('server error or not responding'))); }); } else if (data.status === 1) { - controller.showError(i18n._('Could not post comment: %s', data.message)); + Alert.showError(I18n._('Could not post comment: %s', data.message)); } else { - controller.showError(i18n._('Could not post comment: %s', i18n._('unknown status'))); + Alert.showError(I18n._('Could not post comment: %s', I18n._('unknown status'))); } } }) .fail(function() { - controller.showError(i18n._('Could not post comment: %s', i18n._('server error or not responding'))); + Alert.showError(I18n._('Could not post comment: %s', I18n._('server error or not responding'))); }); }; /** * sends a new paste to server * - * @name controller.submitPaste + * @name PasteEncrypter.submitPaste * @function */ me.submitPaste = function() { // UI loading state - topNav.hideCreateButtons(); - topNav.showLoading(i18n._('Sending paste...'), 0); - topNav.collapseBar(); + TopNav.hideCreateButtons(); + TopNav.showLoading(I18n._('Sending paste...'), 0); + TopNav.collapseBar(); // get data - var plainText = editor.getText(); + var plainText = Editor.getText(), + format = PasteViewer.getFormat(), + files = TopNav.getFileList(); // do not send if there is no data if (plainText.length === 0 && files === null) { // revert loading status… - topNav.hideLoading(); - topNav.showCreateButtons(); + TopNav.hideLoading(); + TopNav.showCreateButtons(); return; } - topNav.showLoading(i18n._('Sending paste...'), 10); + TopNav.showLoading(I18n._('Sending paste...'), 10); // check entropy - if (!cryptTool.isEntropyReady()) { - // display a message and wait - alert.showStatus(i18n._('Please move your mouse for more entropy...')); - - cryptTool.addEntropySeedListener(function() { - me.submitPaste(event); - }); + if (!checkRequirements(function () { + me.submitPaste(); + })) { + return; // to prevent multiple executions } - // prepare uploader - uploader.prepare(topNav.getPassword()); - - // encrypt cipher data - uploader.setData('data', plainText); - - // encrypt attachments - var files = topNav.getFileList(); - if (files !== null) { - var reader = new FileReader(); - - // closure to capture the file information - reader.onload = (function(file) { - return function(event) { - uploader.setData('attachment', event.target.result); - uploader.setData('attachmentname', file.name); - }; - })(files[0]); - - // actually read first file - reader.readAsDataURL(files[0]); - } else if (alert.hasAttachment()) { - var attachment = alert.getAttachment(); - - uploader.setData('attachment', attachment[0]); - uploader.setUnencryptedData('attachmentname', attachment[1]); // @TODO does not encrypt file name??! - } + // prepare Uploader + Uploader.prepare(TopNav.getPassword()); // set success/fail functions - uploader.setSuccess(showCreatedPaste); - uploader.setFailure(function (status, data) { + Uploader.setSuccess(showCreatedPaste); + Uploader.setFailure(function (status, data) { // revert loading status… - topNav.hideLoading(); - topNav.showCreateButtons(); + TopNav.hideLoading(); + TopNav.showCreateButtons(); // show error message switch (status) { - case uploader.error['custom']: - alert.showError(i18n._('Could not create paste: %s', data.message)); + case Uploader.error['custom']: + Alert.showError(I18n._('Could not create paste: %s', data.message)); break; - case uploader.error['unknown']: - alert.showError(i18n._('Could not create paste: %s', i18n._('unknown status'))); + case Uploader.error['unknown']: + Alert.showError(I18n._('Could not create paste: %s', I18n._('unknown status'))); break; - case uploader.error['serverError']: - alert.showError(i18n._('Could not create paste: %s', i18n._('server error or not responding'))); + case Uploader.error['serverError']: + Alert.showError(I18n._('Could not create paste: %s', I18n._('server error or not responding'))); break; default: - alert.showError(i18n._('Could not create paste: %s', i18n._('unknown error'))); + Alert.showError(I18n._('Could not create paste: %s', I18n._('unknown error'))); break; } }); // fill it with unencrypted submitted options - var format = pasteViewer.getFormat(); - uploader.setUnencryptedBulkData({ - expire: topNav.getExpiration(), + Uploader.setUnencryptedBulkData({ + expire: TopNav.getExpiration(), formatter: format, - burnafterreading: topNav.getBurnAfterReading() ? 1 : 0, - opendiscussion: topNav.getOpenDiscussion() ? 1 : 0 + burnafterreading: TopNav.getBurnAfterReading() ? 1 : 0, + opendiscussion: TopNav.getOpenDiscussion() ? 1 : 0 }); // prepare PasteViewer for later preview - pasteViewer.setText(plainText); - pasteViewer.setFormat(format); + PasteViewer.setText(plainText); + PasteViewer.setFormat(format); - // send data - uploader.trigger(); + // encrypt cipher data + Uploader.setData('data', plainText); + + // encrypt attachments + encryptAttachments( + files === null ? null : files[0], + function () { + // send data + Uploader.run(); + } + ); }; + /** + * initialize + * + * @name PasteEncrypter.init + * @function + */ + me.init = function() + { + // nothing yet + }; + + return me; + })(); + + /** + * (controller) Responsible for decrypting cipherdata and passing data to view. + * + * @name state + * @class + */ + var PasteDecrypter = (function () { + var me = {}; + + /** + * decrypt the actual paste text + * + * @private + * @function + * @param {object} paste - paste data in object form + * @param {string} key + * @param {string} password + * @return {bool} - whether action was successful + */ + function decryptPaste(paste, key, password) + { + // try decryption without password + var plaintext = CryptTool.decipher(key, password, paste.data); + + // if it fails, request password + if (plaintext.length === 0 && password.length === 0) { + // get password + password = Prompt.getPassword(); + + // if password is there, re-try + if (password.length !== 0) { + // recursive + // note: an infinite loop is prevented as the previous if + // clause checks whether a password is already set and ignores + // error with password being passed + return decryptPaste(paste, key, password); + } + + // trigger password request + Prompt.requestPassword(); + // the callback (via setPasswordCallback()) should have been set + // by parent function + return false; + } + + // if all tries failed, we can only throw an error + if (plaintext.length === 0) { + throw 'failed to decipher message'; + } + + // on success show paste + PasteViewer.setFormat(paste.meta.formatter); + PasteViewer.setText(plaintext); + // trigger to show the text (attachment loaded afterwards) + PasteViewer.run(); + + return true; + } + + /** + * decrypts any attachment + * + * @private + * @function + * @param {object} paste - paste data in object form + * @param {string} key + * @param {string} password + * @return {bool} - whether action was successful + */ + function decryptAttachment(paste, key, password) + { + // decrypt attachment + var attachment = CryptTool.decipher(key, password, paste.attachment); + if (attachment.length === 0) { + throw 'failed to decipher attachment'; + } + + // decrypt attachment name + var attachmentName; + if (paste.attachmentname) { + attachmentName = attachmentName = CryptTool.decipher(key, password, paste.attachmentname); + if (attachmentName.length === 0) { + // @TODO considering the buggy cloning (?, see other todo comment) this might affect previous pastes + throw 'failed to decipher attachment name'; + } + } + + AttachmentViewer.setAttachment(attachment, attachmentName); + AttachmentViewer.showAttachment(); + } + + /** + * show decrypted text in the display area, including discussion (if open) + * + * @name PasteDecrypter.run + * @function + * @param {Object} [paste] - (optional) object including comments to display (items = array with keys ('data','meta')) + */ + me.run = function(paste) + { + TopNav.showLoading('Decrypting paste…'); + + if (typeof paste === 'undefined') { + paste = $.parseJSON(Modal.getCipherData()); + } + + var key = Modal.getPasteKey(), + password = Prompt.getPassword(); + + if (PasteViewer.isPrettyPrinted()) { + console.error('Too pretty! (don\'t know why this check)'); //@TODO + return; + } + + // try to decrypt the paste + try { + Prompt.setPasswordCallback(function () { + me.run(paste); + }); + + // try to decrypt paste and if it fails (because the password is + // missing) return to let JS continue and wait for user + if (!decryptPaste(paste, key, password)) { + return; + } + + // decrypt attachments + if (paste.attachment) { + decryptAttachment(paste, key, password); + } + } catch(err) { + TopNav.hideLoading(); + + // log and show error + console.error(err); + Alert.showError(I18n._('Could not decrypt data (Wrong key?)')); // @TODO error is not translated + + // still go on to potentially show potentially partially decrypted data + } + + // shows the remaining time (until) deletion + PasteStatus.showRemainingTime(paste.meta); + + // if the discussion is opened on this paste, display it + // @TODO BELOW + if (paste.meta.opendiscussion) { + $comments.html(''); + + var $divComment; + + // iterate over comments + for (var i = 0; i < paste.comments.length; ++i) + { + var $place = $comments, + comment = paste.comments[i], + commentText = CryptTool.decipher(key, password, comment.data), + $parentComment = $('#comment_' + comment.parentid); + + $divComment = $('