diff --git a/cfg/conf.ini b/cfg/conf.ini index 77885c07..31225812 100644 --- a/cfg/conf.ini +++ b/cfg/conf.ini @@ -17,6 +17,9 @@ opendiscussion = false ; enable or disable the password feature, defaults to true password = true +; enable or disable the file upload feature, defaults to false +fileupload = false + ; preselect the burn-after-reading feature, defaults to false burnafterreadingselected = false diff --git a/css/zerobin.css b/css/zerobin.css index 718ccdae..21bd230d 100644 --- a/css/zerobin.css +++ b/css/zerobin.css @@ -25,7 +25,7 @@ body { padding-right: 60px; } -a { color: #0f388f; } +a { color: #0f388f; cursor:pointer; } h1.title { font-size: 3.5em; @@ -76,7 +76,7 @@ h3.title { #aboutbox a { color: #94a3b4; } -#message, #cleartext, #prettymessage, .replymessage { +#message, #cleartext, #prettymessage, #attachment, .replymessage { clear: both; color: #000; background-color: #fff; diff --git a/i18n/de.json b/i18n/de.json index f3a9294d..2e18c064 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -127,5 +127,12 @@ "Format": "Format", "Plain Text": "Nur Text", "Source Code": "Quellcode", - "Markdown": "Markdown" + "Markdown": "Markdown", + "Download attachment": "Anhang herunterladen", + "Cloned file attached.": "Kopierte Datei angehängt.", + "Attach a file:": "Datei anhängen:", + "Remove attachment": "Anhang entfernen", + "Your browser does not support uploading encrypted files. Please use a newer browser.": + "Dein Browser unterstützt das hochladen von verschlüsselten Dateien nicht. Bitte verwende einen neueren Browser.", + "Invalid attachment.": "Ungültiger Datei-Anhang." } diff --git a/i18n/fr.json b/i18n/fr.json index 55ce6205..56cd3c6c 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -136,5 +136,12 @@ "Format": "Format", "Plain Text": "texte", "Source Code": "code source", - "Markdown": "Markdown" + "Markdown": "Markdown", + "Download attachment": "Download attachment", + "Cloned file attached.": "Cloned file attached.", + "Attach a file:": "Attach a file:", + "Remove attachment": "Remove attachment", + "Your browser does not support uploading encrypted files. Please use a newer browser.": + "Your browser does not support uploading encrypted files. Please use a newer browser.", + "Invalid attachment.": "Invalid attachment." } diff --git a/i18n/pl.json b/i18n/pl.json index 9b02aacb..1463ce04 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -127,5 +127,12 @@ "Format": "Format", "Plain Text": "Plain Text", "Source Code": "Source Code", - "Markdown": "Markdown" + "Markdown": "Markdown", + "Download attachment": "Download attachment", + "Cloned file attached.": "Cloned file attached.", + "Attach a file:": "Attach a file:", + "Remove attachment": "Remove attachment", + "Your browser does not support uploading encrypted files. Please use a newer browser.": + "Your browser does not support uploading encrypted files. Please use a newer browser.", + "Invalid attachment.": "Invalid attachment." } diff --git a/js/zerobin.js b/js/zerobin.js index 686b515f..479e384c 100644 --- a/js/zerobin.js +++ b/js/zerobin.js @@ -585,6 +585,15 @@ $(function() { } if (cleartext.length == 0) throw 'failed to decipher message'; this.passwordInput.val(password); + if (comments[0].attachment) + { + var attachment = filter.decipher(key, password, comments[0].attachment); + if (attachment) + { + this.attachmentLink.attr('href', attachment); + this.attachment.removeClass('hidden'); + } + } helper.setElementText(this.clearText, cleartext); helper.setElementText(this.prettyPrint, cleartext); @@ -799,9 +808,10 @@ $(function() { sendData: function(event) { event.preventDefault(); + var files = document.getElementById('file').files; // FileList object // Do not send if no data. - if (this.message.val().length == 0) return; + if (this.message.val().length == 0 && !(files && files[0])) return; // If sjcl has not collected enough entropy yet, display a message. if (!sjcl.random.isReady()) @@ -818,6 +828,48 @@ $(function() { this.showStatus(i18n._('Sending paste...'), true); var randomkey = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0); + var cipherdata_attachment; + var password = this.passwordInput.val(); + if(files && files[0]) + { + if(typeof FileReader === undefined) + { + this.showError(i18n._('Your browser does not support uploading encrypted files. Please use a newer browser.')); + return; + } + var reader = new FileReader(); + // Closure to capture the file information. + reader.onload = (function(theFile) + { + return function(e) { + zerobin.sendDataContinue( + randomkey, + filter.cipher(randomkey, password, e.target.result) + ); + } + })(files[0]); + reader.readAsDataURL(files[0]); + } + else if(this.attachmentLink.attr('href')) + { + this.sendDataContinue( + randomkey, + filter.cipher(randomkey, password, this.attachmentLink.attr('href')) + ); + } + else + { + this.sendDataContinue(randomkey, ''); + } + }, + + /** + * Send a new paste to server, step 2 + * + * @param Event event + */ + sendDataContinue: function(randomkey, cipherdata_attachment) + { var cipherdata = filter.cipher(randomkey, this.passwordInput.val(), this.message.val()); var data_to_send = { data: cipherdata, @@ -826,6 +878,10 @@ $(function() { burnafterreading: this.burnAfterReading.is(':checked') ? 1 : 0, opendiscussion: this.openDiscussion.is(':checked') ? 1 : 0 }; + if (cipherdata_attachment.length > 0) + { + data_to_send.attachment = cipherdata_attachment; + } $.post(this.scriptLocation(), data_to_send, function(data) { if (data.status == 0) { @@ -880,6 +936,8 @@ $(function() { this.openDisc.removeClass('hidden'); this.newButton.removeClass('hidden'); this.password.removeClass('hidden'); + this.attach.removeClass('hidden'); + this.attachment.removeClass('hidden'); this.message.removeClass('hidden'); this.message.focus(); }, @@ -902,6 +960,8 @@ $(function() { } this.rawTextButton.removeClass('hidden'); + this.attach.addClass('hidden'); + this.attachment.addClass('hidden'); this.expiration.addClass('hidden'); this.formatter.addClass('hidden'); this.burnAfterReadingOption.addClass('hidden'); @@ -969,6 +1029,11 @@ $(function() { history.replaceState(document.title, document.title, this.scriptLocation()); this.showStatus('', false); + if (this.attachmentLink.attr('href')) + { + this.clonedFile.removeClass('hidden'); + this.fileWrap.addClass('hidden'); + } this.message.text(this.clearText.text()); $('.navbar-toggle').click(); }, @@ -984,6 +1049,19 @@ $(function() { $('.navbar-toggle').click(); }, + /** + * Removes an attachment. + */ + removeAttachment: function() + { + this.clonedFile.addClass('hidden'); + // removes the saved decrypted file data + $('#attachment a').attr('href', ''); + // the only way to deselect the file is to recreate the input + this.fileWrap.html(this.fileWrap.html()); + this.fileWrap.removeClass('hidden'); + }, + /** * Display an error message * (We use the same function for paste and reply to comments) @@ -1042,6 +1120,7 @@ $(function() { this.sendButton.click($.proxy(this.sendData, this)); this.cloneButton.click($.proxy(this.clonePaste, this)); this.rawTextButton.click($.proxy(this.rawText, this)); + this.fileRemoveButton.click($.proxy(this.removeAttachment, this)); $('.reloadlink').click($.proxy(this.reloadPage, this)); }, @@ -1054,15 +1133,21 @@ $(function() { $('#noscript').hide(); // preload jQuery wrapped DOM elements and bind events + this.attach = $('#attach'); + this.attachment = $('#attachment'); + this.attachmentLink = $('#attachment a'); this.burnAfterReading = $('#burnafterreading'); this.burnAfterReadingOption = $('#burnafterreadingoption'); this.cipherData = $('#cipherdata'); this.clearText = $('#cleartext'); this.cloneButton = $('#clonebutton'); + this.clonedFile = $('#clonedfile'); this.comments = $('#comments'); this.discussion = $('#discussion'); this.errorMessage = $('#errormessage'); this.expiration = $('#expiration'); + this.fileRemoveButton = $('#fileremovebutton'); + this.fileWrap = $('#filewrap'); this.formatter = $('#formatter'); this.message = $('#message'); this.newButton = $('#newbutton'); diff --git a/lib/zerobin.php b/lib/zerobin.php index ad126115..8e2331ce 100644 --- a/lib/zerobin.php +++ b/lib/zerobin.php @@ -116,9 +116,12 @@ class zerobin $this->_init(); // create new paste or comment - if (!empty($_POST['data'])) + if ( + (array_key_exists('data', $_POST) && !empty($_POST['data'])) || + (array_key_exists('attachment', $_POST) && !empty($_POST['attachment'])) + ) { - $this->_create($_POST['data']); + $this->_create(); } // delete an existing paste elseif (!empty($_GET['deletetoken']) && !empty($_GET['pasteid'])) @@ -191,24 +194,30 @@ class zerobin /** * Store new paste or comment * - * POST contains: - * data (mandatory) = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) + * POST contains one or both: + * data = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) + * attachment = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * * All optional data will go to meta information: * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never) + * formatter (optional) = format to display the paste as (plaintext,syntaxhighlighting,markdown) (default:syntaxhighlighting) + * burnafterreading (optional) = if this paste may only viewed once ? (0/1) (default:0) * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0) * nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * parentid (optional) = in discussion, which comment this comment replies to. * pasteid (optional) = in discussion, which paste this comment belongs to. * * @access private - * @param string $data * @return string */ - private function _create($data) + private function _create() { $error = false; + $has_attachment = array_key_exists('attachment', $_POST); + $data = array_key_exists('data', $_POST) ? $_POST['data'] : ''; + $attachment = $has_attachment ? $_POST['attachment'] : ''; + // Make sure last paste from the IP address was more than X seconds ago. trafficlimiter::setLimit($this->_conf['traffic']['limit']); trafficlimiter::setPath($this->_conf['traffic']['dir']); @@ -226,7 +235,7 @@ class zerobin // Make sure content is not too big. $sizelimit = (int) $this->_getMainConfig('sizelimit', 2097152); - if (strlen($data) > $sizelimit) + if (strlen($data) + strlen($attachment) > $sizelimit) { $this->_return_message( 1, @@ -241,6 +250,15 @@ class zerobin // Make sure format is correct. if (!sjcl::isValid($data)) return $this->_return_message(1, 'Invalid data.'); + // Make sure attachments are enabled and format is correct. + if($has_attachment) + { + if ( + !$this->_getMainConfig('fileupload', false) || + !sjcl::isValid($attachment) + ) $this->_return_message(1, 'Invalid attachment.'); + } + // Read additional meta-information. $meta = array(); @@ -416,6 +434,9 @@ class zerobin return; } + // Add attachment if one was sent + if($has_attachment) $storage['attachment'] = $attachment; + // New paste if ( $this->_model()->create($dataid, $storage) === false @@ -634,6 +655,7 @@ class zerobin $page->assign('NOTICE', i18n::_($this->_getMainConfig('notice', ''))); $page->assign('BURNAFTERREADINGSELECTED', $this->_getMainConfig('burnafterreadingselected', false)); $page->assign('PASSWORD', $this->_getMainConfig('password', true)); + $page->assign('FILEUPLOAD', $this->_getMainConfig('fileupload', false)); $page->assign('BASE64JSVERSION', $this->_getMainConfig('base64version', '2.1.9')); $page->assign('EXPIRE', $expire); $page->assign('EXPIREDEFAULT', $this->_conf['expire']['default']); diff --git a/tpl/page.html b/tpl/page.html index a120d1b0..25de327f 100644 --- a/tpl/page.html +++ b/tpl/page.html @@ -74,7 +74,13 @@ + {if="$FILEUPLOAD"} + + {/if} diff --git a/tst/RainTPL.php b/tst/RainTPL.php index cc7c69ea..86db1148 100644 --- a/tst/RainTPL.php +++ b/tst/RainTPL.php @@ -38,6 +38,7 @@ class RainTPLTest extends PHPUnit_Framework_TestCase $page->assign('SYNTAXHIGHLIGHTINGTHEME', 'sons-of-obsidian'); $page->assign('BURNAFTERREADINGSELECTED', false); $page->assign('PASSWORD', true); + $page->assign('FILEUPLOAD', false); $page->assign('BASE64JSVERSION', '2.1.9'); $page->assign('NOTICE', 'example'); $page->assign('EXPIRE', self::$expire);