Paste   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 244
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 88
dl 0
loc 244
ccs 92
cts 92
cp 1
rs 8.5599
c 0
b 0
f 0
wmc 48

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getDeleteToken() 0 9 3
A exists() 0 3 1
A getComment() 0 12 3
C get() 0 52 15
A delete() 0 3 1
A getComments() 0 13 4
A store() 0 17 3
A isOpendiscussion() 0 8 6
A _sanitize() 0 13 3
B _validate() 0 21 9

How to fix   Complexity   

Complex Class

Complex classes like Paste often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Paste, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
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
 */
11
12
namespace PrivateBin\Model;
13
14
use Exception;
15
use PrivateBin\Controller;
16
use PrivateBin\Persistence\ServerSalt;
17
18
/**
19
 * Paste
20
 *
21
 * Model of a PrivateBin paste.
22
 */
23
class Paste extends AbstractModel
24
{
25
    /**
26
     * Get paste data.
27
     *
28
     * @access public
29
     * @throws Exception
30
     * @return array
31
     */
32 49
    public function get()
33
    {
34 49
        $data = $this->_store->read($this->getId());
35 49
        if ($data === false) {
36 1
            throw new Exception(Controller::GENERIC_ERROR, 64);
37
        }
38
39
        // check if paste has expired and delete it if necessary.
40 48
        if (array_key_exists('expire_date', $data['meta'])) {
41 11
            if ($data['meta']['expire_date'] < time()) {
42 6
                $this->delete();
43 6
                throw new Exception(Controller::GENERIC_ERROR, 63);
44
            }
45
            // We kindly provide the remaining time before expiration (in seconds)
46 5
            $data['meta']['time_to_live'] = $data['meta']['expire_date'] - time();
47 5
            unset($data['meta']['expire_date']);
48
        }
49 42
        foreach (array('created', 'postdate') as $key) {
50 42
            if (array_key_exists($key, $data['meta'])) {
51 4
                unset($data['meta'][$key]);
52
            }
53
        }
54
55
        // check if non-expired burn after reading paste needs to be deleted
56
        if (
57 42
            (array_key_exists('adata', $data) && $data['adata'][3] === 1) ||
58 42
            (array_key_exists('burnafterreading', $data['meta']) && $data['meta']['burnafterreading'])
59
        ) {
60 6
            $this->delete();
61
        }
62
63
        // set formatter for the view in version 1 pastes.
64 42
        if (array_key_exists('data', $data) && !array_key_exists('formatter', $data['meta'])) {
65
            // support < 0.21 syntax highlighting
66 4
            if (array_key_exists('syntaxcoloring', $data['meta']) && $data['meta']['syntaxcoloring'] === true) {
67 3
                $data['meta']['formatter'] = 'syntaxhighlighting';
68
            } else {
69 1
                $data['meta']['formatter'] = $this->_conf->getKey('defaultformatter');
70
            }
71
        }
72
73
        // support old paste format with server wide salt
74 42
        if (!array_key_exists('salt', $data['meta'])) {
75 6
            $data['meta']['salt'] = ServerSalt::get();
76
        }
77 42
        $data['comments']       = array_values($this->getComments());
78 42
        $data['comment_count']  = count($data['comments']);
79 42
        $data['comment_offset'] = 0;
80 42
        $data['@context']       = '?jsonld=paste';
81 42
        $this->_data            = $data;
82
83 42
        return $this->_data;
84
    }
85
86
    /**
87
     * Store the paste's data.
88
     *
89
     * @access public
90
     * @throws Exception
91
     */
92 38
    public function store()
93
    {
94
        // Check for improbable collision.
95 38
        if ($this->exists()) {
96 4
            throw new Exception('You are unlucky. Try again.', 75);
97
        }
98
99 35
        $this->_data['meta']['salt'] = ServerSalt::generate();
100
101
        // store paste
102
        if (
103 35
            $this->_store->create(
104 35
                $this->getId(),
105 35
                $this->_data
106 35
            ) === false
107
        ) {
108 1
            throw new Exception('Error saving paste. Sorry.', 76);
109
        }
110
    }
111
112
    /**
113
     * Delete the paste.
114
     *
115
     * @access public
116
     * @throws Exception
117
     */
118 34
    public function delete()
119
    {
120 34
        $this->_store->delete($this->getId());
121
    }
122
123
    /**
124
     * Test if paste exists in store.
125
     *
126
     * @access public
127
     * @return bool
128
     */
129 96
    public function exists()
130
    {
131 96
        return $this->_store->exists($this->getId());
132
    }
133
134
    /**
135
     * Get a comment, optionally a specific instance.
136
     *
137
     * @access public
138
     * @param string $parentId
139
     * @param string $commentId
140
     * @throws Exception
141
     * @return Comment
142
     */
143 23
    public function getComment($parentId, $commentId = '')
144
    {
145 23
        if (!$this->exists()) {
146 1
            throw new Exception('Invalid data.', 62);
147
        }
148 22
        $comment = new Comment($this->_conf, $this->_store);
149 22
        $comment->setPaste($this);
150 22
        $comment->setParentId($parentId);
151 18
        if ($commentId !== '') {
152 2
            $comment->setId($commentId);
153
        }
154 18
        return $comment;
155
    }
156
157
    /**
158
     * Get all comments, if any.
159
     *
160
     * @access public
161
     * @return array
162
     */
163 42
    public function getComments()
164
    {
165 42
        if ($this->_conf->getKey('discussiondatedisplay')) {
166 42
            return $this->_store->readComments($this->getId());
167
        }
168 1
        return array_map(function ($comment) {
169 1
            foreach (array('created', 'postdate') as $key) {
170 1
                if (array_key_exists($key, $comment['meta'])) {
171 1
                    unset($comment['meta'][$key]);
172
                }
173
            }
174 1
            return $comment;
175 1
        }, $this->_store->readComments($this->getId()));
176
    }
177
178
    /**
179
     * Generate the "delete" token.
180
     *
181
     * The token is the hmac of the pastes ID signed with the server salt.
182
     * The paste can be deleted by calling:
183
     * https://example.com/privatebin/?pasteid=<pasteid>&deletetoken=<deletetoken>
184
     *
185
     * @access public
186
     * @return string
187
     */
188 38
    public function getDeleteToken()
189
    {
190 38
        if (!array_key_exists('salt', $this->_data['meta'])) {
191 1
            $this->get();
192
        }
193 38
        return hash_hmac(
194 38
            $this->_conf->getKey('zerobincompatibility') ? 'sha1' : 'sha256',
195 38
            $this->getId(),
196 38
            $this->_data['meta']['salt']
197 38
        );
198
    }
199
200
    /**
201
     * Check if paste has discussions enabled.
202
     *
203
     * @access public
204
     * @throws Exception
205
     * @return bool
206
     */
207 16
    public function isOpendiscussion()
208
    {
209 16
        if (!array_key_exists('adata', $this->_data) && !array_key_exists('data', $this->_data)) {
210 11
            $this->get();
211
        }
212 16
        return
213 16
            (array_key_exists('adata', $this->_data) && $this->_data['adata'][2] === 1) ||
214 16
            (array_key_exists('opendiscussion', $this->_data['meta']) && $this->_data['meta']['opendiscussion']);
215
    }
216
217
    /**
218
     * Sanitizes data to conform with current configuration.
219
     *
220
     * @access protected
221
     * @param  array $data
222
     */
223 45
    protected function _sanitize(array &$data)
224
    {
225 45
        $expiration = $data['meta']['expire'] ?? 0;
226 45
        unset($data['meta']['expire']);
227 45
        $expire_options = $this->_conf->getSection('expire_options');
228 45
        if (array_key_exists($expiration, $expire_options)) {
229 39
            $expire = $expire_options[$expiration];
230
        } else {
231
            // using getKey() to ensure a default value is present
232 7
            $expire = $this->_conf->getKey($this->_conf->getKey('default', 'expire'), 'expire_options');
233
        }
234 45
        if ($expire > 0) {
235 45
            $data['meta']['expire_date'] = time() + $expire;
236
        }
237
    }
238
239
    /**
240
     * Validate data.
241
     *
242
     * @access protected
243
     * @param  array $data
244
     * @throws Exception
245
     */
246 45
    protected function _validate(array &$data)
247
    {
248
        // reject invalid or disabled formatters
249 45
        if (!array_key_exists($data['adata'][1], $this->_conf->getSection('formatter_options'))) {
250 1
            throw new Exception('Invalid data.', 75);
251
        }
252
253
        // discussion requested, but disabled in config or burn after reading requested as well, or invalid integer
254
        if (
255 44
            ($data['adata'][2] === 1 && ( // open discussion flag
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($data['adata'][2] === 1...$data['adata'][2] !== 1, Probably Intended Meaning: $data['adata'][2] === 1 ...data['adata'][2] !== 1)
Loading history...
256 40
                !$this->_conf->getKey('discussion') ||
257 40
                $data['adata'][3] === 1  // burn after reading flag
258
            )) ||
259 44
            ($data['adata'][2] !== 0 && $data['adata'][2] !== 1)
260
        ) {
261 3
            throw new Exception('Invalid data.', 74);
262
        }
263
264
        // reject invalid burn after reading
265 41
        if ($data['adata'][3] !== 0 && $data['adata'][3] !== 1) {
266 3
            throw new Exception('Invalid data.', 73);
267
        }
268
    }
269
}
270