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.2 |
||
11 | */ |
||
12 | |||
13 | namespace PrivateBin; |
||
14 | |||
15 | use Exception; |
||
16 | use PrivateBin\Persistence\ServerSalt; |
||
17 | use PrivateBin\Persistence\TrafficLimiter; |
||
18 | |||
19 | /** |
||
20 | * PrivateBin |
||
21 | * |
||
22 | * Controller, puts it all together. |
||
23 | */ |
||
24 | class PrivateBin |
||
25 | { |
||
26 | /** |
||
27 | * version |
||
28 | * |
||
29 | * @const string |
||
30 | */ |
||
31 | const VERSION = '1.2'; |
||
32 | |||
33 | /** |
||
34 | * minimal required PHP version |
||
35 | * |
||
36 | * @const string |
||
37 | */ |
||
38 | const MIN_PHP_VERSION = '5.4.0'; |
||
39 | |||
40 | /** |
||
41 | * show the same error message if the paste expired or does not exist |
||
42 | * |
||
43 | * @const string |
||
44 | */ |
||
45 | const GENERIC_ERROR = 'Paste does not exist, has expired or has been deleted.'; |
||
46 | |||
47 | /** |
||
48 | * configuration |
||
49 | * |
||
50 | * @access private |
||
51 | * @var Configuration |
||
52 | */ |
||
53 | private $_conf; |
||
54 | |||
55 | /** |
||
56 | * error message |
||
57 | * |
||
58 | * @access private |
||
59 | * @var string |
||
60 | */ |
||
61 | private $_error = ''; |
||
62 | |||
63 | /** |
||
64 | * status message |
||
65 | * |
||
66 | * @access private |
||
67 | * @var string |
||
68 | */ |
||
69 | private $_status = ''; |
||
70 | |||
71 | /** |
||
72 | * JSON message |
||
73 | * |
||
74 | * @access private |
||
75 | * @var string |
||
76 | */ |
||
77 | private $_json = ''; |
||
78 | |||
79 | /** |
||
80 | * Factory of instance models |
||
81 | * |
||
82 | * @access private |
||
83 | * @var model |
||
84 | */ |
||
85 | private $_model; |
||
86 | |||
87 | /** |
||
88 | * request |
||
89 | * |
||
90 | * @access private |
||
91 | * @var request |
||
92 | */ |
||
93 | private $_request; |
||
94 | |||
95 | /** |
||
96 | * URL base |
||
97 | * |
||
98 | * @access private |
||
99 | * @var string |
||
100 | */ |
||
101 | private $_urlBase; |
||
102 | |||
103 | /** |
||
104 | * constructor |
||
105 | * |
||
106 | * initializes and runs PrivateBin |
||
107 | * |
||
108 | * @access public |
||
109 | * @throws Exception |
||
110 | */ |
||
111 | 90 | public function __construct() |
|
112 | { |
||
113 | 90 | if (version_compare(PHP_VERSION, self::MIN_PHP_VERSION) < 0) { |
|
114 | throw new Exception(I18n::_('%s requires php %s or above to work. Sorry.', I18n::_('PrivateBin'), self::MIN_PHP_VERSION), 1); |
||
115 | } |
||
116 | 90 | if (strlen(PATH) < 0 && substr(PATH, -1) !== DIRECTORY_SEPARATOR) { |
|
117 | throw new Exception(I18n::_('%s requires the PATH to end in a "%s". Please update the PATH in your index.php.', I18n::_('PrivateBin'), DIRECTORY_SEPARATOR), 5); |
||
118 | } |
||
119 | |||
120 | // load config from ini file, initialize required classes |
||
121 | 90 | $this->_init(); |
|
122 | |||
123 | 88 | switch ($this->_request->getOperation()) { |
|
124 | 88 | case 'create': |
|
125 | 44 | $this->_create(); |
|
126 | 44 | break; |
|
127 | 44 | case 'delete': |
|
128 | 18 | $this->_delete( |
|
129 | 18 | $this->_request->getParam('pasteid'), |
|
130 | 18 | $this->_request->getParam('deletetoken') |
|
131 | ); |
||
132 | 18 | break; |
|
133 | 26 | case 'read': |
|
134 | 13 | $this->_read($this->_request->getParam('pasteid')); |
|
135 | 13 | break; |
|
136 | 13 | case 'jsonld': |
|
137 | 5 | $this->_jsonld($this->_request->getParam('jsonld')); |
|
138 | 5 | return; |
|
139 | } |
||
140 | |||
141 | // output JSON or HTML |
||
142 | 83 | if ($this->_request->isJsonApiCall()) { |
|
143 | 63 | header('Content-type: ' . Request::MIME_JSON); |
|
144 | 63 | header('Access-Control-Allow-Origin: *'); |
|
145 | 63 | header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE'); |
|
146 | 63 | header('Access-Control-Allow-Headers: X-Requested-With, Content-Type'); |
|
147 | 63 | echo $this->_json; |
|
148 | } else { |
||
149 | 20 | $this->_view(); |
|
150 | } |
||
151 | 83 | } |
|
152 | |||
153 | /** |
||
154 | * initialize privatebin |
||
155 | * |
||
156 | * @access private |
||
157 | */ |
||
158 | 90 | private function _init() |
|
159 | { |
||
160 | 90 | $this->_conf = new Configuration; |
|
161 | 88 | $this->_model = new Model($this->_conf); |
|
162 | 88 | $this->_request = new Request; |
|
163 | 88 | $this->_urlBase = $this->_request->getRequestUri(); |
|
164 | 88 | ServerSalt::setPath($this->_conf->getKey('dir', 'traffic')); |
|
165 | |||
166 | // set default language |
||
167 | 88 | $lang = $this->_conf->getKey('languagedefault'); |
|
168 | 88 | I18n::setLanguageFallback($lang); |
|
169 | // force default language, if language selection is disabled and a default is set |
||
170 | 88 | if (!$this->_conf->getKey('languageselection') && strlen($lang) == 2) { |
|
171 | 2 | $_COOKIE['lang'] = $lang; |
|
172 | 2 | setcookie('lang', $lang); |
|
173 | } |
||
174 | 88 | } |
|
175 | |||
176 | /** |
||
177 | * Store new paste or comment |
||
178 | * |
||
179 | * POST contains one or both: |
||
180 | * data = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) |
||
181 | * attachment = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) |
||
182 | * |
||
183 | * All optional data will go to meta information: |
||
184 | * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never) |
||
185 | * formatter (optional) = format to display the paste as (plaintext,syntaxhighlighting,markdown) (default:syntaxhighlighting) |
||
186 | * burnafterreading (optional) = if this paste may only viewed once ? (0/1) (default:0) |
||
187 | * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0) |
||
188 | * attachmentname = json encoded SJCL encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) |
||
189 | * nickname (optional) = in discussion, encoded SJCL encrypted text nickname of author of comment (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) |
||
190 | * parentid (optional) = in discussion, which comment this comment replies to. |
||
191 | * pasteid (optional) = in discussion, which paste this comment belongs to. |
||
192 | * |
||
193 | * @access private |
||
194 | * @return string |
||
195 | */ |
||
196 | 44 | private function _create() |
|
197 | { |
||
198 | // Ensure last paste from visitors IP address was more than configured amount of seconds ago. |
||
199 | 44 | TrafficLimiter::setConfiguration($this->_conf); |
|
200 | 44 | if (!TrafficLimiter::canPass()) { |
|
201 | 2 | return $this->_return_message( |
|
0 ignored issues
–
show
|
|||
202 | 2 | 1, I18n::_( |
|
203 | 2 | 'Please wait %d seconds between each post.', |
|
204 | 2 | $this->_conf->getKey('limit', 'traffic') |
|
205 | ) |
||
206 | ); |
||
207 | } |
||
208 | |||
209 | 44 | $data = $this->_request->getParam('data'); |
|
210 | 44 | $attachment = $this->_request->getParam('attachment'); |
|
211 | 44 | $attachmentname = $this->_request->getParam('attachmentname'); |
|
212 | |||
213 | // Ensure content is not too big. |
||
214 | 44 | $sizelimit = $this->_conf->getKey('sizelimit'); |
|
215 | if ( |
||
216 | 44 | strlen($data) + strlen($attachment) + strlen($attachmentname) > $sizelimit |
|
217 | ) { |
||
218 | 2 | return $this->_return_message( |
|
0 ignored issues
–
show
Are you sure the usage of
$this->_return_message(1...dableSize($sizelimit))) targeting PrivateBin\PrivateBin::_return_message() seems to always return null.
This check looks for function or method calls that always return null and whose return value is used. class A
{
function getObject()
{
return null;
}
}
$a = new A();
if ($a->getObject()) {
The method The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.
Loading history...
|
|||
219 | 2 | 1, |
|
220 | 2 | I18n::_( |
|
221 | 2 | 'Paste is limited to %s of encrypted data.', |
|
222 | 2 | Filter::formatHumanReadableSize($sizelimit) |
|
223 | ) |
||
224 | ); |
||
225 | } |
||
226 | |||
227 | // Ensure attachment did not get lost due to webserver limits or Suhosin |
||
228 | 42 | if (strlen($attachmentname) > 0 && strlen($attachment) == 0) { |
|
229 | 2 | return $this->_return_message(1, 'Attachment missing in data received by server. Please check your webserver or suhosin configuration for maximum POST parameter limitations.'); |
|
230 | } |
||
231 | |||
232 | // The user posts a comment. |
||
233 | 40 | $pasteid = $this->_request->getParam('pasteid'); |
|
234 | 40 | $parentid = $this->_request->getParam('parentid'); |
|
235 | 40 | if (!empty($pasteid) && !empty($parentid)) { |
|
236 | 12 | $paste = $this->_model->getPaste($pasteid); |
|
237 | 12 | if ($paste->exists()) { |
|
238 | try { |
||
239 | 10 | $comment = $paste->getComment($parentid); |
|
240 | |||
241 | 8 | $nickname = $this->_request->getParam('nickname'); |
|
242 | 8 | if (!empty($nickname)) { |
|
243 | 8 | $comment->setNickname($nickname); |
|
244 | } |
||
245 | |||
246 | 6 | $comment->setData($data); |
|
247 | 6 | $comment->store(); |
|
248 | 8 | } catch (Exception $e) { |
|
249 | 8 | return $this->_return_message(1, $e->getMessage()); |
|
250 | } |
||
251 | 2 | $this->_return_message(0, $comment->getId()); |
|
252 | } else { |
||
253 | 4 | $this->_return_message(1, 'Invalid data.'); |
|
254 | } |
||
255 | } |
||
256 | // The user posts a standard paste. |
||
257 | else { |
||
258 | 28 | $this->_model->purge(); |
|
259 | 28 | $paste = $this->_model->getPaste(); |
|
260 | try { |
||
261 | 28 | $paste->setData($data); |
|
262 | |||
263 | 28 | if (!empty($attachment)) { |
|
264 | 2 | $paste->setAttachment($attachment); |
|
265 | 2 | if (!empty($attachmentname)) { |
|
266 | 2 | $paste->setAttachmentName($attachmentname); |
|
267 | } |
||
268 | } |
||
269 | |||
270 | 28 | $expire = $this->_request->getParam('expire'); |
|
271 | 28 | if (!empty($expire)) { |
|
272 | 6 | $paste->setExpiration($expire); |
|
273 | } |
||
274 | |||
275 | 28 | $burnafterreading = $this->_request->getParam('burnafterreading'); |
|
276 | 28 | if (!empty($burnafterreading)) { |
|
277 | 2 | $paste->setBurnafterreading($burnafterreading); |
|
278 | } |
||
279 | |||
280 | 26 | $opendiscussion = $this->_request->getParam('opendiscussion'); |
|
281 | 26 | if (!empty($opendiscussion)) { |
|
282 | 4 | $paste->setOpendiscussion($opendiscussion); |
|
283 | } |
||
284 | |||
285 | 24 | $formatter = $this->_request->getParam('formatter'); |
|
286 | 24 | if (!empty($formatter)) { |
|
287 | 2 | $paste->setFormatter($formatter); |
|
288 | } |
||
289 | |||
290 | 24 | $paste->store(); |
|
291 | 6 | } catch (Exception $e) { |
|
292 | 6 | return $this->_return_message(1, $e->getMessage()); |
|
293 | } |
||
294 | 22 | $this->_return_message(0, $paste->getId(), array('deletetoken' => $paste->getDeleteToken())); |
|
295 | } |
||
296 | 26 | } |
|
297 | |||
298 | /** |
||
299 | * Delete an existing paste |
||
300 | * |
||
301 | * @access private |
||
302 | * @param string $dataid |
||
303 | * @param string $deletetoken |
||
304 | */ |
||
305 | 18 | private function _delete($dataid, $deletetoken) |
|
306 | { |
||
307 | try { |
||
308 | 18 | $paste = $this->_model->getPaste($dataid); |
|
309 | 16 | if ($paste->exists()) { |
|
310 | // accessing this property ensures that the paste would be |
||
311 | // deleted if it has already expired |
||
312 | 14 | $burnafterreading = $paste->isBurnafterreading(); |
|
313 | if ( |
||
314 | 12 | ($burnafterreading && $deletetoken == 'burnafterreading') || // either we burn-after it has been read //@TODO: not needed anymore now? |
|
315 | 12 | Filter::slowEquals($deletetoken, $paste->getDeleteToken()) // or we manually delete it with this secret token |
|
316 | ) { |
||
317 | // Paste exists and deletion token (if required) is valid: Delete the paste. |
||
318 | 8 | $paste->delete(); |
|
319 | 8 | $this->_status = 'Paste was properly deleted.'; |
|
320 | } else { |
||
321 | 4 | if (!$burnafterreading && $deletetoken == 'burnafterreading') { |
|
322 | 2 | $this->_error = 'Paste is not of burn-after-reading type.'; |
|
323 | } else { |
||
324 | 12 | $this->_error = 'Wrong deletion token. Paste was not deleted.'; |
|
325 | } |
||
326 | } |
||
327 | } else { |
||
328 | 14 | $this->_error = self::GENERIC_ERROR; |
|
329 | } |
||
330 | 4 | } catch (Exception $e) { |
|
331 | 4 | $this->_error = $e->getMessage(); |
|
332 | } |
||
333 | 18 | if ($this->_request->isJsonApiCall()) { |
|
334 | 6 | if (strlen($this->_error)) { |
|
335 | 2 | $this->_return_message(1, $this->_error); |
|
336 | } else { |
||
337 | 4 | $this->_return_message(0, $dataid); |
|
338 | } |
||
339 | } |
||
340 | 18 | } |
|
341 | |||
342 | /** |
||
343 | * Read an existing paste or comment, only allowed via a JSON API call |
||
344 | * |
||
345 | * @access private |
||
346 | * @param string $dataid |
||
347 | */ |
||
348 | 13 | private function _read($dataid) |
|
349 | { |
||
350 | 13 | if (!$this->_request->isJsonApiCall()) { |
|
351 | return; |
||
352 | } |
||
353 | |||
354 | try { |
||
355 | 13 | $paste = $this->_model->getPaste($dataid); |
|
356 | 11 | if ($paste->exists()) { |
|
357 | 9 | $data = $paste->get(); |
|
358 | 7 | if (property_exists($data->meta, 'salt')) { |
|
359 | 7 | unset($data->meta->salt); |
|
360 | } |
||
361 | 7 | $this->_return_message(0, $dataid, (array) $data); |
|
362 | } else { |
||
363 | 9 | $this->_return_message(1, self::GENERIC_ERROR); |
|
364 | } |
||
365 | 4 | } catch (Exception $e) { |
|
366 | 4 | $this->_return_message(1, $e->getMessage()); |
|
367 | } |
||
368 | 13 | } |
|
369 | |||
370 | /** |
||
371 | * Display PrivateBin frontend. |
||
372 | * |
||
373 | * @access private |
||
374 | */ |
||
375 | 20 | private function _view() |
|
376 | { |
||
377 | // set headers to disable caching |
||
378 | 20 | $time = gmdate('D, d M Y H:i:s \G\M\T'); |
|
379 | 20 | header('Cache-Control: no-store, no-cache, no-transform, must-revalidate'); |
|
380 | 20 | header('Pragma: no-cache'); |
|
381 | 20 | header('Expires: ' . $time); |
|
382 | 20 | header('Last-Modified: ' . $time); |
|
383 | 20 | header('Vary: Accept'); |
|
384 | 20 | header('Content-Security-Policy: ' . $this->_conf->getKey('cspheader')); |
|
385 | 20 | header('X-Xss-Protection: 1; mode=block'); |
|
386 | 20 | header('X-Frame-Options: DENY'); |
|
387 | 20 | header('X-Content-Type-Options: nosniff'); |
|
388 | |||
389 | // label all the expiration options |
||
390 | 20 | $expire = array(); |
|
391 | 20 | foreach ($this->_conf->getSection('expire_options') as $time => $seconds) { |
|
392 | 20 | $expire[$time] = ($seconds == 0) ? I18n::_(ucfirst($time)) : Filter::formatHumanReadableTime($time); |
|
393 | } |
||
394 | |||
395 | // translate all the formatter options |
||
396 | 20 | $formatters = array_map('PrivateBin\\I18n::_', $this->_conf->getSection('formatter_options')); |
|
397 | |||
398 | // set language cookie if that functionality was enabled |
||
399 | 20 | $languageselection = ''; |
|
400 | 20 | if ($this->_conf->getKey('languageselection')) { |
|
401 | 2 | $languageselection = I18n::getLanguage(); |
|
402 | 2 | setcookie('lang', $languageselection); |
|
403 | } |
||
404 | |||
405 | 20 | $page = new View; |
|
406 | 20 | $page->assign('NAME', $this->_conf->getKey('name')); |
|
407 | 20 | $page->assign('ERROR', I18n::_($this->_error)); |
|
408 | 20 | $page->assign('STATUS', I18n::_($this->_status)); |
|
409 | 20 | $page->assign('VERSION', self::VERSION); |
|
410 | 20 | $page->assign('DISCUSSION', $this->_conf->getKey('discussion')); |
|
411 | 20 | $page->assign('OPENDISCUSSION', $this->_conf->getKey('opendiscussion')); |
|
412 | 20 | $page->assign('MARKDOWN', array_key_exists('markdown', $formatters)); |
|
413 | 20 | $page->assign('SYNTAXHIGHLIGHTING', array_key_exists('syntaxhighlighting', $formatters)); |
|
414 | 20 | $page->assign('SYNTAXHIGHLIGHTINGTHEME', $this->_conf->getKey('syntaxhighlightingtheme')); |
|
415 | 20 | $page->assign('FORMATTER', $formatters); |
|
416 | 20 | $page->assign('FORMATTERDEFAULT', $this->_conf->getKey('defaultformatter')); |
|
417 | 20 | $page->assign('NOTICE', I18n::_($this->_conf->getKey('notice'))); |
|
418 | 20 | $page->assign('BURNAFTERREADINGSELECTED', $this->_conf->getKey('burnafterreadingselected')); |
|
419 | 20 | $page->assign('PASSWORD', $this->_conf->getKey('password')); |
|
420 | 20 | $page->assign('FILEUPLOAD', $this->_conf->getKey('fileupload')); |
|
421 | 20 | $page->assign('ZEROBINCOMPATIBILITY', $this->_conf->getKey('zerobincompatibility')); |
|
422 | 20 | $page->assign('LANGUAGESELECTION', $languageselection); |
|
423 | 20 | $page->assign('LANGUAGES', I18n::getLanguageLabels(I18n::getAvailableLanguages())); |
|
424 | 20 | $page->assign('EXPIRE', $expire); |
|
425 | 20 | $page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire')); |
|
426 | 20 | $page->assign('URLSHORTENER', $this->_conf->getKey('urlshortener')); |
|
427 | 20 | $page->assign('QRCODE', $this->_conf->getKey('qrcode')); |
|
428 | 20 | $page->draw($this->_conf->getKey('template')); |
|
429 | 20 | } |
|
430 | |||
431 | /** |
||
432 | * outputs requested JSON-LD context |
||
433 | * |
||
434 | * @access private |
||
435 | * @param string $type |
||
436 | */ |
||
437 | 5 | private function _jsonld($type) |
|
438 | { |
||
439 | if ( |
||
440 | 5 | $type !== 'paste' && $type !== 'comment' && |
|
441 | 5 | $type !== 'pastemeta' && $type !== 'commentmeta' |
|
442 | ) { |
||
443 | 1 | $type = ''; |
|
444 | } |
||
445 | 5 | $content = '{}'; |
|
446 | 5 | $file = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . $type . '.jsonld'; |
|
447 | 5 | if (is_readable($file)) { |
|
448 | 4 | $content = str_replace( |
|
449 | 4 | '?jsonld=', |
|
450 | 4 | $this->_urlBase . '?jsonld=', |
|
451 | 4 | file_get_contents($file) |
|
452 | ); |
||
453 | } |
||
454 | |||
455 | 5 | header('Content-type: application/ld+json'); |
|
456 | 5 | header('Access-Control-Allow-Origin: *'); |
|
457 | 5 | header('Access-Control-Allow-Methods: GET'); |
|
458 | 5 | echo $content; |
|
459 | 5 | } |
|
460 | |||
461 | /** |
||
462 | * prepares JSON encoded status message |
||
463 | * |
||
464 | * @access private |
||
465 | * @param int $status |
||
466 | * @param string $message |
||
467 | * @param array $other |
||
468 | */ |
||
469 | 63 | private function _return_message($status, $message, $other = array()) |
|
470 | { |
||
471 | 63 | $result = array('status' => $status); |
|
472 | 63 | if ($status) { |
|
473 | 30 | $result['message'] = I18n::_($message); |
|
474 | } else { |
||
475 | 35 | $result['id'] = $message; |
|
476 | 35 | $result['url'] = $this->_urlBase . '?' . $message; |
|
477 | } |
||
478 | 63 | $result += $other; |
|
479 | 63 | $this->_json = json_encode($result); |
|
480 | 63 | } |
|
481 | } |
||
482 |
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.