diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index 8a650fc7bd..9fe102b9d7 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -1,577 +1,577 @@ shouldRequireLogin()) { return false; } if (!$this->shouldRequireEnabledUser()) { return false; } if ($this->shouldAllowPartialSessions()) { return false; } $user = $this->getRequest()->getUser(); if (!$user->getIsStandardUser()) { return false; } return PhabricatorEnv::getEnvConfig('security.require-multi-factor-auth'); } public function willBeginExecution() { $request = $this->getRequest(); if ($request->getUser()) { // NOTE: Unit tests can set a user explicitly. Normal requests are not // permitted to do this. PhabricatorTestCase::assertExecutingUnitTests(); $user = $request->getUser(); } else { $user = new PhabricatorUser(); $session_engine = new PhabricatorAuthSessionEngine(); $phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); if (strlen($phsid)) { $session_user = $session_engine->loadUserForSession( PhabricatorAuthSession::TYPE_WEB, $phsid); if ($session_user) { $user = $session_user; } } else { // If the client doesn't have a session token, generate an anonymous // session. This is used to provide CSRF protection to logged-out users. $phsid = $session_engine->establishSession( PhabricatorAuthSession::TYPE_WEB, null, $partial = false); // This may be a resource request, in which case we just don't set // the cookie. if ($request->canSetCookies()) { $request->setCookie(PhabricatorCookies::COOKIE_SESSION, $phsid); } } if (!$user->isLoggedIn()) { $user->attachAlternateCSRFString(PhabricatorHash::digest($phsid)); } $request->setUser($user); } $translation = $user->getTranslation(); if ($translation && $translation != PhabricatorEnv::getEnvConfig('translation.provider')) { $translation = newv($translation, array()); PhutilTranslator::getInstance() ->setLanguage($translation->getLanguage()) - ->addTranslations($translation->getTranslations()); + ->addTranslations($translation->getCleanTranslations()); } $preferences = $user->loadPreferences(); if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) { $dark_console = PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE; if ($preferences->getPreference($dark_console) || PhabricatorEnv::getEnvConfig('darkconsole.always-on')) { $console = new DarkConsoleCore(); $request->getApplicationConfiguration()->setConsole($console); } } // NOTE: We want to set up the user first so we can render a real page // here, but fire this before any real logic. $restricted = array( 'code', ); foreach ($restricted as $parameter) { if ($request->getExists($parameter)) { if (!$this->shouldAllowRestrictedParameter($parameter)) { throw new Exception( pht( 'Request includes restricted parameter "%s", but this '. 'controller ("%s") does not whitelist it. Refusing to '. 'serve this request because it might be part of a redirection '. 'attack.', $parameter, get_class($this))); } } } if ($this->shouldRequireEnabledUser()) { if ($user->isLoggedIn() && !$user->getIsApproved()) { $controller = new PhabricatorAuthNeedsApprovalController(); return $this->delegateToController($controller); } if ($user->getIsDisabled()) { $controller = new PhabricatorDisabledUserController(); return $this->delegateToController($controller); } } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_CONTROLLER_CHECKREQUEST, array( 'request' => $request, 'controller' => $this, )); $event->setUser($user); PhutilEventEngine::dispatchEvent($event); $checker_controller = $event->getValue('controller'); if ($checker_controller != $this) { return $this->delegateToController($checker_controller); } $auth_class = 'PhabricatorAuthApplication'; $auth_application = PhabricatorApplication::getByClass($auth_class); // Require partial sessions to finish login before doing anything. if (!$this->shouldAllowPartialSessions()) { if ($user->hasSession() && $user->getSession()->getIsPartial()) { $login_controller = new PhabricatorAuthFinishController(); $this->setCurrentApplication($auth_application); return $this->delegateToController($login_controller); } } // Check if the user needs to configure MFA. $need_mfa = $this->shouldRequireMultiFactorEnrollment(); $have_mfa = $user->getIsEnrolledInMultiFactor(); if ($need_mfa && !$have_mfa) { // Check if the cache is just out of date. Otherwise, roadblock the user // and require MFA enrollment. $user->updateMultiFactorEnrollment(); if (!$user->getIsEnrolledInMultiFactor()) { $mfa_controller = new PhabricatorAuthNeedsMultiFactorController(); $this->setCurrentApplication($auth_application); return $this->delegateToController($mfa_controller); } } if ($this->shouldRequireLogin()) { // This actually means we need either: // - a valid user, or a public controller; and // - permission to see the application. $allow_public = $this->shouldAllowPublic() && PhabricatorEnv::getEnvConfig('policy.allow-public'); // If this controller isn't public, and the user isn't logged in, require // login. if (!$allow_public && !$user->isLoggedIn()) { $login_controller = new PhabricatorAuthStartController(); $this->setCurrentApplication($auth_application); return $this->delegateToController($login_controller); } if ($user->isLoggedIn()) { if ($this->shouldRequireEmailVerification()) { if (!$user->getIsEmailVerified()) { $controller = new PhabricatorMustVerifyEmailController(); $this->setCurrentApplication($auth_application); return $this->delegateToController($controller); } } } // If the user doesn't have access to the application, don't let them use // any of its controllers. We query the application in order to generate // a policy exception if the viewer doesn't have permission. $application = $this->getCurrentApplication(); if ($application) { id(new PhabricatorApplicationQuery()) ->setViewer($user) ->withPHIDs(array($application->getPHID())) ->executeOne(); } } // NOTE: We do this last so that users get a login page instead of a 403 // if they need to login. if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) { return new Aphront403Response(); } } public function buildStandardPageView() { $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->setController($this); return $view; } public function buildStandardPageResponse($view, array $data) { $page = $this->buildStandardPageView(); $page->appendChild($view); $response = new AphrontWebpageResponse(); $response->setContent($page->render()); return $response; } public function getApplicationURI($path = '') { if (!$this->getCurrentApplication()) { throw new Exception('No application!'); } return $this->getCurrentApplication()->getApplicationURI($path); } public function buildApplicationPage($view, array $options) { $page = $this->buildStandardPageView(); $title = PhabricatorEnv::getEnvConfig('phabricator.serious-business') ? 'Phabricator' : pht('Bacon Ice Cream for Breakfast'); $application = $this->getCurrentApplication(); $page->setTitle(idx($options, 'title', $title)); if ($application) { $page->setApplicationName($application->getName()); if ($application->getTitleGlyph()) { $page->setGlyph($application->getTitleGlyph()); } } if (!($view instanceof AphrontSideNavFilterView)) { $nav = new AphrontSideNavFilterView(); $nav->appendChild($view); $view = $nav; } $user = $this->getRequest()->getUser(); $view->setUser($user); $page->appendChild($view); $object_phids = idx($options, 'pageObjects', array()); if ($object_phids) { $page->appendPageObjects($object_phids); foreach ($object_phids as $object_phid) { PhabricatorFeedStoryNotification::updateObjectNotificationViews( $user, $object_phid); } } if (idx($options, 'device', true)) { $page->setDeviceReady(true); } $page->setShowFooter(idx($options, 'showFooter', true)); $page->setShowChrome(idx($options, 'chrome', true)); $application_menu = $this->buildApplicationMenu(); if ($application_menu) { $page->setApplicationMenu($application_menu); } $response = new AphrontWebpageResponse(); return $response->setContent($page->render()); } public function didProcessRequest($response) { // If a bare DialogView is returned, wrap it in a DialogResponse. if ($response instanceof AphrontDialogView) { $response = id(new AphrontDialogResponse())->setDialog($response); } $request = $this->getRequest(); $response->setRequest($request); $seen = array(); while ($response instanceof AphrontProxyResponse) { $hash = spl_object_hash($response); if (isset($seen[$hash])) { $seen[] = get_class($response); throw new Exception( 'Cycle while reducing proxy responses: '. implode(' -> ', $seen)); } $seen[$hash] = get_class($response); $response = $response->reduceProxyResponse(); } if ($response instanceof AphrontDialogResponse) { if (!$request->isAjax()) { $dialog = $response->getDialog(); $title = $dialog->getTitle(); $short = $dialog->getShortTitle(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(coalesce($short, $title)); $page_content = array( $crumbs, $response->buildResponseString(), ); $view = id(new PhabricatorStandardPageView()) ->setRequest($request) ->setController($this) ->setDeviceReady(true) ->setTitle($title) ->appendChild($page_content); $response = id(new AphrontWebpageResponse()) ->setContent($view->render()) ->setHTTPResponseCode($response->getHTTPResponseCode()); } else { $response->getDialog()->setIsStandalone(true); return id(new AphrontAjaxResponse()) ->setContent(array( 'dialog' => $response->buildResponseString(), )); } } else if ($response instanceof AphrontRedirectResponse) { if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent( array( 'redirect' => $response->getURI(), )); } } return $response; } protected function getHandle($phid) { if (empty($this->handles[$phid])) { throw new Exception( "Attempting to access handle which wasn't loaded: {$phid}"); } return $this->handles[$phid]; } protected function loadHandles(array $phids) { $phids = array_filter($phids); $this->handles = $this->loadViewerHandles($phids); return $this; } protected function getLoadedHandles() { return $this->handles; } protected function loadViewerHandles(array $phids) { return id(new PhabricatorHandleQuery()) ->setViewer($this->getRequest()->getUser()) ->withPHIDs($phids) ->execute(); } /** * Render a list of links to handles, identified by PHIDs. The handles must * already be loaded. * * @param list List of PHIDs to render links to. * @param string Style, one of "\n" (to put each item on its own line) * or "," (to list items inline, separated by commas). * @return string Rendered list of handle links. */ protected function renderHandlesForPHIDs(array $phids, $style = "\n") { $style_map = array( "\n" => phutil_tag('br'), ',' => ', ', ); if (empty($style_map[$style])) { throw new Exception("Unknown handle list style '{$style}'!"); } return implode_selected_handle_links($style_map[$style], $this->getLoadedHandles(), array_filter($phids)); } protected function buildApplicationMenu() { return null; } protected function buildApplicationCrumbs() { $crumbs = array(); $application = $this->getCurrentApplication(); if ($application) { $sprite = $application->getIconName(); if (!$sprite) { $sprite = 'application'; } $crumbs[] = id(new PhabricatorCrumbView()) ->setHref($this->getApplicationURI()) ->setAural($application->getName()) ->setIcon($sprite); } $view = new PhabricatorCrumbsView(); foreach ($crumbs as $crumb) { $view->addCrumb($crumb); } return $view; } protected function hasApplicationCapability($capability) { return PhabricatorPolicyFilter::hasCapability( $this->getRequest()->getUser(), $this->getCurrentApplication(), $capability); } protected function requireApplicationCapability($capability) { PhabricatorPolicyFilter::requireCapability( $this->getRequest()->getUser(), $this->getCurrentApplication(), $capability); } protected function explainApplicationCapability( $capability, $positive_message, $negative_message) { $can_act = $this->hasApplicationCapability($capability); if ($can_act) { $message = $positive_message; $icon_name = 'fa-play-circle-o lightgreytext'; } else { $message = $negative_message; $icon_name = 'fa-lock'; } $icon = id(new PHUIIconView()) ->setIconFont($icon_name); require_celerity_resource('policy-css'); $phid = $this->getCurrentApplication()->getPHID(); $explain_uri = "/policy/explain/{$phid}/{$capability}/"; $message = phutil_tag( 'div', array( 'class' => 'policy-capability-explanation', ), array( $icon, javelin_tag( 'a', array( 'href' => $explain_uri, 'sigil' => 'workflow', ), $message), )); return array($can_act, $message); } public function getDefaultResourceSource() { return 'phabricator'; } /** * Create a new @{class:AphrontDialogView} with defaults filled in. * * @return AphrontDialogView New dialog. */ public function newDialog() { $submit_uri = new PhutilURI($this->getRequest()->getRequestURI()); $submit_uri = $submit_uri->getPath(); return id(new AphrontDialogView()) ->setUser($this->getRequest()->getUser()) ->setSubmitURI($submit_uri); } protected function buildTransactionTimeline( PhabricatorApplicationTransactionInterface $object, PhabricatorApplicationTransactionQuery $query, PhabricatorMarkupEngine $engine = null, $render_data = array()) { $viewer = $this->getRequest()->getUser(); $xaction = $object->getApplicationTransactionTemplate(); $view = $xaction->getApplicationTransactionViewObject(); $pager = id(new AphrontCursorPagerView()) ->readFromRequest($this->getRequest()) ->setURI(new PhutilURI( '/transactions/showolder/'.$object->getPHID().'/')); $xactions = $query ->setViewer($viewer) ->withObjectPHIDs(array($object->getPHID())) ->needComments(true) ->setReversePaging(false) ->executeWithCursorPager($pager); $xactions = array_reverse($xactions); if ($engine) { foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $view->setMarkupEngine($engine); } $timeline = $view ->setUser($viewer) ->setObjectPHID($object->getPHID()) ->setTransactions($xactions) ->setPager($pager) ->setRenderData($render_data) ->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID')) ->setQuoteRef($this->getRequest()->getStr('quoteRef')); $object->willRenderTimeline($timeline, $this->getRequest()); return $timeline; } } diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 5d1a65a7df..3b78ebb1c4 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -1,582 +1,582 @@ 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 { private static $sourceStack; private static $repairSource; private static $overrideSource; private static $requestBaseURI; private static $cache; /** * @phutil-external-symbol class PhabricatorStartup */ public static function initializeWebEnvironment() { self::initializeCommonEnvironment(); } public static function initializeScriptEnvironment() { self::initializeCommonEnvironment(); // 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() { PhutilErrorHandler::initialize(); self::buildConfigurationSourceStack(); // Force a valid timezone. If both PHP and Phabricator configuration are // invalid, use UTC. $tz = PhabricatorEnv::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 = PhabricatorEnv::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; PhabricatorEventEngine::initialize(); $translation = PhabricatorEnv::newObjectFromConfig('translation.provider'); PhutilTranslator::getInstance() ->setLanguage($translation->getLanguage()) - ->addTranslations($translation->getTranslations()); + ->addTranslations($translation->getCleanTranslations()); } private static function buildConfigurationSourceStack() { 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 (PhabricatorEnv::getEnvConfig('load-libraries') as $library) { phutil_load_library($library); } // 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 PhutilSymbolLoader()) ->setAncestorClass('PhabricatorConfigSiteSource') ->loadObjects(); $site_sources = msort($site_sources, 'getPriority'); foreach ($site_sources as $site_source) { $stack->pushSource($site_source); } try { $stack->pushSource( id(new PhabricatorConfigDatabaseSource('default')) ->setName(pht('Database'))); } catch (AphrontQueryException $exception) { // If the database is not available, just skip this configuration // source. This happens during `bin/storage upgrade`, `bin/conf` before // schema setup, etc. } } 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; } public static function calculateEnvironmentHash() { $keys = array_keys(self::getAllConfigKeys()); ksort($keys); $values = array(); foreach ($keys as $key) { $values[$key] = self::getEnvConfigIfExists($key); } return PhabricatorHash::digest(json_encode($values)); } /* -( 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 (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("No config value specified for key '{$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 getAllowedURIs($path) { $uri = new PhutilURI($path); if ($uri->getDomain()) { return $path; } $allowed_uris = self::getEnvConfig('phabricator.allowed-uris'); $return = array(); foreach ($allowed_uris as $allowed_uri) { $return[] = rtrim($allowed_uri, '/').$path; } return $return; } /** * 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') { $uri = new PhutilURI('https://secure.phabricator.com/diviner/find/'); $uri->setQueryParam('name', $resource); $uri->setQueryParam('type', $type); $uri->setQueryParam('jump', true); return (string)$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( "Define 'phabricator.base-uri' in your configuration to continue."); } return $base_uri; } public static function getRequestBaseURI() { return self::$requestBaseURI; } public static function setRequestBaseURI($uri) { self::$requestBaseURI = $uri; } /* -( 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( 'Scoped environments were destroyed in a diffent order than they '. 'were initialized.'); } } /* -( URI Validation )----------------------------------------------------- */ /** * Detect if a URI satisfies either @{method:isValidLocalWebResource} or * @{method:isValidRemoteWebResource}, 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 isValidWebResource($uri) { return self::isValidLocalWebResource($uri) || self::isValidRemoteWebResource($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 isValidLocalWebResource($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 remote resource. * * @param string URI to test. * @return bool True if a URI idenfies a remote resource with an allowed * protocol. * @task uri */ public static function isValidRemoteWebResource($uri) { $uri = (string)$uri; $proto = id(new PhutilURI($uri))->getProtocol(); if (!$proto) { return false; } $allowed = self::getEnvConfig('uri.allowed-protocols'); if (empty($allowed[$proto])) { return false; } return true; } /* -( 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(); } } diff --git a/src/infrastructure/internationalization/translation/PhabricatorTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorTranslation.php index 1912781dae..11ae6baa75 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorTranslation.php @@ -1,9 +1,37 @@ Translation map with empty translations removed. + */ + public function getCleanTranslations() { + return $this->clean($this->getTranslations()); + } + + + /** + * Removes NULL-valued translation keys from the translation map, to prevent + * echoing out empty strings. + * + * @param dict Translation map, with empty translations. + * @return dict Map with empty translations removed. + */ + protected function clean(array $translation_array) { + foreach ($translation_array as $key => $translation_string) { + if ($translation_string === null) { + unset($translation_array[$key]); + } + } + + return $translation_array; + } + }