Issues (12)

lib/Model/Paste.php (1 issue)

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
 */
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
     * @return array
223
     */
224 45
    protected function _sanitize(array $data)
225
    {
226 45
        $expiration = $data['meta']['expire'];
227 45
        unset($data['meta']['expire']);
228 45
        $expire_options = $this->_conf->getSection('expire_options');
229 45
        if (array_key_exists($expiration, $expire_options)) {
230 39
            $expire = $expire_options[$expiration];
231
        } else {
232
            // using getKey() to ensure a default value is present
233 6
            $expire = $this->_conf->getKey($this->_conf->getKey('default', 'expire'), 'expire_options');
234
        }
235 45
        if ($expire > 0) {
236 45
            $data['meta']['expire_date'] = time() + $expire;
237
        }
238 45
        return $data;
239
    }
240
241
    /**
242
     * Validate data.
243
     *
244
     * @access protected
245
     * @param  array $data
246
     * @throws Exception
247
     */
248 45
    protected function _validate(array $data)
249
    {
250
        // reject invalid or disabled formatters
251 45
        if (!array_key_exists($data['adata'][1], $this->_conf->getSection('formatter_options'))) {
252 1
            throw new Exception('Invalid data.', 75);
253
        }
254
255
        // discussion requested, but disabled in config or burn after reading requested as well, or invalid integer
256
        if (
257 44
            ($data['adata'][2] === 1 && ( // open discussion flag
0 ignored issues
show
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...
258 40
                !$this->_conf->getKey('discussion') ||
259 40
                $data['adata'][3] === 1  // burn after reading flag
260
            )) ||
261 44
            ($data['adata'][2] !== 0 && $data['adata'][2] !== 1)
262
        ) {
263 3
            throw new Exception('Invalid data.', 74);
264
        }
265
266
        // reject invalid burn after reading
267 41
        if ($data['adata'][3] !== 0 && $data['adata'][3] !== 1) {
268 3
            throw new Exception('Invalid data.', 73);
269
        }
270
    }
271
}
272