Passed
Push — master ( dd721c...db307c )
by El
03:47
created

PrivateBin::_delete()   D

Complexity

Conditions 10
Paths 27

Size

Total Lines 36
Code Lines 24

Duplication

Lines 7
Ratio 19.44 %

Code Coverage

Tests 19
CRAP Score 10

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 7
loc 36
ccs 19
cts 19
cp 1
rs 4.8196
cc 10
eloc 24
nc 27
nop 2
crap 10

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
     * show the same error message if the paste expired or does not exist
35
     *
36
     * @const string
37
     */
38
    const GENERIC_ERROR = 'Paste does not exist, has expired or has been deleted.';
39
40
    /**
41
     * configuration
42
     *
43
     * @access private
44
     * @var    Configuration
45
     */
46
    private $_conf;
47
48
    /**
49
     * data
50
     *
51
     * @access private
52
     * @var    string
53
     */
54
    private $_data = '';
55
56
    /**
57
     * does the paste expire
58
     *
59
     * @access private
60
     * @var    bool
61
     */
62
    private $_doesExpire = false;
63
64
    /**
65
     * error message
66
     *
67
     * @access private
68
     * @var    string
69
     */
70
    private $_error = '';
71
72
    /**
73
     * status message
74
     *
75
     * @access private
76
     * @var    string
77
     */
78
    private $_status = '';
79
80
    /**
81
     * JSON message
82
     *
83
     * @access private
84
     * @var    string
85
     */
86
    private $_json = '';
87
88
    /**
89
     * Factory of instance models
90
     *
91
     * @access private
92
     * @var    model
93
     */
94
    private $_model;
95
96
    /**
97
     * request
98
     *
99
     * @access private
100
     * @var    request
101
     */
102
    private $_request;
103
104
    /**
105
     * URL base
106
     *
107
     * @access private
108
     * @var    string
109
     */
110
    private $_urlBase;
111
112
    /**
113
     * constructor
114
     *
115
     * initializes and runs PrivateBin
116
     *
117
     * @access public
118
     * @throws Exception
119
     * @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...
120
     */
121 96
    public function __construct()
122
    {
123 96
        if (version_compare(PHP_VERSION, '5.3.0') < 0) {
124
            throw new Exception(I18n::_('%s requires php 5.3.0 or above to work. Sorry.', I18n::_('PrivateBin')), 1);
125
        }
126 96
        if (strlen(PATH) < 0 && substr(PATH, -1) !== DIRECTORY_SEPARATOR) {
127
            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);
128
        }
129
130
        // load config from ini file, initialize required classes
131 96
        $this->_init();
132
133 94
        switch ($this->_request->getOperation()) {
134 94
            case 'create':
135 42
                $this->_create();
136 42
                break;
137 52
            case 'delete':
138 18
                $this->_delete(
139 18
                    $this->_request->getParam('pasteid'),
140 18
                    $this->_request->getParam('deletetoken')
141
                );
142 18
                break;
143 34
            case 'read':
144 19
                $this->_read($this->_request->getParam('pasteid'));
145 19
                break;
146 15
            case 'jsonld':
147 5
                $this->_jsonld($this->_request->getParam('jsonld'));
148 5
                return;
149
        }
150
151
        // output JSON or HTML
152 89
        if ($this->_request->isJsonApiCall()) {
153 53
            header('Content-type: ' . Request::MIME_JSON);
154 53
            header('Access-Control-Allow-Origin: *');
155 53
            header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
156 53
            header('Access-Control-Allow-Headers: X-Requested-With, Content-Type');
157 53
            echo $this->_json;
158
        } else {
159 36
            $this->_view();
160
        }
161 89
    }
162
163
    /**
164
     * initialize privatebin
165
     *
166
     * @access private
167
     * @return void
168
     */
169 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...
170
    {
171 96
        foreach (array('cfg', 'lib') as $dir) {
172 96
            if (!is_file(PATH . $dir . DIRECTORY_SEPARATOR . '.htaccess')) {
173 2
                file_put_contents(
174 2
                PATH . $dir . DIRECTORY_SEPARATOR . '.htaccess',
175 2
                'Allow from none' . PHP_EOL .
176 2
                'Deny from all' . PHP_EOL,
177 96
                LOCK_EX
178
            );
179
            }
180
        }
181
182 96
        $this->_conf    = new Configuration;
183 94
        $this->_model   = new Model($this->_conf);
184 94
        $this->_request = new Request;
185 94
        $this->_urlBase = array_key_exists('REQUEST_URI', $_SERVER) ?
186 94
            htmlspecialchars($_SERVER['REQUEST_URI']) : '/';
187 94
        ServerSalt::setPath($this->_conf->getKey('dir', 'traffic'));
188
189
        // set default language
190 94
        $lang = $this->_conf->getKey('languagedefault');
191 94
        I18n::setLanguageFallback($lang);
192
        // force default language, if language selection is disabled and a default is set
193 94
        if (!$this->_conf->getKey('languageselection') && strlen($lang) == 2) {
194 2
            $_COOKIE['lang'] = $lang;
195 2
            setcookie('lang', $lang);
196
        }
197 94
    }
198
199
    /**
200
     * Store new paste or comment
201
     *
202
     * POST contains one or both:
203
     * data = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
204
     * attachment = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
205
     *
206
     * All optional data will go to meta information:
207
     * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never)
208
     * formatter (optional) = format to display the paste as (plaintext,syntaxhighlighting,markdown) (default:syntaxhighlighting)
209
     * burnafterreading (optional) = if this paste may only viewed once ? (0/1) (default:0)
210
     * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0)
211
     * attachmentname = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct)
212
     * 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)
213
     * parentid (optional) = in discussion, which comment this comment replies to.
214
     * pasteid (optional) = in discussion, which paste this comment belongs to.
215
     *
216
     * @access private
217
     * @return string
218
     */
219 42
    private function _create()
220
    {
221
        // Ensure last paste from visitors IP address was more than configured amount of seconds ago.
222 42
        TrafficLimiter::setConfiguration($this->_conf);
223 42
        if (!TrafficLimiter::canPass()) {
224 2
            return $this->_return_message(
225 2
            1, I18n::_(
226 2
                'Please wait %d seconds between each post.',
227 2
                $this->_conf->getKey('limit', 'traffic')
228
            )
229
        );
230
        }
231
232 42
        $data           = $this->_request->getParam('data');
233 42
        $attachment     = $this->_request->getParam('attachment');
234 42
        $attachmentname = $this->_request->getParam('attachmentname');
235
236
        // Ensure content is not too big.
237 42
        $sizelimit = $this->_conf->getKey('sizelimit');
238
        if (
239 42
            strlen($data) + strlen($attachment) + strlen($attachmentname) > $sizelimit
240
        ) {
241 2
            return $this->_return_message(
242 2
            1,
243 2
            I18n::_(
244 2
                'Paste is limited to %s of encrypted data.',
245 2
                Filter::formatHumanReadableSize($sizelimit)
246
            )
247
        );
248
        }
249
250
        // Ensure attachment did not get lost due to webserver limits or Suhosin
251 40
        if (strlen($attachmentname) > 0 && strlen($attachment) == 0) {
252 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.');
253
        }
254
255
        // The user posts a comment.
256 38
        $pasteid  = $this->_request->getParam('pasteid');
257 38
        $parentid = $this->_request->getParam('parentid');
258 38
        if (!empty($pasteid) && !empty($parentid)) {
259 12
            $paste = $this->_model->getPaste($pasteid);
260 12
            if ($paste->exists()) {
261
                try {
262 10
                    $comment = $paste->getComment($parentid);
263
264 8
                    $nickname = $this->_request->getParam('nickname');
265 8
                    if (!empty($nickname)) {
266 8
                        $comment->setNickname($nickname);
267
                    }
268
269 6
                    $comment->setData($data);
270 6
                    $comment->store();
271 8
                } catch (Exception $e) {
272 8
                    return $this->_return_message(1, $e->getMessage());
273
                }
274 2
                $this->_return_message(0, $comment->getId());
275
            } else {
276 4
                $this->_return_message(1, 'Invalid data.');
277
            }
278
        }
279
        // The user posts a standard paste.
280
        else {
281 26
            $this->_model->purge();
282 26
            $paste = $this->_model->getPaste();
283
            try {
284 26
                $paste->setData($data);
285
286 26
                if (!empty($attachment)) {
287 2
                    $paste->setAttachment($attachment);
288 2
                    if (!empty($attachmentname)) {
289 2
                        $paste->setAttachmentName($attachmentname);
290
                    }
291
                }
292
293 26
                $expire = $this->_request->getParam('expire');
294 26
                if (!empty($expire)) {
295 6
                    $paste->setExpiration($expire);
296
                }
297
298 26
                $burnafterreading = $this->_request->getParam('burnafterreading');
299 26
                if (!empty($burnafterreading)) {
300 2
                    $paste->setBurnafterreading($burnafterreading);
301
                }
302
303 24
                $opendiscussion = $this->_request->getParam('opendiscussion');
304 24
                if (!empty($opendiscussion)) {
305 4
                    $paste->setOpendiscussion($opendiscussion);
306
                }
307
308 22
                $formatter = $this->_request->getParam('formatter');
309 22
                if (!empty($formatter)) {
310 2
                    $paste->setFormatter($formatter);
311
                }
312
313 22
                $paste->store();
314 6
            } catch (Exception $e) {
315 6
                return $this->_return_message(1, $e->getMessage());
316
            }
317 20
            $this->_return_message(0, $paste->getId(), array('deletetoken' => $paste->getDeleteToken()));
318
        }
319 24
    }
320
321
    /**
322
     * Delete an existing paste
323
     *
324
     * @access private
325
     * @param  string $dataid
326
     * @param  string $deletetoken
327
     * @return void
328
     */
329 18
    private function _delete($dataid, $deletetoken)
330
    {
331
        try {
332 18
            $paste = $this->_model->getPaste($dataid);
333 16
            if ($paste->exists()) {
334
                // accessing this property ensures that the paste would be
335
                // deleted if it has already expired
336 14
                $burnafterreading = $paste->isBurnafterreading();
337
                if (
338 12
                    ($burnafterreading && $deletetoken == 'burnafterreading') ||
339 12
                    Filter::slowEquals($deletetoken, $paste->getDeleteToken())
340
                ) {
341
                    // Paste exists and deletion token is valid: Delete the paste.
342 8
                    $paste->delete();
343 8
                    $this->_status = 'Paste was properly deleted.';
344
                } else {
345 4
                    if (!$burnafterreading && $deletetoken == 'burnafterreading') {
346 2
                        $this->_error = 'Paste is not of burn-after-reading type.';
347
                    } else {
348 12
                        $this->_error = 'Wrong deletion token. Paste was not deleted.';
349
                    }
350
                }
351
            } else {
352 14
                $this->_error = self::GENERIC_ERROR;
353
            }
354 4
        } catch (Exception $e) {
355 4
            $this->_error = $e->getMessage();
356
        }
357 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...
358 6
            if (strlen($this->_error)) {
359 2
                $this->_return_message(1, $this->_error);
360
            } else {
361 4
                $this->_return_message(0, $dataid);
362
            }
363
        }
364 18
    }
365
366
    /**
367
     * Read an existing paste or comment
368
     *
369
     * @access private
370
     * @param  string $dataid
371
     * @return void
372
     */
373 19
    private function _read($dataid)
374
    {
375
        try {
376 19
            $paste = $this->_model->getPaste($dataid);
377 17
            if ($paste->exists()) {
378 13
                $data              = $paste->get();
379 11
                $this->_doesExpire = property_exists($data, 'meta') && property_exists($data->meta, 'expire_date');
380 11
                if (property_exists($data->meta, 'salt')) {
381 11
                    unset($data->meta->salt);
382
                }
383 11
                $this->_data = json_encode($data);
384
            } else {
385 15
                $this->_error = self::GENERIC_ERROR;
386
            }
387 4
        } catch (Exception $e) {
388 4
            $this->_error = $e->getMessage();
389
        }
390
391 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...
392 5
            if (strlen($this->_error)) {
393 2
                $this->_return_message(1, $this->_error);
394
            } else {
395 3
                $this->_return_message(0, $dataid, json_decode($this->_data, true));
396
            }
397
        }
398 19
    }
399
400
    /**
401
     * Display PrivateBin frontend.
402
     *
403
     * @access private
404
     * @return void
405
     */
406 36
    private function _view()
407
    {
408
        // set headers to disable caching
409 36
        $time = gmdate('D, d M Y H:i:s \G\M\T');
410 36
        header('Cache-Control: no-store, no-cache, no-transform, must-revalidate');
411 36
        header('Pragma: no-cache');
412 36
        header('Expires: ' . $time);
413 36
        header('Last-Modified: ' . $time);
414 36
        header('Vary: Accept');
415 36
        header('Content-Security-Policy: ' . $this->_conf->getKey('cspheader'));
416 36
        header('X-Xss-Protection: 1; mode=block');
417 36
        header('X-Frame-Options: DENY');
418 36
        header('X-Content-Type-Options: nosniff');
419
420
        // label all the expiration options
421 36
        $expire = array();
422 36
        foreach ($this->_conf->getSection('expire_options') as $time => $seconds) {
423 36
            $expire[$time] = ($seconds == 0) ? I18n::_(ucfirst($time)) : Filter::formatHumanReadableTime($time);
424
        }
425
426
        // translate all the formatter options
427 36
        $formatters = array_map('PrivateBin\\I18n::_', $this->_conf->getSection('formatter_options'));
428
429
        // set language cookie if that functionality was enabled
430 36
        $languageselection = '';
431 36
        if ($this->_conf->getKey('languageselection')) {
432 2
            $languageselection = I18n::getLanguage();
433 2
            setcookie('lang', $languageselection);
434
        }
435
436 36
        $page = new View;
437 36
        $page->assign('NAME', $this->_conf->getKey('name'));
438 36
        $page->assign('CIPHERDATA', $this->_data);
439 36
        $page->assign('ERROR', I18n::_($this->_error));
440 36
        $page->assign('STATUS', I18n::_($this->_status));
441 36
        $page->assign('VERSION', self::VERSION);
442 36
        $page->assign('DISCUSSION', $this->_conf->getKey('discussion'));
443 36
        $page->assign('OPENDISCUSSION', $this->_conf->getKey('opendiscussion'));
444 36
        $page->assign('MARKDOWN', array_key_exists('markdown', $formatters));
445 36
        $page->assign('SYNTAXHIGHLIGHTING', array_key_exists('syntaxhighlighting', $formatters));
446 36
        $page->assign('SYNTAXHIGHLIGHTINGTHEME', $this->_conf->getKey('syntaxhighlightingtheme'));
447 36
        $page->assign('FORMATTER', $formatters);
448 36
        $page->assign('FORMATTERDEFAULT', $this->_conf->getKey('defaultformatter'));
449 36
        $page->assign('NOTICE', I18n::_($this->_conf->getKey('notice')));
450 36
        $page->assign('BURNAFTERREADINGSELECTED', $this->_conf->getKey('burnafterreadingselected'));
451 36
        $page->assign('PASSWORD', $this->_conf->getKey('password'));
452 36
        $page->assign('FILEUPLOAD', $this->_conf->getKey('fileupload'));
453 36
        $page->assign('ZEROBINCOMPATIBILITY', $this->_conf->getKey('zerobincompatibility'));
454 36
        $page->assign('LANGUAGESELECTION', $languageselection);
455 36
        $page->assign('LANGUAGES', I18n::getLanguageLabels(I18n::getAvailableLanguages()));
456 36
        $page->assign('EXPIRE', $expire);
457 36
        $page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire'));
458 36
        $page->assign('EXPIRECLONE', !$this->_doesExpire || ($this->_doesExpire && $this->_conf->getKey('clone', 'expire')));
459 36
        $page->assign('URLSHORTENER', $this->_conf->getKey('urlshortener'));
460 36
        $page->draw($this->_conf->getKey('template'));
461 36
    }
462
463
    /**
464
     * outputs requested JSON-LD context
465
     *
466
     * @access private
467
     * @param string $type
468
     * @return void
469
     */
470 5
    private function _jsonld($type)
471
    {
472
        if (
473 5
            $type !== 'paste' && $type !== 'comment' &&
474 5
            $type !== 'pastemeta' && $type !== 'commentmeta'
475
        ) {
476 1
            $type = '';
477
        }
478 5
        $content = '{}';
479 5
        $file    = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . $type . '.jsonld';
480 5
        if (is_readable($file)) {
481 4
            $content = str_replace(
482 4
                '?jsonld=',
483 4
                $this->_urlBase . '?jsonld=',
484
                file_get_contents($file)
485
            );
486
        }
487
488 5
        header('Content-type: application/ld+json');
489 5
        header('Access-Control-Allow-Origin: *');
490 5
        header('Access-Control-Allow-Methods: GET');
491 5
        echo $content;
492 5
    }
493
494
    /**
495
     * prepares JSON encoded status message
496
     *
497
     * @access private
498
     * @param  int $status
499
     * @param  string $message
500
     * @param  array $other
501
     * @return void
502
     */
503 53
    private function _return_message($status, $message, $other = array())
504
    {
505 53
        $result = array('status' => $status);
506 53
        if ($status) {
507 26
            $result['message'] = I18n::_($message);
508
        } else {
509 29
            $result['id']  = $message;
510 29
            $result['url'] = $this->_urlBase . '?' . $message;
511
        }
512 53
        $result += $other;
513 53
        $this->_json = json_encode($result);
514 53
    }
515
}
516