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