diff --git a/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php b/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php index d21e2105fb..fafb4d35af 100644 --- a/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php +++ b/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php @@ -1,326 +1,326 @@ object = $object; return $this; } public function getObject() { return $this->object; } public function setFields(array $fields) { $this->fields = $fields; return $this; } public function getFields() { return $this->fields; } public function render() { $object = $this->getObject(); $fields = $this->getFields(); $uri = 'https://your.install.com/application/edit/'; // Remove fields which do not expose an HTTP parameter type. $types = array(); foreach ($fields as $key => $field) { if (!$field->shouldGenerateTransactionsFromSubmit()) { unset($fields[$key]); continue; } $type = $field->getHTTPParameterType(); if ($type === null) { unset($fields[$key]); continue; } $types[$type->getTypeName()] = $type; } $intro = pht(<<getLabel(), head($field->getAllReadValueFromRequestKeys()), $field->getHTTPParameterType()->getTypeName(), $field->getDescription(), ); } $main_table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Label'), pht('Key'), pht('Type'), pht('Description'), )) ->setColumnClasses( array( 'pri', null, null, 'wide', )); $aliases_text = pht(<<getAllReadValueFromRequestKeys(), 1); if (!$aliases) { continue; } $rows[] = array( $field->getLabel(), $field->getKey(), implode(', ', $aliases), ); } $alias_table = id(new AphrontTableView($rows)) ->setNoDataString(pht('This object has no fields with aliases.')) ->setHeaders( array( pht('Label'), pht('Key'), pht('Aliases'), )) ->setColumnClasses( array( 'pri', null, 'wide', )); $template_text = pht(<<setIcon('fa-check-circle green'); $no = id(new PHUIIconView())->setIcon('fa-times grey'); $rows = array(); foreach ($fields as $field) { $rows[] = array( $field->getLabel(), $field->getIsCopyable() ? $yes : $no, ); } $template_table = id(new AphrontTableView($rows)) ->setNoDataString( pht('None of the fields on this object support templating.')) ->setHeaders( array( pht('Field'), pht('Will Copy'), )) ->setColumnClasses( array( 'pri', 'wide', )); $select_text = pht(<<getOptions(); $label = $field->getLabel(); foreach ($options as $option_key => $option_value) { if (strlen($option_key)) { $option_display = $option_key; } else { $option_display = phutil_tag('em', array(), pht('')); } $rows[] = array( $label, $option_display, $option_value, ); $label = null; } } $select_table = id(new AphrontTableView($rows)) ->setNoDataString(pht('This object has no select fields.')) ->setHeaders( array( pht('Field'), pht('Value'), pht('Label'), )) ->setColumnClasses( array( 'pri', null, 'wide', )); $types_text = pht(<<setHTTPParameterTypes($types); return array( $this->renderInstructions($intro), $main_table, $this->renderInstructions($aliases_text), $alias_table, $this->renderInstructions($template_text), $template_table, $this->renderInstructions($select_text), $select_table, $this->renderInstructions($types_text), $types_table, ); } protected function renderInstructions($corpus) { $viewer = $this->getUser(); $view = new PHUIRemarkupView($viewer, $corpus); $view->setRemarkupOptions( array( PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS => false, )); return $view; } } diff --git a/src/applications/uiexample/examples/PHUIBadgeExample.php b/src/applications/uiexample/examples/PHUIBadgeExample.php index c001a3c751..40f549e305 100644 --- a/src/applications/uiexample/examples/PHUIBadgeExample.php +++ b/src/applications/uiexample/examples/PHUIBadgeExample.php @@ -1,173 +1,173 @@ setIcon('fa-users') ->setHeader(pht('High Command')) ->setHref('/') ->setSource('Projects (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('3 Members'); $badges1[] = id(new PHUIBadgeView()) ->setIcon('fa-lock') ->setHeader(pht('Blessed Committers')) ->setSource('Projects (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('12 Members'); $badges1[] = id(new PHUIBadgeView()) ->setIcon('fa-camera-retro') ->setHeader(pht('Design')) ->setSource('Projects (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('2 Members'); $badges1[] = id(new PHUIBadgeView()) ->setIcon('fa-lock') ->setHeader(pht('Blessed Reviewers')) ->setSource('Projects (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('3 Members'); $badges1[] = id(new PHUIBadgeView()) ->setIcon('fa-umbrella') ->setHeader(pht('Wikipedia')) ->setSource('Projects (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('22 Members'); $badges2 = array(); $badges2[] = id(new PHUIBadgeView()) ->setIcon('fa-user') - ->setHeader(pht('Phabricator User')) + ->setHeader(pht('User')) ->setSubhead(pht('Confirmed your account.')) ->setQuality(PhabricatorBadgesQuality::POOR) ->setSource(pht('People (automatic)')) ->addByline(pht('Dec 31, 1969')) ->addByline('212 Issued (100%)'); $badges2[] = id(new PHUIBadgeView()) ->setIcon('fa-code') ->setHeader(pht('Code Contributor')) ->setSubhead(pht('Wrote code that was acceptable')) ->setQuality(PhabricatorBadgesQuality::COMMON) ->setSource('Diffusion (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('200 Awarded (98%)'); $badges2[] = id(new PHUIBadgeView()) ->setIcon('fa-bug') ->setHeader(pht('Task Master')) ->setSubhead(pht('Closed over 100 tasks')) ->setQuality(PhabricatorBadgesQuality::UNCOMMON) ->setSource('Maniphest (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('56 Awarded (43%)'); $badges2[] = id(new PHUIBadgeView()) ->setIcon('fa-star') ->setHeader(pht('Code Weaver')) ->setSubhead(pht('Landed 1,000 Commits')) ->setQuality(PhabricatorBadgesQuality::RARE) ->setSource('Diffusion (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('42 Awarded (20%)'); $badges2[] = id(new PHUIBadgeView()) ->setIcon('fa-users') ->setHeader(pht('Security Team')) ->setSubhead(pht('')) ->setQuality(PhabricatorBadgesQuality::EPIC) ->setSource('Projects (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('21 Awarded (10%)'); $badges2[] = id(new PHUIBadgeView()) ->setIcon('fa-user') ->setHeader(pht('Administrator')) ->setSubhead(pht('Drew the short stick')) ->setQuality(PhabricatorBadgesQuality::LEGENDARY) ->setSource(pht('People (automatic)')) ->addByline(pht('Dec 31, 1969')) ->addByline('3 Awarded (1.4%)'); $badges2[] = id(new PHUIBadgeView()) ->setIcon('fa-compass') ->setHeader(pht('Lead Developer')) - ->setSubhead(pht('Lead Developer of Phabricator')) + ->setSubhead(pht('Lead Developer of Software')) ->setQuality(PhabricatorBadgesQuality::HEIRLOOM) ->setSource(pht('Direct Award')) ->addByline(pht('Dec 31, 1969')) ->addByline('1 Awarded (0.4%)'); $badges3 = array(); $badges3[] = id(new PHUIBadgeMiniView()) ->setIcon('fa-book') ->setHeader(pht('Documenter')); $badges3[] = id(new PHUIBadgeMiniView()) ->setIcon('fa-star') ->setHeader(pht('Contributor')); $badges3[] = id(new PHUIBadgeMiniView()) ->setIcon('fa-bug') ->setHeader(pht('Bugmeister')); $badges3[] = id(new PHUIBadgeMiniView()) ->setIcon('fa-heart') ->setHeader(pht('Funder')) ->setQuality(PhabricatorBadgesQuality::UNCOMMON); $badges3[] = id(new PHUIBadgeMiniView()) ->setIcon('fa-user') ->setHeader(pht('Administrator')) ->setQuality(PhabricatorBadgesQuality::RARE); $badges3[] = id(new PHUIBadgeMiniView()) ->setIcon('fa-camera-retro') ->setHeader(pht('Designer')) ->setQuality(PhabricatorBadgesQuality::EPIC); $flex1 = new PHUIBadgeBoxView(); $flex1->addItems($badges1); $box1 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Project Membership')) ->appendChild($flex1); $flex2 = new PHUIBadgeBoxView(); $flex2->addItems($badges2); $box2 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Achievements')) ->appendChild($flex2); $flex3 = new PHUIBadgeBoxView(); $flex3->addItems($badges3); $flex3->setCollapsed(true); $flex3->addClass('ml'); $box3 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('PHUIBadgeMiniView')) ->appendChild($flex3); return array($box1, $box2, $box3); } } diff --git a/src/applications/uiexample/examples/PHUIHovercardUIExample.php b/src/applications/uiexample/examples/PHUIHovercardUIExample.php index b88fdc6639..2e68976ae0 100644 --- a/src/applications/uiexample/examples/PHUIHovercardUIExample.php +++ b/src/applications/uiexample/examples/PHUIHovercardUIExample.php @@ -1,83 +1,83 @@ getRequest(); $user = $request->getUser(); $elements = array(); $diff_handle = $this->createBasicDummyHandle( 'D123', DifferentialRevisionPHIDType::TYPECONST, pht('Introduce cooler Differential Revisions')); $panel = $this->createPanel(pht('Differential Hovercard')); $panel->appendChild(id(new PHUIHovercardView()) ->setObjectHandle($diff_handle) ->addField(pht('Author'), $user->getUsername()) ->addField(pht('Updated'), phabricator_datetime(time(), $user)) ->addAction(pht('Subscribe'), '/dev/random') ->setUser($user)); $elements[] = $panel; $task_handle = $this->createBasicDummyHandle( 'T123', ManiphestTaskPHIDType::TYPECONST, - pht('Improve Mobile Experience for Phabricator')); + pht('Improve Mobile Experience')); $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE) ->setName(pht('Closed, Resolved')); $panel = $this->createPanel(pht('Maniphest Hovercard')); $panel->appendChild(id(new PHUIHovercardView()) ->setObjectHandle($task_handle) ->setUser($user) ->addField(pht('Assigned to'), $user->getUsername()) ->addField(pht('Dependent Tasks'), 'T123, T124, T125') ->addAction(pht('Subscribe'), '/dev/random') ->addAction(pht('Create Subtask'), '/dev/urandom') ->addTag($tag)); $elements[] = $panel; $user_handle = $this->createBasicDummyHandle( 'gwashington', PhabricatorPeopleUserPHIDType::TYPECONST, 'George Washington'); $user_handle->setImageURI( celerity_get_resource_uri('/rsrc/image/people/washington.png')); $panel = $this->createPanel(pht('Whatevery Hovercard')); $panel->appendChild(id(new PHUIHovercardView()) ->setObjectHandle($user_handle) ->addField(pht('Status'), pht('Available')) ->addField(pht('Member since'), '30. February 1750') ->addAction(pht('Send a Message'), '/dev/null') ->setUser($user)); $elements[] = $panel; return phutil_implode_html('', $elements); } private function createPanel($header) { $panel = new PHUIBoxView(); $panel->addClass('grouped'); $panel->addClass('ml'); return $panel; } } diff --git a/src/applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php b/src/applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php index 2cc89b56d5..9eb90f6de1 100644 --- a/src/applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php +++ b/src/applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php @@ -1,71 +1,71 @@ getRequest()->getUser(); $root = dirname(phutil_get_library_root('phabricator')); $root = $root.'/resources/builtin/projects/v3/'; Javelin::initBehavior('phabricator-tooltips', array()); $map = array(); $builtin_map = id(new FileFinder($root)) ->withType('f') ->withFollowSymlinks(true) ->find(); $images = array(); foreach ($builtin_map as $image) { $file = PhabricatorFile::loadBuiltin($viewer, 'projects/v3/'.$image); $images[$file->getPHID()] = array( 'uri' => $file->getBestURI(), 'tip' => 'v3/'.$image, ); } $buttons = array(); foreach ($images as $phid => $spec) { $button = javelin_tag( 'img', array( 'height' => 100, 'width' => 100, 'src' => $spec['uri'], 'style' => 'float: left; padding: 4px;', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $spec['tip'], 'size' => 300, ), )); $buttons[] = $button; } $wrap1 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Images')) ->appendChild($buttons) ->addClass('grouped'); return phutil_tag( 'div', array(), array( $wrap1, )); } } diff --git a/src/infrastructure/contentsource/PhabricatorContentSource.php b/src/infrastructure/contentsource/PhabricatorContentSource.php index 3a5ea19f57..ee77052113 100644 --- a/src/infrastructure/contentsource/PhabricatorContentSource.php +++ b/src/infrastructure/contentsource/PhabricatorContentSource.php @@ -1,92 +1,92 @@ getPhobjectClassConstant('SOURCECONST', 32); } final public static function getAllContentSources() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getSourceTypeConstant') ->execute(); } /** * Construct a new content source object. * * @param const The source type constant to build a source for. * @param array Source parameters. * @param bool True to suppress errors and force construction of a source * even if the source type is not valid. * @return PhabricatorContentSource New source object. */ final public static function newForSource( $source, array $params = array(), $force = false) { $map = self::getAllContentSources(); if (isset($map[$source])) { $obj = clone $map[$source]; } else { if ($force) { $obj = new PhabricatorUnknownContentSource(); } else { throw new Exception( pht( - 'Content source type "%s" is not known to Phabricator!', + 'Content source type "%s" is unknown.', $source)); } } $obj->source = $source; $obj->params = $params; return $obj; } public static function newFromSerialized($serialized) { $dict = json_decode($serialized, true); if (!is_array($dict)) { $dict = array(); } $source = idx($dict, 'source'); $params = idx($dict, 'params'); if (!is_array($params)) { $params = array(); } return self::newForSource($source, $params, true); } public static function newFromRequest(AphrontRequest $request) { return self::newForSource( PhabricatorWebContentSource::SOURCECONST); } final public function serialize() { return phutil_json_encode( array( 'source' => $this->getSource(), 'params' => $this->params, )); } final public function getSource() { return $this->source; } final public function getContentSourceParameter($key, $default = null) { return idx($this->params, $key, $default); } } diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 4806289f52..352e9aeb5e 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -1,990 +1,990 @@ overrideEnv('some.key', 'new-value-for-this-test'); * * // Some test which depends on the value of 'some.key'. * * } * * Your changes will persist until the `$env` object leaves scope or is * destroyed. * * You should //not// use this in normal code. * * * @task read Reading Configuration * @task uri URI Validation * @task test Unit Test Support * @task internal Internals */ final class PhabricatorEnv extends Phobject { private static $sourceStack; private static $repairSource; private static $overrideSource; private static $requestBaseURI; private static $cache; private static $localeCode; private static $readOnly; private static $readOnlyReason; const READONLY_CONFIG = 'config'; const READONLY_UNREACHABLE = 'unreachable'; const READONLY_SEVERED = 'severed'; const READONLY_MASTERLESS = 'masterless'; /** * @phutil-external-symbol class PhabricatorStartup */ public static function initializeWebEnvironment() { self::initializeCommonEnvironment(false); } public static function initializeScriptEnvironment($config_optional) { self::initializeCommonEnvironment($config_optional); // NOTE: This is dangerous in general, but we know we're in a script context // and are not vulnerable to CSRF. AphrontWriteGuard::allowDangerousUnguardedWrites(true); // There are several places where we log information (about errors, events, // service calls, etc.) for analysis via DarkConsole or similar. These are // useful for web requests, but grow unboundedly in long-running scripts and // daemons. Discard data as it arrives in these cases. PhutilServiceProfiler::getInstance()->enableDiscardMode(); DarkConsoleErrorLogPluginAPI::enableDiscardMode(); DarkConsoleEventPluginAPI::enableDiscardMode(); } private static function initializeCommonEnvironment($config_optional) { PhutilErrorHandler::initialize(); self::resetUmask(); self::buildConfigurationSourceStack($config_optional); // Force a valid timezone. If both PHP and Phabricator configuration are // invalid, use UTC. $tz = self::getEnvConfig('phabricator.timezone'); if ($tz) { @date_default_timezone_set($tz); } $ok = @date_default_timezone_set(date_default_timezone_get()); if (!$ok) { date_default_timezone_set('UTC'); } // Prepend '/support/bin' and append any paths to $PATH if we need to. $env_path = getenv('PATH'); $phabricator_path = dirname(phutil_get_library_root('phabricator')); $support_path = $phabricator_path.'/support/bin'; $env_path = $support_path.PATH_SEPARATOR.$env_path; $append_dirs = self::getEnvConfig('environment.append-paths'); if (!empty($append_dirs)) { $append_path = implode(PATH_SEPARATOR, $append_dirs); $env_path = $env_path.PATH_SEPARATOR.$append_path; } putenv('PATH='.$env_path); // Write this back into $_ENV, too, so ExecFuture picks it up when creating // subprocess environments. $_ENV['PATH'] = $env_path; // If an instance identifier is defined, write it into the environment so // it's available to subprocesses. $instance = self::getEnvConfig('cluster.instance'); if (strlen($instance)) { putenv('PHABRICATOR_INSTANCE='.$instance); $_ENV['PHABRICATOR_INSTANCE'] = $instance; } PhabricatorEventEngine::initialize(); // TODO: Add a "locale.default" config option once we have some reasonable // defaults which aren't silly nonsense. self::setLocaleCode('en_US'); // Load the preamble utility library if we haven't already. On web // requests this loaded earlier, but we want to load it for non-web // requests so that unit tests can call these functions. require_once $phabricator_path.'/support/startup/preamble-utils.php'; } public static function beginScopedLocale($locale_code) { return new PhabricatorLocaleScopeGuard($locale_code); } public static function getLocaleCode() { return self::$localeCode; } public static function setLocaleCode($locale_code) { if (!$locale_code) { return; } if ($locale_code == self::$localeCode) { return; } try { $locale = PhutilLocale::loadLocale($locale_code); $translations = PhutilTranslation::getTranslationMapForLocale( $locale_code); $override = self::getEnvConfig('translation.override'); if (!is_array($override)) { $override = array(); } PhutilTranslator::getInstance() ->setLocale($locale) ->setTranslations($override + $translations); self::$localeCode = $locale_code; } catch (Exception $ex) { // Just ignore this; the user likely has an out-of-date locale code. } } private static function buildConfigurationSourceStack($config_optional) { self::dropConfigCache(); $stack = new PhabricatorConfigStackSource(); self::$sourceStack = $stack; $default_source = id(new PhabricatorConfigDefaultSource()) ->setName(pht('Global Default')); $stack->pushSource($default_source); $env = self::getSelectedEnvironmentName(); if ($env) { $stack->pushSource( id(new PhabricatorConfigFileSource($env)) ->setName(pht("File '%s'", $env))); } $stack->pushSource( id(new PhabricatorConfigLocalSource()) ->setName(pht('Local Config'))); // If the install overrides the database adapter, we might need to load // the database adapter class before we can push on the database config. // This config is locked and can't be edited from the web UI anyway. foreach (self::getEnvConfig('load-libraries') as $library) { phutil_load_library($library); } // Drop any class map caches, since they will have generated without // any classes from libraries. Without this, preflight setup checks can // cause generation of a setup check cache that omits checks defined in // libraries, for example. PhutilClassMapQuery::deleteCaches(); // If custom libraries specify config options, they won't get default // values as the Default source has already been loaded, so we get it to // pull in all options from non-phabricator libraries now they are loaded. $default_source->loadExternalOptions(); // If this install has site config sources, load them now. $site_sources = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorConfigSiteSource') ->setSortMethod('getPriority') ->execute(); foreach ($site_sources as $site_source) { $stack->pushSource($site_source); // If the site source did anything which reads config, throw it away // to make sure any additional site sources get clean reads. self::dropConfigCache(); } $masters = PhabricatorDatabaseRef::getMasterDatabaseRefs(); if (!$masters) { self::setReadOnly(true, self::READONLY_MASTERLESS); } else { // If any master is severed, we drop to readonly mode. In theory we // could try to continue if we're only missing some applications, but // this is very complex and we're unlikely to get it right. foreach ($masters as $master) { // Give severed masters one last chance to get healthy. if ($master->isSevered()) { $master->checkHealth(); } if ($master->isSevered()) { self::setReadOnly(true, self::READONLY_SEVERED); break; } } } try { // See T13403. If we're starting up in "config optional" mode, suppress // messages about connection retries. if ($config_optional) { $database_source = @new PhabricatorConfigDatabaseSource('default'); } else { $database_source = new PhabricatorConfigDatabaseSource('default'); } $database_source->setName(pht('Database')); $stack->pushSource($database_source); } catch (AphrontSchemaQueryException $exception) { // If the database is not available, just skip this configuration // source. This happens during `bin/storage upgrade`, `bin/conf` before // schema setup, etc. } catch (PhabricatorClusterStrandedException $ex) { // This means we can't connect to any database host. That's fine as // long as we're running a setup script like `bin/storage`. if (!$config_optional) { throw $ex; } } // Drop the config cache one final time to make sure we're getting clean // reads now that we've finished building the stack. self::dropConfigCache(); } public static function repairConfig($key, $value) { if (!self::$repairSource) { self::$repairSource = id(new PhabricatorConfigDictionarySource(array())) ->setName(pht('Repaired Config')); self::$sourceStack->pushSource(self::$repairSource); } self::$repairSource->setKeys(array($key => $value)); self::dropConfigCache(); } public static function overrideConfig($key, $value) { if (!self::$overrideSource) { self::$overrideSource = id(new PhabricatorConfigDictionarySource(array())) ->setName(pht('Overridden Config')); self::$sourceStack->pushSource(self::$overrideSource); } self::$overrideSource->setKeys(array($key => $value)); self::dropConfigCache(); } public static function getUnrepairedEnvConfig($key, $default = null) { foreach (self::$sourceStack->getStack() as $source) { if ($source === self::$repairSource) { continue; } $result = $source->getKeys(array($key)); if ($result) { return $result[$key]; } } return $default; } public static function getSelectedEnvironmentName() { $env_var = 'PHABRICATOR_ENV'; $env = idx($_SERVER, $env_var); if (!$env) { $env = getenv($env_var); } if (!$env) { $env = idx($_ENV, $env_var); } if (!$env) { $root = dirname(phutil_get_library_root('phabricator')); $path = $root.'/conf/local/ENVIRONMENT'; if (Filesystem::pathExists($path)) { $env = trim(Filesystem::readFile($path)); } } return $env; } /* -( Reading Configuration )---------------------------------------------- */ /** * Get the current configuration setting for a given key. * * If the key is not found, then throw an Exception. * * @task read */ public static function getEnvConfig($key) { if (!self::$sourceStack) { throw new Exception( pht( 'Trying to read configuration "%s" before configuration has been '. 'initialized.', $key)); } if (isset(self::$cache[$key])) { return self::$cache[$key]; } if (array_key_exists($key, self::$cache)) { return self::$cache[$key]; } $result = self::$sourceStack->getKeys(array($key)); if (array_key_exists($key, $result)) { self::$cache[$key] = $result[$key]; return $result[$key]; } else { throw new Exception( pht( "No config value specified for key '%s'.", $key)); } } /** * Get the current configuration setting for a given key. If the key * does not exist, return a default value instead of throwing. This is * primarily useful for migrations involving keys which are slated for * removal. * * @task read */ public static function getEnvConfigIfExists($key, $default = null) { try { return self::getEnvConfig($key); } catch (Exception $ex) { return $default; } } /** * Get the fully-qualified URI for a path. * * @task read */ public static function getURI($path) { return rtrim(self::getAnyBaseURI(), '/').$path; } /** * Get the fully-qualified production URI for a path. * * @task read */ public static function getProductionURI($path) { // If we're passed a URI which already has a domain, simply return it // unmodified. In particular, files may have URIs which point to a CDN // domain. $uri = new PhutilURI($path); if ($uri->getDomain()) { return $path; } $production_domain = self::getEnvConfig('phabricator.production-uri'); if (!$production_domain) { $production_domain = self::getAnyBaseURI(); } return rtrim($production_domain, '/').$path; } public static function isSelfURI($raw_uri) { $uri = new PhutilURI($raw_uri); $host = $uri->getDomain(); if (!strlen($host)) { return false; } $host = phutil_utf8_strtolower($host); $self_map = self::getSelfURIMap(); return isset($self_map[$host]); } private static function getSelfURIMap() { $self_uris = array(); $self_uris[] = self::getProductionURI('/'); $self_uris[] = self::getURI('/'); $allowed_uris = self::getEnvConfig('phabricator.allowed-uris'); foreach ($allowed_uris as $allowed_uri) { $self_uris[] = $allowed_uri; } $self_map = array(); foreach ($self_uris as $self_uri) { $host = id(new PhutilURI($self_uri))->getDomain(); if (!strlen($host)) { continue; } $host = phutil_utf8_strtolower($host); $self_map[$host] = $host; } return $self_map; } /** * Get the fully-qualified production URI for a static resource path. * * @task read */ public static function getCDNURI($path) { $alt = self::getEnvConfig('security.alternate-file-domain'); if (!$alt) { $alt = self::getAnyBaseURI(); } $uri = new PhutilURI($alt); $uri->setPath($path); return (string)$uri; } /** * Get the fully-qualified production URI for a documentation resource. * * @task read */ public static function getDoclink($resource, $type = 'article') { $params = array( 'name' => $resource, 'type' => $type, 'jump' => true, ); $uri = new PhutilURI( 'https://secure.phabricator.com/diviner/find/', $params); return phutil_string_cast($uri); } /** * Build a concrete object from a configuration key. * * @task read */ public static function newObjectFromConfig($key, $args = array()) { $class = self::getEnvConfig($key); return newv($class, $args); } public static function getAnyBaseURI() { $base_uri = self::getEnvConfig('phabricator.base-uri'); if (!$base_uri) { $base_uri = self::getRequestBaseURI(); } if (!$base_uri) { throw new Exception( pht( "Define '%s' in your configuration to continue.", 'phabricator.base-uri')); } return $base_uri; } public static function getRequestBaseURI() { return self::$requestBaseURI; } public static function setRequestBaseURI($uri) { self::$requestBaseURI = $uri; } public static function isReadOnly() { if (self::$readOnly !== null) { return self::$readOnly; } return self::getEnvConfig('cluster.read-only'); } public static function setReadOnly($read_only, $reason) { self::$readOnly = $read_only; self::$readOnlyReason = $reason; } public static function getReadOnlyMessage() { $reason = self::getReadOnlyReason(); switch ($reason) { case self::READONLY_MASTERLESS: return pht( - 'Phabricator is in read-only mode (no writable database '. + 'This server is in read-only mode (no writable database '. 'is configured).'); case self::READONLY_UNREACHABLE: return pht( - 'Phabricator is in read-only mode (unreachable master).'); + 'This server is in read-only mode (unreachable master).'); case self::READONLY_SEVERED: return pht( - 'Phabricator is in read-only mode (major interruption).'); + 'This server is in read-only mode (major interruption).'); } - return pht('Phabricator is in read-only mode.'); + return pht('This server is in read-only mode.'); } public static function getReadOnlyURI() { return urisprintf( '/readonly/%s/', self::getReadOnlyReason()); } public static function getReadOnlyReason() { if (!self::isReadOnly()) { return null; } if (self::$readOnlyReason !== null) { return self::$readOnlyReason; } return self::READONLY_CONFIG; } /* -( Unit Test Support )-------------------------------------------------- */ /** * @task test */ public static function beginScopedEnv() { return new PhabricatorScopedEnv(self::pushTestEnvironment()); } /** * @task test */ private static function pushTestEnvironment() { self::dropConfigCache(); $source = new PhabricatorConfigDictionarySource(array()); self::$sourceStack->pushSource($source); return spl_object_hash($source); } /** * @task test */ public static function popTestEnvironment($key) { self::dropConfigCache(); $source = self::$sourceStack->popSource(); $stack_key = spl_object_hash($source); if ($stack_key !== $key) { self::$sourceStack->pushSource($source); throw new Exception( pht( 'Scoped environments were destroyed in a different order than they '. 'were initialized.')); } } /* -( URI Validation )----------------------------------------------------- */ /** * Detect if a URI satisfies either @{method:isValidLocalURIForLink} or * @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the * URI of some other resource which has a valid protocol. This rejects * garbage URIs and URIs with protocols which do not appear in the * `uri.allowed-protocols` configuration, notably 'javascript:' URIs. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to put in an "href" link attribute. * * @param string URI to test. * @return bool True if the URI identifies a web resource. * @task uri */ public static function isValidURIForLink($uri) { return self::isValidLocalURIForLink($uri) || self::isValidRemoteURIForLink($uri); } /** * Detect if a URI identifies some page on this server. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to issue a "Location:" redirect to. * * @param string URI to test. * @return bool True if the URI identifies a local page. * @task uri */ public static function isValidLocalURIForLink($uri) { $uri = (string)$uri; if (!strlen($uri)) { return false; } if (preg_match('/\s/', $uri)) { // PHP hasn't been vulnerable to header injection attacks for a bunch of // years, but we can safely reject these anyway since they're never valid. return false; } // Chrome (at a minimum) interprets backslashes in Location headers and the // URL bar as forward slashes. This is probably intended to reduce user // error caused by confusion over which key is "forward slash" vs "back // slash". // // However, it means a URI like "/\evil.com" is interpreted like // "//evil.com", which is a protocol relative remote URI. // // Since we currently never generate URIs with backslashes in them, reject // these unconditionally rather than trying to figure out how browsers will // interpret them. if (preg_match('/\\\\/', $uri)) { return false; } // Valid URIs must begin with '/', followed by the end of the string or some // other non-'/' character. This rejects protocol-relative URIs like // "//evil.com/evil_stuff/". return (bool)preg_match('@^/([^/]|$)@', $uri); } /** * Detect if a URI identifies some valid linkable remote resource. * * @param string URI to test. * @return bool True if a URI identifies a remote resource with an allowed * protocol. * @task uri */ public static function isValidRemoteURIForLink($uri) { try { self::requireValidRemoteURIForLink($uri); return true; } catch (Exception $ex) { return false; } } /** * Detect if a URI identifies a valid linkable remote resource, throwing a * detailed message if it does not. * * A valid linkable remote resource can be safely linked or redirected to. * This is primarily a protocol whitelist check. * * @param string URI to test. * @return void * @task uri */ public static function requireValidRemoteURIForLink($raw_uri) { $uri = new PhutilURI($raw_uri); $proto = $uri->getProtocol(); if (!strlen($proto)) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must specify a protocol.', $raw_uri)); } $protocols = self::getEnvConfig('uri.allowed-protocols'); if (!isset($protocols[$proto])) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must use one of these protocols: %s.', $raw_uri, implode(', ', array_keys($protocols)))); } $domain = $uri->getDomain(); if (!strlen($domain)) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must specify a domain.', $raw_uri)); } } /** * Detect if a URI identifies a valid fetchable remote resource. * * @param string URI to test. * @param list Allowed protocols. * @return bool True if the URI is a valid fetchable remote resource. * @task uri */ public static function isValidRemoteURIForFetch($uri, array $protocols) { try { self::requireValidRemoteURIForFetch($uri, $protocols); return true; } catch (Exception $ex) { return false; } } /** * Detect if a URI identifies a valid fetchable remote resource, throwing * a detailed message if it does not. * * A valid fetchable remote resource can be safely fetched using a request * originating on this server. This is a primarily an address check against * the outbound address blacklist. * * @param string URI to test. * @param list Allowed protocols. * @return pair Pre-resolved URI and domain. * @task uri */ public static function requireValidRemoteURIForFetch( $raw_uri, array $protocols) { $uri = new PhutilURI($raw_uri); $proto = $uri->getProtocol(); if (!strlen($proto)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must specify a protocol.', $raw_uri)); } $protocols = array_fuse($protocols); if (!isset($protocols[$proto])) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must use one of these protocols: %s.', $raw_uri, implode(', ', array_keys($protocols)))); } $domain = $uri->getDomain(); if (!strlen($domain)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must specify a domain.', $raw_uri)); } $addresses = gethostbynamel($domain); if (!$addresses) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. The domain "%s" could '. 'not be resolved.', $raw_uri, $domain)); } foreach ($addresses as $address) { if (self::isBlacklistedOutboundAddress($address)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. The domain "%s" '. 'resolves to the address "%s", which is blacklisted for '. 'outbound requests.', $raw_uri, $domain, $address)); } } $resolved_uri = clone $uri; $resolved_uri->setDomain(head($addresses)); return array($resolved_uri, $domain); } /** * Determine if an IP address is in the outbound address blacklist. * * @param string IP address. * @return bool True if the address is blacklisted. */ public static function isBlacklistedOutboundAddress($address) { $blacklist = self::getEnvConfig('security.outbound-blacklist'); return PhutilCIDRList::newList($blacklist)->containsAddress($address); } public static function isClusterRemoteAddress() { $cluster_addresses = self::getEnvConfig('cluster.addresses'); if (!$cluster_addresses) { return false; } $address = self::getRemoteAddress(); if (!$address) { throw new Exception( pht( 'Unable to test remote address against cluster whitelist: '. 'REMOTE_ADDR is not defined or not valid.')); } return self::isClusterAddress($address); } public static function isClusterAddress($address) { $cluster_addresses = self::getEnvConfig('cluster.addresses'); if (!$cluster_addresses) { throw new Exception( pht( - 'Phabricator is not configured to serve cluster requests. '. + 'This server is not configured to serve cluster requests. '. 'Set `cluster.addresses` in the configuration to whitelist '. 'cluster hosts before sending requests that use a cluster '. 'authentication mechanism.')); } return PhutilCIDRList::newList($cluster_addresses) ->containsAddress($address); } public static function getRemoteAddress() { $address = idx($_SERVER, 'REMOTE_ADDR'); if (!$address) { return null; } try { return PhutilIPAddress::newAddress($address); } catch (Exception $ex) { return null; } } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ public static function envConfigExists($key) { return array_key_exists($key, self::$sourceStack->getKeys(array($key))); } /** * @task internal */ public static function getAllConfigKeys() { return self::$sourceStack->getAllKeys(); } public static function getConfigSourceStack() { return self::$sourceStack; } /** * @task internal */ public static function overrideTestEnvConfig($stack_key, $key, $value) { $tmp = array(); // If we don't have the right key, we'll throw when popping the last // source off the stack. do { $source = self::$sourceStack->popSource(); array_unshift($tmp, $source); if (spl_object_hash($source) == $stack_key) { $source->setKeys(array($key => $value)); break; } } while (true); foreach ($tmp as $source) { self::$sourceStack->pushSource($source); } self::dropConfigCache(); } private static function dropConfigCache() { self::$cache = array(); } private static function resetUmask() { // Reset the umask to the common standard umask. The umask controls default // permissions when files are created and propagates to subprocesses. // "022" is the most common umask, but sometimes it is set to something // unusual by the calling environment. // Since various things rely on this umask to work properly and we are // not aware of any legitimate reasons to adjust it, unconditionally // normalize it until such reasons arise. See T7475 for discussion. umask(022); } /** * Get the path to an empty directory which is readable by all of the system * user accounts that Phabricator acts as. * * In some cases, a binary needs some valid HOME or CWD to continue, but not * all user accounts have valid home directories and even if they do they * may not be readable after a `sudo` operation. * * @return string Path to an empty directory suitable for use as a CWD. */ public static function getEmptyCWD() { $root = dirname(phutil_get_library_root('phabricator')); return $root.'/support/empty/'; } } diff --git a/src/infrastructure/export/format/PhabricatorExcelExportFormat.php b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php index e7135bd9db..dd33813384 100644 --- a/src/infrastructure/export/format/PhabricatorExcelExportFormat.php +++ b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php @@ -1,183 +1,182 @@ https://github.com/PHPOffice/PHPExcel Briefly: - Clone that repository somewhere on the sever (like `/path/to/example/PHPExcel`). - Update your PHP `%s` setting (in `php.ini`) to include the PHPExcel `Classes` directory (like `/path/to/example/PHPExcel/Classes`). EOHELP , 'include_path'); } public function getFileExtension() { return 'xlsx'; } public function getMIMEContentType() { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; } /** * @phutil-external-symbol class PHPExcel_Cell_DataType */ public function addHeaders(array $fields) { $sheet = $this->getSheet(); $header_format = array( 'font' => array( 'bold' => true, ), ); $row = 1; $col = 0; foreach ($fields as $field) { $cell_value = $field->getLabel(); $cell_name = $this->getCellName($col, $row); $cell = $sheet->setCellValue( $cell_name, $cell_value, $return_cell = true); $sheet->getStyle($cell_name)->applyFromArray($header_format); $cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING); $width = $field->getCharacterWidth(); if ($width !== null) { $col_name = $this->getCellName($col); $sheet->getColumnDimension($col_name) ->setWidth($width); } $col++; } } public function addObject($object, array $fields, array $map) { $sheet = $this->getSheet(); $col = 0; foreach ($fields as $key => $field) { $cell_value = $map[$key]; $cell_value = $field->getPHPExcelValue($cell_value); $cell_name = $this->getCellName($col, $this->rowCursor); $cell = $sheet->setCellValue( $cell_name, $cell_value, $return_cell = true); $style = $sheet->getStyle($cell_name); $field->formatPHPExcelCell($cell, $style); $col++; } $this->rowCursor++; } /** * @phutil-external-symbol class PHPExcel_IOFactory */ public function newFileData() { $workbook = $this->getWorkbook(); $writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007'); ob_start(); $writer->save('php://output'); $data = ob_get_clean(); return $data; } private function getWorkbook() { if (!$this->workbook) { $this->workbook = $this->newWorkbook(); } return $this->workbook; } /** * @phutil-external-symbol class PHPExcel */ private function newWorkbook() { include_once 'PHPExcel.php'; return new PHPExcel(); } private function getSheet() { if (!$this->sheet) { $workbook = $this->getWorkbook(); $sheet = $workbook->setActiveSheetIndex(0); $sheet->setTitle($this->getTitle()); $this->sheet = $sheet; // The row cursor starts on the second row, after the header row. $this->rowCursor = 2; } return $this->sheet; } /** * @phutil-external-symbol class PHPExcel_Cell */ private function getCellName($col, $row = null) { $col_name = PHPExcel_Cell::stringFromColumnIndex($col); if ($row === null) { return $col_name; } return $col_name.$row; } } diff --git a/src/infrastructure/markup/markuprule/PhutilRemarkupEvalRule.php b/src/infrastructure/markup/markuprule/PhutilRemarkupEvalRule.php index ea20dfedef..cb67041c62 100644 --- a/src/infrastructure/markup/markuprule/PhutilRemarkupEvalRule.php +++ b/src/infrastructure/markup/markuprule/PhutilRemarkupEvalRule.php @@ -1,100 +1,100 @@ isFlatText($expression)) { return $matches[0]; } $engine = $this->getEngine(); $token = $engine->storeText($expression); $list_key = self::KEY_EVAL; $expression_list = $engine->getTextMetadata($list_key, array()); $expression_list[] = array( 'token' => $token, 'expression' => $expression, 'original' => $matches[0], ); $engine->setTextMetadata($list_key, $expression_list); return $token; } public function didMarkupText() { $engine = $this->getEngine(); $list_key = self::KEY_EVAL; $expression_list = $engine->getTextMetadata($list_key, array()); foreach ($expression_list as $expression_item) { $token = $expression_item['token']; $expression = $expression_item['expression']; $result = $this->evaluateExpression($expression); if ($result === null) { $result = $expression_item['original']; } $engine->overwriteStoredText($token, $result); } } private function evaluateExpression($expression) { static $string_map; if ($string_map === null) { $string_map = array( 'strings' => array( 'platform' => array( 'server' => array( - 'name' => pht('Phabricator'), + 'name' => PlatformSymbols::getPlatformServerName(), 'path' => pht('phabricator/'), ), 'client' => array( - 'name' => pht('Arcanist'), + 'name' => PlatformSymbols::getPlatformClientName(), 'path' => pht('arcanist/'), ), ), ), ); } $parts = explode('.', $expression); $cursor = $string_map; foreach ($parts as $part) { if (isset($cursor[$part])) { $cursor = $cursor[$part]; } else { break; } } if (is_string($cursor)) { return $cursor; } return null; } } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php index ef6735ec8f..00689ab5fe 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php @@ -1,27 +1,27 @@ setName('databases') ->setExamples('**databases** [__options__]') - ->setSynopsis(pht('List Phabricator databases.')); + ->setSynopsis(pht('List databases.')); } protected function isReadOnlyWorkflow() { return true; } public function didExecute(PhutilArgumentParser $args) { $api = $this->getAnyAPI(); $patches = $this->getPatches(); $databases = $api->getDatabaseList($patches, true); echo implode("\n", $databases)."\n"; return 0; } } diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php index fdd6609288..8916b37a0e 100644 --- a/src/infrastructure/util/PhabricatorHash.php +++ b/src/infrastructure/util/PhabricatorHash.php @@ -1,281 +1,281 @@ $max) { throw new Exception(pht('Maximum must be larger than minimum.')); } if ($min == $max) { return $min; } $hash = sha1($string, $raw_output = true); // Make sure this ends up positive, even on 32-bit machines. $value = head(unpack('L', $hash)) & 0x7FFFFFFF; return $min + ($value % (1 + $max - $min)); } /** * Shorten a string to a maximum byte length in a collision-resistant way * while retaining some degree of human-readability. * * This function converts an input string into a prefix plus a hash. For * example, a very long string beginning with "crabapplepie..." might be * digested to something like "crabapp-N1wM1Nz3U84k". * * This allows the maximum length of identifiers to be fixed while * maintaining a high degree of collision resistance and a moderate degree * of human readability. * * @param string The string to shorten. * @param int Maximum length of the result. * @return string String shortened in a collision-resistant way. */ public static function digestToLength($string, $length) { // We need at least two more characters than the hash length to fit in a // a 1-character prefix and a separator. $min_length = self::INDEX_DIGEST_LENGTH + 2; if ($length < $min_length) { throw new Exception( pht( 'Length parameter in %s must be at least %s, '. 'but %s was provided.', 'digestToLength()', new PhutilNumber($min_length), new PhutilNumber($length))); } // We could conceivably return the string unmodified if it's shorter than // the specified length. Instead, always hash it. This makes the output of // the method more recognizable and consistent (no surprising new behavior // once you hit a string longer than `$length`) and prevents an attacker // who can control the inputs from intentionally using the hashed form // of a string to cause a collision. $hash = self::digestForIndex($string); $prefix = substr($string, 0, ($length - ($min_length - 1))); return $prefix.'-'.$hash; } public static function digestWithNamedKey($message, $key_name) { $key_bytes = self::getNamedHMACKey($key_name); return self::digestHMACSHA256($message, $key_bytes); } public static function digestHMACSHA256($message, $key) { if (!is_string($message)) { throw new Exception( pht('HMAC-SHA256 can only digest strings.')); } if (!is_string($key)) { throw new Exception( pht('HMAC-SHA256 keys must be strings.')); } if (!strlen($key)) { throw new Exception( pht('HMAC-SHA256 requires a nonempty key.')); } $result = hash_hmac('sha256', $message, $key, $raw_output = false); // Although "hash_hmac()" is documented as returning `false` when it fails, // it can also return `null` if you pass an object as the "$message". if ($result === false || $result === null) { throw new Exception( pht('Unable to compute HMAC-SHA256 digest of message.')); } return $result; } /* -( HMAC Key Management )------------------------------------------------ */ private static function getNamedHMACKey($hmac_name) { $cache = PhabricatorCaches::getImmutableCache(); $cache_key = "hmac.key({$hmac_name})"; $hmac_key = $cache->getKey($cache_key); if (($hmac_key === null) || !strlen($hmac_key)) { $hmac_key = self::readHMACKey($hmac_name); if ($hmac_key === null) { $hmac_key = self::newHMACKey($hmac_name); self::writeHMACKey($hmac_name, $hmac_key); } $cache->setKey($cache_key, $hmac_key); } // The "hex2bin()" function doesn't exist until PHP 5.4.0 so just // implement it inline. $result = ''; for ($ii = 0; $ii < strlen($hmac_key); $ii += 2) { $result .= pack('H*', substr($hmac_key, $ii, 2)); } return $result; } private static function newHMACKey($hmac_name) { $hmac_key = Filesystem::readRandomBytes(64); return bin2hex($hmac_key); } private static function writeHMACKey($hmac_name, $hmac_key) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); id(new PhabricatorAuthHMACKey()) ->setKeyName($hmac_name) ->setKeyValue($hmac_key) ->save(); unset($unguarded); } private static function readHMACKey($hmac_name) { $table = new PhabricatorAuthHMACKey(); $conn = $table->establishConnection('r'); $row = queryfx_one( $conn, 'SELECT keyValue FROM %T WHERE keyName = %s', $table->getTableName(), $hmac_name); if (!$row) { return null; } return $row['keyValue']; } }