Passed
Push — master ( 76c147...6eb882 )
by El
04:25 queued 01:03
created

lib/PrivateBin.php (2 issues)

1
<?php
2
/**
3
 * PrivateBin
4
 *
5
 * a zero-knowledge paste bin
6
 *
7
 * @link      https://github.com/PrivateBin/PrivateBin
8
 * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
9
 * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
10
 * @version   1.1.1
11
 */
12
13
namespace PrivateBin;
14
15
use Exception;
16
use PrivateBin\Persistence\ServerSalt;
17
use PrivateBin\Persistence\TrafficLimiter;
18
19
/**
20
 * PrivateBin
21
 *
22
 * Controller, puts it all together.
23
 */
24
class PrivateBin
25
{
26
    /**
27
     * version
28
     *
29
     * @const string
30
     */
31
    const VERSION = '1.1.1';
32
33
    /**
34
     * minimal required PHP version
35
     *
36
     * @const string
37
     */
38
    const MIN_PHP_VERSION = '5.4.0';
39
40
    /**
41
     * show the same error message if the paste expired or does not exist
42
     *
43
     * @const string
44
     */
45
    const GENERIC_ERROR = 'Paste does not exist, has expired or has been deleted.';
46
47
    /**
48
     * configuration
49
     *
50
     * @access private
51
     * @var    Configuration
52
     */
53
    private $_conf;
54
55
    /**
56
     * data
57
     *
58
     * @access private
59
     * @var    string
60
     */
61
    private $_data = '';
62
63
    /**
64
     * does the paste expire
65
     *
66
     * @access private
67
     * @var    bool
68
     */
69
    private $_doesExpire = false;
70
71
    /**
72
     * error message
73
     *
74
     * @access private
75
     * @var    string
76
     */
77
    private $_error = '';
78
79
    /**
80
     * status message
81
     *
82
     * @access private
83
     * @var    string
84
     */
85
    private $_status = '';
86
87
    /**
88
     * JSON message
89
     *
90
     * @access private
91
     * @var    string
92
     */
93
    private $_json = '';
94
95
    /**
96
     * Factory of instance models
97
     *
98
     * @access private
99
     * @var    model
100
     */
101
    private $_model;
102
103
    /**
104
     * request
105
     *
106
     * @access private
107
     * @var    request
108
     */
109
    private $_request;
110
111
    /**
112
     * URL base
113
     *
114
     * @access private
115
     * @var    string
116
     */
117
    private $_urlBase;
118
119
    /**
120
     * constructor
121
     *
122
     * initializes and runs PrivateBin
123
     *
124
     * @access public
125
     * @throws Exception
126
     */
127 98
    public function __construct()
128
    {
129 98
        if (version_compare(PHP_VERSION, self::MIN_PHP_VERSION) < 0) {
130
            throw new Exception(I18n::_('%s requires php %s or above to work. Sorry.', I18n::_('PrivateBin'), self::MIN_PHP_VERSION), 1);
131
        }
132 98
        if (strlen(PATH) < 0 && substr(PATH, -1) !== DIRECTORY_SEPARATOR) {
133
            throw new Exception(I18n::_('%s requires the PATH to end in a "%s". Please update the PATH in your index.php.', I18n::_('PrivateBin'), DIRECTORY_SEPARATOR), 5);
134
        }
135
136
        // load config from ini file, initialize required classes
137 98
        $this->_init();
138
139 96
        switch ($this->_request->getOperation()) {
140 96
            case 'create':
141 44
                $this->_create();
142 44
                break;
143 52
            case 'delete':
144 18
                $this->_delete(
145 18
                    $this->_request->getParam('pasteid'),
146 18
                    $this->_request->getParam('deletetoken')
147
                );
148 18
                break;
149 34
            case 'read':
150 21
                $this->_read($this->_request->getParam('pasteid'));
151 21
                break;
152 13
            case 'jsonld':
153 5
                $this->_jsonld($this->_request->getParam('jsonld'));
154 5
                return;
155
        }
156
157
        // output JSON or HTML
158 91
        if ($this->_request->isJsonApiCall()) {
159 55
            header('Content-type: ' . Request::MIME_JSON);
160 55
            header('Access-Control-Allow-Origin: *');
161 55
            header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
162 55
            header('Access-Control-Allow-Headers: X-Requested-With, Content-Type');
163 55
            echo $this->_json;
164
        } else {
165 36
            $this->_view();
166
        }
167 91
    }
168
169
    /**
170
     * initialize privatebin
171
     *
172
     * @access private
173
     */
174 98
    private function _init()
175
    {
176 98
        $this->_conf    = new Configuration;
177 96
        $this->_model   = new Model($this->_conf);
178 96
        $this->_request = new Request;
179 96
        $this->_urlBase = array_key_exists('REQUEST_URI', $_SERVER) ?
180 96
            htmlspecialchars($_SERVER['REQUEST_URI']) : '/';
181 96
        ServerSalt::setPath($this->_conf->getKey('dir', 'traffic'));
182
183
        // set default language
184 96
        $lang = $this->_conf->getKey('languagedefault');
185 96
        I18n::setLanguageFallback($lang);
186
        // force default language, if language selection is disabled and a default is set
187 96
        if (!$this->_conf->getKey('languageselection') && strlen($lang) == 2) {
188 2
            $_COOKIE['lang'] = $lang;
189 2
            setcookie('lang', $lang);
190
        }
191 96
    }
192
193
    /**
194
     * Store new paste or comment
195
     *
196
     * POST contains one or both:
197
     * data = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
198
     * attachment = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
199
     *
200
     * All optional data will go to meta information:
201
     * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never)
202
     * formatter (optional) = format to display the paste as (plaintext,syntaxhighlighting,markdown) (default:syntaxhighlighting)
203
     * burnafterreading (optional) = if this paste may only viewed once ? (0/1) (default:0)
204
     * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0)
205
     * attachmentname = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
206
     * 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)
207
     * parentid (optional) = in discussion, which comment this comment replies to.
208
     * pasteid (optional) = in discussion, which paste this comment belongs to.
209
     *
210
     * @access private
211
     * @return string
212
     */
213 44
    private function _create()
214
    {
215
        // Ensure last paste from visitors IP address was more than configured amount of seconds ago.
216 44
        TrafficLimiter::setConfiguration($this->_conf);
217 44
        if (!TrafficLimiter::canPass()) {
218 2
            return $this->_return_message(
0 ignored issues
show
Are you sure the usage of $this->_return_message(1...y('limit', 'traffic'))) targeting PrivateBin\PrivateBin::_return_message() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
219 2
            1, I18n::_(
220 2
                'Please wait %d seconds between each post.',
221 2
                $this->_conf->getKey('limit', 'traffic')
222
            )
223
        );
224
        }
225
226 44
        $data           = $this->_request->getParam('data');
227 44
        $attachment     = $this->_request->getParam('attachment');
228 44
        $attachmentname = $this->_request->getParam('attachmentname');
229
230
        // Ensure content is not too big.
231 44
        $sizelimit = $this->_conf->getKey('sizelimit');
232
        if (
233 44
            strlen($data) + strlen($attachment) + strlen($attachmentname) > $sizelimit
234
        ) {
235 2
            return $this->_return_message(
0 ignored issues
show
Are you sure the usage of $this->_return_message(1...dableSize($sizelimit))) targeting PrivateBin\PrivateBin::_return_message() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
236 2
            1,
237 2
            I18n::_(
238 2
                'Paste is limited to %s of encrypted data.',
239 2
                Filter::formatHumanReadableSize($sizelimit)
240
            )
241
        );
242
        }
243
244
        // Ensure attachment did not get lost due to webserver limits or Suhosin
245 42
        if (strlen($attachmentname) > 0 && strlen($attachment) == 0) {
246 2
            return $this->_return_message(1, 'Attachment missing in data received by server. Please check your webserver or suhosin configuration for maximum POST parameter limitations.');
247
        }
248
249
        // The user posts a comment.
250 40
        $pasteid  = $this->_request->getParam('pasteid');
251 40
        $parentid = $this->_request->getParam('parentid');
252 40
        if (!empty($pasteid) && !empty($parentid)) {
253 12
            $paste = $this->_model->getPaste($pasteid);
254 12
            if ($paste->exists()) {
255
                try {
256 10
                    $comment = $paste->getComment($parentid);
257
258 8
                    $nickname = $this->_request->getParam('nickname');
259 8
                    if (!empty($nickname)) {
260 8
                        $comment->setNickname($nickname);
261
                    }
262
263 6
                    $comment->setData($data);
264 6
                    $comment->store();
265 8
                } catch (Exception $e) {
266 8
                    return $this->_return_message(1, $e->getMessage());
267
                }
268 2
                $this->_return_message(0, $comment->getId());
269
            } else {
270 4
                $this->_return_message(1, 'Invalid data.');
271
            }
272
        }
273
        // The user posts a standard paste.
274
        else {
275 28
            $this->_model->purge();
276 28
            $paste = $this->_model->getPaste();
277
            try {
278 28
                $paste->setData($data);
279
280 28
                if (!empty($attachment)) {
281 2
                    $paste->setAttachment($attachment);
282 2
                    if (!empty($attachmentname)) {
283 2
                        $paste->setAttachmentName($attachmentname);
284
                    }
285
                }
286
287 28
                $expire = $this->_request->getParam('expire');
288 28
                if (!empty($expire)) {
289 6
                    $paste->setExpiration($expire);
290
                }
291
292 28
                $burnafterreading = $this->_request->getParam('burnafterreading');
293 28
                if (!empty($burnafterreading)) {
294 2
                    $paste->setBurnafterreading($burnafterreading);
295
                }
296
297 26
                $opendiscussion = $this->_request->getParam('opendiscussion');
298 26
                if (!empty($opendiscussion)) {
299 4
                    $paste->setOpendiscussion($opendiscussion);
300
                }
301
302 24
                $formatter = $this->_request->getParam('formatter');
303 24
                if (!empty($formatter)) {
304 2
                    $paste->setFormatter($formatter);
305
                }
306
307 24
                $paste->store();
308 6
            } catch (Exception $e) {
309 6
                return $this->_return_message(1, $e->getMessage());
310
            }
311 22
            $this->_return_message(0, $paste->getId(), array('deletetoken' => $paste->getDeleteToken()));
312
        }
313 26
    }
314
315
    /**
316
     * Delete an existing paste
317
     *
318
     * @access private
319
     * @param  string $dataid
320
     * @param  string $deletetoken
321
     */
322 18
    private function _delete($dataid, $deletetoken)
323
    {
324
        try {
325 18
            $paste = $this->_model->getPaste($dataid);
326 16
            if ($paste->exists()) {
327
                // accessing this property ensures that the paste would be
328
                // deleted if it has already expired
329 14
                $burnafterreading = $paste->isBurnafterreading();
330
                if (
331 12
                    ($burnafterreading && $deletetoken == 'burnafterreading') ||
332 12
                    Filter::slowEquals($deletetoken, $paste->getDeleteToken())
333
                ) {
334
                    // Paste exists and deletion token is valid: Delete the paste.
335 8
                    $paste->delete();
336 8
                    $this->_status = 'Paste was properly deleted.';
337
                } else {
338 4
                    if (!$burnafterreading && $deletetoken == 'burnafterreading') {
339 2
                        $this->_error = 'Paste is not of burn-after-reading type.';
340
                    } else {
341 12
                        $this->_error = 'Wrong deletion token. Paste was not deleted.';
342
                    }
343
                }
344
            } else {
345 14
                $this->_error = self::GENERIC_ERROR;
346
            }
347 4
        } catch (Exception $e) {
348 4
            $this->_error = $e->getMessage();
349
        }
350 18
        if ($this->_request->isJsonApiCall()) {
351 6
            if (strlen($this->_error)) {
352 2
                $this->_return_message(1, $this->_error);
353
            } else {
354 4
                $this->_return_message(0, $dataid);
355
            }
356
        }
357 18
    }
358
359
    /**
360
     * Read an existing paste or comment
361
     *
362
     * @access private
363
     * @param  string $dataid
364
     */
365 21
    private function _read($dataid)
366
    {
367
        try {
368 21
            $paste = $this->_model->getPaste($dataid);
369 19
            if ($paste->exists()) {
370 15
                $data              = $paste->get();
371 13
                $this->_doesExpire = property_exists($data, 'meta') && property_exists($data->meta, 'expire_date');
372 13
                if (property_exists($data->meta, 'salt')) {
373 13
                    unset($data->meta->salt);
374
                }
375 13
                $this->_data = json_encode($data);
376
            } else {
377 17
                $this->_error = self::GENERIC_ERROR;
378
            }
379 4
        } catch (Exception $e) {
380 4
            $this->_error = $e->getMessage();
381
        }
382
383 21
        if ($this->_request->isJsonApiCall()) {
384 5
            if (strlen($this->_error)) {
385 2
                $this->_return_message(1, $this->_error);
386
            } else {
387 3
                $this->_return_message(0, $dataid, json_decode($this->_data, true));
388
            }
389
        }
390 21
    }
391
392
    /**
393
     * Display PrivateBin frontend.
394
     *
395
     * @access private
396
     */
397 36
    private function _view()
398
    {
399
        // set headers to disable caching
400 36
        $time = gmdate('D, d M Y H:i:s \G\M\T');
401 36
        header('Cache-Control: no-store, no-cache, no-transform, must-revalidate');
402 36
        header('Pragma: no-cache');
403 36
        header('Expires: ' . $time);
404 36
        header('Last-Modified: ' . $time);
405 36
        header('Vary: Accept');
406 36
        header('Content-Security-Policy: ' . $this->_conf->getKey('cspheader'));
407 36
        header('X-Xss-Protection: 1; mode=block');
408 36
        header('X-Frame-Options: DENY');
409 36
        header('X-Content-Type-Options: nosniff');
410
411
        // label all the expiration options
412 36
        $expire = array();
413 36
        foreach ($this->_conf->getSection('expire_options') as $time => $seconds) {
414 36
            $expire[$time] = ($seconds == 0) ? I18n::_(ucfirst($time)) : Filter::formatHumanReadableTime($time);
415
        }
416
417
        // translate all the formatter options
418 36
        $formatters = array_map('PrivateBin\\I18n::_', $this->_conf->getSection('formatter_options'));
419
420
        // set language cookie if that functionality was enabled
421 36
        $languageselection = '';
422 36
        if ($this->_conf->getKey('languageselection')) {
423 2
            $languageselection = I18n::getLanguage();
424 2
            setcookie('lang', $languageselection);
425
        }
426
427 36
        $page = new View;
428 36
        $page->assign('NAME', $this->_conf->getKey('name'));
429 36
        $page->assign('CIPHERDATA', $this->_data);
430 36
        $page->assign('ERROR', I18n::_($this->_error));
431 36
        $page->assign('STATUS', I18n::_($this->_status));
432 36
        $page->assign('VERSION', self::VERSION);
433 36
        $page->assign('DISCUSSION', $this->_conf->getKey('discussion'));
434 36
        $page->assign('OPENDISCUSSION', $this->_conf->getKey('opendiscussion'));
435 36
        $page->assign('MARKDOWN', array_key_exists('markdown', $formatters));
436 36
        $page->assign('SYNTAXHIGHLIGHTING', array_key_exists('syntaxhighlighting', $formatters));
437 36
        $page->assign('SYNTAXHIGHLIGHTINGTHEME', $this->_conf->getKey('syntaxhighlightingtheme'));
438 36
        $page->assign('FORMATTER', $formatters);
439 36
        $page->assign('FORMATTERDEFAULT', $this->_conf->getKey('defaultformatter'));
440 36
        $page->assign('NOTICE', I18n::_($this->_conf->getKey('notice')));
441 36
        $page->assign('BURNAFTERREADINGSELECTED', $this->_conf->getKey('burnafterreadingselected'));
442 36
        $page->assign('PASSWORD', $this->_conf->getKey('password'));
443 36
        $page->assign('FILEUPLOAD', $this->_conf->getKey('fileupload'));
444 36
        $page->assign('ZEROBINCOMPATIBILITY', $this->_conf->getKey('zerobincompatibility'));
445 36
        $page->assign('LANGUAGESELECTION', $languageselection);
446 36
        $page->assign('LANGUAGES', I18n::getLanguageLabels(I18n::getAvailableLanguages()));
447 36
        $page->assign('EXPIRE', $expire);
448 36
        $page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire'));
449 36
        $page->assign('EXPIRECLONE', !$this->_doesExpire || ($this->_doesExpire && $this->_conf->getKey('clone', 'expire')));
450 36
        $page->assign('URLSHORTENER', $this->_conf->getKey('urlshortener'));
451 36
        $page->assign('QRCODE', $this->_conf->getKey('qrcode'));
452 36
        $page->draw($this->_conf->getKey('template'));
453 36
    }
454
455
    /**
456
     * outputs requested JSON-LD context
457
     *
458
     * @access private
459
     * @param string $type
460
     */
461 5
    private function _jsonld($type)
462
    {
463
        if (
464 5
            $type !== 'paste' && $type !== 'comment' &&
465 5
            $type !== 'pastemeta' && $type !== 'commentmeta'
466
        ) {
467 1
            $type = '';
468
        }
469 5
        $content = '{}';
470 5
        $file    = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . $type . '.jsonld';
471 5
        if (is_readable($file)) {
472 4
            $content = str_replace(
473 4
                '?jsonld=',
474 4
                $this->_urlBase . '?jsonld=',
475 4
                file_get_contents($file)
476
            );
477
        }
478
479 5
        header('Content-type: application/ld+json');
480 5
        header('Access-Control-Allow-Origin: *');
481 5
        header('Access-Control-Allow-Methods: GET');
482 5
        echo $content;
483 5
    }
484
485
    /**
486
     * prepares JSON encoded status message
487
     *
488
     * @access private
489
     * @param  int $status
490
     * @param  string $message
491
     * @param  array $other
492
     */
493 55
    private function _return_message($status, $message, $other = array())
494
    {
495 55
        $result = array('status' => $status);
496 55
        if ($status) {
497 26
            $result['message'] = I18n::_($message);
498
        } else {
499 31
            $result['id']  = $message;
500 31
            $result['url'] = $this->_urlBase . '?' . $message;
501
        }
502 55
        $result += $other;
503 55
        $this->_json = json_encode($result);
504 55
    }
505
}
506