Просмотр исходного кода

Merge branch 'burnafterreading-fix'

El RIDO 8 лет назад
Родитель
Сommit
4ccb4b1af4
15 измененных файлов с 348 добавлено и 336 удалено
  1. 0 3
      .editorconfig
  2. 1 0
      CHANGELOG.md
  3. 1 1
      INSTALL.md
  4. 0 8
      cfg/conf.sample.php
  5. 262 77
      js/privatebin.js
  6. 0 42
      js/test/Model.js
  7. 4 2
      js/test/Prompt.js
  8. 0 2
      lib/Configuration.php
  9. 8 2
      lib/Model/Paste.php
  10. 12 35
      lib/PrivateBin.php
  11. 0 2
      tpl/.editorconfig
  12. 8 10
      tpl/bootstrap.php
  13. 4 10
      tpl/page.php
  14. 48 133
      tst/PrivateBinTest.php
  15. 0 9
      tst/ViewTest.php

+ 0 - 3
.editorconfig

@@ -11,7 +11,6 @@ insert_final_newline = true
 
 [*.css]
 indent_style = tab
-indent_size = 4
 
 [*.js]
 indent_style = space
@@ -23,7 +22,6 @@ indent_size = 4
 
 [*.jsonld]
 indent_style = tab
-indent_size = 4
 
 [*.php]
 indent_style = space
@@ -31,7 +29,6 @@ indent_size = 4
 
 [*.{htm,html}]
 indent_style = tab
-indent_size = 4
 
 [*.{md,markdown}]
 indent_style = space

+ 1 - 0
CHANGELOG.md

@@ -6,6 +6,7 @@
     * CHANGED: Minimum required PHP version is 5.4 (#186)
     * CHANGED: Shipped .htaccess files were updated for Apache 2.4 (#192)
     * CHANGED: Cleanup of bootstrap template variants and moved icons to `img` directory
+    * CHANGED: Removed option to hide clone button on expiring pastes, since this requires reading the paste for rendering the template, which leaks information on the pastes state
   * **1.1.1 (2017-10-06)**
     * CHANGED: Switched to `.php` file extension for configuration file, to avoid leaking configuration data in unprotected installation.
   * **1.1 (2016-12-26)**

+ 1 - 1
INSTALL.md

@@ -39,7 +39,7 @@ process (see also
 > #### PATH Example
 > Your PrivateBin installation lives in a subfolder called "paste" inside of
 > your document root. The URL looks like this:
-> http://example.com/paste/
+> https://example.com/paste/
 >
 > The full path of PrivateBin on your webserver is:
 > /home/example.com/htdocs/paste

+ 0 - 8
cfg/conf.sample.php

@@ -22,10 +22,6 @@ fileupload = false
 ; preselect the burn-after-reading feature, defaults to false
 burnafterreadingselected = false
 
-; delete a burn after reading paste immediatly after it is first accessed from
-; the server and do not wait for a successful decryption
-instantburnafterreading = false
-
 ; which display mode to preselect by default, defaults to "plaintext"
 ; make sure the value exists in [formatter_options]
 defaultformatter = "plaintext"
@@ -85,10 +81,6 @@ zerobincompatibility = false
 ; make sure the value exists in [expire_options]
 default = "1week"
 
-; optionally the "clone" button can be disabled on expiring pastes
-; note that this only hides the button, copy & paste is still possible
-; clone = false
-
 [expire_options]
 ; Set each one of these to the number of seconds in the expiration period,
 ; or 0 if it should never expire

+ 262 - 77
js/privatebin.js

@@ -45,6 +45,18 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
     var Helper = (function () {
         var me = {};
 
+        /**
+         * blacklist of UserAgents (parts) known to belong to a bot
+         *
+         * @private
+         * @enum   {Object}
+         * @readonly
+         */
+        var BadBotUA = [
+            'Bot',
+            'bot'
+        ];
+
         /**
          * cache for script location
          *
@@ -121,7 +133,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
          * URLs to handle:
          * <pre>
          *     magnet:?xt.1=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C&xt.2=urn:sha1:TXGCZQTH26NL6OUQAJJPFALHG2LTGBC7
-         *     http://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
+         *     https://example.com:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
          *     http://user:example.com@localhost:8800/zero/?6f09182b8ea51997#WtLEUO5Epj9UHAV9JFs+6pUQZp13TuspAUjnF+iM+dM=
          * </pre>
          *
@@ -204,7 +216,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
 
         /**
          * get the current location (without search or hash part of the URL),
-         * eg. http://example.com/path/?aaaa#bbbb --> http://example.com/path/
+         * eg. https://example.com/path/?aaaa#bbbb --> https://example.com/path/
          *
          * @name   Helper.baseUri
          * @function
@@ -232,6 +244,26 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             baseUri = null;
         };
 
+        /**
+         * checks whether this is a bot we dislike
+         *
+         * @name   Helper.isBadBot
+         * @function
+         * @return {bool}
+         */
+        me.isBadBot = function() {
+            // check whether a bot user agent part can be found in the current
+            // user agent
+            var arrayLength = BadBotUA.length;
+            for (var i = 0; i < arrayLength; i++) {
+                if (navigator.userAgent.indexOf(BadBotUA) >= 0) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
         return me;
     })();
 
@@ -358,7 +390,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
                     // file is loaded
                 }
 
-                // for all other langauges than English for which this behaviour
+                // for all other languages than English for which this behaviour
                 // is expected as it is built-in, log error
                 if (language !== null && language !== 'en') {
                     console.error('Missing translation for: \'' + messageId + '\' in language ' + language);
@@ -623,7 +655,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
     var Model = (function () {
         var me = {};
 
-        var $cipherData,
+        var pasteData = null,
             $templates;
 
         var id = null, symmetricKey = null;
@@ -653,32 +685,53 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
         };
 
         /**
-         * check if cipher data was supplied
+         * returns the paste data (including the cipher data)
          *
-         * @name   Model.getCipherData
-         * @function
-         * @return boolean
-         */
-        me.hasCipherData = function()
-        {
-            return me.getCipherData().length > 0;
-        };
-
-        /**
-         * returns the cipher data
-         *
-         * @name   Model.getCipherData
+         * @name   Model.getPasteData
          * @function
+         * @param {function} callback (optional) Called when data is available
+         * @param {function} useCache (optional) Whether to use the cache or
+         *                            force a data reload. Default: true
          * @return string
          */
-        me.getCipherData = function()
+        me.getPasteData = function(callback, useCache)
         {
-            return $cipherData.text();
+            // use cache if possible/allowed
+            if (useCache !== false && pasteData !== null) {
+                //execute callback
+                if (typeof callback === 'function') {
+                    return callback(pasteData);
+                }
+
+                // alternatively just using inline
+                return pasteData;
+            }
+
+            // reload data
+            Uploader.prepare();
+            Uploader.setUrl(Helper.baseUri() + '?' + me.getPasteId());
+
+            Uploader.setFailure(function (status, data) {
+                // revert loading status…
+                Alert.hideLoading();
+                TopNav.showViewButtons();
+
+                // show error message
+                Alert.showError(Uploader.parseUploadError(status, data, 'getting paste data'));
+            });
+            Uploader.setSuccess(function (status, data) {
+                pasteData = data;
+
+                if (typeof callback === 'function') {
+                    return callback(data);
+                }
+            });
+            Uploader.run();
         };
 
         /**
          * get the pastes unique identifier from the URL,
-         * eg. http://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
+         * eg. https://example.com/path/?c05354954c49a487#dfdsdgdgdfgdf returns c05354954c49a487
          *
          * @name   Model.getPasteId
          * @function
@@ -688,6 +741,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
         me.getPasteId = function()
         {
             if (id === null) {
+                // Attention: This also returns the delete token inside of the ID, if it is specified
                 id = window.location.search.substring(1);
 
                 if (id === '') {
@@ -696,7 +750,19 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             }
 
             return id;
-        };
+        }
+
+        /**
+         * Returns true, when the URL has a delete token and the current call was used for deleting a paste.
+         *
+         * @name   Model.hasDeleteToken
+         * @function
+         * @return {bool}
+         */
+        me.hasDeleteToken = function()
+        {
+            return window.location.search.indexOf('deletetoken') !== -1;
+        }
 
         /**
          * return the deciphering key stored in anchor part of the URL
@@ -751,7 +817,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
          */
         me.reset = function()
         {
-            $cipherData = $templates = id = symmetricKey = null;
+            pasteData = $templates = id = symmetricKey = null;
         };
 
         /**
@@ -764,7 +830,6 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
          */
         me.init = function()
         {
-            $cipherData = $('#cipherdata');
             $templates = $('#templates');
         };
 
@@ -1259,8 +1324,8 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             if (pasteMetaData.burnafterreading) {
                 // display paste "for your eyes only" if it is deleted
 
-                // actually remove paste, before we claim it is deleted
-                Controller.removePaste(Model.getPasteId(), 'burnafterreading');
+                // the paste has been deleted when the JSON with the ciphertext
+                // has been downloaded
 
                 Alert.showRemaining("FOR YOUR EYES ONLY. Don't close this window, this message can't be displayed again.");
                 $remainingTime.addClass('foryoureyesonly');
@@ -1402,6 +1467,21 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             return password;
         };
 
+        /**
+         * resets the password to an empty string
+         *
+         * @name   Prompt.reset
+         * @function
+         */
+        me.reset = function()
+        {
+            // reset internal
+            password = '';
+
+            // and also reset UI
+            $passwordDecrypt.val('');
+        }
+
         /**
          * init status manager
          *
@@ -2149,7 +2229,6 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
                 loadedFile = $fileInput[0].files[0];
                 $dragAndDropFileName.text('');
             } else {
-                // TODO: cannot set original $fileWrap here for security reasons…
                 $dragAndDropFileName.text(loadedFile.name);
             }
 
@@ -2295,6 +2374,10 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
                     if (items.hasOwnProperty(i)) {
                         var item = items[i];
                         if (item.kind === 'file') {
+                            //Clear the file input:
+                            $fileInput.wrap('<form>').closest('form').get(0).reset();
+                            $fileInput.unwrap();
+
                             readFileData(item.getAsFile());
                         }
                     }
@@ -2685,9 +2768,11 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             $passwordInput,
             $rawTextButton,
             $qrCodeLink,
-            $sendButton;
+            $sendButton,
+            $retryButton;
 
-        var pasteExpiration = '1week';
+        var pasteExpiration = '1week',
+            retryButtonCallback;
 
         /**
          * set the expiration on bootstrap templates in dropdown
@@ -2836,6 +2921,19 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             Controller.newPaste();
         }
 
+        /**
+         * retrys some callback registered before
+         *
+         * @name   TopNav.clickRetryButton
+         * @private
+         * @function
+         * @param  {Event} event
+         */
+        function clickRetryButton(event)
+        {
+            retryButtonCallback(event);
+        }
+
         /**
          * removes the existing attachment
          *
@@ -2915,8 +3013,8 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
                 return;
             }
 
-            $newButton.addClass('hidden');
             $cloneButton.addClass('hidden');
+            $newButton.addClass('hidden');
             $rawTextButton.addClass('hidden');
             $qrCodeLink.addClass('hidden');
 
@@ -2948,14 +3046,14 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
                 return;
             }
 
-            $sendButton.removeClass('hidden');
+            $attach.removeClass('hidden');
+            $burnAfterReadingOption.removeClass('hidden');
             $expiration.removeClass('hidden');
             $formatter.removeClass('hidden');
-            $burnAfterReadingOption.removeClass('hidden');
-            $openDiscussionOption.removeClass('hidden');
             $newButton.removeClass('hidden');
+            $openDiscussionOption.removeClass('hidden');
             $password.removeClass('hidden');
-            $attach.removeClass('hidden');
+            $sendButton.removeClass('hidden');
 
             createButtonsDisplayed = true;
         };
@@ -2996,6 +3094,28 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             $newButton.removeClass('hidden');
         };
 
+        /**
+         * only shows the "retry" button
+         *
+         * @name   TopNav.showRetryButton
+         * @function
+         */
+        me.showRetryButton = function()
+        {
+            $retryButton.removeClass('hidden');
+        }
+
+        /**
+         * hides the "retry" button
+         *
+         * @name   TopNav.hideRetryButton
+         * @function
+         */
+        me.hideRetryButton = function()
+        {
+            $retryButton.addClass('hidden');
+        }
+
         /**
          * only hides the clone button
          *
@@ -3140,6 +3260,18 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             return $customAttachment;
         };
 
+        /**
+         * Set a function to call when the retry button is clicked.
+         *
+         * @name   TopNav.setRetryCallback
+         * @function
+         * @param {function} callback
+         */
+        me.setRetryCallback = function(callback)
+        {
+            retryButtonCallback = callback;
+        }
+
         /**
          * init navigation manager
          *
@@ -3165,6 +3297,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             $password = $('#password');
             $passwordInput = $('#passwordinput');
             $rawTextButton = $('#rawtextbutton');
+            $retryButton = $('#retrybutton');
             $sendButton = $('#sendbutton');
             $qrCodeLink = $('#qrcodelink');
 
@@ -3180,6 +3313,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             $sendButton.click(PasteEncrypter.sendPaste);
             $cloneButton.click(Controller.clonePaste);
             $rawTextButton.click(rawText);
+            $retryButton.click(clickRetryButton);
             $fileRemoveButton.click(removeAttachment);
             $qrCodeLink.click(displayQrCode);
 
@@ -3815,7 +3949,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
         var me = {};
 
         /**
-         * decrypt data or prompts for password in cvase of failure
+         * decrypt data or prompts for password in case of failure
          *
          * @name   PasteDecrypter.decryptOrPromptPassword
          * @private
@@ -3833,18 +3967,23 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
 
             // if it fails, request password
             if (plaindata.length === 0 && password.length === 0) {
-                // try to get cached password first
-                password = Prompt.getPassword();
-
-                // if password is there, re-try
-                if (password.length === 0) {
-                    password = Prompt.requestPassword();
+                // show prompt
+                Prompt.requestPassword();
+
+                // if password is there instantly (legacy method), re-try encryption
+                if (Prompt.getPassword().length !== 0) {
+                    // recursive
+                    // note: an infinite loop is prevented as the previous if
+                    // clause checks whether a password is already set and ignores
+                    // errors when a password has been passed
+                    return decryptOrPromptPassword(key, password, cipherdata);
                 }
-                // recursive
-                // note: an infinite loop is prevented as the previous if
-                // clause checks whether a password is already set and ignores
-                // errors when a password has been passed
-                return decryptOrPromptPassword.apply(key, password, cipherdata);
+
+                // if password could not be received yet, the new modal is used,
+                // which uses asyncronous event-driven methods to get the password.
+                // Thus, we cannot do anything yet, we need to wait for the user
+                // input.
+                return false;
             }
 
             // if all tries failed, we can only return an error
@@ -3858,7 +3997,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
         /**
          * decrypt the actual paste text
          *
-         * @name   PasteDecrypter.decryptOrPromptPassword
+         * @name   PasteDecrypter.decryptPaste
          * @private
          * @function
          * @param  {object} paste - paste data in object form
@@ -3981,7 +4120,9 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             Alert.showLoading('Decrypting paste…', 'cloud-download');
 
             if (typeof paste === 'undefined') {
-                paste = $.parseJSON(Model.getCipherData());
+                // get cipher data and wait until it is available
+                Model.getPasteData(me.run);
+                return;
             }
 
             var key = Model.getPasteKey(),
@@ -4006,10 +4147,11 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
                     // ignore empty paste, as this is allowed when pasting attachments
                     decryptPaste(paste, key, password, true);
                 } else {
-                    decryptPaste(paste, key, password);
+                    if (decryptPaste(paste, key, password) === false) {
+                        return false;
+                    }
                 }
 
-
                 // shows the remaining time (until) deletion
                 PasteStatus.showRemainingTime(paste.meta);
 
@@ -4025,7 +4167,15 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
 
                 // log and show error
                 console.error(err);
-                Alert.showError('Could not decrypt data (Wrong key?)');
+                Alert.showError('Could not decrypt data. Did you enter a wrong password? Retry with the button at the top.');
+                // reset password, so it can be re-entered and sow retry button
+                Prompt.reset();
+                TopNav.setRetryCallback(function () {
+                    TopNav.hideRetryButton();
+
+                    me.run(paste);
+                });
+                TopNav.showRetryButton();
             }
         };
 
@@ -4090,6 +4240,18 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             Alert.hideLoading();
         };
 
+        /**
+         * shows how we much we love bots that execute JS ;)
+         *
+         * @name   Controller.showBadBotMessage
+         * @function
+         */
+        me.showBadBotMessage = function()
+        {
+            TopNav.hideAllButtons();
+            Alert.showError('I love you too, bot…');
+        }
+
         /**
          * shows the loaded paste
          *
@@ -4099,7 +4261,6 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
         me.showPaste = function()
         {
             try {
-                Model.getPasteId();
                 Model.getPasteKey();
             } catch (err) {
                 console.error(err);
@@ -4127,29 +4288,36 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             // save window position to restore it later
             var orgPosition = $(window).scrollTop();
 
-            Uploader.prepare();
-            Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
+            Model.getPasteData(function (data) {
+                Uploader.prepare();
+                Uploader.setUrl(Helper.baseUri() + '?' + Model.getPasteId());
 
-            Uploader.setFailure(function (status, data) {
-                // revert loading status…
-                Alert.hideLoading();
-                TopNav.showViewButtons();
+                Uploader.setFailure(function (status, data) {
+                    // revert loading status…
+                    Alert.hideLoading();
+                    TopNav.showViewButtons();
 
-                // show error message
-                Alert.showError(
-                    Uploader.parseUploadError(status, data, 'refresh display')
-                );
-            });
-            Uploader.setSuccess(function (status, data) {
-                PasteDecrypter.run(data);
+                    // show error message
+                    Alert.showError(
+                        Uploader.parseUploadError(status, data, 'refresh display')
+                    );
+                });
+                Uploader.setSuccess(function (status, data) {
+                    PasteDecrypter.run(data);
 
-                // restore position
-                window.scrollTo(0, orgPosition);
+                    // restore position
+                    window.scrollTo(0, orgPosition);
 
-                callback();
-            });
-            Uploader.run();
-        };
+                    PasteDecrypter.run(data);
+
+                    // NOTE: could create problems as callback may be called
+                    // asyncronously if PasteDecrypter e.g. needs to wait for a
+                    // password being entered
+                    callback();
+                });
+                Uploader.run();
+            }, false); // this false is important as it circumvents the cache
+        }
 
         /**
          * clone the current paste
@@ -4207,6 +4375,7 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
          * @function
          * @param  {string} pasteId
          * @param  {string} deleteToken
+         * @deprecated not used anymore, de we still need it?
          */
         me.removePaste = function(pasteId, deleteToken) {
             // unfortunately many web servers don't support DELETE (and PUT) out of the box
@@ -4251,14 +4420,30 @@ jQuery.PrivateBin = (function($, sjcl, Base64, RawDeflate) {
             UiHelper.init();
             Uploader.init();
 
-            // display an existing paste
-            if (Model.hasCipherData()) {
-                return me.showPaste();
+            // check whether existing paste needs to be shown
+            try {
+                Model.getPasteId();
+            } catch (e) {
+                // otherwise create a new paste
+                return me.newPaste();
             }
 
-            // otherwise create a new paste
-            me.newPaste();
-        };
+            // if delete token is passed (i.e. paste has been deleted by this access)
+            // there is no more stuf we need to do
+            if (Model.hasDeleteToken()) {
+                return;
+            }
+
+            // prevent bots from viewing a paste and potentially deleting data
+            // when burn-after-reading is set
+            // see https://github.com/elrido/ZeroBin/issues/11
+            if (Helper.isBadBot()) {
+                return me.showBadBotMessage();
+            }
+
+            // display an existing paste
+            return me.showPaste();
+        }
 
         return me;
     })(window, document);

+ 0 - 42
js/test/Model.js

@@ -70,48 +70,6 @@ describe('Model', function () {
         );
     });
 
-    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 = common.htmlEntities(value).trim();
-                $('body').html('<div id="cipherdata">' + value + '</div>');
-                $.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 = common.htmlEntities(value).trim();
-                $('body').html('<div id="cipherdata">' + value + '</div>');
-                $.PrivateBin.Model.init();
-                var result = common.htmlEntities(
-                    $.PrivateBin.Model.getCipherData()
-                );
-                $.PrivateBin.Model.reset();
-                return value === result;
-            }
-        );
-    });
-
     describe('getPasteId', function () {
         this.timeout(30000);
         before(function () {

+ 4 - 2
js/test/Prompt.js

@@ -7,6 +7,7 @@ describe('Prompt', function () {
     describe('requestPassword & getPassword', function () {
         this.timeout(30000);
         before(function () {
+            $.PrivateBin.Model.reset();
             cleanup();
         });
 
@@ -15,7 +16,7 @@ describe('Prompt', function () {
             'string',
             function (password) {
                 password = password.replace(/\r+/g, '');
-                var clean = jsdom('', {url: 'ftp://example.com/#0'});
+                var clean = jsdom('', {url: 'ftp://example.com/?0'});
                 $('body').html(
                     '<div id="passwordmodal" class="modal fade" role="dialog">' +
                     '<div class="modal-dialog"><div class="modal-content">' +
@@ -23,7 +24,7 @@ describe('Prompt', function () {
                     '<div class="form-group"><input id="passworddecrypt" ' +
                     'type="password" class="form-control" placeholder="Enter ' +
                     'password"></div><button type="submit">Decrypt</button>' +
-                    '</form></div></div></div></div><div id="cipherdata">{}</div>'
+                    '</form></div></div></div></div>'
                 );
                 $.PrivateBin.Model.init();
                 $.PrivateBin.Prompt.init();
@@ -31,6 +32,7 @@ describe('Prompt', function () {
                 $('#passworddecrypt').val(password);
                 $('#passwordform').submit();
                 var result = $.PrivateBin.Prompt.getPassword();
+                $.PrivateBin.Model.reset();
                 clean();
                 return result === password;
             }

+ 0 - 2
lib/Configuration.php

@@ -43,7 +43,6 @@ class Configuration
             'password'                 => true,
             'fileupload'               => false,
             'burnafterreadingselected' => false,
-            'instantburnafterreading'  => false,
             'defaultformatter'         => 'plaintext',
             'syntaxhighlightingtheme'  => null,
             'sizelimit'                => 2097152,
@@ -59,7 +58,6 @@ class Configuration
         ),
         'expire' => array(
             'default' => '1week',
-            'clone'   => true,
         ),
         'expire_options' => array(
             '5min'   => 300,

+ 8 - 2
lib/Model/Paste.php

@@ -49,7 +49,7 @@ class Paste extends AbstractModel
         }
 
         // check if non-expired burn after reading paste needs to be deleted
-        if (property_exists($data->meta, 'burnafterreading') && $data->meta->burnafterreading && $this->_conf->getKey('instantburnafterreading')) {
+        if (property_exists($data->meta, 'burnafterreading') && $data->meta->burnafterreading) {
             $this->delete();
         }
 
@@ -72,6 +72,12 @@ class Paste extends AbstractModel
         $data->comment_offset = 0;
         $data->{'@context'}   = 'js/paste.jsonld';
         $this->_data          = $data;
+
+        // If the paste was meant to be read only once, delete it.
+        if ($this->isBurnafterreading()) {
+            $this->delete();
+        }
+
         return $this->_data;
     }
 
@@ -163,7 +169,7 @@ class Paste extends AbstractModel
      *
      * The token is the hmac of the pastes ID signed with the server salt.
      * The paste can be deleted by calling:
-     * http://example.com/privatebin/?pasteid=<pasteid>&deletetoken=<deletetoken>
+     * https://example.com/privatebin/?pasteid=<pasteid>&deletetoken=<deletetoken>
      *
      * @access public
      * @return string

+ 12 - 35
lib/PrivateBin.php

@@ -52,22 +52,6 @@ class PrivateBin
      */
     private $_conf;
 
-    /**
-     * data
-     *
-     * @access private
-     * @var    string
-     */
-    private $_data = '';
-
-    /**
-     * does the paste expire
-     *
-     * @access private
-     * @var    bool
-     */
-    private $_doesExpire = false;
-
     /**
      * error message
      *
@@ -327,10 +311,10 @@ class PrivateBin
                 // deleted if it has already expired
                 $burnafterreading = $paste->isBurnafterreading();
                 if (
-                    ($burnafterreading && $deletetoken == 'burnafterreading') ||
-                    Filter::slowEquals($deletetoken, $paste->getDeleteToken())
+                    ($burnafterreading && $deletetoken == 'burnafterreading') || // either we burn-after it has been read //@TODO: not needed anymore now?
+                    Filter::slowEquals($deletetoken, $paste->getDeleteToken()) // or we manually delete it with this secret token
                 ) {
-                    // Paste exists and deletion token is valid: Delete the paste.
+                    // Paste exists and deletion token (if required) is valid: Delete the paste.
                     $paste->delete();
                     $this->_status = 'Paste was properly deleted.';
                 } else {
@@ -356,35 +340,30 @@ class PrivateBin
     }
 
     /**
-     * Read an existing paste or comment
+     * Read an existing paste or comment, only allowed via a JSON API call
      *
      * @access private
      * @param  string $dataid
      */
     private function _read($dataid)
     {
+        if (!$this->_request->isJsonApiCall()) {
+            return;
+        }
+
         try {
             $paste = $this->_model->getPaste($dataid);
             if ($paste->exists()) {
-                $data              = $paste->get();
-                $this->_doesExpire = property_exists($data, 'meta') && property_exists($data->meta, 'expire_date');
+                $data = $paste->get();
                 if (property_exists($data->meta, 'salt')) {
                     unset($data->meta->salt);
                 }
-                $this->_data = json_encode($data);
+                $this->_return_message(0, $dataid, (array) $data);
             } else {
-                $this->_error = self::GENERIC_ERROR;
+                $this->_return_message(1, self::GENERIC_ERROR);
             }
         } catch (Exception $e) {
-            $this->_error = $e->getMessage();
-        }
-
-        if ($this->_request->isJsonApiCall()) {
-            if (strlen($this->_error)) {
-                $this->_return_message(1, $this->_error);
-            } else {
-                $this->_return_message(0, $dataid, json_decode($this->_data, true));
-            }
+            $this->_return_message(1, $e->getMessage());
         }
     }
 
@@ -425,7 +404,6 @@ class PrivateBin
 
         $page = new View;
         $page->assign('NAME', $this->_conf->getKey('name'));
-        $page->assign('CIPHERDATA', $this->_data);
         $page->assign('ERROR', I18n::_($this->_error));
         $page->assign('STATUS', I18n::_($this->_status));
         $page->assign('VERSION', self::VERSION);
@@ -445,7 +423,6 @@ class PrivateBin
         $page->assign('LANGUAGES', I18n::getLanguageLabels(I18n::getAvailableLanguages()));
         $page->assign('EXPIRE', $expire);
         $page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire'));
-        $page->assign('EXPIRECLONE', !$this->_doesExpire || ($this->_doesExpire && $this->_conf->getKey('clone', 'expire')));
         $page->assign('URLSHORTENER', $this->_conf->getKey('urlshortener'));
         $page->assign('QRCODE', $this->_conf->getKey('qrcode'));
         $page->draw($this->_conf->getKey('template'));

+ 0 - 2
tpl/.editorconfig

@@ -5,5 +5,3 @@ root = false
 # special format for PHP templates
 [*.php]
 indent_style = tab
-indent_size = 4
-

+ 8 - 10
tpl/bootstrap.php

@@ -75,7 +75,7 @@ if ($MARKDOWN):
 <?php
 endif;
 ?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Q3ZXxdH/2epUyO1qorIGVt2JLRZg7QU5YYeBe1evtjOGuHQPUr3zppGtncyNLmaYXbac09NJXeANiy7WeU2NmA==" crossorigin="anonymous"></script>
+		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-P+tLS0U2r7om1G6T0qLVMntNKoQnBvd3qcHGTgyOfHnj5qkyRWdMppa5q5hdQrxjGevb4hBnLsQKrLMyyRpsiw==" 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]-->
@@ -147,6 +147,11 @@ endif;
 						<?php echo I18n::_('Loading…'), PHP_EOL; ?>
 					</li>
 					<li>
+						<button id="retrybutton" type="button" class="reloadlink hidden btn btn-<?php echo $isDark ? 'warning' : 'primary'; ?> navbar-btn">
+							<span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> <?php echo I18n::_('Retry'), PHP_EOL; ?>
+						</button>
+					</li>
+					<li>
 <?php
 if ($isPage):
 ?>
@@ -159,15 +164,9 @@ else:
 endif;
 ?>
 						</button>
-<?php
-if ($EXPIRECLONE):
-?>
 						<button id="clonebutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
 							<span class="glyphicon glyphicon-duplicate" aria-hidden="true"></span> <?php echo I18n::_('Clone'), PHP_EOL; ?>
 						</button>
-<?php
-endif;
-?>
 						<button id="rawtextbutton" type="button" class="hidden btn btn-<?php echo $isDark ? 'warning' : 'default'; ?> navbar-btn">
 							<span class="glyphicon glyphicon-text-background" aria-hidden="true"></span> <?php echo I18n::_('Raw text'), PHP_EOL; ?>
 						</button>
@@ -519,19 +518,18 @@ endif;
 				</div>
 			</footer>
 		</main>
-		<div id="serverdata" class="hidden" aria-hidden="true">
-			<div id="cipherdata"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
 <?php
 if ($DISCUSSION):
 ?>
+		<div id="serverdata" class="hidden" aria-hidden="true">
 			<div id="templates">
 				<article id="commenttemplate" class="comment"><div class="commentmeta"><span class="nickname">name</span><span class="commentdate">0000-00-00</span></div><div class="commentdata">c</div><button class="btn btn-default btn-sm"><?php echo I18n::_('Reply'); ?></button></article>
 				<p id="commenttailtemplate" class="comment"><button class="btn btn-default btn-sm"><?php echo I18n::_('Add comment'); ?></button></p>
 				<div id="replytemplate" class="reply hidden"><input type="text" id="nickname" class="form-control" title="<?php echo I18n::_('Optional nickname…'); ?>" placeholder="<?php echo I18n::_('Optional nickname…'); ?>" /><textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea><br /><div id="replystatus" role="alert" class="statusmessage hidden alert"><span class="glyphicon" aria-hidden="true"></span> </div><button id="replybutton" class="btn btn-default btn-sm"><?php echo I18n::_('Post comment'); ?></button></div>
 			</div>
+		</div>
 <?php
 endif;
 ?>
-		</div>
 	</body>
 </html>

+ 4 - 10
tpl/page.php

@@ -54,7 +54,7 @@ if ($QRCODE):
 <?php
 endif;
 ?>
-		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-Q3ZXxdH/2epUyO1qorIGVt2JLRZg7QU5YYeBe1evtjOGuHQPUr3zppGtncyNLmaYXbac09NJXeANiy7WeU2NmA==" crossorigin="anonymous"></script>
+		<script type="text/javascript" src="js/privatebin.js?<?php echo rawurlencode($VERSION); ?>" integrity="sha512-P+tLS0U2r7om1G6T0qLVMntNKoQnBvd3qcHGTgyOfHnj5qkyRWdMppa5q5hdQrxjGevb4hBnLsQKrLMyyRpsiw==" 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]-->
@@ -96,14 +96,9 @@ endif;
 				<div id="errormessage" class="hidden"><?php echo htmlspecialchars($ERROR); ?></div>
 				<div id="toolbar">
 					<button id="newbutton" class="reloadlink hidden"><img src="img/icon_new.png" width="11" height="15" alt="" /><?php echo I18n::_('New'); ?></button>
+					<button id="retrybutton" class="reloadlink hidden"><?php echo I18n::_('Retry'), PHP_EOL; ?></button>
 					<button id="sendbutton" class="hidden"><img src="img/icon_send.png" width="18" height="15" alt="" /><?php echo I18n::_('Send'); ?></button>
-<?php
-if ($EXPIRECLONE):
-?>
 					<button id="clonebutton" class="hidden"><img src="img/icon_clone.png" width="15" height="17" alt="" /><?php echo I18n::_('Clone'); ?></button>
-<?php
-endif;
-?>
 					<button id="rawtextbutton" class="hidden"><img src="img/icon_raw.png" width="15" height="15" alt="" /><?php echo I18n::_('Raw text'); ?></button>
 <?php
 if ($QRCODE):
@@ -246,20 +241,19 @@ endif;
 				<div id="commentcontainer"></div>
 			</div>
 		</section>
-		<div id="serverdata" class="hidden" aria-hidden="true">
-		<div id="cipherdata" class="hidden"><?php echo htmlspecialchars($CIPHERDATA, ENT_NOQUOTES); ?></div>
 <?php
 if ($DISCUSSION):
 ?>
+		<div id="serverdata" class="hidden" aria-hidden="true">
 			<div id="templates">
 				<article id="commenttemplate" class="comment"><div class="commentmeta"><span class="nickname">name</span><span class="commentdate">0000-00-00</span></div><div class="commentdata">c</div><button class="btn btn-default btn-sm"><?php echo I18n::_('Reply'); ?></button></article>
 				<div id="commenttailtemplate" class="comment"><button class="btn btn-default btn-sm"><?php echo I18n::_('Add comment'); ?></button></div>
 				<div id="replytemplate" class="reply hidden"><input type="text" id="nickname" class="form-control" title="<?php echo I18n::_('Optional nickname…'); ?>" placeholder="<?php echo I18n::_('Optional nickname…'); ?>" /><textarea id="replymessage" class="replymessage form-control" cols="80" rows="7"></textarea><br /><div id="replystatus" role="alert" class="statusmessage hidden alert"><span class="glyphicon" aria-hidden="true"></span> </div><button id="replybutton" class="btn btn-default btn-sm"><?php echo I18n::_('Post comment'); ?></button></div>
 			</div>
+		</div>
 <?php
 endif;
 ?>
-		</div>
 		<section class="container">
 			<div id="noscript" role="alert" class="nonworking alert alert-info noscript-hide"><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true">
 				<span> <?php echo I18n::_('Loading…'); ?></span><br>

+ 48 - 133
tst/PrivateBinTest.php

@@ -674,41 +674,20 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'paste exists after posting data');
     }
 
-    /**
-     * @runInSeparateProcess
-     */
-    public function testRead()
-    {
-        $this->_model->create(Helper::getPasteId(), Helper::getPaste());
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
-        ob_start();
-        new PrivateBin;
-        $content = ob_get_contents();
-        ob_end_clean();
-        $this->assertRegExp(
-            '#<div id="cipherdata"[^>]*>' .
-            preg_quote(htmlspecialchars(Helper::getPasteAsJson(), ENT_NOQUOTES)) .
-            '</div>#',
-            $content,
-            'outputs data correctly'
-        );
-    }
-
     /**
      * @runInSeparateProcess
      */
     public function testReadInvalidId()
     {
-        $_SERVER['QUERY_STRING'] = 'foo';
+        $_SERVER['QUERY_STRING']          = 'foo';
+        $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
-        $this->assertRegExp(
-            '#<div[^>]*id="errormessage"[^>]*>.*Invalid paste ID\.#s',
-            $content,
-            'outputs error correctly'
-        );
+        $response = json_decode($content, true);
+        $this->assertEquals(1, $response['status'], 'outputs error status');
+        $this->assertEquals('Invalid paste ID.', $response['message'], 'outputs error message');
     }
 
     /**
@@ -716,16 +695,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
      */
     public function testReadNonexisting()
     {
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
+        $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
-        $this->assertRegExp(
-            '#<div[^>]*id="errormessage"[^>]*>.*Paste does not exist, has expired or has been deleted\.#s',
-            $content,
-            'outputs error correctly'
-        );
+        $response = json_decode($content, true);
+        $this->assertEquals(1, $response['status'], 'outputs error status');
+        $this->assertEquals('Paste does not exist, has expired or has been deleted.', $response['message'], 'outputs error message');
     }
 
     /**
@@ -735,16 +713,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
     {
         $expiredPaste = Helper::getPaste(array('expire_date' => 1344803344));
         $this->_model->create(Helper::getPasteId(), $expiredPaste);
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
+        $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
-        $this->assertRegExp(
-            '#<div[^>]*id="errormessage"[^>]*>.*Paste does not exist, has expired or has been deleted\.#s',
-            $content,
-            'outputs error correctly'
-        );
+        $response = json_decode($content, true);
+        $this->assertEquals(1, $response['status'], 'outputs error status');
+        $this->assertEquals('Paste does not exist, has expired or has been deleted.', $response['message'], 'outputs error message');
     }
 
     /**
@@ -752,60 +729,7 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
      */
     public function testReadBurn()
     {
-        $burnPaste = Helper::getPaste(array('burnafterreading' => true));
-        $this->_model->create(Helper::getPasteId(), $burnPaste);
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
-        ob_start();
-        new PrivateBin;
-        $content = ob_get_contents();
-        ob_end_clean();
-        unset($burnPaste['meta']['salt']);
-        $this->assertRegExp(
-            '#<div id="cipherdata"[^>]*>' .
-            preg_quote(htmlspecialchars(Helper::getPasteAsJson($burnPaste['meta']), ENT_NOQUOTES)) .
-            '</div>#',
-            $content,
-            'outputs data correctly'
-        );
-        // by default it will be deleted after encryption by the JS
-        $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after reading');
-    }
-
-    /**
-     * @runInSeparateProcess
-     */
-    public function testReadInstantBurn()
-    {
-        $this->reset();
-        $options                                    = parse_ini_file(CONF, true);
-        $options['main']['instantburnafterreading'] = 1;
-        Helper::confBackup();
-        Helper::createIniFile(CONF, $options);
-        $burnPaste = Helper::getPaste(array('burnafterreading' => true));
-        $this->_model->create(Helper::getPasteId(), $burnPaste);
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
-        ob_start();
-        new PrivateBin;
-        $content = ob_get_contents();
-        ob_end_clean();
-        unset($burnPaste['meta']['salt']);
-        $this->assertRegExp(
-            '#<div id="cipherdata"[^>]*>' .
-            preg_quote(htmlspecialchars(Helper::getPasteAsJson($burnPaste['meta']), ENT_NOQUOTES)) .
-            '</div>#',
-            $content,
-            'outputs data correctly'
-        );
-        // in this case the changed configuration deletes it instantly
-        $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste exists after reading');
-    }
-
-    /**
-     * @runInSeparateProcess
-     */
-    public function testReadJson()
-    {
-        $paste = Helper::getPaste();
+        $paste = Helper::getPaste(array('burnafterreading' => true));
         $this->_model->create(Helper::getPasteId(), $paste);
         $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
@@ -821,15 +745,20 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $this->assertEquals($paste['meta']['formatter'], $response['meta']['formatter'], 'outputs format correctly');
         $this->assertEquals($paste['meta']['postdate'], $response['meta']['postdate'], 'outputs postdate correctly');
         $this->assertEquals($paste['meta']['opendiscussion'], $response['meta']['opendiscussion'], 'outputs opendiscussion correctly');
+        $this->assertEquals(1, $response['meta']['burnafterreading'], 'outputs burnafterreading correctly');
         $this->assertEquals(0, $response['comment_count'], 'outputs comment_count correctly');
         $this->assertEquals(0, $response['comment_offset'], 'outputs comment_offset correctly');
+        // by default it will be deleted instantly after it is read
+        $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste exists after reading');
     }
 
     /**
      * @runInSeparateProcess
      */
-    public function testReadInvalidJson()
+    public function testReadJson()
     {
+        $paste = Helper::getPaste();
+        $this->_model->create(Helper::getPasteId(), $paste);
         $_SERVER['QUERY_STRING']          = Helper::getPasteId();
         $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
@@ -837,7 +766,15 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
         $content = ob_get_contents();
         ob_end_clean();
         $response = json_decode($content, true);
-        $this->assertEquals(1, $response['status'], 'outputs error status');
+        $this->assertEquals(0, $response['status'], 'outputs success status');
+        $this->assertEquals(Helper::getPasteId(), $response['id'], 'outputs data correctly');
+        $this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
+        $this->assertEquals($paste['data'], $response['data'], 'outputs data correctly');
+        $this->assertEquals($paste['meta']['formatter'], $response['meta']['formatter'], 'outputs format correctly');
+        $this->assertEquals($paste['meta']['postdate'], $response['meta']['postdate'], 'outputs postdate correctly');
+        $this->assertEquals($paste['meta']['opendiscussion'], $response['meta']['opendiscussion'], 'outputs opendiscussion correctly');
+        $this->assertEquals(0, $response['comment_count'], 'outputs comment_count correctly');
+        $this->assertEquals(0, $response['comment_offset'], 'outputs comment_offset correctly');
     }
 
     /**
@@ -845,51 +782,29 @@ class PrivateBinTest extends PHPUnit_Framework_TestCase
      */
     public function testReadOldSyntax()
     {
-        $oldPaste = Helper::getPaste();
-        $meta     = array(
+        $paste         = Helper::getPaste();
+        $paste['meta'] = array(
             'syntaxcoloring' => true,
-            'postdate'       => $oldPaste['meta']['postdate'],
-            'opendiscussion' => $oldPaste['meta']['opendiscussion'],
-        );
-        $oldPaste['meta'] = $meta;
-        $this->_model->create(Helper::getPasteId(), $oldPaste);
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
-        ob_start();
-        new PrivateBin;
-        $content = ob_get_contents();
-        ob_end_clean();
-        $meta['formatter'] = 'syntaxhighlighting';
-        $this->assertRegExp(
-            '#<div id="cipherdata"[^>]*>' .
-            preg_quote(htmlspecialchars(Helper::getPasteAsJson($meta), ENT_NOQUOTES)) .
-            '</div>#',
-            $content,
-            'outputs data correctly'
+            'postdate'       => $paste['meta']['postdate'],
+            'opendiscussion' => $paste['meta']['opendiscussion'],
         );
-    }
-
-    /**
-     * @runInSeparateProcess
-     */
-    public function testReadOldFormat()
-    {
-        $oldPaste = Helper::getPaste();
-        unset($oldPaste['meta']['formatter']);
-        $this->_model->create(Helper::getPasteId(), $oldPaste);
-        $_SERVER['QUERY_STRING'] = Helper::getPasteId();
+        $this->_model->create(Helper::getPasteId(), $paste);
+        $_SERVER['QUERY_STRING']          = Helper::getPasteId();
+        $_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
         ob_start();
         new PrivateBin;
         $content = ob_get_contents();
         ob_end_clean();
-        $oldPaste['meta']['formatter'] = 'plaintext';
-        unset($oldPaste['meta']['salt']);
-        $this->assertRegExp(
-            '#<div id="cipherdata"[^>]*>' .
-            preg_quote(htmlspecialchars(Helper::getPasteAsJson($oldPaste['meta']), ENT_NOQUOTES)) .
-            '</div>#',
-            $content,
-            'outputs data correctly'
-        );
+        $response = json_decode($content, true);
+        $this->assertEquals(0, $response['status'], 'outputs success status');
+        $this->assertEquals(Helper::getPasteId(), $response['id'], 'outputs data correctly');
+        $this->assertStringEndsWith('?' . $response['id'], $response['url'], 'returned URL points to new paste');
+        $this->assertEquals($paste['data'], $response['data'], 'outputs data correctly');
+        $this->assertEquals('syntaxhighlighting', $response['meta']['formatter'], 'outputs format correctly');
+        $this->assertEquals($paste['meta']['postdate'], $response['meta']['postdate'], 'outputs postdate correctly');
+        $this->assertEquals($paste['meta']['opendiscussion'], $response['meta']['opendiscussion'], 'outputs opendiscussion correctly');
+        $this->assertEquals(0, $response['comment_count'], 'outputs comment_count correctly');
+        $this->assertEquals(0, $response['comment_offset'], 'outputs comment_offset correctly');
     }
 
     /**

+ 0 - 9
tst/ViewTest.php

@@ -34,7 +34,6 @@ class ViewTest extends PHPUnit_Framework_TestCase
         /* Setup Routine */
         $page = new View;
         $page->assign('NAME', 'PrivateBinTest');
-        $page->assign('CIPHERDATA', Helper::getPaste()['data']);
         $page->assign('ERROR', self::$error);
         $page->assign('STATUS', self::$status);
         $page->assign('VERSION', self::$version);
@@ -54,7 +53,6 @@ class ViewTest extends PHPUnit_Framework_TestCase
         $page->assign('LANGUAGES', I18n::getLanguageLabels(I18n::getAvailableLanguages()));
         $page->assign('EXPIRE', self::$expire);
         $page->assign('EXPIREDEFAULT', self::$expire_default);
-        $page->assign('EXPIRECLONE', true);
         $page->assign('URLSHORTENER', '');
         $page->assign('QRCODE', true);
 
@@ -97,13 +95,6 @@ class ViewTest extends PHPUnit_Framework_TestCase
     public function testTemplateRendersCorrectly()
     {
         foreach ($this->_content as $template => $content) {
-            $this->assertRegExp(
-                '#<div[^>]+id="cipherdata"[^>]*>' .
-                preg_quote(htmlspecialchars(Helper::getPaste()['data'], ENT_NOQUOTES)) .
-                '</div>#',
-                $content,
-                $template . ': outputs data correctly'
-            );
             $this->assertRegExp(
                 '#<div[^>]+id="errormessage"[^>]*>.*' . self::$error . '#s',
                 $content,