Completed
Push — master ( 1cb1c1...b2ea65 )
by El
05:50
created

PrivateBin::_init()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 13
cts 13
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 12
nc 4
nop 0
crap 4
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
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';
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
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
127
     */
128 96
    public function __construct()
129
    {
130 96
        if (version_compare(PHP_VERSION, self::MIN_PHP_VERSION) < 0) {
131
            throw new Exception(I18n::_('%s requires php %s or above to work. Sorry.', I18n::_('PrivateBin'), self::MIN_PHP_VERSION), 1);
132
        }
133 96
        if (strlen(PATH) < 0 && substr(PATH, -1) !== DIRECTORY_SEPARATOR) {
134
            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);
135
        }
136
137
        // load config from ini file, initialize required classes
138 96
        $this->_init();
139
140 94
        switch ($this->_request->getOperation()) {
141 94
            case 'create':
142 44
                $this->_create();
143 44
                break;
144 50
            case 'delete':
145 18
                $this->_delete(
146 18
                    $this->_request->getParam('pasteid'),
147 18
                    $this->_request->getParam('deletetoken')
148
                );
149 18
                break;
150 32
            case 'read':
151 19
                $this->_read($this->_request->getParam('pasteid'));
152 19
                break;
153 13
            case 'jsonld':
154 5
                $this->_jsonld($this->_request->getParam('jsonld'));
155 5
                return;
156
        }
157
158
        // output JSON or HTML
159 89
        if ($this->_request->isJsonApiCall()) {
160 55
            header('Content-type: ' . Request::MIME_JSON);
161 55
            header('Access-Control-Allow-Origin: *');
162 55
            header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
163 55
            header('Access-Control-Allow-Headers: X-Requested-With, Content-Type');
164 55
            echo $this->_json;
165
        } else {
166 34
            $this->_view();
167
        }
168 89
    }
169
170
    /**
171
     * initialize privatebin
172
     *
173
     * @access private
174
     * @return void
175
     */
176 96
    private function _init()
0 ignored issues
show
Coding Style introduced by
_init uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
_init uses the super-global variable $_COOKIE which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
177
    {
178 96
        $this->_conf    = new Configuration;
179 94
        $this->_model   = new Model($this->_conf);
180 94
        $this->_request = new Request;
181 94
        $this->_urlBase = array_key_exists('REQUEST_URI', $_SERVER) ?
182 94
            htmlspecialchars($_SERVER['REQUEST_URI']) : '/';
183 94
        ServerSalt::setPath($this->_conf->getKey('dir', 'traffic'));
184
185
        // set default language
186 94
        $lang = $this->_conf->getKey('languagedefault');
187 94
        I18n::setLanguageFallback($lang);
188
        // force default language, if language selection is disabled and a default is set
189 94
        if (!$this->_conf->getKey('languageselection') && strlen($lang) == 2) {
190 2
            $_COOKIE['lang'] = $lang;
191 2
            setcookie('lang', $lang);
192
        }
193 94
    }
194
195
    /**
196
     * Store new paste or comment
197
     *
198
     * POST contains one or both:
199
     * data = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
200
     * attachment = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
201
     *
202
     * All optional data will go to meta information:
203
     * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never)
204
     * formatter (optional) = format to display the paste as (plaintext,syntaxhighlighting,markdown) (default:syntaxhighlighting)
205
     * burnafterreading (optional) = if this paste may only viewed once ? (0/1) (default:0)
206
     * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0)
207
     * attachmentname = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
208
     * 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)
209
     * parentid (optional) = in discussion, which comment this comment replies to.
210
     * pasteid (optional) = in discussion, which paste this comment belongs to.
211
     *
212
     * @access private
213
     * @return string
214
     */
215 44
    private function _create()
216
    {
217
        // Ensure last paste from visitors IP address was more than configured amount of seconds ago.
218 44
        TrafficLimiter::setConfiguration($this->_conf);
219 44
        if (!TrafficLimiter::canPass()) {
220 2
            return $this->_return_message(
221 2
            1, I18n::_(
222 2
                'Please wait %d seconds between each post.',
223 2
                $this->_conf->getKey('limit', 'traffic')
224
            )
225
        );
226
        }
227
228 44
        $data           = $this->_request->getParam('data');
229 44
        $attachment     = $this->_request->getParam('attachment');
230 44
        $attachmentname = $this->_request->getParam('attachmentname');
231
232
        // Ensure content is not too big.
233 44
        $sizelimit = $this->_conf->getKey('sizelimit');
234
        if (
235 44
            strlen($data) + strlen($attachment) + strlen($attachmentname) > $sizelimit
236
        ) {
237 2
            return $this->_return_message(
238 2
            1,
239 2
            I18n::_(
240 2
                'Paste is limited to %s of encrypted data.',
241 2
                Filter::formatHumanReadableSize($sizelimit)
242
            )
243
        );
244
        }
245
246
        // Ensure attachment did not get lost due to webserver limits or Suhosin
247 42
        if (strlen($attachmentname) > 0 && strlen($attachment) == 0) {
248 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.');
249
        }
250
251
        // The user posts a comment.
252 40
        $pasteid  = $this->_request->getParam('pasteid');
253 40
        $parentid = $this->_request->getParam('parentid');
254 40
        if (!empty($pasteid) && !empty($parentid)) {
255 12
            $paste = $this->_model->getPaste($pasteid);
256 12
            if ($paste->exists()) {
257
                try {
258 10
                    $comment = $paste->getComment($parentid);
259
260 8
                    $nickname = $this->_request->getParam('nickname');
261 8
                    if (!empty($nickname)) {
262 8
                        $comment->setNickname($nickname);
263
                    }
264
265 6
                    $comment->setData($data);
266 6
                    $comment->store();
267 8
                } catch (Exception $e) {
268 8
                    return $this->_return_message(1, $e->getMessage());
269
                }
270 2
                $this->_return_message(0, $comment->getId());
271
            } else {
272 4
                $this->_return_message(1, 'Invalid data.');
273
            }
274
        }
275
        // The user posts a standard paste.
276
        else {
277 28
            $this->_model->purge();
278 28
            $paste = $this->_model->getPaste();
279
            try {
280 28
                $paste->setData($data);
281
282 28
                if (!empty($attachment)) {
283 2
                    $paste->setAttachment($attachment);
284 2
                    if (!empty($attachmentname)) {
285 2
                        $paste->setAttachmentName($attachmentname);
286
                    }
287
                }
288
289 28
                $expire = $this->_request->getParam('expire');
290 28
                if (!empty($expire)) {
291 6
                    $paste->setExpiration($expire);
292
                }
293
294 28
                $burnafterreading = $this->_request->getParam('burnafterreading');
295 28
                if (!empty($burnafterreading)) {
296 2
                    $paste->setBurnafterreading($burnafterreading);
297
                }
298
299 26
                $opendiscussion = $this->_request->getParam('opendiscussion');
300 26
                if (!empty($opendiscussion)) {
301 4
                    $paste->setOpendiscussion($opendiscussion);
302
                }
303
304 24
                $formatter = $this->_request->getParam('formatter');
305 24
                if (!empty($formatter)) {
306 2
                    $paste->setFormatter($formatter);
307
                }
308
309 24
                $paste->store();
310 6
            } catch (Exception $e) {
311 6
                return $this->_return_message(1, $e->getMessage());
312
            }
313 22
            $this->_return_message(0, $paste->getId(), array('deletetoken' => $paste->getDeleteToken()));
314
        }
315 26
    }
316
317
    /**
318
     * Delete an existing paste
319
     *
320
     * @access private
321
     * @param  string $dataid
322
     * @param  string $deletetoken
323
     * @return void
324
     */
325 18
    private function _delete($dataid, $deletetoken)
326
    {
327
        try {
328 18
            $paste = $this->_model->getPaste($dataid);
329 16
            if ($paste->exists()) {
330
                // accessing this property ensures that the paste would be
331
                // deleted if it has already expired
332 14
                $burnafterreading = $paste->isBurnafterreading();
333
                if (
334 12
                    ($burnafterreading && $deletetoken == 'burnafterreading') ||
335 12
                    Filter::slowEquals($deletetoken, $paste->getDeleteToken())
336
                ) {
337
                    // Paste exists and deletion token is valid: Delete the paste.
338 8
                    $paste->delete();
339 8
                    $this->_status = 'Paste was properly deleted.';
340
                } else {
341 4
                    if (!$burnafterreading && $deletetoken == 'burnafterreading') {
342 2
                        $this->_error = 'Paste is not of burn-after-reading type.';
343
                    } else {
344 12
                        $this->_error = 'Wrong deletion token. Paste was not deleted.';
345
                    }
346
                }
347
            } else {
348 14
                $this->_error = self::GENERIC_ERROR;
349
            }
350 4
        } catch (Exception $e) {
351 4
            $this->_error = $e->getMessage();
352
        }
353 18 View Code Duplication
        if ($this->_request->isJsonApiCall()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
354 6
            if (strlen($this->_error)) {
355 2
                $this->_return_message(1, $this->_error);
356
            } else {
357 4
                $this->_return_message(0, $dataid);
358
            }
359
        }
360 18
    }
361
362
    /**
363
     * Read an existing paste or comment
364
     *
365
     * @access private
366
     * @param  string $dataid
367
     * @return void
368
     */
369 19
    private function _read($dataid)
370
    {
371
        try {
372 19
            $paste = $this->_model->getPaste($dataid);
373 17
            if ($paste->exists()) {
374 13
                $data              = $paste->get();
375 11
                $this->_doesExpire = property_exists($data, 'meta') && property_exists($data->meta, 'expire_date');
376 11
                if (property_exists($data->meta, 'salt')) {
377 11
                    unset($data->meta->salt);
378
                }
379 11
                $this->_data = json_encode($data);
380
            } else {
381 15
                $this->_error = self::GENERIC_ERROR;
382
            }
383 4
        } catch (Exception $e) {
384 4
            $this->_error = $e->getMessage();
385
        }
386
387 19 View Code Duplication
        if ($this->_request->isJsonApiCall()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
388 5
            if (strlen($this->_error)) {
389 2
                $this->_return_message(1, $this->_error);
390
            } else {
391 3
                $this->_return_message(0, $dataid, json_decode($this->_data, true));
392
            }
393
        }
394 19
    }
395
396
    /**
397
     * Display PrivateBin frontend.
398
     *
399
     * @access private
400
     * @return void
401
     */
402 34
    private function _view()
403
    {
404
        // set headers to disable caching
405 34
        $time = gmdate('D, d M Y H:i:s \G\M\T');
406 34
        header('Cache-Control: no-store, no-cache, no-transform, must-revalidate');
407 34
        header('Pragma: no-cache');
408 34
        header('Expires: ' . $time);
409 34
        header('Last-Modified: ' . $time);
410 34
        header('Vary: Accept');
411 34
        header('Content-Security-Policy: ' . $this->_conf->getKey('cspheader'));
412 34
        header('X-Xss-Protection: 1; mode=block');
413 34
        header('X-Frame-Options: DENY');
414 34
        header('X-Content-Type-Options: nosniff');
415
416
        // label all the expiration options
417 34
        $expire = array();
418 34
        foreach ($this->_conf->getSection('expire_options') as $time => $seconds) {
419 34
            $expire[$time] = ($seconds == 0) ? I18n::_(ucfirst($time)) : Filter::formatHumanReadableTime($time);
420
        }
421
422
        // translate all the formatter options
423 34
        $formatters = array_map('PrivateBin\\I18n::_', $this->_conf->getSection('formatter_options'));
424
425
        // set language cookie if that functionality was enabled
426 34
        $languageselection = '';
427 34
        if ($this->_conf->getKey('languageselection')) {
428 2
            $languageselection = I18n::getLanguage();
429 2
            setcookie('lang', $languageselection);
430
        }
431
432 34
        $page = new View;
433 34
        $page->assign('NAME', $this->_conf->getKey('name'));
434 34
        $page->assign('CIPHERDATA', $this->_data);
435 34
        $page->assign('ERROR', I18n::_($this->_error));
436 34
        $page->assign('STATUS', I18n::_($this->_status));
437 34
        $page->assign('VERSION', self::VERSION);
438 34
        $page->assign('DISCUSSION', $this->_conf->getKey('discussion'));
439 34
        $page->assign('OPENDISCUSSION', $this->_conf->getKey('opendiscussion'));
440 34
        $page->assign('MARKDOWN', array_key_exists('markdown', $formatters));
441 34
        $page->assign('SYNTAXHIGHLIGHTING', array_key_exists('syntaxhighlighting', $formatters));
442 34
        $page->assign('SYNTAXHIGHLIGHTINGTHEME', $this->_conf->getKey('syntaxhighlightingtheme'));
443 34
        $page->assign('FORMATTER', $formatters);
444 34
        $page->assign('FORMATTERDEFAULT', $this->_conf->getKey('defaultformatter'));
445 34
        $page->assign('NOTICE', I18n::_($this->_conf->getKey('notice')));
446 34
        $page->assign('BURNAFTERREADINGSELECTED', $this->_conf->getKey('burnafterreadingselected'));
447 34
        $page->assign('PASSWORD', $this->_conf->getKey('password'));
448 34
        $page->assign('FILEUPLOAD', $this->_conf->getKey('fileupload'));
449 34
        $page->assign('ZEROBINCOMPATIBILITY', $this->_conf->getKey('zerobincompatibility'));
450 34
        $page->assign('LANGUAGESELECTION', $languageselection);
451 34
        $page->assign('LANGUAGES', I18n::getLanguageLabels(I18n::getAvailableLanguages()));
452 34
        $page->assign('EXPIRE', $expire);
453 34
        $page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire'));
454 34
        $page->assign('EXPIRECLONE', !$this->_doesExpire || ($this->_doesExpire && $this->_conf->getKey('clone', 'expire')));
455 34
        $page->assign('URLSHORTENER', $this->_conf->getKey('urlshortener'));
456 34
        $page->draw($this->_conf->getKey('template'));
457 34
    }
458
459
    /**
460
     * outputs requested JSON-LD context
461
     *
462
     * @access private
463
     * @param string $type
464
     * @return void
465
     */
466 5
    private function _jsonld($type)
467
    {
468
        if (
469 5
            $type !== 'paste' && $type !== 'comment' &&
470 5
            $type !== 'pastemeta' && $type !== 'commentmeta'
471
        ) {
472 1
            $type = '';
473
        }
474 5
        $content = '{}';
475 5
        $file    = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . $type . '.jsonld';
476 5
        if (is_readable($file)) {
477 4
            $content = str_replace(
478 4
                '?jsonld=',
479 4
                $this->_urlBase . '?jsonld=',
480
                file_get_contents($file)
481
            );
482
        }
483
484 5
        header('Content-type: application/ld+json');
485 5
        header('Access-Control-Allow-Origin: *');
486 5
        header('Access-Control-Allow-Methods: GET');
487 5
        echo $content;
488 5
    }
489
490
    /**
491
     * prepares JSON encoded status message
492
     *
493
     * @access private
494
     * @param  int $status
495
     * @param  string $message
496
     * @param  array $other
497
     * @return void
498
     */
499 55
    private function _return_message($status, $message, $other = array())
500
    {
501 55
        $result = array('status' => $status);
502 55
        if ($status) {
503 26
            $result['message'] = I18n::_($message);
504
        } else {
505 31
            $result['id']  = $message;
506 31
            $result['url'] = $this->_urlBase . '?' . $message;
507
        }
508 55
        $result += $other;
509 55
        $this->_json = json_encode($result);
510 55
    }
511
}
512