public static function runSetup() { header("Content-Type: text/plain"); self::write("PHABRICATOR SETUP\n\n"); // Force browser to stop buffering. self::write(str_repeat(' ', 2048)); usleep(250000); self::write("This setup mode will guide you through setting up your " . "Phabricator configuration.\n"); self::writeHeader("CORE CONFIGURATION"); // NOTE: Test this first since other tests depend on the ability to // execute system commands and will fail if safe_mode is enabled. $safe_mode = ini_get('safe_mode'); if ($safe_mode) { self::writeFailure(); self::write("Setup failure! You have 'safe_mode' enabled. Phabricator will not " . "run in safe mode, and it has been deprecated in PHP 5.3 and removed " . "in PHP 5.4.\n"); return; } else { self::write(" okay PHP's deprecated 'safe_mode' is disabled.\n"); } // NOTE: Also test this early since we can't include files from other // libraries if this is set strictly. $open_basedir = ini_get('open_basedir'); if ($open_basedir) { // 'open_basedir' restricts which files we're allowed to access with // file operations. This might be okay -- we don't need to write to // arbitrary places in the filesystem -- but we need to access certain // resources. This setting is unlikely to be providing any real measure // of security so warn even if things look OK. try { $open_libphutil = class_exists('Future'); } catch (Exception $ex) { $message = $ex->getMessage(); self::write("Unable to load modules from libphutil: {$message}\n"); $open_libphutil = false; } try { $open_arcanist = class_exists('ArcanistDiffParser'); } catch (Exception $ex) { $message = $ex->getMessage(); self::write("Unable to load modules from Arcanist: {$message}\n"); $open_arcanist = false; } $open_urandom = false; try { Filesystem::readRandomBytes(1); $open_urandom = true; } catch (FilesystemException $ex) { self::write($ex->getMessage() . "\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 makes your installation vulnerable to attack. Make sure you " . "read the documentation for this parameter and understand the " . "consequences of leaving it unconfigured.\n"); } $path = getenv('PATH'); if (empty($path)) { self::writeFailure(); self::write("Setup failure! The environmental \$PATH variable is empty. " . "Phabricator needs to execute system commands like 'svn', 'git', " . "'hg', and 'diff'. Set up your webserver so that it passes a valid " . "\$PATH to the PHP process.\n\n"); if (php_sapi_name() == 'fpm-fcgi') { self::write("You're running php-fpm, so the easiest way to do this is to add " . "this line to your php-fpm.conf:\n\n" . " env[PATH] = /usr/local/bin:/usr/bin:/bin\n\n" . "Then restart php-fpm.\n"); } return; } else { self::write(" okay \$PATH is nonempty.\n"); } self::write("[OKAY] Core configuration OKAY.\n"); self::writeHeader("REQUIRED PHP EXTENSIONS"); $extensions = array('mysql', 'hash', 'json', 'openssl', 'mbstring', 'iconv', 'curl'); foreach ($extensions as $extension) { $ok = self::requireExtension($extension); if (!$ok) { self::writeFailure(); self::write("Setup failure! Install PHP extension '{$extension}'."); return; } } list($err, $stdout, $stderr) = exec_manual('which php'); if ($err) { self::writeFailure(); self::write("Unable to locate 'php' on the command line from the web " . "server. Verify that 'php' is in the webserver's PATH.\n" . " err: {$err}\n" . "stdout: {$stdout}\n" . "stderr: {$stderr}\n"); return; } else { self::write(" okay PHP binary found on the command line.\n"); $php_bin = trim($stdout); } // NOTE: In cPanel + suphp installs, 'php' may be the PHP CGI SAPI, not the // PHP CLI SAPI. proc_open() will pass the environment to the child process, // which will re-execute the webpage (causing an infinite number of // processes to spawn). To test that the 'php' binary is safe to execute, // we call php_sapi_name() using "env -i" to wipe the environment so it // doesn't execute another reuqest if it's the wrong binary. We can't use // "-r" because php-cgi doesn't support that flag. $tmp_file = new TempFile('sapi.php'); Filesystem::writeFile($tmp_file, '<?php echo php_sapi_name();'); list($err, $stdout, $stderr) = exec_manual('/usr/bin/env -i %s -f %s', $php_bin, $tmp_file); if ($err) { self::writeFailure(); self::write("Unable to execute 'php' on the command line from the web " . "server.\n" . " err: {$err}\n" . "stdout: {$stdout}\n" . "stderr: {$stderr}\n"); return; } else { self::write(" okay PHP is available from the command line.\n"); $sapi = trim($stdout); if ($sapi != 'cli') { self::writeFailure(); self::write("The 'php' binary on this system uses the '{$sapi}' SAPI, but the " . "'cli' SAPI is expected. Replace 'php' with the php-cli SAPI " . "binary, or edit your webserver configuration so the first 'php' " . "in PATH is the 'cli' SAPI.\n\n" . "If you're running cPanel with suphp, the easiest way to fix this " . "is to add '/usr/local/bin' before '/usr/bin' for 'env_path' in " . "suconf.php:\n\n" . ' env_path="/bin:/usr/local/bin:/usr/bin"' . "\n\n"); return; } else { self::write(" okay 'php' is CLI SAPI.\n"); } } $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('php %s', "{$root}/scripts/setup/pcntl_available.php"); if ($err) { self::writeFailure(); self::write("Unable to execute scripts/setup/pcntl_available.php to " . "test for the availability of pcntl from the CLI.\n" . " err: {$err}\n" . "stdout: {$stdout}\n" . "stderr: {$stderr}\n"); 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'); $host_uri = new PhutilURI($host); $protocol = $host_uri->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 protocol\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; } $host_domain = $host_uri->getDomain(); if (strpos($host_domain, '.') !== false) { self::write(" okay phabricator.base-uri domain\n"); } else { self::writeFailure(); self::write("You must host Phabricator on a domain that contains a dot ('.'). " . "The current domain, '{$host_domain}', does not have a dot, so some " . "browsers will not set cookies on it. For instance, " . "'http://example.com/ is OK, but 'http://example/' won't work. " . "If you are using localhost, create an entry in the hosts file like " . "'127.0.0.1 example.com', and access the localhost with " . "'http://example.com/'."); 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://www.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 = PhabricatorEnv::newObjectFromConfig('mysql.configuration-provider'); $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 to 5 or below 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 = PhabricatorEnv::newObjectFromConfig('mysql.implementation', array(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) { $message = $ex->getMessage(); self::writeFailure(); self::write("Setup failure! MySQL exception: {$message} \n" . "Edit Phabricator configuration keys 'mysql.user', " . "'mysql.host' and 'mysql.pass' to enable Phabricator " . "to connect."); return; } $engines = queryfx_all($conn_raw, 'SHOW ENGINES'); $engines = ipull($engines, 'Support', 'Engine'); $innodb = idx($engines, 'InnoDB'); if ($innodb != 'YES' && $innodb != 'DEFAULT') { self::writeFailure(); self::write("Setup failure! The 'InnoDB' engine is not available. Enable " . "InnoDB in your MySQL configuration. If you already created tables, " . "MySQL incorrectly used some other engine. You need to convert " . "them or drop and reinitialize them."); return; } else { self::write(" okay InnoDB is available.\n"); } $namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace'); $databases = queryfx_all($conn_raw, 'SHOW DATABASES'); $databases = ipull($databases, 'Database', 'Database'); if (empty($databases[$namespace . '_meta_data'])) { self::writeFailure(); self::write("Setup failure! You haven't run 'bin/storage upgrade'. See this " . "article for instructions:\n"); self::writeDoc('article/Configuration_Guide.html'); return; } else { self::write(" okay Databases have been initialized.\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 {$namespace}_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"); } $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 == '*****@*****.**') { 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('CONFIG CLASSES'); foreach (PhabricatorEnv::getRequiredClasses() as $key => $instanceof) { $config = PhabricatorEnv::getEnvConfig($key); if (!$config) { self::writeNote("'{$key}' is not set."); } else { try { $r = new ReflectionClass($config); if (!$r->isSubclassOf($instanceof)) { throw new Exception("Config setting '{$key}' must be an instance of '{$instanceof}'."); } else { if (!$r->isInstantiable()) { throw new Exception("Config setting '{$key}' must be instantiable."); } } } catch (Exception $ex) { self::writeFailure(); self::write("Setup failure! " . $ex->getMessage()); return; } } } self::write("[OKAY] Config classes 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."); }