Controller::_return_message()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 11
ccs 8
cts 8
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 3
crap 2
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.7.1
11
 */
12
13
namespace PrivateBin;
14
15
use Exception;
16
use PrivateBin\Persistence\ServerSalt;
17
use PrivateBin\Persistence\TrafficLimiter;
18
19
/**
20
 * Controller
21
 *
22
 * Puts it all together.
23
 */
24
class Controller
25
{
26
    /**
27
     * version
28
     *
29
     * @const string
30
     */
31
    const VERSION = '1.7.1';
32
33
    /**
34
     * minimal required PHP version
35
     *
36
     * @const string
37
     */
38
    const MIN_PHP_VERSION = '7.3.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
     * error message
57
     *
58
     * @access private
59
     * @var    string
60
     */
61
    private $_error = '';
62
63
    /**
64
     * status message
65
     *
66
     * @access private
67
     * @var    string
68
     */
69
    private $_status = '';
70
71
    /**
72
     * JSON message
73
     *
74
     * @access private
75
     * @var    string
76
     */
77
    private $_json = '';
78
79
    /**
80
     * Factory of instance models
81
     *
82
     * @access private
83
     * @var    model
84
     */
85
    private $_model;
86
87
    /**
88
     * request
89
     *
90
     * @access private
91
     * @var    request
92
     */
93
    private $_request;
94
95
    /**
96
     * URL base
97
     *
98
     * @access private
99
     * @var    string
100
     */
101
    private $_urlBase;
102
103
    /**
104
     * constructor
105
     *
106
     * initializes and runs PrivateBin
107
     *
108
     * @access public
109
     * @throws Exception
110
     */
111 126
    public function __construct()
112
    {
113 126
        if (version_compare(PHP_VERSION, self::MIN_PHP_VERSION) < 0) {
114
            error_log(I18n::_('%s requires php %s or above to work. Sorry.', I18n::_('PrivateBin'), self::MIN_PHP_VERSION));
115
            return;
116
        }
117 126
        if (strlen(PATH) < 0 && substr(PATH, -1) !== DIRECTORY_SEPARATOR) {
118
            error_log(I18n::_('%s requires the PATH to end in a "%s". Please update the PATH in your index.php.', I18n::_('PrivateBin'), DIRECTORY_SEPARATOR));
119
            return;
120
        }
121
122
        // load config from ini file, initialize required classes
123 126
        $this->_init();
124
125 123
        switch ($this->_request->getOperation()) {
126 123
            case 'create':
127 56
                $this->_create();
128 56
                break;
129 67
            case 'delete':
130 23
                $this->_delete(
131 23
                    $this->_request->getParam('pasteid'),
132 23
                    $this->_request->getParam('deletetoken')
133 23
                );
134 23
                break;
135 44
            case 'read':
136 25
                $this->_read($this->_request->getParam('pasteid'));
137 25
                break;
138 19
            case 'jsonld':
139 6
                $this->_jsonld($this->_request->getParam('jsonld'));
140 6
                return;
141 13
            case 'yourlsproxy':
142 4
                $this->_yourlsproxy($this->_request->getParam('link'));
143 4
                break;
144
        }
145
146 117
        $this->_setCacheHeaders();
147
148
        // output JSON or HTML
149 117
        if ($this->_request->isJsonApiCall()) {
150 83
            header('Content-type: ' . Request::MIME_JSON);
151 83
            header('Access-Control-Allow-Origin: *');
152 83
            header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
153 83
            header('Access-Control-Allow-Headers: X-Requested-With, Content-Type');
154 83
            header('X-Uncompressed-Content-Length: ' . strlen($this->_json));
155 83
            header('Access-Control-Expose-Headers: X-Uncompressed-Content-Length');
156 83
            echo $this->_json;
157
        } else {
158 34
            $this->_view();
159
        }
160
    }
161
162
    /**
163
     * initialize PrivateBin
164
     *
165
     * @access private
166
     * @throws Exception
167
     */
168 126
    private function _init()
169
    {
170 126
        $this->_conf    = new Configuration;
171 123
        $this->_model   = new Model($this->_conf);
172 123
        $this->_request = new Request;
173 123
        $this->_urlBase = $this->_request->getRequestUri();
174
175
        // set default language
176 123
        $lang = $this->_conf->getKey('languagedefault');
177 123
        I18n::setLanguageFallback($lang);
178
        // force default language, if language selection is disabled and a default is set
179 123
        if (!$this->_conf->getKey('languageselection') && strlen($lang) == 2) {
180 3
            $_COOKIE['lang'] = $lang;
181 3
            setcookie('lang', $lang, 0, '', '', true);
182
        }
183
    }
184
185
    /**
186
     * Turn off browser caching
187
     *
188
     * @access private
189
     */
190 117
    private function _setCacheHeaders()
191
    {
192
        // set headers to disable caching
193 117
        $time = gmdate('D, d M Y H:i:s \G\M\T');
194 117
        header('Cache-Control: no-store, no-cache, no-transform, must-revalidate');
195 117
        header('Pragma: no-cache');
196 117
        header('Expires: ' . $time);
197 117
        header('Last-Modified: ' . $time);
198 117
        header('Vary: Accept');
199
    }
200
201
    /**
202
     * Store new paste or comment
203
     *
204
     * POST contains one or both:
205
     * data = json encoded FormatV2 encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
206
     * attachment = json encoded FormatV2 encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
207
     *
208
     * All optional data will go to meta information:
209
     * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never)
210
     * formatter (optional) = format to display the paste as (plaintext,syntaxhighlighting,markdown) (default:syntaxhighlighting)
211
     * burnafterreading (optional) = if this paste may only viewed once ? (0/1) (default:0)
212
     * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0)
213
     * attachmentname = json encoded FormatV2 encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
214
     * nickname (optional) = in discussion, encoded FormatV2 encrypted text nickname of author of comment (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
215
     * parentid (optional) = in discussion, which comment this comment replies to.
216
     * pasteid (optional) = in discussion, which paste this comment belongs to.
217
     *
218
     * @access private
219
     * @return string
220
     */
221 56
    private function _create()
222
    {
223
        // Ensure last paste from visitors IP address was more than configured amount of seconds ago.
224 56
        ServerSalt::setStore($this->_model->getStore());
225 56
        TrafficLimiter::setConfiguration($this->_conf);
226 56
        TrafficLimiter::setStore($this->_model->getStore());
227
        try {
228 56
            TrafficLimiter::canPass();
229 3
        } catch (Exception $e) {
230 3
            $this->_return_message(1, $e->getMessage());
231 3
            return;
232
        }
233
234 56
        $data      = $this->_request->getData();
235 56
        $isComment = array_key_exists('pasteid', $data) &&
236 56
            !empty($data['pasteid']) &&
237 56
            array_key_exists('parentid', $data) &&
238 56
            !empty($data['parentid']);
239 56
        if (!FormatV2::isValid($data, $isComment)) {
240 6
            $this->_return_message(1, I18n::_('Invalid data.'));
241 6
            return;
242
        }
243 50
        $sizelimit = $this->_conf->getKey('sizelimit');
244
        // Ensure content is not too big.
245 50
        if (strlen($data['ct']) > $sizelimit) {
246 3
            $this->_return_message(
247 3
                1,
248 3
                I18n::_(
249 3
                    'Paste is limited to %s of encrypted data.',
250 3
                    Filter::formatHumanReadableSize($sizelimit)
251 3
                )
252 3
            );
253 3
            return;
254
        }
255
256
        // The user posts a comment.
257 47
        if ($isComment) {
258 15
            $paste = $this->_model->getPaste($data['pasteid']);
259 15
            if ($paste->exists()) {
260
                try {
261 12
                    $comment = $paste->getComment($data['parentid']);
262 9
                    $comment->setData($data);
263 9
                    $comment->store();
264 9
                } catch (Exception $e) {
265 9
                    $this->_return_message(1, $e->getMessage());
266 9
                    return;
267
                }
268 3
                $this->_return_message(0, $comment->getId());
269
            } else {
270 6
                $this->_return_message(1, I18n::_('Invalid data.'));
271
            }
272
        }
273
        // The user posts a standard paste.
274
        else {
275
            try {
276 32
                $this->_model->purge();
277
            } catch (Exception $e) {
278
                error_log('Error purging pastes: ' . $e->getMessage() . PHP_EOL .
279
                    'Use the administration scripts statistics to find ' .
280
                    'damaged paste IDs and either delete them or restore them ' .
281
                    'from backup.');
282
            }
283 32
            $paste = $this->_model->getPaste();
284
            try {
285 32
                $paste->setData($data);
286 26
                $paste->store();
287 9
            } catch (Exception $e) {
288 9
                return $this->_return_message(1, $e->getMessage());
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->_return_message(1, $e->getMessage()) targeting PrivateBin\Controller::_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...
289
            }
290 23
            $this->_return_message(0, $paste->getId(), array('deletetoken' => $paste->getDeleteToken()));
291
        }
292
    }
293
294
    /**
295
     * Delete an existing paste
296
     *
297
     * @access private
298
     * @param  string $dataid
299
     * @param  string $deletetoken
300
     */
301 23
    private function _delete($dataid, $deletetoken)
302
    {
303
        try {
304 23
            $paste = $this->_model->getPaste($dataid);
305 20
            if ($paste->exists()) {
306
                // accessing this method ensures that the paste would be
307
                // deleted if it has already expired
308 17
                $paste->get();
309 14
                if (hash_equals($paste->getDeleteToken(), $deletetoken)) {
310
                    // Paste exists and deletion token is valid: Delete the paste.
311 8
                    $paste->delete();
312 8
                    $this->_status = 'Paste was properly deleted.';
313
                } else {
314 14
                    $this->_error = 'Wrong deletion token. Paste was not deleted.';
315
                }
316
            } else {
317 17
                $this->_error = self::GENERIC_ERROR;
318
            }
319 6
        } catch (Exception $e) {
320 6
            $this->_error = $e->getMessage();
321
        }
322 23
        if ($this->_request->isJsonApiCall()) {
323 5
            if (strlen($this->_error)) {
324 3
                $this->_return_message(1, $this->_error);
325
            } else {
326 2
                $this->_return_message(0, $dataid);
327
            }
328
        }
329
    }
330
331
    /**
332
     * Read an existing paste or comment, only allowed via a JSON API call
333
     *
334
     * @access private
335
     * @param  string $dataid
336
     */
337 25
    private function _read($dataid)
338
    {
339 25
        if (!$this->_request->isJsonApiCall()) {
340 3
            return;
341
        }
342
343
        try {
344 22
            $paste = $this->_model->getPaste($dataid);
345 19
            if ($paste->exists()) {
346 16
                $data = $paste->get();
347 13
                if (array_key_exists('salt', $data['meta'])) {
348 13
                    unset($data['meta']['salt']);
349
                }
350 13
                $this->_return_message(0, $dataid, (array) $data);
351
            } else {
352 16
                $this->_return_message(1, self::GENERIC_ERROR);
353
            }
354 6
        } catch (Exception $e) {
355 6
            $this->_return_message(1, $e->getMessage());
356
        }
357
    }
358
359
    /**
360
     * Display frontend.
361
     *
362
     * @access private
363
     */
364 34
    private function _view()
365
    {
366 34
        header('Content-Security-Policy: ' . $this->_conf->getKey('cspheader'));
367 34
        header('Cross-Origin-Resource-Policy: same-origin');
368 34
        header('Cross-Origin-Embedder-Policy: require-corp');
369
        // disabled, because it prevents links from a paste to the same site to
370
        // be opened. Didn't work with `same-origin-allow-popups` either.
371
        // See issue https://github.com/PrivateBin/PrivateBin/issues/970 for details.
372
        // header('Cross-Origin-Opener-Policy: same-origin');
373 34
        header('Permissions-Policy: browsing-topics=()');
374 34
        header('Referrer-Policy: no-referrer');
375 34
        header('X-Content-Type-Options: nosniff');
376 34
        header('X-Frame-Options: deny');
377 34
        header('X-XSS-Protection: 1; mode=block');
378
379
        // label all the expiration options
380 34
        $expire = array();
381 34
        foreach ($this->_conf->getSection('expire_options') as $time => $seconds) {
382 34
            $expire[$time] = ($seconds == 0) ? I18n::_(ucfirst($time)) : Filter::formatHumanReadableTime($time);
383
        }
384
385
        // translate all the formatter options
386 34
        $formatters = array_map('PrivateBin\\I18n::_', $this->_conf->getSection('formatter_options'));
387
388
        // set language cookie if that functionality was enabled
389 34
        $languageselection = '';
390 34
        if ($this->_conf->getKey('languageselection')) {
391 3
            $languageselection = I18n::getLanguage();
392 3
            setcookie('lang', $languageselection, 0, '', '', true);
393
        }
394
395
        // strip policies that are unsupported in meta tag
396 34
        $metacspheader = str_replace(
397 34
            array(
398 34
                'frame-ancestors \'none\'; ',
399 34
                '; sandbox allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-downloads',
400 34
            ),
401 34
            '',
402 34
            $this->_conf->getKey('cspheader')
403 34
        );
404
405 34
        $page = new View;
406 34
        $page->assign('CSPHEADER', $metacspheader);
407 34
        $page->assign('ERROR', I18n::_($this->_error));
408 34
        $page->assign('NAME', $this->_conf->getKey('name'));
409 34
        if ($this->_request->getOperation() === 'yourlsproxy') {
410 4
            $page->assign('SHORTURL', $this->_status);
411 4
            $page->draw('yourlsproxy');
412 4
            return;
413
        }
414 30
        $page->assign('BASEPATH', I18n::_($this->_conf->getKey('basepath')));
415 30
        $page->assign('STATUS', I18n::_($this->_status));
416 30
        $page->assign('VERSION', self::VERSION);
417 30
        $page->assign('DISCUSSION', $this->_conf->getKey('discussion'));
418 30
        $page->assign('OPENDISCUSSION', $this->_conf->getKey('opendiscussion'));
419 30
        $page->assign('MARKDOWN', array_key_exists('markdown', $formatters));
420 30
        $page->assign('SYNTAXHIGHLIGHTING', array_key_exists('syntaxhighlighting', $formatters));
421 30
        $page->assign('SYNTAXHIGHLIGHTINGTHEME', $this->_conf->getKey('syntaxhighlightingtheme'));
422 30
        $page->assign('FORMATTER', $formatters);
423 30
        $page->assign('FORMATTERDEFAULT', $this->_conf->getKey('defaultformatter'));
424 30
        $page->assign('INFO', I18n::_(str_replace("'", '"', $this->_conf->getKey('info'))));
425 30
        $page->assign('NOTICE', I18n::_($this->_conf->getKey('notice')));
426 30
        $page->assign('BURNAFTERREADINGSELECTED', $this->_conf->getKey('burnafterreadingselected'));
427 30
        $page->assign('PASSWORD', $this->_conf->getKey('password'));
428 30
        $page->assign('FILEUPLOAD', $this->_conf->getKey('fileupload'));
429 30
        $page->assign('ZEROBINCOMPATIBILITY', $this->_conf->getKey('zerobincompatibility'));
430 30
        $page->assign('LANGUAGESELECTION', $languageselection);
431 30
        $page->assign('LANGUAGES', I18n::getLanguageLabels(I18n::getAvailableLanguages()));
432 30
        $page->assign('EXPIRE', $expire);
433 30
        $page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire'));
434 30
        $page->assign('URLSHORTENER', $this->_conf->getKey('urlshortener'));
435 30
        $page->assign('QRCODE', $this->_conf->getKey('qrcode'));
436 30
        $page->assign('EMAIL', $this->_conf->getKey('email'));
437 30
        $page->assign('HTTPWARNING', $this->_conf->getKey('httpwarning'));
438 30
        $page->assign('HTTPSLINK', 'https://' . $this->_request->getHost() . $this->_request->getRequestUri());
439 30
        $page->assign('COMPRESSION', $this->_conf->getKey('compression'));
440 30
        $page->draw($this->_conf->getKey('template'));
441
    }
442
443
    /**
444
     * outputs requested JSON-LD context
445
     *
446
     * @access private
447
     * @param string $type
448
     */
449 6
    private function _jsonld($type)
450
    {
451 6
        if (!in_array($type, array(
452 6
            'comment',
453 6
            'commentmeta',
454 6
            'paste',
455 6
            'pastemeta',
456 6
            'types',
457 6
        ))) {
458 1
            $type = '';
459
        }
460 6
        $content = '{}';
461 6
        $file    = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . $type . '.jsonld';
462 6
        if (is_readable($file)) {
463 5
            $content = str_replace(
464 5
                '?jsonld=',
465 5
                $this->_urlBase . '?jsonld=',
466 5
                file_get_contents($file)
467 5
            );
468
        }
469 6
        if ($type === 'types') {
470 1
            $content = str_replace(
471 1
                implode('", "', array_keys($this->_conf->getDefaults()['expire_options'])),
472 1
                implode('", "', array_keys($this->_conf->getSection('expire_options'))),
473 1
                $content
474 1
            );
475
        }
476
477 6
        header('Content-type: application/ld+json');
478 6
        header('Access-Control-Allow-Origin: *');
479 6
        header('Access-Control-Allow-Methods: GET');
480 6
        echo $content;
481
    }
482
483
    /**
484
     * proxies link to YOURLS, updates status or error with response
485
     *
486
     * @access private
487
     * @param string $link
488
     */
489 4
    private function _yourlsproxy($link)
490
    {
491 4
        $yourls = new YourlsProxy($this->_conf, $link);
492 4
        if ($yourls->isError()) {
493 1
            $this->_error = $yourls->getError();
494
        } else {
495 3
            $this->_status = $yourls->getUrl();
496
        }
497
    }
498
499
    /**
500
     * prepares JSON encoded status message
501
     *
502
     * @access private
503
     * @param  int $status
504
     * @param  string $message
505
     * @param  array $other
506
     */
507 83
    private function _return_message($status, $message, $other = array())
508
    {
509 83
        $result = array('status' => $status);
510 83
        if ($status) {
511 45
            $result['message'] = I18n::_($message);
512
        } else {
513 41
            $result['id']  = $message;
514 41
            $result['url'] = $this->_urlBase . '?' . $message;
515
        }
516 83
        $result += $other;
517 83
        $this->_json = Json::encode($result);
518
    }
519
}
520