'.$string.''.
'The page you requested was not found.
'); $view = new PhabricatorStandardPageView(); $view->setTitle('404 Not Found'); $view->setRequest($this->getRequest()); $view->appendChild($failure); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); $response->setHTTPResponseCode(404); return $response; } return $response; } public function build404Controller() { return array(new Phabricator404Controller($this->getRequest()), array()); } public function buildRedirectController($uri) { return array( new PhabricatorRedirectController($this->getRequest()), array( 'uri' => $uri, )); } } diff --git a/src/aphront/request/AphrontRequest.php b/src/aphront/request/AphrontRequest.php index 30a23b238a..fdb672b7b3 100644 --- a/src/aphront/request/AphrontRequest.php +++ b/src/aphront/request/AphrontRequest.php @@ -1,216 +1,248 @@ host = $host; $this->path = $path; } final public function setApplicationConfiguration( $application_configuration) { $this->applicationConfiguration = $application_configuration; return $this; } final public function getApplicationConfiguration() { return $this->applicationConfiguration; } final public function setRequestData(array $request_data) { $this->requestData = $request_data; return $this; } final public function getPath() { return $this->path; } final public function getHost() { return $this->host; } final public function getInt($name, $default = null) { if (isset($this->requestData[$name])) { return (int)$this->requestData[$name]; } else { return $default; } } final public function getBool($name, $default = null) { if (isset($this->requestData[$name])) { if ($this->requestData[$name] === 'true') { return true; } else if ($this->requestData[$name] === 'false') { return false; } else { return (bool)$this->requestData[$name]; } } else { return $default; } } final public function getStr($name, $default = null) { if (isset($this->requestData[$name])) { $str = (string)$this->requestData[$name]; // Normalize newline craziness. $str = str_replace( array("\r\n", "\r"), array("\n", "\n"), $str); return $str; } else { return $default; } } final public function getArr($name, $default = array()) { if (isset($this->requestData[$name]) && is_array($this->requestData[$name])) { return $this->requestData[$name]; } else { return $default; } } final public function getExists($name) { return array_key_exists($name, $this->requestData); } final public function isHTTPPost() { return ($_SERVER['REQUEST_METHOD'] == 'POST'); } final public function isAjax() { return $this->getExists(self::TYPE_AJAX); } public static function getCSRFTokenName() { return '__csrf__'; } public static function getCSRFHeaderName() { return 'X-Phabricator-Csrf'; } final public function validateCSRF() { $token_name = self::getCSRFTokenName(); $token = $this->getStr($token_name); // No token in the request, check the HTTP header which is added for Ajax // requests. if (empty($token)) { // PHP mangles HTTP headers by uppercasing them and replacing hyphens with // underscores, then prepending 'HTTP_'. $php_index = self::getCSRFHeaderName(); $php_index = strtoupper($php_index); $php_index = str_replace('-', '_', $php_index); $php_index = 'HTTP_'.$php_index; $token = idx($_SERVER, $php_index); } $valid = $this->getUser()->validateCSRFToken($token); if (!$valid) { // This should only be able to happen if you load a form, pull your // internet for 6 hours, and then reconnect and immediately submit, // but give the user some indication of what happened since the workflow // is incredibly confusing otherwise. throw new AphrontCSRFException( "The form you just submitted did not include a valid CSRF token. ". "This token is a technical security measure which prevents a ". "certain type of login hijacking attack. However, the token can ". "become invalid if you leave a page open for more than six hours ". "without a connection to the internet. To fix this problem: reload ". "the page, and then resubmit it."); } return true; } final public function isFormPost() { $post = $this->getExists(self::TYPE_FORM) && $this->isHTTPPost(); if (!$post) { return false; } return $this->validateCSRF(); } final public function getCookie($name, $default = null) { return idx($_COOKIE, $name, $default); } final public function clearCookie($name) { $this->setCookie($name, '', time() - (60 * 60 * 24 * 30)); } final public function setCookie($name, $value, $expire = null) { + + // Ensure cookies are only set on the configured domain. + + $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); + $base_uri = new PhutilURI($base_uri); + + $base_domain = $base_uri->getDomain(); + $base_protocol = $base_uri->getProtocol(); + + $actual_host = $this->getHost(); + if ($base_domain != $actual_host) { + throw new Exception( + "This install of Phabricator is configured as '{$base_domain}' but ". + "you are accessing it via '{$actual_host}'. Access Phabricator via ". + "the primary configured domain."); + } + if ($expire === null) { $expire = time() + (60 * 60 * 24 * 365 * 5); } + + if ($value == '') { + // NOTE: If we're clearing the cookie, also clear it on the entire + // domain. This allows us to clear older cookies which we didn't scope + // as tightly. + setcookie( + $name, + $value, + $expire, + $path = '/', + $domain = '', + $secure = ($base_protocol == 'https'), + $http_only = true); + } + setcookie( $name, $value, $expire, $path = '/', - $domain = '', - $secure = false, + $base_domain, + $secure = ($base_protocol == 'https'), $http_only = true); } final public function setUser($user) { $this->user = $user; return $this; } final public function getUser() { return $this->user; } final public function getRequestURI() { $get = $_GET; unset($get['__path__']); return id(new PhutilURI($this->getPath()))->setQueryParams($get); } final public function isDialogFormPost() { return $this->isFormPost() && $this->getStr('__dialog__'); } } diff --git a/src/aphront/request/__init__.php b/src/aphront/request/__init__.php index f3296f0d91..7e0a7e8115 100644 --- a/src/aphront/request/__init__.php +++ b/src/aphront/request/__init__.php @@ -1,15 +1,16 @@ phid = $data['phid']; + $this->key = $data['key']; + } + + public function shouldRequireLogin() { + return false; + } + + public function processRequest() { + + $alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); + if (!$alt) { + return new Aphront400Response(); + } + + $request = $this->getRequest(); + + $alt_domain = id(new PhutilURI($alt))->getDomain(); + if ($alt_domain != $request->getHost()) { + return new Aphront400Response(); + } + + $file = id(new PhabricatorFile())->loadOneWhere( + 'phid = %s', + $this->phid); + if (!$file) { + return new Aphront404Response(); + } + + if (!$file->validateSecretKey($this->key)) { + return new Aphront404Response(); + } + + // It's safe to bypass view restrictions because we know we are being served + // off an alternate domain which we will not set cookies on. + + $data = $file->loadFileData(); + $response = new AphrontFileResponse(); + $response->setContent($data); + $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); + + return $response; + } +} diff --git a/src/applications/files/controller/altview/__init__.php b/src/applications/files/controller/altview/__init__.php new file mode 100644 index 0000000000..a447af1b27 --- /dev/null +++ b/src/applications/files/controller/altview/__init__.php @@ -0,0 +1,20 @@ +getRequest(); $user = $request->getUser(); $upload_panel = $this->renderUploadPanel(); $author = null; $author_username = $request->getStr('author'); if ($author_username) { $author = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $author_username); if (!$author) { return id(new Aphront404Response()); } $title = 'Files Uploaded by '.phutil_escape_html($author->getUsername()); } else { $title = 'Files'; } $pager = new AphrontPagerView(); $pager->setOffset($request->getInt('page')); if ($author) { $files = id(new PhabricatorFile())->loadAllWhere( 'authorPHID = %s ORDER BY id DESC LIMIT %d, %d', $author->getPHID(), $pager->getOffset(), $pager->getPageSize() + 1); } else { $files = id(new PhabricatorFile())->loadAllWhere( '1 = 1 ORDER BY id DESC LIMIT %d, %d', $pager->getOffset(), $pager->getPageSize() + 1); } $files = $pager->sliceResults($files); $pager->setURI($request->getRequestURI(), 'page'); $phids = mpull($files, 'getAuthorPHID'); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $highlighted = $request->getStr('h'); $highlighted = explode('-', $highlighted); $highlighted = array_fill_keys($highlighted, true); $rows = array(); $rowc = array(); foreach ($files as $file) { if ($file->isViewableInBrowser()) { $view_button = phutil_render_tag( 'a', array( 'class' => 'small button grey', - 'href' => '/file/view/'.$file->getPHID().'/', + 'href' => $file->getViewURI(), ), 'View'); } else { $view_button = null; } if (isset($highlighted[$file->getID()])) { $rowc[] = 'highlighted'; } else { $rowc[] = ''; } $rows[] = array( phutil_escape_html('F'.$file->getID()), $file->getAuthorPHID() ? $handles[$file->getAuthorPHID()]->renderLink() : null, phutil_render_tag( 'a', array( 'href' => $file->getBestURI(), ), phutil_escape_html($file->getName())), phutil_escape_html(number_format($file->getByteSize()).' bytes'), phutil_render_tag( 'a', array( 'class' => 'small button grey', 'href' => '/file/info/'.$file->getPHID().'/', ), 'Info'), $view_button, phabricator_date($file->getDateCreated(), $user), phabricator_time($file->getDateCreated(), $user), ); } $table = new AphrontTableView($rows); $table->setRowClasses($rowc); $table->setHeaders( array( 'File ID', 'Author', 'Name', 'Size', '', '', 'Created', '', )); $table->setColumnClasses( array( null, '', 'wide pri', 'right', 'action', 'action', '', 'right', )); $panel = new AphrontPanelView(); $panel->appendChild($table); $panel->setHeader($title); $panel->appendChild($pager); return $this->buildStandardPageResponse( array( $upload_panel, $panel, ), array( 'title' => 'Files', 'tab' => 'files', )); } private function renderUploadPanel() { $request = $this->getRequest(); $user = $request->getUser(); require_celerity_resource('files-css'); $upload_id = celerity_generate_unique_node_id(); $panel_id = celerity_generate_unique_node_id(); $upload_panel = new AphrontPanelView(); $upload_panel->setHeader('Upload Files'); $upload_panel->setCreateButton( 'Basic Uploader', '/file/upload/'); $upload_panel->setWidth(AphrontPanelView::WIDTH_FULL); $upload_panel->setID($panel_id); $upload_panel->appendChild( phutil_render_tag( 'div', array( 'id' => $upload_id, 'style' => 'display: none;', 'class' => 'files-drag-and-drop', ), '')); Javelin::initBehavior( 'files-drag-and-drop', array( 'uri' => '/file/dropupload/', 'browseURI' => '/file/?author='.$user->getUsername(), 'control' => $upload_id, 'target' => $panel_id, 'activatedClass' => 'aphront-panel-view-drag-and-drop', )); return $upload_panel; } } diff --git a/src/applications/files/controller/view/PhabricatorFileViewController.php b/src/applications/files/controller/view/PhabricatorFileViewController.php index a5dc977037..6201e67fd3 100644 --- a/src/applications/files/controller/view/PhabricatorFileViewController.php +++ b/src/applications/files/controller/view/PhabricatorFileViewController.php @@ -1,206 +1,218 @@ phid = $data['phid']; $this->view = $data['view']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $file = id(new PhabricatorFile())->loadOneWhere( 'phid = %s', $this->phid); if (!$file) { return new Aphront404Response(); } switch ($this->view) { case 'download': case 'view': $data = $file->loadFileData(); $response = new AphrontFileResponse(); $response->setContent($data); $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); if ($this->view == 'view') { if (!$file->isViewableInBrowser()) { return new Aphront400Response(); } $download = false; } else { $download = true; } if ($download) { if (!$request->isFormPost()) { // Require a POST to download files to hinder attacks where you // on some // other domain. return id(new AphrontRedirectResponse()) ->setURI($file->getInfoURI()); } } if ($download) { $mime_type = $file->getMimeType(); } else { $mime_type = $file->getViewableMimeType(); } + // If an alternate file domain is configured, forbid all views which + // don't originate from it. + if (!$download) { + $alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); + if ($alt) { + $domain = id(new PhutilURI($alt))->getDomain(); + if ($domain != $request->getHost()) { + return new Aphront400Response(); + } + } + } + $response->setMimeType($mime_type); if ($download) { $response->setDownload($file->getName()); } return $response; default: break; } $author_child = null; if ($file->getAuthorPHID()) { $author = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $file->getAuthorPHID()); if ($author) { $author_child = id(new AphrontFormStaticControl()) ->setLabel('Author') ->setName('author') ->setValue($author->getUserName()); } } $form = new AphrontFormView(); if ($file->isViewableInBrowser()) { - $form->setAction('/file/view/'.$file->getPHID().'/'); + $form->setAction($file->getViewURI()); $button_name = 'View File'; } else { $form->setAction('/file/download/'.$file->getPHID().'/'); $button_name = 'Download File'; } $file_id = 'F'.$file->getID(); $form->setUser($user); $form ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Name') ->setName('name') ->setValue($file->getName())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('ID') ->setName('id') ->setValue($file_id) ->setCaption( 'Download this file with: arc download '. phutil_escape_html($file_id).'')) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('PHID') ->setName('phid') ->setValue($file->getPHID())) ->appendChild($author_child) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Created') ->setName('created') ->setValue(phabricator_datetime($file->getDateCreated(), $user))) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Mime Type') ->setName('mime') ->setValue($file->getMimeType())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Size') ->setName('size') ->setValue($file->getByteSize().' bytes')) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Engine') ->setName('storageEngine') ->setValue($file->getStorageEngine())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Format') ->setName('storageFormat') ->setValue($file->getStorageFormat())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Handle') ->setName('storageHandle') ->setValue($file->getStorageHandle())) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($button_name)); $panel = new AphrontPanelView(); $panel->setHeader('File Info - '.$file->getName()); $panel->appendChild($form); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $transformations = id(new PhabricatorTransformedFile())->loadAllWhere( 'originalPHID = %s', $file->getPHID()); $rows = array(); foreach ($transformations as $transformed) { $phid = $transformed->getTransformedPHID(); $rows[] = array( phutil_escape_html($transformed->getTransform()), phutil_render_tag( 'a', array( 'href' => PhabricatorFileURI::getViewURIForPHID($phid), ), $phid)); } $table = new AphrontTableView($rows); $table->setHeaders( array( 'Transform', 'File', )); $xform_panel = new AphrontPanelView(); $xform_panel->appendChild($table); $xform_panel->setWidth(AphrontPanelView::WIDTH_FORM); $xform_panel->setHeader('Transformations'); return $this->buildStandardPageResponse( array($panel, $xform_panel), array( 'title' => 'File Info - '.$file->getName(), )); } } diff --git a/src/applications/files/controller/view/__init__.php b/src/applications/files/controller/view/__init__.php index afe7459ce5..715df129d0 100644 --- a/src/applications/files/controller/view/__init__.php +++ b/src/applications/files/controller/view/__init__.php @@ -1,29 +1,31 @@ true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_FILE); } public static function newFromPHPUpload($spec, array $params = array()) { if (!$spec) { throw new Exception("No file was uploaded!"); } $err = idx($spec, 'error'); if ($err) { throw new PhabricatorFileUploadException($err); } $tmp_name = idx($spec, 'tmp_name'); $is_valid = @is_uploaded_file($tmp_name); if (!$is_valid) { throw new Exception("File is not an uploaded file."); } $file_data = Filesystem::readFile($tmp_name); $file_size = idx($spec, 'size'); if (strlen($file_data) != $file_size) { throw new Exception("File size disagrees with uploaded size."); } $file_name = nonempty( idx($params, 'name'), idx($spec, 'name')); $params = array( 'name' => $file_name, ) + $params; return self::newFromFileData($file_data, $params); } public static function newFromFileData($data, array $params = array()) { $selector_class = PhabricatorEnv::getEnvConfig('storage.engine-selector'); $selector = newv($selector_class, array()); $engines = $selector->selectStorageEngines($data, $params); if (!$engines) { throw new Exception("No valid storage engines are available!"); } $data_handle = null; $engine_identifier = null; foreach ($engines as $engine) { try { // Perform the actual write. $data_handle = $engine->writeFile($data, $params); if (!$data_handle || strlen($data_handle) > 255) { // This indicates an improperly implemented storage engine. throw new Exception( "Storage engine '{$engine}' executed writeFile() but did not ". "return a valid handle ('{$data_handle}') to the data: it must ". "be nonempty and no longer than 255 characters."); } $engine_identifier = $engine->getEngineIdentifier(); if (!$engine_identifier || strlen($engine_identifier) > 32) { throw new Exception( "Storage engine '{$engine}' returned an improper engine ". "identifier '{$engine_identifier}': it must be nonempty ". "and no longer than 32 characters."); } // We stored the file somewhere so stop trying to write it to other // places. break; } catch (Exception $ex) { // If an engine doesn't work, keep trying all the other valid engines // in case something else works. phlog($ex); } } if (!$data_handle) { throw new Exception("All storage engines failed to write file!"); } $file_name = idx($params, 'name'); $file_name = self::normalizeFileName($file_name); // If for whatever reason, authorPHID isn't passed as a param // (always the case with newFromFileDownload()), store a '' $authorPHID = idx($params, 'authorPHID'); $file = new PhabricatorFile(); $file->setName($file_name); $file->setByteSize(strlen($data)); $file->setAuthorPHID($authorPHID); $file->setStorageEngine($engine_identifier); $file->setStorageHandle($data_handle); // TODO: This is probably YAGNI, but allows for us to do encryption or // compression later if we want. $file->setStorageFormat(self::STORAGE_FORMAT_RAW); if (isset($params['mime-type'])) { $file->setMimeType($params['mime-type']); } else { try { $tmp = new TempFile(); Filesystem::writeFile($tmp, $data); list($stdout) = execx('file -b --mime %s', $tmp); $file->setMimeType($stdout); } catch (Exception $ex) { // Be robust here since we don't really care that much about mime types. } } $file->save(); return $file; } public static function newFromFileDownload($uri, $name) { $uri = new PhutilURI($uri); $protocol = $uri->getProtocol(); switch ($protocol) { case 'http': case 'https': break; default: // Make sure we are not accessing any file:// URIs or similar. return null; } $timeout = stream_context_create( array( 'http' => array( 'timeout' => 5, ), )); $file_data = @file_get_contents($uri, false, $timeout); if ($file_data === false) { return null; } return self::newFromFileData($file_data, array('name' => $name)); } public static function normalizeFileName($file_name) { return preg_replace('/[^a-zA-Z0-9.~_-]/', '_', $file_name); } public function delete() { $engine = $this->instantiateStorageEngine(); $ret = parent::delete(); $engine->deleteFile($this->getStorageHandle()); return $ret; } public function loadFileData() { $engine = $this->instantiateStorageEngine(); $data = $engine->readFile($this->getStorageHandle()); switch ($this->getStorageFormat()) { case self::STORAGE_FORMAT_RAW: $data = $data; break; default: throw new Exception("Unknown storage format."); } return $data; } public function getViewURI() { - return PhabricatorFileURI::getViewURIForPHID($this->getPHID()); + $alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); + if ($alt) { + $path = '/file/alt/'.$this->generateSecretKey().'/'.$this->getPHID().'/'; + $uri = new PhutilURI($alt); + $uri->setPath($path); + + return (string)$uri; + } else { + return '/file/view/'.$this->getPHID().'/'; + } } public function getInfoURI() { return '/file/info/'.$this->getPHID().'/'; } public function getBestURI() { if ($this->isViewableInBrowser()) { return $this->getViewURI(); } else { return $this->getInfoURI(); } } public function getThumb60x45URI() { return '/file/xform/thumb-60x45/'.$this->getPHID().'/'; } public function getThumb160x120URI() { return '/file/xform/thumb-160x120/'.$this->getPHID().'/'; } public function isViewableInBrowser() { return ($this->getViewableMimeType() !== null); } public function isTransformableImage() { // NOTE: The way the 'gd' extension works in PHP is that you can install it // with support for only some file types, so it might be able to handle // PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup // warns you if you don't have complete support. $matches = null; $ok = preg_match( '@^image/(gif|png|jpe?g)@', $this->getViewableMimeType(), $matches); if (!$ok) { return false; } switch ($matches[1]) { case 'jpg'; case 'jpeg': return function_exists('imagejpeg'); break; case 'png': return function_exists('imagepng'); break; case 'gif': return function_exists('imagegif'); break; default: throw new Exception('Unknown type matched as image MIME type.'); } } protected function instantiateStorageEngine() { $engines = id(new PhutilSymbolLoader()) ->setType('class') ->setAncestorClass('PhabricatorFileStorageEngine') ->selectAndLoadSymbols(); foreach ($engines as $engine_class) { $engine = newv($engine_class['name'], array()); if ($engine->getEngineIdentifier() == $this->getStorageEngine()) { return $engine; } } throw new Exception("File's storage engine could be located!"); } public function getViewableMimeType() { $mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); $mime_type = $this->getMimeType(); $mime_parts = explode(';', $mime_type); $mime_type = trim(reset($mime_parts)); return idx($mime_map, $mime_type); } + public function validateSecretKey($key) { + return ($key == $this->generateSecretKey()); + } + + private function generateSecretKey() { + $file_key = PhabricatorEnv::getEnvConfig('phabricator.file-key'); + $hash = sha1($this->phid.$this->storageHandle.$file_key); + return substr($hash, 0, 20); + } + } diff --git a/src/applications/files/storage/file/__init__.php b/src/applications/files/storage/file/__init__.php index 4947cce2b8..258006cd1f 100644 --- a/src/applications/files/storage/file/__init__.php +++ b/src/applications/files/storage/file/__init__.php @@ -1,25 +1,24 @@ loadOneWhere( + 'phid = %s', + $phid); + if ($file) { + return $file->getViewURI(); + } + + return null; } } diff --git a/src/applications/files/uri/__init__.php b/src/applications/files/uri/__init__.php index d53daa769b..bedf04f978 100644 --- a/src/applications/files/uri/__init__.php +++ b/src/applications/files/uri/__init__.php @@ -1,10 +1,14 @@ getMessage(); self::write("Unable to load modules from libphutil: {$message}\n"); $open_libphutil = false; } try { phutil_require_module('arcanist', 'workflow/base'); $open_arcanist = true; } catch (Exception $ex) { $message = $ex->getMessage(); self::write("Unable to load modules from Arcanist: {$message}\n"); $open_arcanist = false; } $open_urandom = @fopen('/dev/urandom', 'r'); if (!$open_urandom) { self::write("Unable to open /dev/urandom!\n"); } try { $tmp = new TempFile(); file_put_contents($tmp, '.'); $open_tmp = @fopen((string)$tmp, 'r'); } catch (Exception $ex) { $message = $ex->getMessage(); $dir = sys_get_temp_dir(); self::write("Unable to open temp files from '{$dir}': {$message}\n"); $open_tmp = false; } if (!$open_urandom || !$open_tmp || !$open_libphutil || !$open_arcanist) { self::writeFailure(); self::write( "Setup failure! Your server is configured with 'open_basedir' in ". "php.ini which prevents Phabricator from opening files it needs to ". "access. Either make the setting more permissive or remove it. It ". "is unlikely you derive significant security benefits from having ". "this configured; files outside this directory can still be ". "accessed through system command execution."); return; } else { self::write( "[WARN] You have an 'open_basedir' configured in your php.ini. ". "Although the setting seems permissive enough that Phabricator ". "will run properly, you may run into problems because of it. It is ". "unlikely you gain much real security benefit from having it ". "configured, because the application can still access files outside ". "the 'open_basedir' by running system commands.\n"); } } else { self::write(" okay 'open_basedir' is not set.\n"); } + if (!PhabricatorEnv::getEnvConfig('security.alternate-file-domain')) { + self::write( + "[WARN] You have not configured 'security.alternate-file-domain'. ". + "This may make your installation vulnerable to attack. Make sure ". + "you read the documentation for this parameter and understand the ". + "consequences of leaving it unconfigured.\n"); + } + self::write("[OKAY] Core configuration OKAY.\n"); self::writeHeader("REQUIRED PHP EXTENSIONS"); $extensions = array( 'mysql', 'hash', 'json', 'openssl', // There is a chance we might not need this, but some configurations (like // Amazon SES) will require it. Just mark it 'required' since it's widely // available and relatively core. 'curl', ); foreach ($extensions as $extension) { $ok = self::requireExtension($extension); if (!$ok) { self::writeFailure(); self::write("Setup failure! Install PHP extension '{$extension}'."); return; } } $root = dirname(phutil_get_library_root('phabricator')); // On RHEL6, doing a distro install of pcntl makes it available from the // CLI binary but not from the Apache module. This isn't entirely // unreasonable and we don't need it from Apache, so do an explicit test // for CLI availability. list($err, $stdout, $stderr) = exec_manual( '%s/scripts/setup/pcntl_available.php', $root); if ($err) { self::writeFailure(); self::write("Unable to execute scripts/setup/pcntl_available.php."); return; } else { if (trim($stdout) == 'YES') { self::write(" okay pcntl is available from the command line.\n"); self::write("[OKAY] All extensions OKAY\n"); } else { self::write(" warn pcntl is not available!\n"); self::write("[WARN] *** WARNING *** pcntl extension not available. ". "You will not be able to run daemons.\n"); } } self::writeHeader("GIT SUBMODULES"); if (!Filesystem::pathExists($root.'/.git')) { self::write(" skip Not a git clone.\n\n"); } else { list($info) = execx( '(cd %s && git submodule status)', $root); foreach (explode("\n", rtrim($info)) as $line) { $matches = null; if (!preg_match('/^(.)([0-9a-f]{40}) (\S+)(?: |$)/', $line, $matches)) { self::writeFailure(); self::write( "Setup failure! 'git submodule' produced unexpected output:\n". $line); return; } $status = $matches[1]; $module = $matches[3]; switch ($status) { case '-': case '+': case 'U': self::writeFailure(); self::write( "Setup failure! Git submodule '{$module}' is not up to date. ". "Run:\n\n". " cd {$root} && git submodule update --init\n\n". "...to update submodules."); return; case ' ': self::write(" okay Git submodule '{$module}' up to date.\n"); break; default: self::writeFailure(); self::write( "Setup failure! 'git submodule' reported unknown status ". "'{$status}' for submodule '{$module}'. This is a bug; report ". "it to the Phabricator maintainers."); return; } } } self::write("[OKAY] All submodules OKAY.\n"); self::writeHeader("BASIC CONFIGURATION"); $env = PhabricatorEnv::getEnvConfig('phabricator.env'); if ($env == 'production' || $env == 'default' || $env == 'development') { self::writeFailure(); self::write( "Setup failure! Your PHABRICATOR_ENV is set to '{$env}', which is ". "a Phabricator environmental default. You should create a custom ". "environmental configuration instead of editing the defaults ". "directly. See this document for instructions:\n"); self::writeDoc('article/Configuration_Guide.html'); return; } else { $host = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); $protocol = id(new PhutilURI($host))->getProtocol(); $allowed_protocols = array( 'http' => true, 'https' => true, ); if (empty($allowed_protocols[$protocol])) { self::writeFailure(); self::write( "You must specify the protocol over which your host works (e.g.: ". "\"http:// or https://\")\nin your custom config file.\nRefer to ". "'default.conf.php' for documentation on configuration options.\n"); return; } if (preg_match('/.*\/$/', $host)) { self::write(" okay phabricator.base-uri\n"); } else { self::writeFailure(); self::write( "You must add a trailing slash at the end of the host\n(e.g.: ". "\"http://phabricator.example.com/ instead of ". "http://phabricator.example.com\")\nin your custom config file.". "\nRefer to 'default.conf.php' for documentation on configuration ". "options.\n"); return; } } $timezone = nonempty( PhabricatorEnv::getEnvConfig('phabricator.timezone'), ini_get('date.timezone')); if (!$timezone) { self::writeFailure(); self::write( "Setup failure! Your configuration fails to specify a server ". "timezone. Either set 'date.timezone' in your php.ini or ". "'phabricator.timezone' in your Phabricator configuration. See the ". "PHP documentation for a list of supported timezones:\n\n". "http://us.php.net/manual/en/timezones.php\n"); return; } else { self::write(" okay Timezone '{$timezone}' configured.\n"); } self::write("[OKAY] Basic configuration OKAY\n"); $issue_gd_warning = false; self::writeHeader('GD LIBRARY'); if (extension_loaded('gd')) { self::write(" okay Extension 'gd' is loaded.\n"); $image_type_map = array( 'imagepng' => 'PNG', 'imagegif' => 'GIF', 'imagejpeg' => 'JPEG', ); foreach ($image_type_map as $function => $image_type) { if (function_exists($function)) { self::write(" okay Support for '{$image_type}' is available.\n"); } else { self::write(" warn Support for '{$image_type}' is not available!\n"); $issue_gd_warning = true; } } } else { self::write(" warn Extension 'gd' is not loaded.\n"); $issue_gd_warning = true; } if ($issue_gd_warning) { self::write( "[WARN] The 'gd' library is missing or lacks full support. ". "Phabricator will not be able to generate image thumbnails without ". "gd.\n"); } else { self::write("[OKAY] 'gd' loaded and has full image type support.\n"); } self::writeHeader('FACEBOOK INTEGRATION'); $fb_auth = PhabricatorEnv::getEnvConfig('facebook.auth-enabled'); if (!$fb_auth) { self::write(" skip 'facebook.auth-enabled' not enabled.\n"); } else { self::write(" okay 'facebook.auth-enabled' is enabled.\n"); $app_id = PhabricatorEnv::getEnvConfig('facebook.application-id'); $app_secret = PhabricatorEnv::getEnvConfig('facebook.application-secret'); if (!$app_id) { self::writeFailure(); self::write( "Setup failure! 'facebook.auth-enabled' is true but there is no ". "setting for 'facebook.application-id'.\n"); return; } else { self::write(" okay 'facebook.application-id' is set.\n"); } if (!is_string($app_id)) { self::writeFailure(); self::write( "Setup failure! 'facebook.application-id' should be a string."); return; } else { self::write(" okay 'facebook.application-id' is string.\n"); } if (!$app_secret) { self::writeFailure(); self::write( "Setup failure! 'facebook.auth-enabled' is true but there is no ". "setting for 'facebook.application-secret'."); return; } else { self::write(" okay 'facebook.application-secret is set.\n"); } self::write("[OKAY] Facebook integration OKAY\n"); } self::writeHeader("MySQL DATABASE & STORAGE CONFIGURATION"); $conf = DatabaseConfigurationProvider::getConfiguration(); $conn_user = $conf->getUser(); $conn_pass = $conf->getPassword(); $conn_host = $conf->getHost(); $timeout = ini_get('mysql.connect_timeout'); if ($timeout > 5) { self::writeNote( "Your MySQL connect timeout is very high ({$timeout} seconds). ". "Consider reducing it by setting 'mysql.connect_timeout' in your ". "php.ini."); } self::write(" okay Trying to connect to MySQL database ". "{$conn_user}@{$conn_host}...\n"); ini_set('mysql.connect_timeout', 2); $conn_raw = new AphrontMySQLDatabaseConnection( array( 'user' => $conn_user, 'pass' => $conn_pass, 'host' => $conn_host, 'database' => null, )); try { queryfx($conn_raw, 'SELECT 1'); self::write(" okay Connection successful!\n"); } catch (AphrontQueryConnectionException $ex) { self::writeFailure(); self::write( "Setup failure! Unable to connect to MySQL database ". "'{$conn_host}' with user '{$conn_user}'. Edit Phabricator ". "configuration keys 'mysql.user', 'mysql.host' and 'mysql.pass' to ". "enable Phabricator to connect."); return; } $databases = queryfx_all($conn_raw, 'SHOW DATABASES'); $databases = ipull($databases, 'Database'); $databases = array_fill_keys($databases, true); if (empty($databases['phabricator_meta_data'])) { self::writeFailure(); self::write( "Setup failure! You haven't loaded the 'initialize.sql' file into ". "MySQL. This file initializes necessary databases. See this guide for ". "instructions:\n"); self::writeDoc('article/Configuration_Guide.html'); return; } else { self::write(" okay Databases have been initialized.\n"); } $schema_version = queryfx_one( $conn_raw, 'SELECT version FROM phabricator_meta_data.schema_version'); $schema_version = idx($schema_version, 'version', 'null'); $expect = PhabricatorSQLPatchList::getExpectedSchemaVersion(); if ($schema_version != $expect) { self::writeFailure(); self::write( "Setup failure! You haven't upgraded your database schema to the ". "latest version. Expected version is '{$expect}', but your local ". "version is '{$schema_version}'. See this guide for instructions:\n"); self::writeDoc('article/Upgrading_Schema.html'); return; } else { self::write(" okay Database schema are up to date (v{$expect}).\n"); } $index_min_length = queryfx_one( $conn_raw, 'SHOW VARIABLES LIKE %s', 'ft_min_word_len'); $index_min_length = idx($index_min_length, 'Value', 4); if ($index_min_length >= 4) { self::writeNote( "MySQL is configured with a 'ft_min_word_len' of 4 or greater, which ". "means you will not be able to search for 3-letter terms. Consider ". "setting this in your configuration:\n". "\n". " [mysqld]\n". " ft_min_word_len=3\n". "\n". "Then optionally run:\n". "\n". " REPAIR TABLE phabricator_search.search_documentfield QUICK;\n". "\n". "...to reindex existing documents."); } $max_allowed_packet = queryfx_one( $conn_raw, 'SHOW VARIABLES LIKE %s', 'max_allowed_packet'); $max_allowed_packet = idx($max_allowed_packet, 'Value', PHP_INT_MAX); $recommended_minimum = 1024 * 1024; if ($max_allowed_packet < $recommended_minimum) { self::writeNote( "MySQL is configured with a small 'max_allowed_packet' ". "('{$max_allowed_packet}'), which may cause some large writes to ". "fail. Consider raising this to at least {$recommended_minimum}."); } else { self::write(" okay max_allowed_packet = {$max_allowed_packet}.\n"); } $mysql_key = 'storage.mysql-engine.max-size'; $mysql_limit = PhabricatorEnv::getEnvConfig($mysql_key); if ($mysql_limit && ($mysql_limit + 8192) > $max_allowed_packet) { self::writeFailure(); self::write( "Setup failure! Your Phabricator 'storage.mysql-engine.max-size' ". "configuration ('{$mysql_limit}') must be at least 8KB smaller ". "than your MySQL 'max_allowed_packet' configuration ". "('{$max_allowed_packet}'). Raise the 'max_allowed_packet' in your ". "MySQL configuration, or reduce the maximum file size allowed by ". "the Phabricator configuration.\n"); return; } else if (!$mysql_limit) { self::write(" skip MySQL file storage engine not configured.\n"); } else { self::write(" okay MySQL file storage engine configuration okay.\n"); } $local_key = 'storage.local-disk.path'; $local_path = PhabricatorEnv::getEnvConfig($local_key); if ($local_path) { if (!Filesystem::pathExists($local_path) || !is_readable($local_path) || !is_writable($local_path)) { self::writeFailure(); self::write( "Setup failure! You have configured local disk storage but the ". "path you specified ('{$local_path}') does not exist or is not ". "readable or writable.\n"); if ($open_basedir) { self::write( "You have an 'open_basedir' setting -- make sure Phabricator is ". "allowed to open files in the local storage directory.\n"); } return; } else { self::write(" okay Local disk storage exists and is writable.\n"); } } else { self::write(" skip Not configured for local disk storage.\n"); } $selector = PhabricatorEnv::getEnvConfig('storage.engine-selector'); try { $storage_selector_exists = class_exists($selector); } catch (Exception $ex) { $storage_selector_exists = false; } if ($storage_selector_exists) { self::write(" okay Using '{$selector}' as a storage engine selector.\n"); } else { self::writeFailure(); self::write( "Setup failure! You have configured '{$selector}' as a storage engine ". "selector but it does not exist or could not be loaded.\n"); return; } self::write("[OKAY] Database and storage configuration OKAY\n"); self::writeHeader("OUTBOUND EMAIL CONFIGURATION"); $have_adapter = false; $is_ses = false; $adapter = PhabricatorEnv::getEnvConfig('metamta.mail-adapter'); switch ($adapter) { case 'PhabricatorMailImplementationPHPMailerLiteAdapter': $have_adapter = true; if (!Filesystem::pathExists('/usr/bin/sendmail') && !Filesystem::pathExists('/usr/sbin/sendmail')) { self::writeFailure(); self::write( "Setup failure! You don't have a 'sendmail' binary on this system ". "but outbound email is configured to use sendmail. Install an MTA ". "(like sendmail, qmail or postfix) or use a different outbound ". "mail configuration. See this guide for configuring outbound ". "email:\n"); self::writeDoc('article/Configuring_Outbound_Email.html'); return; } else { self::write(" okay Sendmail is configured.\n"); } break; case 'PhabricatorMailImplementationAmazonSESAdapter': $is_ses = true; $have_adapter = true; if (PhabricatorEnv::getEnvConfig('metamta.can-send-as-user')) { self::writeFailure(); self::write( "Setup failure! 'metamta.can-send-as-user' must be false when ". "configured with Amazon SES."); return; } else { self::write(" okay Sender config looks okay.\n"); } if (!PhabricatorEnv::getEnvConfig('amazon-ses.access-key')) { self::writeFailure(); self::write( "Setup failure! 'amazon-ses.access-key' is not set, but ". "outbound mail is configured to deliver via Amazon SES."); return; } else { self::write(" okay Amazon SES access key is set.\n"); } if (!PhabricatorEnv::getEnvConfig('amazon-ses.secret-key')) { self::writeFailure(); self::write( "Setup failure! 'amazon-ses.secret-key' is not set, but ". "outbound mail is configured to deliver via Amazon SES."); return; } else { self::write(" okay Amazon SES secret key is set.\n"); } if (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) { self::writeNote( "Your configuration uses Amazon SES to deliver email but tries ". "to send it immediately. This will work, but it's slow. ". "Consider configuring the MetaMTA daemon."); } break; case 'PhabricatorMailImplementationTestAdapter': self::write(" skip You have disabled outbound email.\n"); break; default: self::write(" skip Configured with a custom adapter.\n"); break; } if ($have_adapter) { $default = PhabricatorEnv::getEnvConfig('metamta.default-address'); if (!$default || $default == 'noreply@example.com') { self::writeFailure(); self::write( "Setup failure! You have not set 'metamta.default-address'."); return; } else { self::write(" okay metamta.default-address is set.\n"); } if ($is_ses) { self::writeNote( "Make sure you've verified your 'from' address ('{$default}') with ". "Amazon SES. Until you verify it, you will be unable to send mail ". "using Amazon SES."); } $domain = PhabricatorEnv::getEnvConfig('metamta.domain'); if (!$domain || $domain == 'example.com') { self::writeFailure(); self::write( "Setup failure! You have not set 'metamta.domain'."); return; } else { self::write(" okay metamta.domain is set.\n"); } self::write("[OKAY] Mail configuration OKAY\n"); } self::writeHeader('SUCCESS!'); self::write( "Congratulations! Your setup seems mostly correct, or at least fairly ". "reasonable.\n\n". "*** NEXT STEP ***\n". "Edit your configuration file (conf/{$env}.conf.php) and remove the ". "'phabricator.setup' line to finish installation."); } public static function requireExtension($extension) { if (extension_loaded($extension)) { self::write(" okay Extension '{$extension}' installed.\n"); return true; } else { self::write("[FAIL] Extension '{$extension}' is NOT INSTALLED!\n"); return false; } } private static function writeFailure() { self::write("\n\n<<< *** FAILURE! *** >>>\n"); } private static function write($str) { echo $str; ob_flush(); flush(); // This, uh, makes it look cool. -_- usleep(20000); } private static function writeNote($note) { $note = "*** NOTE: ".wordwrap($note, 75, "\n", true); $note = "\n".str_replace("\n", "\n ", $note)."\n\n"; self::write($note); } public static function writeHeader($header) { $template = '>>>'.str_repeat('-', 77); $template = substr_replace( $template, ' '.$header.' ', 3, strlen($header) + 4); self::write("\n\n{$template}\n\n"); } public static function writeDoc($doc) { self::write( "\n". ' http://phabricator.com/docs/phabricator/'.$doc. "\n\n"); } }