Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
83 / 83
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
Paste
100.00% covered (success)
100.00%
83 / 83
100.00% covered (success)
100.00%
10 / 10
48
100.00% covered (success)
100.00%
1 / 1
 get
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
15
 store
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 exists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getComment
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getComments
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getDeleteToken
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 isOpendiscussion
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
6
 _sanitize
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 _validate
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
9
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
12namespace PrivateBin\Model;
13
14use Exception;
15use PrivateBin\Controller;
16use PrivateBin\Persistence\ServerSalt;
17
18/**
19 * Paste
20 *
21 * Model of a PrivateBin paste.
22 */
23class Paste extends AbstractModel
24{
25    /**
26     * Get paste data.
27     *
28     * @access public
29     * @throws Exception
30     * @return array
31     */
32    public function get()
33    {
34        $data = $this->_store->read($this->getId());
35        if ($data === false) {
36            throw new Exception(Controller::GENERIC_ERROR, 64);
37        }
38
39        // check if paste has expired and delete it if necessary.
40        if (array_key_exists('expire_date', $data['meta'])) {
41            if ($data['meta']['expire_date'] < time()) {
42                $this->delete();
43                throw new Exception(Controller::GENERIC_ERROR, 63);
44            }
45            // We kindly provide the remaining time before expiration (in seconds)
46            $data['meta']['time_to_live'] = $data['meta']['expire_date'] - time();
47            unset($data['meta']['expire_date']);
48        }
49        foreach (array('created', 'postdate') as $key) {
50            if (array_key_exists($key, $data['meta'])) {
51                unset($data['meta'][$key]);
52            }
53        }
54
55        // check if non-expired burn after reading paste needs to be deleted
56        if (
57            (array_key_exists('adata', $data) && $data['adata'][3] === 1) ||
58            (array_key_exists('burnafterreading', $data['meta']) && $data['meta']['burnafterreading'])
59        ) {
60            $this->delete();
61        }
62
63        // set formatter for the view in version 1 pastes.
64        if (array_key_exists('data', $data) && !array_key_exists('formatter', $data['meta'])) {
65            // support < 0.21 syntax highlighting
66            if (array_key_exists('syntaxcoloring', $data['meta']) && $data['meta']['syntaxcoloring'] === true) {
67                $data['meta']['formatter'] = 'syntaxhighlighting';
68            } else {
69                $data['meta']['formatter'] = $this->_conf->getKey('defaultformatter');
70            }
71        }
72
73        // support old paste format with server wide salt
74        if (!array_key_exists('salt', $data['meta'])) {
75            $data['meta']['salt'] = ServerSalt::get();
76        }
77        $data['comments']       = array_values($this->getComments());
78        $data['comment_count']  = count($data['comments']);
79        $data['comment_offset'] = 0;
80        $data['@context']       = '?jsonld=paste';
81        $this->_data            = $data;
82
83        return $this->_data;
84    }
85
86    /**
87     * Store the paste's data.
88     *
89     * @access public
90     * @throws Exception
91     */
92    public function store()
93    {
94        // Check for improbable collision.
95        if ($this->exists()) {
96            throw new Exception('You are unlucky. Try again.', 75);
97        }
98
99        $this->_data['meta']['salt'] = ServerSalt::generate();
100
101        // store paste
102        if (
103            $this->_store->create(
104                $this->getId(),
105                $this->_data
106            ) === false
107        ) {
108            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    public function delete()
119    {
120        $this->_store->delete($this->getId());
121    }
122
123    /**
124     * Test if paste exists in store.
125     *
126     * @access public
127     * @return bool
128     */
129    public function exists()
130    {
131        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    public function getComment($parentId, $commentId = '')
144    {
145        if (!$this->exists()) {
146            throw new Exception('Invalid data.', 62);
147        }
148        $comment = new Comment($this->_conf, $this->_store);
149        $comment->setPaste($this);
150        $comment->setParentId($parentId);
151        if ($commentId !== '') {
152            $comment->setId($commentId);
153        }
154        return $comment;
155    }
156
157    /**
158     * Get all comments, if any.
159     *
160     * @access public
161     * @return array
162     */
163    public function getComments()
164    {
165        if ($this->_conf->getKey('discussiondatedisplay')) {
166            return $this->_store->readComments($this->getId());
167        }
168        return array_map(function ($comment) {
169            foreach (array('created', 'postdate') as $key) {
170                if (array_key_exists($key, $comment['meta'])) {
171                    unset($comment['meta'][$key]);
172                }
173            }
174            return $comment;
175        }, $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    public function getDeleteToken()
189    {
190        if (!array_key_exists('salt', $this->_data['meta'])) {
191            $this->get();
192        }
193        return hash_hmac(
194            $this->_conf->getKey('zerobincompatibility') ? 'sha1' : 'sha256',
195            $this->getId(),
196            $this->_data['meta']['salt']
197        );
198    }
199
200    /**
201     * Check if paste has discussions enabled.
202     *
203     * @access public
204     * @throws Exception
205     * @return bool
206     */
207    public function isOpendiscussion()
208    {
209        if (!array_key_exists('adata', $this->_data) && !array_key_exists('data', $this->_data)) {
210            $this->get();
211        }
212        return
213            (array_key_exists('adata', $this->_data) && $this->_data['adata'][2] === 1) ||
214            (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    protected function _sanitize(array $data)
225    {
226        $expiration = $data['meta']['expire'];
227        unset($data['meta']['expire']);
228        $expire_options = $this->_conf->getSection('expire_options');
229        if (array_key_exists($expiration, $expire_options)) {
230            $expire = $expire_options[$expiration];
231        } else {
232            // using getKey() to ensure a default value is present
233            $expire = $this->_conf->getKey($this->_conf->getKey('default', 'expire'), 'expire_options');
234        }
235        if ($expire > 0) {
236            $data['meta']['expire_date'] = time() + $expire;
237        }
238        return $data;
239    }
240
241    /**
242     * Validate data.
243     *
244     * @access protected
245     * @param  array $data
246     * @throws Exception
247     */
248    protected function _validate(array $data)
249    {
250        // reject invalid or disabled formatters
251        if (!array_key_exists($data['adata'][1], $this->_conf->getSection('formatter_options'))) {
252            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            ($data['adata'][2] === 1 && ( // open discussion flag
258                !$this->_conf->getKey('discussion') ||
259                $data['adata'][3] === 1  // burn after reading flag
260            )) ||
261            ($data['adata'][2] !== 0 && $data['adata'][2] !== 1)
262        ) {
263            throw new Exception('Invalid data.', 74);
264        }
265
266        // reject invalid burn after reading
267        if ($data['adata'][3] !== 0 && $data['adata'][3] !== 1) {
268            throw new Exception('Invalid data.', 73);
269        }
270    }
271}