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