diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php index 9b718e231d..d9ad8b8375 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php @@ -1,133 +1,134 @@ setName('destroy') ->setExamples('**destroy** [__options__]') ->setSynopsis(pht('Permanently destroy all storage and data.')) ->setArguments( array( array( 'name' => 'unittest-fixtures', 'help' => pht( 'Restrict **destroy** operations to databases created '. 'by %s test fixtures.', 'PhabricatorTestCase'), ), )); } public function didExecute(PhutilArgumentParser $args) { $api = $this->getSingleAPI(); $host_display = $api->getDisplayName(); if (!$this->isDryRun() && !$this->isForce()) { if ($args->getArg('unittest-fixtures')) { $warning = pht( 'Are you completely sure you really want to destroy all unit '. 'test fixure data on host "%s"? This operation can not be undone.', $host_display); echo tsprintf( '%B', id(new PhutilConsoleBlock()) ->addParagraph($warning) ->drawConsoleString()); if (!phutil_console_confirm(pht('Destroy all unit test data?'))) { $this->logFail( pht('CANCELLED'), pht('User cancelled operation.')); exit(1); } } else { $warning = pht( 'Are you completely sure you really want to permanently destroy '. - 'all storage for Phabricator data on host "%s"? This operation '. + 'all storage for %s data on host "%s"? This operation '. 'can not be undone and your data will not be recoverable if '. 'you proceed.', + PlatformSymbols::getPlatformServerName(), $host_display); echo tsprintf( '%B', id(new PhutilConsoleBlock()) ->addParagraph($warning) ->drawConsoleString()); if (!phutil_console_confirm(pht('Permanently destroy all data?'))) { $this->logFail( pht('CANCELLED'), pht('User cancelled operation.')); exit(1); } if (!phutil_console_confirm(pht('Really destroy all data forever?'))) { $this->logFail( pht('CANCELLED'), pht('User cancelled operation.')); exit(1); } } } $patches = $this->getPatches(); if ($args->getArg('unittest-fixtures')) { $conn = $api->getConn(null); $databases = queryfx_all( $conn, 'SELECT DISTINCT(TABLE_SCHEMA) AS db '. 'FROM INFORMATION_SCHEMA.TABLES '. 'WHERE TABLE_SCHEMA LIKE %>', PhabricatorTestCase::NAMESPACE_PREFIX); $databases = ipull($databases, 'db'); } else { $databases = $api->getDatabaseList($patches); $databases[] = $api->getDatabaseName('meta_data'); // These are legacy databases that were dropped long ago. See T2237. $databases[] = $api->getDatabaseName('phid'); $databases[] = $api->getDatabaseName('directory'); } asort($databases); foreach ($databases as $database) { if ($this->isDryRun()) { $this->logInfo( pht('DRY RUN'), pht( 'Would drop database "%s" on host "%s".', $database, $host_display)); } else { $this->logWarn( pht('DESTROY'), pht( 'Dropping database "%s" on host "%s"...', $database, $host_display)); queryfx( $api->getConn(null), 'DROP DATABASE IF EXISTS %T', $database); } } if (!$this->isDryRun()) { $this->logOkay( pht('DONE'), pht( 'Storage on "%s" was destroyed.', $host_display)); } return 0; } } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php index aab4701bbf..8cc5b1ae63 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php @@ -1,440 +1,440 @@ setName('dump') ->setExamples('**dump** [__options__]') ->setSynopsis(pht('Dump all data in storage to stdout.')) ->setArguments( array( array( 'name' => 'for-replica', 'help' => pht( 'Add __--master-data__ to the __mysqldump__ command, '. 'generating a CHANGE MASTER statement in the output. This '. 'option also dumps all data, including caches.'), ), array( 'name' => 'output', 'param' => 'file', 'help' => pht( 'Write output directly to disk. This handles errors better '. 'than using pipes. Use with __--compress__ to gzip the '. 'output.'), ), array( 'name' => 'compress', 'help' => pht( 'With __--output__, write a compressed file to disk instead '. 'of a plaintext file.'), ), array( 'name' => 'no-indexes', 'help' => pht( 'Do not dump data in rebuildable index tables. This means '. 'backups are smaller and faster, but you will need to manually '. 'rebuild indexes after performing a restore.'), ), array( 'name' => 'overwrite', 'help' => pht( 'With __--output__, overwrite the output file if it already '. 'exists.'), ), array( 'name' => 'database', 'param' => 'database-name', 'help' => pht( 'Dump only tables in the named database (or databases, if '. 'the flag is repeated). Specify database names without the '. 'namespace prefix (that is: use "differential", not '. '"phabricator_differential").'), 'repeat' => true, ), )); } protected function isReadOnlyWorkflow() { return true; } public function didExecute(PhutilArgumentParser $args) { $output_file = $args->getArg('output'); $is_compress = $args->getArg('compress'); $is_overwrite = $args->getArg('overwrite'); $is_noindex = $args->getArg('no-indexes'); $is_replica = $args->getArg('for-replica'); $database_filter = $args->getArg('database'); if ($is_compress) { if ($output_file === null) { throw new PhutilArgumentUsageException( pht( 'The "--compress" flag can only be used alongside "--output".')); } if (!function_exists('gzopen')) { throw new PhutilArgumentUsageException( pht( 'The "--compress" flag requires the PHP "zlib" extension, but '. 'that extension is not available. Install the extension or '. 'omit the "--compress" option.')); } } if ($is_overwrite) { if ($output_file === null) { throw new PhutilArgumentUsageException( pht( 'The "--overwrite" flag can only be used alongside "--output".')); } } if ($is_replica && $is_noindex) { throw new PhutilArgumentUsageException( pht( 'The "--for-replica" flag can not be used with the '. '"--no-indexes" flag. Replication dumps must contain a complete '. 'representation of database state.')); } if ($output_file !== null) { if (Filesystem::pathExists($output_file)) { if (!$is_overwrite) { throw new PhutilArgumentUsageException( pht( 'Output file "%s" already exists. Use "--overwrite" '. 'to overwrite.', $output_file)); } } } $api = $this->getSingleAPI(); $patches = $this->getPatches(); $applied = $api->getAppliedPatches(); if ($applied === null) { throw new PhutilArgumentUsageException( pht( 'There is no database storage initialized in the current storage '. 'namespace ("%s"). Use "bin/storage upgrade" to initialize '. 'storage or use "--namespace" to choose a different namespace.', $api->getNamespace())); } $ref = $api->getRef(); $ref_key = $ref->getRefKey(); $schemata_query = id(new PhabricatorConfigSchemaQuery()) ->setAPIs(array($api)) ->setRefs(array($ref)); $actual_map = $schemata_query->loadActualSchemata(); $expect_map = $schemata_query->loadExpectedSchemata(); $schemata = $actual_map[$ref_key]; $expect = $expect_map[$ref_key]; if ($database_filter) { $internal_names = array(); $expect_databases = $expect->getDatabases(); foreach ($expect_databases as $expect_database) { $database_name = $expect_database->getName(); $internal_name = $api->getInternalDatabaseName($database_name); if ($internal_name !== null) { $internal_names[$internal_name] = $database_name; } } ksort($internal_names); $seen = array(); foreach ($database_filter as $filter) { if (!isset($internal_names[$filter])) { throw new PhutilArgumentUsageException( pht( 'Database "%s" is unknown. This script can only dump '. - 'databases known to the current version of Phabricator. '. + 'databases known to the current version of this software. '. 'Valid databases are: %s.', $filter, implode(', ', array_keys($internal_names)))); } if (isset($seen[$filter])) { throw new PhutilArgumentUsageException( pht( 'Database "%s" is specified more than once. Specify each '. 'database at most once.', $filter)); } $seen[$filter] = true; } $dump_databases = array_select_keys($internal_names, $database_filter); $dump_databases = array_fuse($dump_databases); } else { $dump_databases = array_keys($schemata->getDatabases()); $dump_databases = array_fuse($dump_databases); } $with_caches = $is_replica; $with_indexes = !$is_noindex; $targets = array(); foreach ($schemata->getDatabases() as $database_name => $database) { if (!isset($dump_databases[$database_name])) { continue; } $expect_database = $expect->getDatabase($database_name); foreach ($database->getTables() as $table_name => $table) { // NOTE: It's possible for us to find tables in these database which // we don't expect to be there. For example, an older version of // Phabricator may have had a table that was later dropped. We assume // these are data tables and always dump them, erring on the side of // caution. $persistence = PhabricatorConfigTableSchema::PERSISTENCE_DATA; if ($expect_database) { $expect_table = $expect_database->getTable($table_name); if ($expect_table) { $persistence = $expect_table->getPersistenceType(); } } switch ($persistence) { case PhabricatorConfigTableSchema::PERSISTENCE_CACHE: // When dumping tables, leave the data in cache tables in the // database. This will be automatically rebuild after the data // is restored and does not need to be persisted in backups. $with_data = $with_caches; break; case PhabricatorConfigTableSchema::PERSISTENCE_INDEX: // When dumping tables, leave index data behind of the caller // specified "--no-indexes". These tables can be rebuilt manually // from other tables, but do not rebuild automatically. $with_data = $with_indexes; break; case PhabricatorConfigTableSchema::PERSISTENCE_DATA: default: $with_data = true; break; } $targets[] = array( 'database' => $database_name, 'table' => $table_name, 'data' => $with_data, ); } } list($host, $port) = $this->getBareHostAndPort($api->getHost()); $has_password = false; $password = $api->getPassword(); if ($password) { if (strlen($password->openEnvelope())) { $has_password = true; } } $argv = array(); $argv[] = '--hex-blob'; $argv[] = '--single-transaction'; $argv[] = '--default-character-set'; $argv[] = $api->getClientCharset(); if ($is_replica) { $argv[] = '--master-data'; } $argv[] = '-u'; $argv[] = $api->getUser(); $argv[] = '-h'; $argv[] = $host; // MySQL's default "max_allowed_packet" setting is fairly conservative // (16MB). If we try to dump a row which is larger than this limit, the // dump will fail. // We encourage users to increase this limit during setup, but modifying // the "[mysqld]" section of the configuration file (instead of // "[mysqldump]" section) won't apply to "mysqldump" and we can not easily // detect what the "mysqldump" setting is. // Since no user would ever reasonably want a dump to fail because a row // was too large, just manually force this setting to the largest supported // value. $argv[] = '--max-allowed-packet'; $argv[] = '1G'; if ($port) { $argv[] = '--port'; $argv[] = $port; } $commands = array(); foreach ($targets as $target) { $target_argv = $argv; if (!$target['data']) { $target_argv[] = '--no-data'; } if ($has_password) { $command = csprintf( 'mysqldump -p%P %Ls -- %R %R', $password, $target_argv, $target['database'], $target['table']); } else { $command = csprintf( 'mysqldump %Ls -- %R %R', $target_argv, $target['database'], $target['table']); } $commands[] = array( 'command' => $command, 'database' => $target['database'], ); } // Decrease the CPU priority of this process so it doesn't contend with // other more important things. if (function_exists('proc_nice')) { proc_nice(19); } // If we are writing to a file, stream the command output to disk. This // mode makes sure the whole command fails if there's an error (commonly, // a full disk). See T6996 for discussion. if ($output_file === null) { $file = null; } else if ($is_compress) { $file = gzopen($output_file, 'wb1'); } else { $file = fopen($output_file, 'wb'); } if (($output_file !== null) && !$file) { throw new Exception( pht( 'Failed to open file "%s" for writing.', $file)); } $created = array(); try { foreach ($commands as $spec) { // Because we're dumping database-by-database, we need to generate our // own CREATE DATABASE and USE statements. $database = $spec['database']; $preamble = array(); if (!isset($created[$database])) { $preamble[] = "CREATE DATABASE /*!32312 IF NOT EXISTS*/ `{$database}` ". "/*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */;\n"; $created[$database] = true; } $preamble[] = "USE `{$database}`;\n"; $preamble = implode('', $preamble); $this->writeData($preamble, $file, $is_compress, $output_file); // See T13328. The "mysql" command may produce output very quickly. // Don't buffer more than a fixed amount. $future = id(new ExecFuture('%C', $spec['command'])) ->setReadBufferSize(32 * 1024 * 1024); $iterator = id(new FutureIterator(array($future))) ->setUpdateInterval(0.010); foreach ($iterator as $ready) { list($stdout, $stderr) = $future->read(); $future->discardBuffers(); if (strlen($stderr)) { fwrite(STDERR, $stderr); } $this->writeData($stdout, $file, $is_compress, $output_file); if ($ready !== null) { $ready->resolvex(); } } } if (!$file) { $ok = true; } else if ($is_compress) { $ok = gzclose($file); } else { $ok = fclose($file); } if ($ok !== true) { throw new Exception( pht( 'Failed to close file "%s".', $output_file)); } } catch (Exception $ex) { // If we might have written a partial file to disk, try to remove it so // we don't leave any confusing artifacts laying around. try { if ($file !== null) { Filesystem::remove($output_file); } } catch (Exception $ex) { // Ignore any errors we hit. } throw $ex; } return 0; } private function writeData($data, $file, $is_compress, $output_file) { if (!strlen($data)) { return; } if (!$file) { $ok = fwrite(STDOUT, $data); } else if ($is_compress) { $ok = gzwrite($file, $data); } else { $ok = fwrite($file, $data); } if ($ok !== strlen($data)) { throw new Exception( pht( 'Failed to write %d byte(s) to file "%s".', new PhutilNumber(strlen($data)), $output_file)); } } } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php index e181063ac4..b492764c99 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php @@ -1,180 +1,180 @@ setName('quickstart') ->setExamples('**quickstart** [__options__]') ->setSynopsis( pht( 'Generate a new quickstart database dump. This command is mostly '. - 'useful when developing Phabricator.')) + 'useful for internal development.')) ->setArguments( array( array( 'name' => 'output', 'param' => 'file', 'help' => pht('Specify output file to write.'), ), )); } public function execute(PhutilArgumentParser $args) { parent::execute($args); $output = $args->getArg('output'); if (!$output) { throw new PhutilArgumentUsageException( pht( 'Specify a file to write with `%s`.', '--output')); } $namespace = 'phabricator_quickstart_'.Filesystem::readRandomCharacters(8); $bin = dirname(phutil_get_library_root('phabricator')).'/bin/storage'; // We don't care which database we're using to generate a quickstart file, // since all of the schemata should be identical. $api = $this->getAnyAPI(); $ref = $api->getRef(); $ref_key = $ref->getRefKey(); if (!$api->isCharacterSetAvailable('utf8mb4')) { throw new PhutilArgumentUsageException( pht( 'You can only generate a new quickstart file if MySQL supports '. 'the %s character set (available in MySQL 5.5 and newer). The '. 'configured server does not support %s.', 'utf8mb4', 'utf8mb4')); } $err = phutil_passthru( '%s upgrade --force --no-quickstart --namespace %s --ref %s', $bin, $namespace, $ref_key); if ($err) { return $err; } $err = phutil_passthru( '%s adjust --force --namespace %s --ref %s', $bin, $namespace, $ref_key); if ($err) { return $err; } $tmp = new TempFile(); $err = phutil_passthru( '%s dump --namespace %s --ref %s > %s', $bin, $namespace, $ref_key, $tmp); if ($err) { return $err; } $err = phutil_passthru( '%s destroy --force --namespace %s --ref %s', $bin, $namespace, $ref_key); if ($err) { return $err; } $dump = Filesystem::readFile($tmp); $dump = str_replace( $namespace, '{$NAMESPACE}', $dump); // NOTE: This is a hack. We can not use `binary` for these columns, because // they are a part of a fulltext index. This regex is avoiding matching a // possible NOT NULL at the end of the line. $old = $dump; $dump = preg_replace( '/`corpus` longtext CHARACTER SET .*? COLLATE [^\s,]+/mi', '`corpus` longtext CHARACTER SET {$CHARSET_FULLTEXT} '. 'COLLATE {$COLLATE_FULLTEXT}', $dump); if ($dump == $old) { // If we didn't make any changes, yell about it. We'll produce an invalid // dump otherwise. throw new PhutilArgumentUsageException( pht( 'Failed to apply hack to adjust %s search column!', 'FULLTEXT')); } $dump = str_replace( 'utf8mb4_bin', '{$COLLATE_TEXT}', $dump); $dump = str_replace( 'utf8mb4_unicode_ci', '{$COLLATE_SORT}', $dump); $dump = str_replace( 'utf8mb4', '{$CHARSET}', $dump); $old = $dump; $dump = preg_replace( '/CHARACTER SET {\$CHARSET} COLLATE {\$COLLATE_SORT}/mi', 'CHARACTER SET {$CHARSET_SORT} COLLATE {$COLLATE_SORT}', $dump); if ($dump == $old) { throw new PhutilArgumentUsageException( pht('Failed to adjust SORT columns!')); } // Strip out a bunch of unnecessary commands which make the dump harder // to handle and slower to import. // Remove character set adjustments and key disables. $dump = preg_replace( '(^/\*.*\*/;$)m', '', $dump); // Remove comments. $dump = preg_replace('/^--.*$/m', '', $dump); // Remove table drops, locks, and unlocks. These are never relevant when // performing a quickstart. $dump = preg_replace( '/^(DROP TABLE|LOCK TABLES|UNLOCK TABLES).*$/m', '', $dump); // Collapse adjacent newlines. $dump = preg_replace('/\n\s*\n/', "\n", $dump); $dump = str_replace(';', ";\n", $dump); $dump = trim($dump)."\n"; Filesystem::writeFile($output, $dump); $console = PhutilConsole::getConsole(); $console->writeOut( "** %s ** %s\n", pht('SUCCESS'), pht('Wrote fresh quickstart SQL.')); return 0; } } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php index e0b8d884c5..6a268cd440 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php @@ -1,149 +1,149 @@ setName('upgrade') ->setExamples('**upgrade** [__options__]') ->setSynopsis(pht('Upgrade database schemata.')) ->setArguments( array( array( 'name' => 'apply', 'param' => 'patch', 'help' => pht( 'Apply __patch__ explicitly. This is an advanced feature for '. 'development and debugging; you should not normally use this '. 'flag. This skips adjustment.'), ), array( 'name' => 'no-quickstart', 'help' => pht( 'Build storage patch-by-patch from scratch, even if it could '. 'be loaded from the quickstart template.'), ), array( 'name' => 'init-only', 'help' => pht( 'Initialize storage only; do not apply patches or adjustments.'), ), array( 'name' => 'no-adjust', 'help' => pht( 'Do not apply storage adjustments after storage upgrades.'), ), )); } public function didExecute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $patches = $this->getPatches(); if (!$this->isDryRun() && !$this->isForce()) { $console->writeOut( phutil_console_wrap( pht( 'Before running storage upgrades, you should take down the '. - 'Phabricator web interface and stop any running Phabricator '. - 'daemons (you can disable this warning with %s).', + 'web interface and stop any running daemons (you can disable '. + 'this warning with %s).', '--force'))); if (!phutil_console_confirm(pht('Are you ready to continue?'))) { $console->writeOut("%s\n", pht('Cancelled.')); return 1; } } $apply_only = $args->getArg('apply'); if ($apply_only) { if (empty($patches[$apply_only])) { throw new PhutilArgumentUsageException( pht( "%s argument '%s' is not a valid patch. ". "Use '%s' to show patch status.", '--apply', $apply_only, './bin/storage status')); } } $no_quickstart = $args->getArg('no-quickstart'); $init_only = $args->getArg('init-only'); $no_adjust = $args->getArg('no-adjust'); $apis = $this->getMasterAPIs(); $this->upgradeSchemata($apis, $apply_only, $no_quickstart, $init_only); if ($apply_only || $init_only) { echo tsprintf( "%s\n", pht('Declining to synchronize static tables.')); } else { echo tsprintf( "%s\n", pht('Synchronizing static tables...')); $this->synchronizeSchemata(); } if ($no_adjust || $init_only || $apply_only) { $console->writeOut( "%s\n", pht('Declining to apply storage adjustments.')); } else { foreach ($apis as $api) { $err = $this->adjustSchemata($api, false); if ($err) { return $err; } } } return 0; } private function synchronizeSchemata() { // Synchronize the InnoDB fulltext stopwords table from the stopwords file // on disk. $table = new PhabricatorSearchDocument(); $conn = $table->establishConnection('w'); $table_ref = PhabricatorSearchDocument::STOPWORDS_TABLE; $stopwords_database = queryfx_all( $conn, 'SELECT value FROM %T', $table_ref); $stopwords_database = ipull($stopwords_database, 'value', 'value'); $stopwords_path = phutil_get_library_root('phabricator'); $stopwords_path = $stopwords_path.'/../resources/sql/stopwords.txt'; $stopwords_file = Filesystem::readFile($stopwords_path); $stopwords_file = phutil_split_lines($stopwords_file, false); $stopwords_file = array_fuse($stopwords_file); $rem_words = array_diff_key($stopwords_database, $stopwords_file); if ($rem_words) { queryfx( $conn, 'DELETE FROM %T WHERE value IN (%Ls)', $table_ref, $rem_words); } $add_words = array_diff_key($stopwords_file, $stopwords_database); if ($add_words) { foreach ($add_words as $word) { queryfx( $conn, 'INSERT IGNORE INTO %T (value) VALUES (%s)', $table_ref, $word); } } } } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php index 71f6374b2a..38a7b7788c 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php @@ -1,1287 +1,1289 @@ apis = $apis; return $this; } final public function getAnyAPI() { return head($this->getAPIs()); } final public function getMasterAPIs() { $apis = $this->getAPIs(); $results = array(); foreach ($apis as $api) { if ($api->getRef()->getIsMaster()) { $results[] = $api; } } if (!$results) { throw new PhutilArgumentUsageException( pht( 'This command only operates on database masters, but the selected '. 'database hosts do not include any masters.')); } return $results; } final public function getSingleAPI() { $apis = $this->getAPIs(); if (count($apis) == 1) { return head($apis); } throw new PhutilArgumentUsageException( pht( - 'Phabricator is configured in cluster mode, with multiple database '. + 'This server is configured in cluster mode, with multiple database '. 'hosts. Use "--host" to specify which host you want to operate on.')); } final public function getAPIs() { return $this->apis; } final protected function isDryRun() { return $this->dryRun; } final protected function setDryRun($dry_run) { $this->dryRun = $dry_run; return $this; } final protected function isForce() { return $this->force; } final protected function setForce($force) { $this->force = $force; return $this; } public function getPatches() { return $this->patches; } public function setPatches(array $patches) { assert_instances_of($patches, 'PhabricatorStoragePatch'); $this->patches = $patches; return $this; } protected function isReadOnlyWorkflow() { return false; } public function execute(PhutilArgumentParser $args) { $this->setDryRun($args->getArg('dryrun')); $this->setForce($args->getArg('force')); if (!$this->isReadOnlyWorkflow()) { if (PhabricatorEnv::isReadOnly()) { if ($this->isForce()) { PhabricatorEnv::setReadOnly(false, null); } else { throw new PhutilArgumentUsageException( pht( - 'Phabricator is currently in read-only mode. Use --force to '. + 'This server is currently in read-only mode. Use --force to '. 'override this mode.')); } } } return $this->didExecute($args); } public function didExecute(PhutilArgumentParser $args) {} private function loadSchemata(PhabricatorStorageManagementAPI $api) { $query = id(new PhabricatorConfigSchemaQuery()); $ref = $api->getRef(); $ref_key = $ref->getRefKey(); $query->setAPIs(array($api)); $query->setRefs(array($ref)); $actual = $query->loadActualSchemata(); $expect = $query->loadExpectedSchemata(); $comp = $query->buildComparisonSchemata($expect, $actual); return array( $comp[$ref_key], $expect[$ref_key], $actual[$ref_key], ); } final protected function adjustSchemata( PhabricatorStorageManagementAPI $api, $unsafe) { $lock = $this->lock($api); try { $err = $this->doAdjustSchemata($api, $unsafe); // Analyze tables if we're not doing a dry run and adjustments are either // all clear or have minor errors like surplus tables. if (!$this->dryRun) { $should_analyze = (($err == 0) || ($err == 2)); if ($should_analyze) { $this->analyzeTables($api); } } } catch (Exception $ex) { $lock->unlock(); throw $ex; } $lock->unlock(); return $err; } private function doAdjustSchemata( PhabricatorStorageManagementAPI $api, $unsafe) { $console = PhutilConsole::getConsole(); $console->writeOut( "%s\n", pht( 'Verifying database schemata on "%s"...', $api->getRef()->getRefKey())); list($adjustments, $errors) = $this->findAdjustments($api); if (!$adjustments) { $console->writeOut( "%s\n", pht('Found no adjustments for schemata.')); return $this->printErrors($errors, 0); } if (!$this->force && !$api->isCharacterSetAvailable('utf8mb4')) { $message = pht( "You have an old version of MySQL (older than 5.5) which does not ". "support the utf8mb4 character set. We strongly recommend upgrading ". "to 5.5 or newer.\n\n". "If you apply adjustments now and later update MySQL to 5.5 or newer, ". "you'll need to apply adjustments again (and they will take a long ". "time).\n\n". "You can exit this workflow, update MySQL now, and then run this ". "workflow again. This is recommended, but may cause a lot of downtime ". "right now.\n\n". - "You can exit this workflow, continue using Phabricator without ". + "You can exit this workflow, continue using this software without ". "applying adjustments, update MySQL at a later date, and then run ". "this workflow again. This is also a good approach, and will let you ". "delay downtime until later.\n\n". "You can proceed with this workflow, and then optionally update ". "MySQL at a later date. After you do, you'll need to apply ". "adjustments again.\n\n". "For more information, see \"Managing Storage Adjustments\" in ". "the documentation."); $console->writeOut( "\n** %s **\n\n%s\n", pht('OLD MySQL VERSION'), phutil_console_wrap($message)); $prompt = pht('Continue with old MySQL version?'); if (!phutil_console_confirm($prompt, $default_no = true)) { return; } } $table = id(new PhutilConsoleTable()) ->addColumn('database', array('title' => pht('Database'))) ->addColumn('table', array('title' => pht('Table'))) ->addColumn('name', array('title' => pht('Name'))) ->addColumn('info', array('title' => pht('Issues'))); foreach ($adjustments as $adjust) { $info = array(); foreach ($adjust['issues'] as $issue) { $info[] = PhabricatorConfigStorageSchema::getIssueName($issue); } $table->addRow(array( 'database' => $adjust['database'], 'table' => idx($adjust, 'table'), 'name' => idx($adjust, 'name'), 'info' => implode(', ', $info), )); } $console->writeOut("\n\n"); $table->draw(); if ($this->dryRun) { $console->writeOut( "%s\n", pht('DRYRUN: Would apply adjustments.')); return 0; } else if ($this->didInitialize) { // If we just initialized the database, continue without prompting. This // is nicer for first-time setup and there's no reasonable reason any // user would ever answer "no" to the prompt against an empty schema. } else if (!$this->force) { $console->writeOut( "\n%s\n", pht( "Found %s adjustment(s) to apply, detailed above.\n\n". "You can review adjustments in more detail from the web interface, ". "in Config > Database Status. To better understand the adjustment ". "workflow, see \"Managing Storage Adjustments\" in the ". "documentation.\n\n". "MySQL needs to copy table data to make some adjustments, so these ". "migrations may take some time.", phutil_count($adjustments))); $prompt = pht('Apply these schema adjustments?'); if (!phutil_console_confirm($prompt, $default_no = true)) { return 1; } } $console->writeOut( "%s\n", pht('Applying schema adjustments...')); $conn = $api->getConn(null); if ($unsafe) { queryfx($conn, 'SET SESSION sql_mode = %s', ''); } else { queryfx($conn, 'SET SESSION sql_mode = %s', 'STRICT_ALL_TABLES'); } $failed = array(); // We make changes in several phases. $phases = array( // Drop surplus autoincrements. This allows us to drop primary keys on // autoincrement columns. 'drop_auto', // Drop all keys we're going to adjust. This prevents them from // interfering with column changes. 'drop_keys', // Apply all database, table, and column changes. 'main', // Restore adjusted keys. 'add_keys', // Add missing autoincrements. 'add_auto', ); $bar = id(new PhutilConsoleProgressBar()) ->setTotal(count($adjustments) * count($phases)); foreach ($phases as $phase) { foreach ($adjustments as $adjust) { try { switch ($adjust['kind']) { case 'database': if ($phase == 'main') { queryfx( $conn, 'ALTER DATABASE %T CHARACTER SET = %s COLLATE = %s', $adjust['database'], $adjust['charset'], $adjust['collation']); } break; case 'table': if ($phase == 'main') { queryfx( $conn, 'ALTER TABLE %T.%T COLLATE = %s, ENGINE = %s', $adjust['database'], $adjust['table'], $adjust['collation'], $adjust['engine']); } break; case 'column': $apply = false; $auto = false; $new_auto = idx($adjust, 'auto'); if ($phase == 'drop_auto') { if ($new_auto === false) { $apply = true; $auto = false; } } else if ($phase == 'main') { $apply = true; if ($new_auto === false) { $auto = false; } else { $auto = $adjust['is_auto']; } } else if ($phase == 'add_auto') { if ($new_auto === true) { $apply = true; $auto = true; } } if ($apply) { $parts = array(); if ($auto) { $parts[] = qsprintf( $conn, 'AUTO_INCREMENT'); } if ($adjust['charset']) { switch ($adjust['charset']) { case 'binary': $charset_value = qsprintf($conn, 'binary'); break; case 'utf8': $charset_value = qsprintf($conn, 'utf8'); break; case 'utf8mb4': $charset_value = qsprintf($conn, 'utf8mb4'); break; default: throw new Exception( pht( 'Unsupported character set "%s".', $adjust['charset'])); } switch ($adjust['collation']) { case 'binary': $collation_value = qsprintf($conn, 'binary'); break; case 'utf8_general_ci': $collation_value = qsprintf($conn, 'utf8_general_ci'); break; case 'utf8mb4_bin': $collation_value = qsprintf($conn, 'utf8mb4_bin'); break; case 'utf8mb4_unicode_ci': $collation_value = qsprintf($conn, 'utf8mb4_unicode_ci'); break; default: throw new Exception( pht( 'Unsupported collation set "%s".', $adjust['collation'])); } $parts[] = qsprintf( $conn, 'CHARACTER SET %Q COLLATE %Q', $charset_value, $collation_value); } if ($parts) { $parts = qsprintf($conn, '%LJ', $parts); } else { $parts = qsprintf($conn, ''); } if ($adjust['nullable']) { $nullable = qsprintf($conn, 'NULL'); } else { $nullable = qsprintf($conn, 'NOT NULL'); } // TODO: We're using "%Z" here for the column type, which is // technically unsafe. It would be nice to be able to use "%Q" // instead, but this requires a fair amount of legwork to // enumerate all column types. queryfx( $conn, 'ALTER TABLE %T.%T MODIFY %T %Z %Q %Q', $adjust['database'], $adjust['table'], $adjust['name'], $adjust['type'], $parts, $nullable); } break; case 'key': if (($phase == 'drop_keys') && $adjust['exists']) { if ($adjust['name'] == 'PRIMARY') { $key_name = qsprintf($conn, 'PRIMARY KEY'); } else { $key_name = qsprintf($conn, 'KEY %T', $adjust['name']); } queryfx( $conn, 'ALTER TABLE %T.%T DROP %Q', $adjust['database'], $adjust['table'], $key_name); } if (($phase == 'add_keys') && $adjust['keep']) { // Different keys need different creation syntax. Notable // special cases are primary keys and fulltext keys. if ($adjust['name'] == 'PRIMARY') { $key_name = qsprintf($conn, 'PRIMARY KEY'); } else if ($adjust['indexType'] == 'FULLTEXT') { $key_name = qsprintf($conn, 'FULLTEXT %T', $adjust['name']); } else { if ($adjust['unique']) { $key_name = qsprintf( $conn, 'UNIQUE KEY %T', $adjust['name']); } else { $key_name = qsprintf( $conn, '/* NONUNIQUE */ KEY %T', $adjust['name']); } } queryfx( $conn, 'ALTER TABLE %T.%T ADD %Q (%LK)', $adjust['database'], $adjust['table'], $key_name, $adjust['columns']); } break; default: throw new Exception( pht('Unknown schema adjustment kind "%s"!', $adjust['kind'])); } } catch (AphrontQueryException $ex) { $failed[] = array($adjust, $ex); } $bar->update(1); } } $bar->done(); if (!$failed) { $console->writeOut( "%s\n", pht('Completed applying all schema adjustments.')); $err = 0; } else { $table = id(new PhutilConsoleTable()) ->addColumn('target', array('title' => pht('Target'))) ->addColumn('error', array('title' => pht('Error'))); foreach ($failed as $failure) { list($adjust, $ex) = $failure; $pieces = array_select_keys( $adjust, array('database', 'table', 'name')); $pieces = array_filter($pieces); $target = implode('.', $pieces); $table->addRow( array( 'target' => $target, 'error' => $ex->getMessage(), )); } $console->writeOut("\n"); $table->draw(); $console->writeOut( "\n%s\n", pht('Failed to make some schema adjustments, detailed above.')); $console->writeOut( "%s\n", pht( 'For help troubleshooting adjustments, see "Managing Storage '. 'Adjustments" in the documentation.')); $err = 1; } return $this->printErrors($errors, $err); } private function findAdjustments( PhabricatorStorageManagementAPI $api) { list($comp, $expect, $actual) = $this->loadSchemata($api); $issue_charset = PhabricatorConfigStorageSchema::ISSUE_CHARSET; $issue_collation = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $issue_columntype = PhabricatorConfigStorageSchema::ISSUE_COLUMNTYPE; $issue_surpluskey = PhabricatorConfigStorageSchema::ISSUE_SURPLUSKEY; $issue_missingkey = PhabricatorConfigStorageSchema::ISSUE_MISSINGKEY; $issue_columns = PhabricatorConfigStorageSchema::ISSUE_KEYCOLUMNS; $issue_unique = PhabricatorConfigStorageSchema::ISSUE_UNIQUE; $issue_longkey = PhabricatorConfigStorageSchema::ISSUE_LONGKEY; $issue_auto = PhabricatorConfigStorageSchema::ISSUE_AUTOINCREMENT; $issue_engine = PhabricatorConfigStorageSchema::ISSUE_ENGINE; $adjustments = array(); $errors = array(); foreach ($comp->getDatabases() as $database_name => $database) { foreach ($this->findErrors($database) as $issue) { $errors[] = array( 'database' => $database_name, 'issue' => $issue, ); } $expect_database = $expect->getDatabase($database_name); $actual_database = $actual->getDatabase($database_name); if (!$expect_database || !$actual_database) { // If there's a real issue here, skip this stuff. continue; } if ($actual_database->getAccessDenied()) { // If we can't access the database, we can't access the tables either. continue; } $issues = array(); if ($database->hasIssue($issue_charset)) { $issues[] = $issue_charset; } if ($database->hasIssue($issue_collation)) { $issues[] = $issue_collation; } if ($issues) { $adjustments[] = array( 'kind' => 'database', 'database' => $database_name, 'issues' => $issues, 'charset' => $expect_database->getCharacterSet(), 'collation' => $expect_database->getCollation(), ); } foreach ($database->getTables() as $table_name => $table) { foreach ($this->findErrors($table) as $issue) { $errors[] = array( 'database' => $database_name, 'table' => $table_name, 'issue' => $issue, ); } $expect_table = $expect_database->getTable($table_name); $actual_table = $actual_database->getTable($table_name); if (!$expect_table || !$actual_table) { continue; } $issues = array(); if ($table->hasIssue($issue_collation)) { $issues[] = $issue_collation; } if ($table->hasIssue($issue_engine)) { $issues[] = $issue_engine; } if ($issues) { $adjustments[] = array( 'kind' => 'table', 'database' => $database_name, 'table' => $table_name, 'issues' => $issues, 'collation' => $expect_table->getCollation(), 'engine' => $expect_table->getEngine(), ); } foreach ($table->getColumns() as $column_name => $column) { foreach ($this->findErrors($column) as $issue) { $errors[] = array( 'database' => $database_name, 'table' => $table_name, 'name' => $column_name, 'issue' => $issue, ); } $expect_column = $expect_table->getColumn($column_name); $actual_column = $actual_table->getColumn($column_name); if (!$expect_column || !$actual_column) { continue; } $issues = array(); if ($column->hasIssue($issue_collation)) { $issues[] = $issue_collation; } if ($column->hasIssue($issue_charset)) { $issues[] = $issue_charset; } if ($column->hasIssue($issue_columntype)) { $issues[] = $issue_columntype; } if ($column->hasIssue($issue_auto)) { $issues[] = $issue_auto; } if ($issues) { if ($expect_column->getCharacterSet() === null) { // For non-text columns, we won't be specifying a collation or // character set. $charset = null; $collation = null; } else { $charset = $expect_column->getCharacterSet(); $collation = $expect_column->getCollation(); } $adjustment = array( 'kind' => 'column', 'database' => $database_name, 'table' => $table_name, 'name' => $column_name, 'issues' => $issues, 'collation' => $collation, 'charset' => $charset, 'type' => $expect_column->getColumnType(), // NOTE: We don't adjust column nullability because it is // dangerous, so always use the current nullability. 'nullable' => $actual_column->getNullable(), // NOTE: This always stores the current value, because we have // to make these updates separately. 'is_auto' => $actual_column->getAutoIncrement(), ); if ($column->hasIssue($issue_auto)) { $adjustment['auto'] = $expect_column->getAutoIncrement(); } $adjustments[] = $adjustment; } } foreach ($table->getKeys() as $key_name => $key) { foreach ($this->findErrors($key) as $issue) { $errors[] = array( 'database' => $database_name, 'table' => $table_name, 'name' => $key_name, 'issue' => $issue, ); } $expect_key = $expect_table->getKey($key_name); $actual_key = $actual_table->getKey($key_name); $issues = array(); $keep_key = true; if ($key->hasIssue($issue_surpluskey)) { $issues[] = $issue_surpluskey; $keep_key = false; } if ($key->hasIssue($issue_missingkey)) { $issues[] = $issue_missingkey; } if ($key->hasIssue($issue_columns)) { $issues[] = $issue_columns; } if ($key->hasIssue($issue_unique)) { $issues[] = $issue_unique; } // NOTE: We can't really fix this, per se, but we may need to remove // the key to change the column type. In the best case, the new // column type won't be overlong and recreating the key really will // fix the issue. In the worst case, we get the right column type and // lose the key, which is still better than retaining the key having // the wrong column type. if ($key->hasIssue($issue_longkey)) { $issues[] = $issue_longkey; } if ($issues) { $adjustment = array( 'kind' => 'key', 'database' => $database_name, 'table' => $table_name, 'name' => $key_name, 'issues' => $issues, 'exists' => (bool)$actual_key, 'keep' => $keep_key, ); if ($keep_key) { $adjustment += array( 'columns' => $expect_key->getColumnNames(), 'unique' => $expect_key->getUnique(), 'indexType' => $expect_key->getIndexType(), ); } $adjustments[] = $adjustment; } } } } return array($adjustments, $errors); } private function findErrors(PhabricatorConfigStorageSchema $schema) { $result = array(); foreach ($schema->getLocalIssues() as $issue) { $status = PhabricatorConfigStorageSchema::getIssueStatus($issue); if ($status == PhabricatorConfigStorageSchema::STATUS_FAIL) { $result[] = $issue; } } return $result; } private function printErrors(array $errors, $default_return) { if (!$errors) { return $default_return; } $console = PhutilConsole::getConsole(); $table = id(new PhutilConsoleTable()) ->addColumn('target', array('title' => pht('Target'))) ->addColumn('error', array('title' => pht('Error'))); $any_surplus = false; $all_surplus = true; $any_access = false; $all_access = true; foreach ($errors as $error) { $pieces = array_select_keys( $error, array('database', 'table', 'name')); $pieces = array_filter($pieces); $target = implode('.', $pieces); $name = PhabricatorConfigStorageSchema::getIssueName($error['issue']); $issue = $error['issue']; if ($issue === PhabricatorConfigStorageSchema::ISSUE_SURPLUS) { $any_surplus = true; } else { $all_surplus = false; } if ($issue === PhabricatorConfigStorageSchema::ISSUE_ACCESSDENIED) { $any_access = true; } else { $all_access = false; } $table->addRow( array( 'target' => $target, 'error' => $name, )); } $console->writeOut("\n"); $table->draw(); $console->writeOut("\n"); $message = array(); if ($all_surplus) { $message[] = pht( - 'You have surplus schemata (extra tables or columns which Phabricator '. - 'does not expect). For information on resolving these '. + 'You have surplus schemata (extra tables or columns which this '. + 'software does not expect). For information on resolving these '. 'issues, see the "Surplus Schemata" section in the "Managing Storage '. 'Adjustments" article in the documentation.'); } else if ($all_access) { $message[] = pht( 'The user you are connecting to MySQL with does not have the correct '. 'permissions, and can not access some databases or tables that it '. 'needs to be able to access. GRANT the user additional permissions.'); } else { $message[] = pht( 'The schemata have errors (detailed above) which the adjustment '. 'workflow can not fix.'); if ($any_access) { $message[] = pht( 'Some of these errors are caused by access control problems. '. 'The user you are connecting with does not have permission to see '. - 'all of the database or tables that Phabricator uses. You need to '. + 'all of the database or tables that this software uses. You need to '. 'GRANT the user more permission, or use a different user.'); } if ($any_surplus) { $message[] = pht( 'Some of these errors are caused by surplus schemata (extra '. - 'tables or columns which Phabricator does not expect). These are '. + 'tables or columns which this software does not expect). These are '. 'not serious. For information on resolving these issues, see the '. '"Surplus Schemata" section in the "Managing Storage Adjustments" '. 'article in the documentation.'); } $message[] = pht( - 'If you are not developing Phabricator itself, report this issue to '. - 'the upstream.'); + 'If you are not developing %s itself, report this issue to '. + 'the upstream.', + PlatformSymbols::getPlatformServerName()); $message[] = pht( - 'If you are developing Phabricator, these errors usually indicate '. + 'If you are developing %s, these errors usually indicate '. 'that your schema specifications do not agree with the schemata your '. - 'code actually builds.'); + 'code actually builds.', + PlatformSymbols::getPlatformServerName()); } $message = implode("\n\n", $message); if ($all_surplus) { $console->writeOut( "** %s **\n\n%s\n", pht('SURPLUS SCHEMATA'), phutil_console_wrap($message)); } else if ($all_access) { $console->writeOut( "** %s **\n\n%s\n", pht('ACCESS DENIED'), phutil_console_wrap($message)); } else { $console->writeOut( "** %s **\n\n%s\n", pht('SCHEMATA ERRORS'), phutil_console_wrap($message)); } return 2; } final protected function upgradeSchemata( array $apis, $apply_only = null, $no_quickstart = false, $init_only = false) { $locks = array(); foreach ($apis as $api) { $locks[] = $this->lock($api); } try { $this->doUpgradeSchemata($apis, $apply_only, $no_quickstart, $init_only); } catch (Exception $ex) { foreach ($locks as $lock) { $lock->unlock(); } throw $ex; } foreach ($locks as $lock) { $lock->unlock(); } } private function doUpgradeSchemata( array $apis, $apply_only, $no_quickstart, $init_only) { $patches = $this->patches; $is_dryrun = $this->dryRun; // We expect that patches should already be sorted properly. However, // phase behavior will be wrong if they aren't, so make sure. $patches = msortv($patches, 'newSortVector'); $api_map = array(); foreach ($apis as $api) { $api_map[$api->getRef()->getRefKey()] = $api; } foreach ($api_map as $ref_key => $api) { $applied = $api->getAppliedPatches(); $needs_init = ($applied === null); if (!$needs_init) { continue; } if ($is_dryrun) { echo tsprintf( "%s\n", pht( 'DRYRUN: Storage on host "%s" does not exist yet, so it '. 'would be created.', $ref_key)); continue; } if ($apply_only) { throw new PhutilArgumentUsageException( pht( 'Storage on host "%s" has not been initialized yet. You must '. 'initialize storage before selectively applying patches.', $ref_key)); } // If we're initializing storage for the first time on any host, track // it so that we can give the user a nicer experience during the // subsequent adjustment phase. $this->didInitialize = true; $legacy = $api->getLegacyPatches($patches); if ($legacy || $no_quickstart || $init_only) { // If we have legacy patches, we can't quickstart. $api->createDatabase('meta_data'); $api->createTable( 'meta_data', 'patch_status', array( 'patch VARCHAR(255) NOT NULL PRIMARY KEY COLLATE utf8_general_ci', 'applied INT UNSIGNED NOT NULL', )); foreach ($legacy as $patch) { $api->markPatchApplied($patch); } } else { echo tsprintf( "%s\n", pht( 'Loading quickstart template onto "%s"...', $ref_key)); $root = dirname(phutil_get_library_root('phabricator')); $sql = $root.'/resources/sql/quickstart.sql'; $api->applyPatchSQL($sql); } } if ($init_only) { echo pht('Storage initialized.')."\n"; return 0; } $applied_map = array(); $state_map = array(); foreach ($api_map as $ref_key => $api) { $applied = $api->getAppliedPatches(); // If we still have nothing applied, this is a dry run and we didn't // actually initialize storage. Here, just do nothing. if ($applied === null) { if ($is_dryrun) { continue; } else { throw new Exception( pht( 'Database initialization on host "%s" applied no patches!', $ref_key)); } } $applied = array_fuse($applied); $state_map[$ref_key] = $applied; if ($apply_only) { if (isset($applied[$apply_only])) { if (!$this->force && !$is_dryrun) { echo phutil_console_wrap( pht( 'Patch "%s" has already been applied on host "%s". Are you '. 'sure you want to apply it again? This may put your storage '. 'in a state that the upgrade scripts can not automatically '. 'manage.', $apply_only, $ref_key)); if (!phutil_console_confirm(pht('Apply patch again?'))) { echo pht('Cancelled.')."\n"; return 1; } } // Mark this patch as not yet applied on this host. unset($applied[$apply_only]); } } $applied_map[$ref_key] = $applied; } // If we're applying only a specific patch, select just that patch. if ($apply_only) { $patches = array_select_keys($patches, array($apply_only)); } // Apply each patch to each database. We apply patches patch-by-patch, // not database-by-database: for each patch we apply it to every database, // then move to the next patch. // We must do this because ".php" patches may depend on ".sql" patches // being up to date on all masters, and that will work fine if we put each // patch on every host before moving on. If we try to bring database hosts // up to date one at a time we can end up in a big mess. $duration_map = array(); // First, find any global patches which have been applied to ANY database. // We are just going to mark these as applied without actually running // them. Otherwise, adding new empty masters to an existing cluster will // try to apply them against invalid states. foreach ($patches as $key => $patch) { if ($patch->getIsGlobalPatch()) { foreach ($applied_map as $ref_key => $applied) { if (isset($applied[$key])) { $duration_map[$key] = 1; } } } } while (true) { $applied_something = false; foreach ($patches as $key => $patch) { // First, check if any databases need this patch. We can just skip it // if it has already been applied everywhere. $need_patch = array(); foreach ($applied_map as $ref_key => $applied) { if (isset($applied[$key])) { continue; } $need_patch[] = $ref_key; } if (!$need_patch) { unset($patches[$key]); continue; } // Check if we can apply this patch yet. Before we can apply a patch, // all of the dependencies for the patch must have been applied on all // databases. Requiring that all databases stay in sync prevents one // database from racing ahead if it happens to get a patch that nothing // else has yet. $missing_patch = null; foreach ($patch->getAfter() as $after) { foreach ($applied_map as $ref_key => $applied) { if (isset($applied[$after])) { // This database already has the patch. We can apply it to // other databases but don't need to apply it here. continue; } $missing_patch = $after; break 2; } } if ($missing_patch) { if ($apply_only) { echo tsprintf( "%s\n", pht( 'Unable to apply patch "%s" because it depends on patch '. '"%s", which has not been applied on some hosts: %s.', $apply_only, $missing_patch, implode(', ', $need_patch))); return 1; } else { // Some databases are missing the dependencies, so keep trying // other patches instead. If everything goes right, we'll apply the // dependencies and then come back and apply this patch later. continue; } } $is_global = $patch->getIsGlobalPatch(); $patch_apis = array_select_keys($api_map, $need_patch); foreach ($patch_apis as $ref_key => $api) { if ($is_global) { // If this is a global patch which we previously applied, just // read the duration from the map without actually applying // the patch. $duration = idx($duration_map, $key); } else { $duration = null; } if ($duration === null) { if ($is_dryrun) { echo tsprintf( "%s\n", pht( 'DRYRUN: Would apply patch "%s" to host "%s".', $key, $ref_key)); } else { echo tsprintf( "%s\n", pht( 'Applying patch "%s" to host "%s"...', $key, $ref_key)); } $t_begin = microtime(true); if (!$is_dryrun) { $api->applyPatch($patch); } $t_end = microtime(true); $duration = ($t_end - $t_begin); $duration_map[$key] = $duration; } // If we're explicitly reapplying this patch, we don't need to // mark it as applied. if (!isset($state_map[$ref_key][$key])) { if (!$is_dryrun) { $api->markPatchApplied($key, ($t_end - $t_begin)); } $applied_map[$ref_key][$key] = true; } } // We applied this everywhere, so we're done with the patch. unset($patches[$key]); $applied_something = true; } if (!$applied_something) { if ($patches) { throw new Exception( pht( 'Some patches could not be applied: %s', implode(', ', array_keys($patches)))); } else if (!$is_dryrun && !$apply_only) { echo pht( 'Storage is up to date. Use "%s" for details.', 'storage status')."\n"; } break; } } } final protected function getBareHostAndPort($host) { // Split out port information, since the command-line client requires a // separate flag for the port. $uri = new PhutilURI('mysql://'.$host); if ($uri->getPort()) { $port = $uri->getPort(); $bare_hostname = $uri->getDomain(); } else { $port = null; $bare_hostname = $host; } return array($bare_hostname, $port); } /** * Acquires a @{class:PhabricatorGlobalLock}. * * @return PhabricatorGlobalLock */ final protected function lock(PhabricatorStorageManagementAPI $api) { // Although we're holding this lock on different databases so it could // have the same name on each as far as the database is concerned, the // locks would be the same within this process. $parameters = array( 'refKey' => $api->getRef()->getRefKey(), ); // We disable logging for this lock because we may not have created the // log table yet, or may need to adjust it. return PhabricatorGlobalLock::newLock('adjust', $parameters) ->setExternalConnection($api->getConn(null)) ->setDisableLogging(true) ->lock(); } final protected function analyzeTables( PhabricatorStorageManagementAPI $api) { // Analyzing tables can sometimes have a significant effect on query // performance, particularly for the fulltext ngrams tables. See T12819 // for some specific examples. $conn = $api->getConn(null); $patches = $this->getPatches(); $databases = $api->getDatabaseList($patches, true); $this->logInfo( pht('ANALYZE'), pht('Analyzing tables...')); $targets = array(); foreach ($databases as $database) { queryfx($conn, 'USE %C', $database); $tables = queryfx_all($conn, 'SHOW TABLE STATUS'); foreach ($tables as $table) { $table_name = $table['Name']; $targets[] = array( 'database' => $database, 'table' => $table_name, ); } } $bar = id(new PhutilConsoleProgressBar()) ->setTotal(count($targets)); foreach ($targets as $target) { queryfx( $conn, 'ANALYZE TABLE %T.%T', $target['database'], $target['table']); $bar->update(1); } $bar->done(); $this->logOkay( pht('ANALYZED'), pht( 'Analyzed %d table(s).', count($targets))); } } diff --git a/src/infrastructure/util/password/PhabricatorPasswordHasher.php b/src/infrastructure/util/password/PhabricatorPasswordHasher.php index f0f30045c0..fe35d2c296 100644 --- a/src/infrastructure/util/password/PhabricatorPasswordHasher.php +++ b/src/infrastructure/util/password/PhabricatorPasswordHasher.php @@ -1,420 +1,420 @@ getPasswordHash($password)->openEnvelope(); $expect_hash = $hash->openEnvelope(); return phutil_hashes_are_identical($actual_hash, $expect_hash); } /** * Check if an existing hash created by this algorithm is upgradeable. * * The default implementation returns `false`. However, hash algorithms which * have (for example) an internal cost function may be able to upgrade an * existing hash to a stronger one with a higher cost. * * @param PhutilOpaqueEnvelope Bare hash. * @return bool True if the hash can be upgraded without * changing the algorithm (for example, to a * higher cost). * @task hasher */ protected function canUpgradeInternalHash(PhutilOpaqueEnvelope $hash) { return false; } /* -( Using Hashers )------------------------------------------------------ */ /** * Get the hash of a password for storage. * * @param PhutilOpaqueEnvelope Password text. * @return PhutilOpaqueEnvelope Hashed text. * @task hashing */ final public function getPasswordHashForStorage( PhutilOpaqueEnvelope $envelope) { $name = $this->getHashName(); $hash = $this->getPasswordHash($envelope); $actual_len = strlen($hash->openEnvelope()); $expect_len = $this->getHashLength(); if ($actual_len > $expect_len) { throw new Exception( pht( "Password hash '%s' produced a hash of length %d, but a ". "maximum length of %d was expected.", $name, new PhutilNumber($actual_len), new PhutilNumber($expect_len))); } return new PhutilOpaqueEnvelope($name.':'.$hash->openEnvelope()); } /** * Parse a storage hash into its components, like the hash type and hash * data. * * @return map Dictionary of information about the hash. * @task hashing */ private static function parseHashFromStorage(PhutilOpaqueEnvelope $hash) { $raw_hash = $hash->openEnvelope(); if (strpos($raw_hash, ':') === false) { throw new Exception( pht( 'Malformed password hash, expected "name:hash".')); } list($name, $hash) = explode(':', $raw_hash); return array( 'name' => $name, 'hash' => new PhutilOpaqueEnvelope($hash), ); } /** * Get all available password hashers. This may include hashers which can not * actually be used (for example, a required extension is missing). * * @return list Hasher objects. * @task hashing */ public static function getAllHashers() { $objects = id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getHashName') ->execute(); foreach ($objects as $object) { $name = $object->getHashName(); $potential_length = strlen($name) + $object->getHashLength() + 1; $maximum_length = self::MAXIMUM_STORAGE_SIZE; if ($potential_length > $maximum_length) { throw new Exception( pht( 'Hasher "%s" may produce hashes which are too long to fit in '. 'storage. %d characters are available, but its hashes may be '. 'up to %d characters in length.', $name, $maximum_length, $potential_length)); } } return $objects; } /** * Get all usable password hashers. This may include hashers which are * not desirable or advisable. * * @return list Hasher objects. * @task hashing */ public static function getAllUsableHashers() { $hashers = self::getAllHashers(); foreach ($hashers as $key => $hasher) { if (!$hasher->canHashPasswords()) { unset($hashers[$key]); } } return $hashers; } /** * Get the best (strongest) available hasher. * * @return PhabricatorPasswordHasher Best hasher. * @task hashing */ public static function getBestHasher() { $hashers = self::getAllUsableHashers(); $hashers = msort($hashers, 'getStrength'); $hasher = last($hashers); if (!$hasher) { throw new PhabricatorPasswordHasherUnavailableException( pht( 'There are no password hashers available which are usable for '. 'new passwords.')); } return $hasher; } /** * Get the hasher for a given stored hash. * * @return PhabricatorPasswordHasher Corresponding hasher. * @task hashing */ public static function getHasherForHash(PhutilOpaqueEnvelope $hash) { $info = self::parseHashFromStorage($hash); $name = $info['name']; $usable = self::getAllUsableHashers(); if (isset($usable[$name])) { return $usable[$name]; } $all = self::getAllHashers(); if (isset($all[$name])) { throw new PhabricatorPasswordHasherUnavailableException( pht( 'Attempting to compare a password saved with the "%s" hash. The '. 'hasher exists, but is not currently usable. %s', $name, $all[$name]->getInstallInstructions())); } throw new PhabricatorPasswordHasherUnavailableException( pht( 'Attempting to compare a password saved with the "%s" hash. No such '. - 'hasher is known to Phabricator.', + 'hasher is known.', $name)); } /** * Test if a password is using an weaker hash than the strongest available * hash. This can be used to prompt users to upgrade, or automatically upgrade * on login. * * @return bool True to indicate that rehashing this password will improve * the hash strength. * @task hashing */ public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) { if (!strlen($hash->openEnvelope())) { throw new Exception( pht('Expected a password hash, received nothing!')); } $current_hasher = self::getHasherForHash($hash); $best_hasher = self::getBestHasher(); if ($current_hasher->getHashName() != $best_hasher->getHashName()) { // If the algorithm isn't the best one, we can upgrade. return true; } $info = self::parseHashFromStorage($hash); if ($current_hasher->canUpgradeInternalHash($info['hash'])) { // If the algorithm provides an internal upgrade, we can also upgrade. return true; } // Already on the best algorithm with the best settings. return false; } /** * Generate a new hash for a password, using the best available hasher. * * @param PhutilOpaqueEnvelope Password to hash. * @return PhutilOpaqueEnvelope Hashed password, using best available * hasher. * @task hashing */ public static function generateNewPasswordHash( PhutilOpaqueEnvelope $password) { $hasher = self::getBestHasher(); return $hasher->getPasswordHashForStorage($password); } /** * Compare a password to a stored hash. * * @param PhutilOpaqueEnvelope Password to compare. * @param PhutilOpaqueEnvelope Stored password hash. * @return bool True if the passwords match. * @task hashing */ public static function comparePassword( PhutilOpaqueEnvelope $password, PhutilOpaqueEnvelope $hash) { $hasher = self::getHasherForHash($hash); $parts = self::parseHashFromStorage($hash); return $hasher->verifyPassword($password, $parts['hash']); } /** * Get the human-readable algorithm name for a given hash. * * @param PhutilOpaqueEnvelope Storage hash. * @return string Human-readable algorithm name. */ public static function getCurrentAlgorithmName(PhutilOpaqueEnvelope $hash) { $raw_hash = $hash->openEnvelope(); if (!strlen($raw_hash)) { return pht('None'); } try { $current_hasher = self::getHasherForHash($hash); return $current_hasher->getHumanReadableName(); } catch (Exception $ex) { $info = self::parseHashFromStorage($hash); $name = $info['name']; return pht('Unknown ("%s")', $name); } } /** * Get the human-readable algorithm name for the best available hash. * * @return string Human-readable name for best hash. */ public static function getBestAlgorithmName() { try { $best_hasher = self::getBestHasher(); return $best_hasher->getHumanReadableName(); } catch (Exception $ex) { return pht('Unknown'); } } } diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index ecb12bb81d..6f6ce56cad 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -1,914 +1,914 @@ showFooter = $show_footer; return $this; } public function getShowFooter() { return $this->showFooter; } public function setApplicationName($application_name) { $this->applicationName = $application_name; return $this; } public function setDisableConsole($disable) { $this->disableConsole = $disable; return $this; } public function getApplicationName() { return $this->applicationName; } public function setBaseURI($base_uri) { $this->baseURI = $base_uri; return $this; } public function getBaseURI() { return $this->baseURI; } public function setShowChrome($show_chrome) { $this->showChrome = $show_chrome; return $this; } public function getShowChrome() { return $this->showChrome; } public function addClass($class) { $this->classes[] = $class; return $this; } public function setPageObjectPHIDs(array $phids) { $this->pageObjects = $phids; return $this; } public function setShowDurableColumn($show) { $this->showDurableColumn = $show; return $this; } public function getShowDurableColumn() { $request = $this->getRequest(); if (!$request) { return false; } $viewer = $request->getUser(); if (!$viewer->isLoggedIn()) { return false; } $conpherence_installed = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorConpherenceApplication', $viewer); if (!$conpherence_installed) { return false; } if ($this->isQuicksandBlacklistURI()) { return false; } return true; } private function isQuicksandBlacklistURI() { $request = $this->getRequest(); if (!$request) { return false; } $patterns = $this->getQuicksandURIPatternBlacklist(); $path = $request->getRequestURI()->getPath(); foreach ($patterns as $pattern) { if (preg_match('(^'.$pattern.'$)', $path)) { return true; } } return false; } public function getDurableColumnVisible() { $column_key = PhabricatorConpherenceColumnVisibleSetting::SETTINGKEY; return (bool)$this->getUserPreference($column_key, false); } public function getDurableColumnMinimize() { $column_key = PhabricatorConpherenceColumnMinimizeSetting::SETTINGKEY; return (bool)$this->getUserPreference($column_key, false); } public function addQuicksandConfig(array $config) { $this->quicksandConfig = $config + $this->quicksandConfig; return $this; } public function getQuicksandConfig() { return $this->quicksandConfig; } public function setCrumbs(PHUICrumbsView $crumbs) { $this->crumbs = $crumbs; return $this; } public function getCrumbs() { return $this->crumbs; } public function setTabs(PHUIListView $tabs) { $tabs->setType(PHUIListView::TABBAR_LIST); $tabs->addClass('phabricator-standard-page-tabs'); $this->tabs = $tabs; return $this; } public function getTabs() { return $this->tabs; } public function setNavigation(AphrontSideNavFilterView $navigation) { $this->navigation = $navigation; return $this; } public function getNavigation() { return $this->navigation; } public function getTitle() { $glyph_key = PhabricatorTitleGlyphsSetting::SETTINGKEY; $glyph_on = PhabricatorTitleGlyphsSetting::VALUE_TITLE_GLYPHS; $glyph_setting = $this->getUserPreference($glyph_key, $glyph_on); $use_glyph = ($glyph_setting == $glyph_on); $title = parent::getTitle(); $prefix = null; if ($use_glyph) { $prefix = $this->getGlyph(); } else { $application_name = $this->getApplicationName(); if (strlen($application_name)) { $prefix = '['.$application_name.']'; } } if (strlen($prefix)) { $title = $prefix.' '.$title; } return $title; } protected function willRenderPage() { $footer = $this->renderFooter(); // NOTE: A cleaner solution would be to let body layout elements implement // some kind of "LayoutInterface" so content can be embedded inside frames, // but there's only really one use case for this for now. $children = $this->renderChildren(); if ($children) { $layout = head($children); if ($layout instanceof PHUIFormationView) { $layout->setFooter($footer); $footer = null; } } $this->footer = $footer; parent::willRenderPage(); if (!$this->getRequest()) { throw new Exception( pht( 'You must set the %s to render a %s.', 'Request', __CLASS__)); } $console = $this->getConsole(); require_celerity_resource('phabricator-core-css'); require_celerity_resource('phabricator-zindex-css'); require_celerity_resource('phui-button-css'); require_celerity_resource('phui-spacing-css'); require_celerity_resource('phui-form-css'); require_celerity_resource('phabricator-standard-page-view'); require_celerity_resource('conpherence-durable-column-view'); require_celerity_resource('font-lato'); Javelin::initBehavior('workflow', array()); $request = $this->getRequest(); $user = null; if ($request) { $user = $request->getUser(); } if ($user) { if ($user->isUserActivated()) { $offset = $user->getTimeZoneOffset(); $ignore_key = PhabricatorTimezoneIgnoreOffsetSetting::SETTINGKEY; $ignore = $user->getUserSetting($ignore_key); Javelin::initBehavior( 'detect-timezone', array( 'offset' => $offset, 'uri' => '/settings/timezone/', 'message' => pht( 'Your browser timezone setting differs from the timezone '. 'setting in your profile, click to reconcile.'), 'ignoreKey' => $ignore_key, 'ignore' => $ignore, )); if ($user->getIsAdmin()) { $server_https = $request->isHTTPS(); $server_protocol = $server_https ? 'HTTPS' : 'HTTP'; $client_protocol = $server_https ? 'HTTP' : 'HTTPS'; $doc_name = 'Configuring a Preamble Script'; $doc_href = PhabricatorEnv::getDoclink($doc_name); Javelin::initBehavior( 'setup-check-https', array( 'server_https' => $server_https, 'doc_name' => pht('See Documentation'), 'doc_href' => $doc_href, 'message' => pht( - 'Phabricator thinks you are using %s, but your '. + 'This server thinks you are using %s, but your '. 'client is convinced that it is using %s. This is a serious '. 'misconfiguration with subtle, but significant, consequences.', $server_protocol, $client_protocol), )); } } Javelin::initBehavior('lightbox-attachments'); } Javelin::initBehavior('aphront-form-disable-on-submit'); Javelin::initBehavior('toggle-class', array()); Javelin::initBehavior('history-install'); Javelin::initBehavior('phabricator-gesture'); $current_token = null; if ($user) { $current_token = $user->getCSRFToken(); } Javelin::initBehavior( 'refresh-csrf', array( 'tokenName' => AphrontRequest::getCSRFTokenName(), 'header' => AphrontRequest::getCSRFHeaderName(), 'viaHeader' => AphrontRequest::getViaHeaderName(), 'current' => $current_token, )); Javelin::initBehavior('device'); Javelin::initBehavior( 'high-security-warning', $this->getHighSecurityWarningConfig()); if (PhabricatorEnv::isReadOnly()) { Javelin::initBehavior( 'read-only-warning', array( 'message' => PhabricatorEnv::getReadOnlyMessage(), 'uri' => PhabricatorEnv::getReadOnlyURI(), )); } // If we aren't showing the page chrome, skip rendering DarkConsole and the // main menu, since they won't be visible on the page. if (!$this->getShowChrome()) { return; } if ($console) { require_celerity_resource('aphront-dark-console-css'); $headers = array(); if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) { $headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page'; } if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) { $headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true; } Javelin::initBehavior( 'dark-console', $this->getConsoleConfig()); } if ($user) { $viewer = $user; } else { $viewer = new PhabricatorUser(); } $menu = id(new PhabricatorMainMenuView()) ->setUser($viewer); if ($this->getController()) { $menu->setController($this->getController()); } $application_menu = $this->applicationMenu; if ($application_menu) { if ($application_menu instanceof PHUIApplicationMenuView) { $crumbs = $this->getCrumbs(); if ($crumbs) { $application_menu->setCrumbs($crumbs); } $application_menu = $application_menu->buildListView(); } $menu->setApplicationMenu($application_menu); } $this->menuContent = $menu->render(); } protected function getHead() { $monospaced = null; $request = $this->getRequest(); if ($request) { $user = $request->getUser(); if ($user) { $monospaced = $user->getUserSetting( PhabricatorMonospacedFontSetting::SETTINGKEY); } } $response = CelerityAPI::getStaticResourceResponse(); $font_css = null; if (!empty($monospaced)) { // We can't print this normally because escaping quotation marks will // break the CSS. Instead, filter it strictly and then mark it as safe. $monospaced = new PhutilSafeHTML( PhabricatorMonospacedFontSetting::filterMonospacedCSSRule( $monospaced)); $font_css = hsprintf( '', $monospaced); } return hsprintf( '%s%s%s', parent::getHead(), $font_css, $response->renderSingleResource('javelin-magical-init', 'phabricator')); } public function setGlyph($glyph) { $this->glyph = $glyph; return $this; } public function getGlyph() { return $this->glyph; } protected function willSendResponse($response) { $request = $this->getRequest(); $response = parent::willSendResponse($response); $console = $request->getApplicationConfiguration()->getConsole(); if ($console) { $response = PhutilSafeHTML::applyFunction( 'str_replace', hsprintf(''), $console->render($request), $response); } return $response; } protected function getBody() { $user = null; $request = $this->getRequest(); if ($request) { $user = $request->getUser(); } $header_chrome = null; if ($this->getShowChrome()) { $header_chrome = $this->menuContent; } $classes = array(); $classes[] = 'main-page-frame'; $developer_warning = null; if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') && DarkConsoleErrorLogPluginAPI::getErrors()) { $developer_warning = phutil_tag_div( 'aphront-developer-error-callout', pht( 'This page raised PHP errors. Find them in DarkConsole '. 'or the error log.')); } $main_page = phutil_tag( 'div', array( 'id' => 'phabricator-standard-page', 'class' => 'phabricator-standard-page', ), array( $developer_warning, $header_chrome, phutil_tag( 'div', array( 'id' => 'phabricator-standard-page-body', 'class' => 'phabricator-standard-page-body', ), $this->renderPageBodyContent()), )); $durable_column = null; if ($this->getShowDurableColumn()) { $is_visible = $this->getDurableColumnVisible(); $is_minimize = $this->getDurableColumnMinimize(); $durable_column = id(new ConpherenceDurableColumnView()) ->setSelectedConpherence(null) ->setUser($user) ->setQuicksandConfig($this->buildQuicksandConfig()) ->setVisible($is_visible) ->setMinimize($is_minimize) ->setInitialLoad(true); if ($is_minimize) { $this->classes[] = 'minimize-column'; } } Javelin::initBehavior('quicksand-blacklist', array( 'patterns' => $this->getQuicksandURIPatternBlacklist(), )); return phutil_tag( 'div', array( 'class' => implode(' ', $classes), 'id' => 'main-page-frame', ), array( $main_page, $durable_column, )); } private function renderPageBodyContent() { $console = $this->getConsole(); $body = parent::getBody(); $nav = $this->getNavigation(); $tabs = $this->getTabs(); if ($nav) { $crumbs = $this->getCrumbs(); if ($crumbs) { $nav->setCrumbs($crumbs); } $nav->appendChild($body); $nav->appendFooter($this->footer); $content = phutil_implode_html('', array($nav->render())); } else { $content = array(); $crumbs = $this->getCrumbs(); if ($crumbs) { if ($this->getTabs()) { $crumbs->setBorder(true); } $content[] = $crumbs; } $tabs = $this->getTabs(); if ($tabs) { $content[] = $tabs; } $content[] = $body; $content[] = $this->footer; $content = phutil_implode_html('', $content); } return array( ($console ? hsprintf('') : null), $content, ); } protected function getTail() { $request = $this->getRequest(); $user = $request->getUser(); $tail = array( parent::getTail(), ); $response = CelerityAPI::getStaticResourceResponse(); if ($request->isHTTPS()) { $with_protocol = 'https'; } else { $with_protocol = 'http'; } $servers = PhabricatorNotificationServerRef::getEnabledClientServers( $with_protocol); if ($servers) { if ($user && $user->isLoggedIn()) { // TODO: We could tell the browser about all the servers and let it // do random reconnects to improve reliability. shuffle($servers); $server = head($servers); $client_uri = $server->getWebsocketURI(); Javelin::initBehavior( 'aphlict-listen', array( 'websocketURI' => (string)$client_uri, ) + $this->buildAphlictListenConfigData()); CelerityAPI::getStaticResourceResponse() ->addContentSecurityPolicyURI('connect-src', $client_uri); } } $tail[] = $response->renderHTMLFooter($this->getFrameable()); return $tail; } protected function getBodyClasses() { $classes = array(); if (!$this->getShowChrome()) { $classes[] = 'phabricator-chromeless-page'; } $agent = AphrontRequest::getHTTPHeader('User-Agent'); // Try to guess the device resolution based on UA strings to avoid a flash // of incorrectly-styled content. $device_guess = 'device-desktop'; if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) { $device_guess = 'device-phone device'; } else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) { $device_guess = 'device-tablet device'; } $classes[] = $device_guess; if (preg_match('@Windows@', $agent)) { $classes[] = 'platform-windows'; } else if (preg_match('@Macintosh@', $agent)) { $classes[] = 'platform-mac'; } else if (preg_match('@X11@', $agent)) { $classes[] = 'platform-linux'; } if ($this->getRequest()->getStr('__print__')) { $classes[] = 'printable'; } if ($this->getRequest()->getStr('__aural__')) { $classes[] = 'audible'; } $classes[] = 'phui-theme-'.PhabricatorEnv::getEnvConfig('ui.header-color'); foreach ($this->classes as $class) { $classes[] = $class; } return implode(' ', $classes); } private function getConsole() { if ($this->disableConsole) { return null; } return $this->getRequest()->getApplicationConfiguration()->getConsole(); } private function getConsoleConfig() { $user = $this->getRequest()->getUser(); $headers = array(); if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) { $headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page'; } if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) { $headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true; } if ($user) { $setting_tab = PhabricatorDarkConsoleTabSetting::SETTINGKEY; $setting_visible = PhabricatorDarkConsoleVisibleSetting::SETTINGKEY; $tab = $user->getUserSetting($setting_tab); $visible = $user->getUserSetting($setting_visible); } else { $tab = null; $visible = true; } return array( // NOTE: We use a generic label here to prevent input reflection // and mitigate compression attacks like BREACH. See discussion in // T3684. 'uri' => pht('Main Request'), 'selected' => $tab, 'visible' => $visible, 'headers' => $headers, ); } private function getHighSecurityWarningConfig() { $user = $this->getRequest()->getUser(); $show = false; if ($user->hasSession()) { $hisec = ($user->getSession()->getHighSecurityUntil() - time()); if ($hisec > 0) { $show = true; } } return array( 'show' => $show, 'uri' => '/auth/session/downgrade/', 'message' => pht( 'Your session is in high security mode. When you '. 'finish using it, click here to leave.'), ); } private function renderFooter() { if (!$this->getShowChrome()) { return null; } if (!$this->getShowFooter()) { return null; } $items = PhabricatorEnv::getEnvConfig('ui.footer-items'); if (!$items) { return null; } $foot = array(); foreach ($items as $item) { $name = idx($item, 'name', pht('Unnamed Footer Item')); $href = idx($item, 'href'); if (!PhabricatorEnv::isValidURIForLink($href)) { $href = null; } if ($href !== null) { $tag = 'a'; } else { $tag = 'span'; } $foot[] = phutil_tag( $tag, array( 'href' => $href, ), $name); } $foot = phutil_implode_html(" \xC2\xB7 ", $foot); return phutil_tag( 'div', array( 'class' => 'phabricator-standard-page-footer grouped', ), $foot); } public function renderForQuicksand() { parent::willRenderPage(); $response = $this->renderPageBodyContent(); $response = $this->willSendResponse($response); $extra_config = $this->getQuicksandConfig(); return array( 'content' => hsprintf('%s', $response), ) + $this->buildQuicksandConfig() + $extra_config; } private function buildQuicksandConfig() { $viewer = $this->getRequest()->getUser(); $controller = $this->getController(); $dropdown_query = id(new AphlictDropdownDataQuery()) ->setViewer($viewer); $dropdown_query->execute(); $hisec_warning_config = $this->getHighSecurityWarningConfig(); $console_config = null; $console = $this->getConsole(); if ($console) { $console_config = $this->getConsoleConfig(); } $upload_enabled = false; if ($controller) { $upload_enabled = $controller->isGlobalDragAndDropUploadEnabled(); } $application_class = null; $application_search_icon = null; $application_help = null; $controller = $this->getController(); if ($controller) { $application = $controller->getCurrentApplication(); if ($application) { $application_class = get_class($application); if ($application->getApplicationSearchDocumentTypes()) { $application_search_icon = $application->getIcon(); } $help_items = $application->getHelpMenuItems($viewer); if ($help_items) { $help_list = id(new PhabricatorActionListView()) ->setViewer($viewer); foreach ($help_items as $help_item) { $help_list->addAction($help_item); } $application_help = $help_list->getDropdownMenuMetadata(); } } } return array( 'title' => $this->getTitle(), 'bodyClasses' => $this->getBodyClasses(), 'aphlictDropdownData' => array( $dropdown_query->getNotificationData(), $dropdown_query->getConpherenceData(), ), 'globalDragAndDrop' => $upload_enabled, 'hisecWarningConfig' => $hisec_warning_config, 'consoleConfig' => $console_config, 'applicationClass' => $application_class, 'applicationSearchIcon' => $application_search_icon, 'helpItems' => $application_help, ) + $this->buildAphlictListenConfigData(); } private function buildAphlictListenConfigData() { $user = $this->getRequest()->getUser(); $subscriptions = $this->pageObjects; $subscriptions[] = $user->getPHID(); return array( 'pageObjects' => array_fill_keys($this->pageObjects, true), 'subscriptions' => $subscriptions, ); } private function getQuicksandURIPatternBlacklist() { $applications = PhabricatorApplication::getAllApplications(); $blacklist = array(); foreach ($applications as $application) { $blacklist[] = $application->getQuicksandURIPatternBlacklist(); } // See T4340. Currently, Phortune and Auth both require pulling in external // Javascript (for Stripe card management and Recaptcha, respectively). // This can put us in a position where the user loads a page with a // restrictive Content-Security-Policy, then uses Quicksand to navigate to // a page which needs to load external scripts. For now, just blacklist // these entire applications since we aren't giving up anything // significant by doing so. $blacklist[] = array( '/phortune/.*', '/auth/.*', ); return array_mergev($blacklist); } private function getUserPreference($key, $default = null) { $request = $this->getRequest(); if (!$request) { return $default; } $user = $request->getUser(); if (!$user) { return $default; } return $user->getUserSetting($key); } public function produceAphrontResponse() { $controller = $this->getController(); $viewer = $this->getUser(); if ($viewer && $viewer->getPHID()) { $object_phids = $this->pageObjects; foreach ($object_phids as $object_phid) { PhabricatorFeedStoryNotification::updateObjectNotificationViews( $viewer, $object_phid); } } if ($this->getRequest()->isQuicksand()) { $content = $this->renderForQuicksand(); $response = id(new AphrontAjaxResponse()) ->setContent($content); } else { // See T13247. Try to find some navigational menu items to create a // mobile navigation menu from. $application_menu = $controller->buildApplicationMenu(); if (!$application_menu) { $navigation = $this->getNavigation(); if ($navigation) { $application_menu = $navigation->getMenu(); } } $this->applicationMenu = $application_menu; $content = $this->render(); $response = id(new AphrontWebpageResponse()) ->setContent($content) ->setFrameable($this->getFrameable()); } return $response; } }