_init(); switch ($this->_request->getOperation()) { case 'create': $this->_create(); break; case 'delete': $this->_delete( $this->_request->getParam('pasteid'), $this->_request->getParam('deletetoken') ); break; case 'read': $this->_read($this->_request->getParam('pasteid')); break; case 'jsonld': $this->_jsonld($this->_request->getParam('jsonld')); return; } // output JSON or HTML if ($this->_request->isJsonApiCall()) { header('Content-type: ' . Request::MIME_JSON); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE'); header('Access-Control-Allow-Headers: X-Requested-With, Content-Type'); echo $this->_json; } else { $this->_view(); } } /** * initialize PrivateBin * * @access private * @throws Exception */ private function _init() { $this->_conf = new Configuration; $this->_model = new Model($this->_conf); $this->_request = new Request; $this->_urlBase = $this->_request->getRequestUri(); ServerSalt::setPath($this->_conf->getKey('dir', 'traffic')); // set default language $lang = $this->_conf->getKey('languagedefault'); I18n::setLanguageFallback($lang); // force default language, if language selection is disabled and a default is set if (!$this->_conf->getKey('languageselection') && strlen($lang) == 2) { $_COOKIE['lang'] = $lang; setcookie('lang', $lang); } } /** * Store new paste or comment * * POST contains one or both: * data = json encoded FormatV2 encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * attachment = json encoded FormatV2 encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * * All optional data will go to meta information: * expire (optional) = expiration delay (never,5min,10min,1hour,1day,1week,1month,1year,burn) (default:never) * formatter (optional) = format to display the paste as (plaintext,syntaxhighlighting,markdown) (default:syntaxhighlighting) * burnafterreading (optional) = if this paste may only viewed once ? (0/1) (default:0) * opendiscusssion (optional) = is the discussion allowed on this paste ? (0/1) (default:0) * attachmentname = json encoded FormatV2 encrypted text (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * nickname (optional) = in discussion, encoded FormatV2 encrypted text nickname of author of comment (containing keys: iv,v,iter,ks,ts,mode,adata,cipher,salt,ct) * parentid (optional) = in discussion, which comment this comment replies to. * pasteid (optional) = in discussion, which paste this comment belongs to. * * @access private * @return string */ private function _create() { // Ensure last paste from visitors IP address was more than configured amount of seconds ago. TrafficLimiter::setConfiguration($this->_conf); if (!TrafficLimiter::canPass()) { $this->_return_message( 1, I18n::_( 'Please wait %d seconds between each post.', $this->_conf->getKey('limit', 'traffic') ) ); return; } $data = $this->_request->getData(); $isComment = array_key_exists('pasteid', $data) && !empty($data['pasteid']) && array_key_exists('parentid', $data) && !empty($data['parentid']); if (!FormatV2::isValid($data, $isComment)) { $this->_return_message(1, I18n::_('Invalid data.')); return; } $sizelimit = $this->_conf->getKey('sizelimit'); // Ensure content is not too big. if (strlen($data['ct']) > $sizelimit) { $this->_return_message( 1, I18n::_( 'Paste is limited to %s of encrypted data.', Filter::formatHumanReadableSize($sizelimit) ) ); return; } // The user posts a comment. if ($isComment) { $paste = $this->_model->getPaste($data['pasteid']); if ($paste->exists()) { try { $comment = $paste->getComment($data['parentid']); $comment->setData($data); $comment->store(); } catch (Exception $e) { $this->_return_message(1, $e->getMessage()); return; } $this->_return_message(0, $comment->getId()); } else { $this->_return_message(1, I18n::_('Invalid data.')); } } // The user posts a standard paste. else { $this->_model->purge(); $paste = $this->_model->getPaste(); try { $paste->setData($data); $paste->store(); } catch (Exception $e) { return $this->_return_message(1, $e->getMessage()); } $this->_return_message(0, $paste->getId(), array('deletetoken' => $paste->getDeleteToken())); } } /** * Delete an existing paste * * @access private * @param string $dataid * @param string $deletetoken */ private function _delete($dataid, $deletetoken) { try { $paste = $this->_model->getPaste($dataid); if ($paste->exists()) { // accessing this method ensures that the paste would be // deleted if it has already expired $paste->get(); if ($paste->isDeleteTokenCorrect($deletetoken)) { // Paste exists and deletion token is valid: Delete the paste. $paste->delete(); $this->_status = 'Paste was properly deleted.'; } else { $this->_error = 'Wrong deletion token. Paste was not deleted.'; } } else { $this->_error = self::GENERIC_ERROR; } } catch (Exception $e) { $this->_error = $e->getMessage(); } if ($this->_request->isJsonApiCall()) { if (strlen($this->_error)) { $this->_return_message(1, $this->_error); } else { $this->_return_message(0, $dataid); } } } /** * Read an existing paste or comment, only allowed via a JSON API call * * @access private * @param string $dataid */ private function _read($dataid) { if (!$this->_request->isJsonApiCall()) { return; } try { $paste = $this->_model->getPaste($dataid); if ($paste->exists()) { // handle challenge response if (!$paste->isTokenCorrect($this->_request->getParam('token'))) { // we send a generic error to avoid leaking information // about the existance of a burn after reading pastes // this avoids an attacker being able to poll, if it has // been read by the intended recipient or not $this->_return_message(1, self::GENERIC_ERROR); return; } $data = $paste->get(); foreach (array('salt', 'challenge') as $key) { if (array_key_exists($key, $data['meta'])) { unset($data['meta'][$key]); } } $this->_return_message(0, $dataid, (array) $data); } else { $this->_return_message(1, self::GENERIC_ERROR); } } catch (Exception $e) { $this->_return_message(1, $e->getMessage()); } } /** * Display frontend. * * @access private */ private function _view() { // set headers to disable caching $time = gmdate('D, d M Y H:i:s \G\M\T'); header('Cache-Control: no-store, no-cache, no-transform, must-revalidate'); header('Pragma: no-cache'); header('Expires: ' . $time); header('Last-Modified: ' . $time); header('Vary: Accept'); header('Content-Security-Policy: ' . $this->_conf->getKey('cspheader')); header('Referrer-Policy: no-referrer'); header('X-Xss-Protection: 1; mode=block'); header('X-Frame-Options: DENY'); header('X-Content-Type-Options: nosniff'); // label all the expiration options $expire = array(); foreach ($this->_conf->getSection('expire_options') as $time => $seconds) { $expire[$time] = ($seconds == 0) ? I18n::_(ucfirst($time)) : Filter::formatHumanReadableTime($time); } // translate all the formatter options $formatters = array_map('PrivateBin\\I18n::_', $this->_conf->getSection('formatter_options')); // set language cookie if that functionality was enabled $languageselection = ''; if ($this->_conf->getKey('languageselection')) { $languageselection = I18n::getLanguage(); setcookie('lang', $languageselection); } $page = new View; $page->assign('NAME', $this->_conf->getKey('name')); $page->assign('ERROR', I18n::_($this->_error)); $page->assign('STATUS', I18n::_($this->_status)); $page->assign('VERSION', self::VERSION); $page->assign('DISCUSSION', $this->_conf->getKey('discussion')); $page->assign('OPENDISCUSSION', $this->_conf->getKey('opendiscussion')); $page->assign('MARKDOWN', array_key_exists('markdown', $formatters)); $page->assign('SYNTAXHIGHLIGHTING', array_key_exists('syntaxhighlighting', $formatters)); $page->assign('SYNTAXHIGHLIGHTINGTHEME', $this->_conf->getKey('syntaxhighlightingtheme')); $page->assign('FORMATTER', $formatters); $page->assign('FORMATTERDEFAULT', $this->_conf->getKey('defaultformatter')); $page->assign('NOTICE', I18n::_($this->_conf->getKey('notice'))); $page->assign('BURNAFTERREADINGSELECTED', $this->_conf->getKey('burnafterreadingselected')); $page->assign('PASSWORD', $this->_conf->getKey('password')); $page->assign('FILEUPLOAD', $this->_conf->getKey('fileupload')); $page->assign('ZEROBINCOMPATIBILITY', $this->_conf->getKey('zerobincompatibility')); $page->assign('LANGUAGESELECTION', $languageselection); $page->assign('LANGUAGES', I18n::getLanguageLabels(I18n::getAvailableLanguages())); $page->assign('EXPIRE', $expire); $page->assign('EXPIREDEFAULT', $this->_conf->getKey('default', 'expire')); $page->assign('URLSHORTENER', $this->_conf->getKey('urlshortener')); $page->assign('QRCODE', $this->_conf->getKey('qrcode')); $page->assign('HTTPWARNING', $this->_conf->getKey('httpwarning')); $page->assign('COMPRESSION', $this->_conf->getKey('compression')); $page->draw($this->_conf->getKey('template')); } /** * outputs requested JSON-LD context * * @access private * @param string $type */ private function _jsonld($type) { if ( $type !== 'paste' && $type !== 'comment' && $type !== 'pastemeta' && $type !== 'commentmeta' ) { $type = ''; } $content = '{}'; $file = PUBLIC_PATH . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . $type . '.jsonld'; if (is_readable($file)) { $content = str_replace( '?jsonld=', $this->_urlBase . '?jsonld=', file_get_contents($file) ); } header('Content-type: application/ld+json'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET'); echo $content; } /** * prepares JSON encoded status message * * @access private * @param int $status * @param string $message * @param array $other */ private function _return_message($status, $message, $other = array()) { $result = array('status' => $status); if ($status) { $result['message'] = I18n::_($message); } else { $result['id'] = $message; $result['url'] = $this->_urlBase . '?' . $message; } $result += $other; $this->_json = Json::encode($result); } }