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