Passed
Pull Request — master (#447)
by El
02:44
created

lib/Model/Paste.php (1 issue)

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