Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php
index 3bdd2c00bc..3198bb9fd4 100644
--- a/src/aphront/configuration/AphrontApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontApplicationConfiguration.php
@@ -1,879 +1,880 @@
<?php
/**
* @task routing URI Routing
* @task response Response Handling
* @task exception Exception Handling
*/
final class AphrontApplicationConfiguration
extends Phobject {
private $request;
private $host;
private $path;
private $console;
public function buildRequest() {
$parser = new PhutilQueryStringParser();
$data = array();
$data += $_POST;
$data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));
$cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix');
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($data);
$request->setApplicationConfiguration($this);
$request->setCookiePrefix($cookie_prefix);
$request->updateEphemeralCookies();
return $request;
}
public function buildRedirectController($uri, $external) {
return array(
new PhabricatorRedirectController(),
array(
'uri' => $uri,
'external' => $external,
),
);
}
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function getConsole() {
return $this->console;
}
public function setConsole($console) {
$this->console = $console;
return $this;
}
public function setHost($host) {
$this->host = $host;
return $this;
}
public function getHost() {
return $this->host;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public static function runHTTPRequest(AphrontHTTPSink $sink) {
if (isset($_SERVER['HTTP_X_SETUP_SELFCHECK'])) {
$response = self::newSelfCheckResponse();
return self::writeResponse($sink, $response);
}
PhabricatorStartup::beginStartupPhase('multimeter');
$multimeter = MultimeterControl::newInstance();
$multimeter->setEventContext('<http-init>');
$multimeter->setEventViewer('<none>');
// Build a no-op write guard for the setup phase. We'll replace this with a
// real write guard later on, but we need to survive setup and build a
// request object first.
$write_guard = new AphrontWriteGuard('id');
PhabricatorStartup::beginStartupPhase('preflight');
$response = PhabricatorSetupCheck::willPreflightRequest();
if ($response) {
return self::writeResponse($sink, $response);
}
PhabricatorStartup::beginStartupPhase('env.init');
self::readHTTPPOSTData();
try {
PhabricatorEnv::initializeWebEnvironment();
$database_exception = null;
} catch (PhabricatorClusterStrandedException $ex) {
$database_exception = $ex;
}
// If we're in developer mode, set a flag so that top-level exception
// handlers can add more information.
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$sink->setShowStackTraces(true);
}
if ($database_exception) {
$issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(
$database_exception,
true);
$response = PhabricatorSetupCheck::newIssueResponse($issue);
return self::writeResponse($sink, $response);
}
$multimeter->setSampleRate(
PhabricatorEnv::getEnvConfig('debug.sample-rate'));
$debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit');
if ($debug_time_limit) {
PhabricatorStartup::setDebugTimeLimit($debug_time_limit);
}
// This is the earliest we can get away with this, we need env config first.
PhabricatorStartup::beginStartupPhase('log.access');
PhabricatorAccessLog::init();
$access_log = PhabricatorAccessLog::getLog();
PhabricatorStartup::setAccessLog($access_log);
$address = PhabricatorEnv::getRemoteAddress();
if ($address) {
$address_string = $address->getAddress();
} else {
$address_string = '-';
}
$access_log->setData(
array(
'R' => AphrontRequest::getHTTPHeader('Referer', '-'),
'r' => $address_string,
'M' => idx($_SERVER, 'REQUEST_METHOD', '-'),
));
DarkConsoleXHProfPluginAPI::hookProfiler();
// We just activated the profiler, so we don't need to keep track of
// startup phases anymore: it can take over from here.
PhabricatorStartup::beginStartupPhase('startup.done');
DarkConsoleErrorLogPluginAPI::registerErrorHandler();
$response = PhabricatorSetupCheck::willProcessRequest();
if ($response) {
return self::writeResponse($sink, $response);
}
$host = AphrontRequest::getHTTPHeader('Host');
$path = PhabricatorStartup::getRequestPath();
$application = new self();
$application->setHost($host);
$application->setPath($path);
$request = $application->buildRequest();
// Now that we have a request, convert the write guard into one which
// actually checks CSRF tokens.
$write_guard->dispose();
$write_guard = new AphrontWriteGuard(array($request, 'validateCSRF'));
// Build the server URI implied by the request headers. If an administrator
// has not configured "phabricator.base-uri" yet, we'll use this to generate
// links.
$request_protocol = ($request->isHTTPS() ? 'https' : 'http');
$request_base_uri = "{$request_protocol}://{$host}/";
PhabricatorEnv::setRequestBaseURI($request_base_uri);
$access_log->setData(
array(
'U' => (string)$request->getRequestURI()->getPath(),
));
$processing_exception = null;
try {
$response = $application->processRequest(
$request,
$access_log,
$sink,
$multimeter);
$response_code = $response->getHTTPResponseCode();
} catch (Exception $ex) {
$processing_exception = $ex;
$response_code = 500;
}
$write_guard->dispose();
$access_log->setData(
array(
'c' => $response_code,
'T' => PhabricatorStartup::getMicrosecondsSinceStart(),
));
$multimeter->newEvent(
MultimeterEvent::TYPE_REQUEST_TIME,
$multimeter->getEventContext(),
PhabricatorStartup::getMicrosecondsSinceStart());
$access_log->write();
$multimeter->saveEvents();
DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log);
PhabricatorStartup::disconnectRateLimits(
array(
'viewer' => $request->getUser(),
));
if ($processing_exception) {
throw $processing_exception;
}
}
public function processRequest(
AphrontRequest $request,
PhutilDeferredLog $access_log,
AphrontHTTPSink $sink,
MultimeterControl $multimeter) {
$this->setRequest($request);
list($controller, $uri_data) = $this->buildController();
$controller_class = get_class($controller);
$access_log->setData(
array(
'C' => $controller_class,
));
$multimeter->setEventContext('web.'.$controller_class);
$request->setController($controller);
$request->setURIMap($uri_data);
$controller->setRequest($request);
// If execution throws an exception and then trying to render that
// exception throws another exception, we want to show the original
// exception, as it is likely the root cause of the rendering exception.
$original_exception = null;
try {
$response = $controller->willBeginExecution();
if ($request->getUser() && $request->getUser()->getPHID()) {
$access_log->setData(
array(
'u' => $request->getUser()->getUserName(),
'P' => $request->getUser()->getPHID(),
));
$multimeter->setEventViewer('user.'.$request->getUser()->getPHID());
}
if (!$response) {
$controller->willProcessRequest($uri_data);
$response = $controller->handleRequest($request);
$this->validateControllerResponse($controller, $response);
}
} catch (Exception $ex) {
$original_exception = $ex;
} catch (Throwable $ex) {
$original_exception = $ex;
}
$response_exception = null;
try {
if ($original_exception) {
$response = $this->handleThrowable($original_exception);
}
$response = $this->produceResponse($request, $response);
$response = $controller->willSendResponse($response);
$response->setRequest($request);
self::writeResponse($sink, $response);
} catch (Exception $ex) {
$response_exception = $ex;
} catch (Throwable $ex) {
$response_exception = $ex;
}
if ($response_exception) {
// If we encountered an exception while building a normal response, then
// encountered another exception while building a response for the first
// exception, throw an aggregate exception that will be unpacked by the
// higher-level handler. This is above our pay grade.
if ($original_exception) {
throw new PhutilAggregateException(
pht(
'Encountered a processing exception, then another exception when '.
'trying to build a response for the first exception.'),
array(
$response_exception,
$original_exception,
));
}
// If we built a response successfully and then ran into an exception
// trying to render it, try to handle and present that exception to the
// user using the standard handler.
// The problem here might be in rendering (more common) or in the actual
// response mechanism (less common). If it's in rendering, we can likely
// still render a nice exception page: the majority of rendering issues
// are in main page content, not content shared with the exception page.
$handling_exception = null;
try {
$response = $this->handleThrowable($response_exception);
$response = $this->produceResponse($request, $response);
$response = $controller->willSendResponse($response);
$response->setRequest($request);
self::writeResponse($sink, $response);
} catch (Exception $ex) {
$handling_exception = $ex;
} catch (Throwable $ex) {
$handling_exception = $ex;
}
// If we didn't have any luck with that, raise the original response
// exception. As above, this is the root cause exception and more likely
// to be useful. This will go to the fallback error handler at top
// level.
if ($handling_exception) {
throw $response_exception;
}
}
return $response;
}
private static function writeResponse(
AphrontHTTPSink $sink,
AphrontResponse $response) {
$unexpected_output = PhabricatorStartup::endOutputCapture();
if ($unexpected_output) {
$unexpected_output = pht(
"Unexpected output:\n\n%s",
$unexpected_output);
phlog($unexpected_output);
if ($response instanceof AphrontWebpageResponse) {
$response->setUnexpectedOutput($unexpected_output);
}
}
$sink->writeResponse($response);
}
/* -( URI Routing )-------------------------------------------------------- */
/**
* Build a controller to respond to the request.
*
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
private function buildController() {
$request = $this->getRequest();
// If we're configured to operate in cluster mode, reject requests which
// were not received on a cluster interface.
//
// For example, a host may have an internal address like "170.0.0.1", and
// also have a public address like "51.23.95.16". Assuming the cluster
// is configured on a range like "170.0.0.0/16", we want to reject the
// requests received on the public interface.
//
// Ideally, nodes in a cluster should only be listening on internal
// interfaces, but they may be configured in such a way that they also
// listen on external interfaces, since this is easy to forget about or
// get wrong. As a broad security measure, reject requests received on any
// interfaces which aren't on the whitelist.
$cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses');
if ($cluster_addresses) {
$server_addr = idx($_SERVER, 'SERVER_ADDR');
if (!$server_addr) {
if (php_sapi_name() == 'cli') {
// This is a command line script (probably something like a unit
// test) so it's fine that we don't have SERVER_ADDR defined.
} else {
throw new AphrontMalformedRequestException(
pht('No %s', 'SERVER_ADDR'),
pht(
'This service is configured to operate in cluster mode, but '.
'%s is not defined in the request context. Your webserver '.
'configuration needs to forward %s to PHP so the software can '.
'reject requests received on external interfaces.',
'SERVER_ADDR',
'SERVER_ADDR'));
}
} else {
if (!PhabricatorEnv::isClusterAddress($server_addr)) {
throw new AphrontMalformedRequestException(
pht('External Interface'),
pht(
'This service is configured in cluster mode and the address '.
'this request was received on ("%s") is not whitelisted as '.
'a cluster address.',
$server_addr));
}
}
}
$site = $this->buildSiteForRequest($request);
if ($site->shouldRequireHTTPS()) {
if (!$request->isHTTPS()) {
// Don't redirect intracluster requests: doing so drops headers and
// parameters, imposes a performance penalty, and indicates a
// misconfiguration.
if ($request->isProxiedClusterRequest()) {
throw new AphrontMalformedRequestException(
pht('HTTPS Required'),
pht(
'This request reached a site which requires HTTPS, but the '.
'request is not marked as HTTPS.'));
}
$https_uri = $request->getRequestURI();
$https_uri->setDomain($request->getHost());
$https_uri->setProtocol('https');
// In this scenario, we'll be redirecting to HTTPS using an absolute
// URI, so we need to permit an external redirect.
return $this->buildRedirectController($https_uri, true);
}
}
$maps = $site->getRoutingMaps();
$path = $request->getPath();
$result = $this->routePath($maps, $path);
if ($result) {
return $result;
}
// If we failed to match anything but don't have a trailing slash, try
// to add a trailing slash and issue a redirect if that resolves.
// NOTE: We only do this for GET, since redirects switch to GET and drop
// data like POST parameters.
if (!preg_match('@/$@', $path) && $request->isHTTPGet()) {
$result = $this->routePath($maps, $path.'/');
if ($result) {
$target_uri = $request->getAbsoluteRequestURI();
// We need to restore URI encoding because the webserver has
// interpreted it. For example, this allows us to redirect a path
// like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be
// resolved meaningfully by an application.
$target_path = phutil_escape_uri($path.'/');
$target_uri->setPath($target_path);
$target_uri = (string)$target_uri;
return $this->buildRedirectController($target_uri, true);
}
}
$result = $site->new404Controller($request);
if ($result) {
return array($result, array());
}
throw new Exception(
pht(
'Aphront site ("%s") failed to build a 404 controller.',
get_class($site)));
}
/**
* Map a specific path to the corresponding controller. For a description
* of routing, see @{method:buildController}.
*
* @param list<AphrontRoutingMap> List of routing maps.
* @param string Path to route.
* @return pair<AphrontController,dict> Controller and dictionary of request
* parameters.
* @task routing
*/
private function routePath(array $maps, $path) {
foreach ($maps as $map) {
$result = $map->routePath($path);
if ($result) {
return array($result->getController(), $result->getURIData());
}
}
+ return null;
}
private function buildSiteForRequest(AphrontRequest $request) {
$sites = PhabricatorSite::getAllSites();
$site = null;
foreach ($sites as $candidate) {
$site = $candidate->newSiteForRequest($request);
if ($site) {
break;
}
}
if (!$site) {
$path = $request->getPath();
$host = $request->getHost();
throw new AphrontMalformedRequestException(
pht('Site Not Found'),
pht(
'This request asked for "%s" on host "%s", but no site is '.
'configured which can serve this request.',
$path,
$host),
true);
}
$request->setSite($site);
return $site;
}
/* -( Response Handling )-------------------------------------------------- */
/**
* Tests if a response is of a valid type.
*
* @param wild Supposedly valid response.
* @return bool True if the object is of a valid type.
* @task response
*/
private function isValidResponseObject($response) {
if ($response instanceof AphrontResponse) {
return true;
}
if ($response instanceof AphrontResponseProducerInterface) {
return true;
}
return false;
}
/**
* Verifies that the return value from an @{class:AphrontController} is
* of an allowed type.
*
* @param AphrontController Controller which returned the response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateControllerResponse(
AphrontController $controller,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Controller "%s" returned an invalid response from call to "%s". '.
'This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($controller),
'handleRequest()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Verifies that the return value from an
* @{class:AphrontResponseProducerInterface} is of an allowed type.
*
* @param AphrontResponseProducerInterface Object which produced
* this response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateProducerResponse(
AphrontResponseProducerInterface $producer,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Producer "%s" returned an invalid response from call to "%s". '.
'This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($producer),
'produceAphrontResponse()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Verifies that the return value from an
* @{class:AphrontRequestExceptionHandler} is of an allowed type.
*
* @param AphrontRequestExceptionHandler Object which produced this
* response.
* @param wild Supposedly valid response.
* @return void
* @task response
*/
private function validateErrorHandlerResponse(
AphrontRequestExceptionHandler $handler,
$response) {
if ($this->isValidResponseObject($response)) {
return;
}
throw new Exception(
pht(
'Exception handler "%s" returned an invalid response from call to '.
'"%s". This method must return an object of class "%s", or an object '.
'which implements the "%s" interface.',
get_class($handler),
'handleRequestException()',
'AphrontResponse',
'AphrontResponseProducerInterface'));
}
/**
* Resolves a response object into an @{class:AphrontResponse}.
*
* Controllers are permitted to return actual responses of class
* @{class:AphrontResponse}, or other objects which implement
* @{interface:AphrontResponseProducerInterface} and can produce a response.
*
* If a controller returns a response producer, invoke it now and produce
* the real response.
*
* @param AphrontRequest Request being handled.
* @param AphrontResponse|AphrontResponseProducerInterface Response, or
* response producer.
* @return AphrontResponse Response after any required production.
* @task response
*/
private function produceResponse(AphrontRequest $request, $response) {
$original = $response;
// Detect cycles on the exact same objects. It's still possible to produce
// infinite responses as long as they're all unique, but we can only
// reasonably detect cycles, not guarantee that response production halts.
$seen = array();
while (true) {
// NOTE: It is permissible for an object to be both a response and a
// response producer. If so, being a producer is "stronger". This is
// used by AphrontProxyResponse.
// If this response is a valid response, hand over the request first.
if ($response instanceof AphrontResponse) {
$response->setRequest($request);
}
// If this isn't a producer, we're all done.
if (!($response instanceof AphrontResponseProducerInterface)) {
break;
}
$hash = spl_object_hash($response);
if (isset($seen[$hash])) {
throw new Exception(
pht(
'Failure while producing response for object of class "%s": '.
'encountered production cycle (identical object, of class "%s", '.
'was produced twice).',
get_class($original),
get_class($response)));
}
$seen[$hash] = true;
$new_response = $response->produceAphrontResponse();
$this->validateProducerResponse($response, $new_response);
$response = $new_response;
}
return $response;
}
/* -( Error Handling )----------------------------------------------------- */
/**
* Convert an exception which has escaped the controller into a response.
*
* This method delegates exception handling to available subclasses of
* @{class:AphrontRequestExceptionHandler}.
*
* @param Throwable Exception which needs to be handled.
* @return wild Response or response producer, or null if no available
* handler can produce a response.
* @task exception
*/
private function handleThrowable($throwable) {
$handlers = AphrontRequestExceptionHandler::getAllHandlers();
$request = $this->getRequest();
foreach ($handlers as $handler) {
if ($handler->canHandleRequestThrowable($request, $throwable)) {
$response = $handler->handleRequestThrowable($request, $throwable);
$this->validateErrorHandlerResponse($handler, $response);
return $response;
}
}
throw $throwable;
}
private static function newSelfCheckResponse() {
$path = PhabricatorStartup::getRequestPath();
$query = idx($_SERVER, 'QUERY_STRING', '');
$pairs = id(new PhutilQueryStringParser())
->parseQueryStringToPairList($query);
$params = array();
foreach ($pairs as $v) {
$params[] = array(
'name' => $v[0],
'value' => $v[1],
);
}
$raw_input = @file_get_contents('php://input');
if ($raw_input !== false) {
$base64_input = base64_encode($raw_input);
} else {
$base64_input = null;
}
$result = array(
'path' => $path,
'params' => $params,
'user' => idx($_SERVER, 'PHP_AUTH_USER'),
'pass' => idx($_SERVER, 'PHP_AUTH_PW'),
'raw.base64' => $base64_input,
// This just makes sure that the response compresses well, so reasonable
// algorithms should want to gzip or deflate it.
'filler' => str_repeat('Q', 1024 * 16),
);
return id(new AphrontJSONResponse())
->setAddJSONShield(false)
->setContent($result);
}
private static function readHTTPPOSTData() {
$request_method = idx($_SERVER, 'REQUEST_METHOD');
if ($request_method === 'PUT') {
// For PUT requests, do nothing: in particular, do NOT read input. This
// allows us to stream input later and process very large PUT requests,
// like those coming from Git LFS.
return;
}
// For POST requests, we're going to read the raw input ourselves here
// if we can. Among other things, this corrects variable names with
// the "." character in them, which PHP normally converts into "_".
// If "enable_post_data_reading" is on, the documentation suggests we
// can not read the body. In practice, we seem to be able to. This may
// need to be resolved at some point, likely by instructing installs
// to disable this option.
// If the content type is "multipart/form-data", we need to build both
// $_POST and $_FILES, which is involved. The body itself is also more
// difficult to parse than other requests.
$raw_input = PhabricatorStartup::getRawInput();
$parser = new PhutilQueryStringParser();
if (phutil_nonempty_string($raw_input)) {
$content_type = idx($_SERVER, 'CONTENT_TYPE');
$is_multipart = preg_match('@^multipart/form-data@i', $content_type);
if ($is_multipart) {
$multipart_parser = id(new AphrontMultipartParser())
->setContentType($content_type);
$multipart_parser->beginParse();
$multipart_parser->continueParse($raw_input);
$parts = $multipart_parser->endParse();
// We're building and then parsing a query string so that requests
// with arrays (like "x[]=apple&x[]=banana") work correctly. This also
// means we can't use "phutil_build_http_querystring()", since it
// can't build a query string with duplicate names.
$query_string = array();
foreach ($parts as $part) {
if (!$part->isVariable()) {
continue;
}
$name = $part->getName();
$value = $part->getVariableValue();
$query_string[] = rawurlencode($name).'='.rawurlencode($value);
}
$query_string = implode('&', $query_string);
$post = $parser->parseQueryString($query_string);
$files = array();
foreach ($parts as $part) {
if ($part->isVariable()) {
continue;
}
$files[$part->getName()] = $part->getPHPFileDictionary();
}
$_FILES = $files;
} else {
$post = $parser->parseQueryString($raw_input);
}
$_POST = $post;
PhabricatorStartup::rebuildRequest();
} else if ($_POST) {
$post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW);
if (is_array($post)) {
$_POST = $post;
PhabricatorStartup::rebuildRequest();
}
}
}
}
diff --git a/src/applications/people/storage/PhabricatorUserEmail.php b/src/applications/people/storage/PhabricatorUserEmail.php
index d9866f2c43..47c2911601 100644
--- a/src/applications/people/storage/PhabricatorUserEmail.php
+++ b/src/applications/people/storage/PhabricatorUserEmail.php
@@ -1,338 +1,337 @@
<?php
/**
* @task restrictions Domain Restrictions
* @task email Email About Email
*/
final class PhabricatorUserEmail
extends PhabricatorUserDAO
implements
PhabricatorDestructibleInterface,
PhabricatorPolicyInterface {
protected $userPHID;
protected $address;
protected $isVerified;
protected $isPrimary;
protected $verificationCode;
private $user = self::ATTACHABLE;
const MAX_ADDRESS_LENGTH = 128;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'address' => 'sort128',
'isVerified' => 'bool',
'isPrimary' => 'bool',
'verificationCode' => 'text64?',
),
self::CONFIG_KEY_SCHEMA => array(
'address' => array(
'columns' => array('address'),
'unique' => true,
),
'userPHID' => array(
'columns' => array('userPHID', 'isPrimary'),
),
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorPeopleUserEmailPHIDType::TYPECONST;
}
public function getVerificationURI() {
return '/emailverify/'.$this->getVerificationCode().'/';
}
public function save() {
if (!$this->verificationCode) {
$this->setVerificationCode(Filesystem::readRandomCharacters(24));
}
return parent::save();
}
public function attachUser(PhabricatorUser $user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->assertAttached($this->user);
}
/* -( Domain Restrictions )------------------------------------------------ */
/**
* @task restrictions
*/
public static function isValidAddress($address) {
if (strlen($address) > self::MAX_ADDRESS_LENGTH) {
return false;
}
// Very roughly validate that this address isn't so mangled that a
// reasonable piece of code might completely misparse it. In particular,
// the major risks are:
//
// - `PhutilEmailAddress` needs to be able to extract the domain portion
// from it.
// - Reasonable mail adapters should be hard-pressed to interpret one
// address as several addresses.
//
// To this end, we're roughly verifying that there's some normal text, an
// "@" symbol, and then some more normal text.
$email_regex = '(^[a-z0-9_+.!-]+@[a-z0-9_+:.-]+\z)i';
if (!preg_match($email_regex, $address)) {
return false;
}
return true;
}
/**
* @task restrictions
*/
public static function describeValidAddresses() {
return pht(
'Email addresses should be in the form "user@domain.com". The maximum '.
'length of an email address is %s characters.',
new PhutilNumber(self::MAX_ADDRESS_LENGTH));
}
/**
* @task restrictions
*/
public static function isAllowedAddress($address) {
if (!self::isValidAddress($address)) {
return false;
}
$allowed_domains = PhabricatorEnv::getEnvConfig('auth.email-domains');
if (!$allowed_domains) {
return true;
}
$addr_obj = new PhutilEmailAddress($address);
$domain = $addr_obj->getDomainName();
if (!$domain) {
return false;
}
$lower_domain = phutil_utf8_strtolower($domain);
foreach ($allowed_domains as $allowed_domain) {
$lower_allowed = phutil_utf8_strtolower($allowed_domain);
if ($lower_allowed === $lower_domain) {
return true;
}
}
return false;
}
/**
* @task restrictions
*/
public static function describeAllowedAddresses() {
$domains = PhabricatorEnv::getEnvConfig('auth.email-domains');
if (!$domains) {
return null;
}
if (count($domains) == 1) {
return pht('Email address must be @%s', head($domains));
} else {
return pht(
'Email address must be at one of: %s',
implode(', ', $domains));
}
}
/**
* Check if this install requires email verification.
*
* @return bool True if email addresses must be verified.
*
* @task restrictions
*/
public static function isEmailVerificationRequired() {
// NOTE: Configuring required email domains implies required verification.
return PhabricatorEnv::getEnvConfig('auth.require-email-verification') ||
PhabricatorEnv::getEnvConfig('auth.email-domains');
}
/* -( Email About Email )-------------------------------------------------- */
/**
* Send a verification email from $user to this address.
*
* @param PhabricatorUser The user sending the verification.
* @return this
* @task email
*/
public function sendVerificationEmail(PhabricatorUser $user) {
$username = $user->getUsername();
$address = $this->getAddress();
$link = PhabricatorEnv::getProductionURI($this->getVerificationURI());
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$signature = null;
if (!$is_serious) {
$signature = pht(
"Get Well Soon,\n%s",
PlatformSymbols::getPlatformServerName());
}
$body = sprintf(
"%s\n\n%s\n\n %s\n\n%s",
pht('Hi %s', $username),
pht(
'Please verify that you own this email address (%s) by '.
'clicking this link:',
$address),
$link,
$signature);
id(new PhabricatorMetaMTAMail())
->addRawTos(array($address))
->setForceDelivery(true)
->setSubject(
pht(
'[%s] Email Verification',
PlatformSymbols::getPlatformServerName()))
->setBody($body)
->setRelatedPHID($user->getPHID())
->saveAndSend();
return $this;
}
/**
* Send a notification email from $user to this address, informing the
* recipient that this is no longer their account's primary address.
*
* @param PhabricatorUser The user sending the notification.
* @param PhabricatorUserEmail New primary email address.
- * @return this
* @task email
*/
public function sendOldPrimaryEmail(
PhabricatorUser $user,
PhabricatorUserEmail $new) {
$username = $user->getUsername();
$old_address = $this->getAddress();
$new_address = $new->getAddress();
$body = sprintf(
"%s\n\n%s\n",
pht('Hi %s', $username),
pht(
'This email address (%s) is no longer your primary email address. '.
'Going forward, all email will be sent to your new primary email '.
'address (%s).',
$old_address,
$new_address));
id(new PhabricatorMetaMTAMail())
->addRawTos(array($old_address))
->setForceDelivery(true)
->setSubject(
pht(
'[%s] Primary Address Changed',
PlatformSymbols::getPlatformServerName()))
->setBody($body)
->setFrom($user->getPHID())
->setRelatedPHID($user->getPHID())
->saveAndSend();
}
/**
* Send a notification email from $user to this address, informing the
* recipient that this is now their account's new primary email address.
*
* @param PhabricatorUser The user sending the verification.
* @return this
* @task email
*/
public function sendNewPrimaryEmail(PhabricatorUser $user) {
$username = $user->getUsername();
$new_address = $this->getAddress();
$body = sprintf(
"%s\n\n%s\n",
pht('Hi %s', $username),
pht(
'This is now your primary email address (%s). Going forward, '.
'all email will be sent here.',
$new_address));
id(new PhabricatorMetaMTAMail())
->addRawTos(array($new_address))
->setForceDelivery(true)
->setSubject(
pht(
'[%s] Primary Address Changed',
PlatformSymbols::getPlatformServerName()))
->setBody($body)
->setFrom($user->getPHID())
->setRelatedPHID($user->getPHID())
->saveAndSend();
return $this;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
$user = $this->getUser();
if ($this->getIsSystemAgent() || $this->getIsMailingList()) {
return PhabricatorPolicies::POLICY_ADMIN;
}
return $user->getPHID();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
}
diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php
index 5291bcd465..9b4a6ba01a 100644
--- a/src/applications/phame/storage/PhamePost.php
+++ b/src/applications/phame/storage/PhamePost.php
@@ -1,405 +1,406 @@
<?php
final class PhamePost extends PhameDAO
implements
PhabricatorPolicyInterface,
PhabricatorMarkupInterface,
PhabricatorFlaggableInterface,
PhabricatorProjectInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorDestructibleInterface,
PhabricatorTokenReceiverInterface,
PhabricatorConduitResultInterface,
PhabricatorEditEngineLockableInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface {
const MARKUP_FIELD_BODY = 'markup:body';
protected $bloggerPHID;
protected $title;
protected $subtitle;
protected $phameTitle;
protected $body;
protected $visibility;
protected $configData;
protected $datePublished;
protected $blogPHID;
protected $mailKey;
protected $headerImagePHID;
protected $interactPolicy;
private $blog = self::ATTACHABLE;
private $headerImageFile = self::ATTACHABLE;
public static function initializePost(
PhabricatorUser $blogger,
PhameBlog $blog) {
$post = id(new PhamePost())
->setBloggerPHID($blogger->getPHID())
->setBlogPHID($blog->getPHID())
->attachBlog($blog)
->setDatePublished(PhabricatorTime::getNow())
->setVisibility(PhameConstants::VISIBILITY_PUBLISHED)
->setInteractPolicy(
id(new PhameInheritBlogPolicyRule())
->getObjectPolicyFullKey());
return $post;
}
public function attachBlog(PhameBlog $blog) {
$this->blog = $blog;
return $this;
}
public function getBlog() {
return $this->assertAttached($this->blog);
}
public function getMonogram() {
return 'J'.$this->getID();
}
public function getLiveURI() {
$blog = $this->getBlog();
$is_draft = $this->isDraft();
$is_archived = $this->isArchived();
if (phutil_nonempty_string($blog->getDomain()) &&
!$is_draft && !$is_archived) {
return $this->getExternalLiveURI();
} else {
return $this->getInternalLiveURI();
}
}
public function getExternalLiveURI() {
$id = $this->getID();
$slug = $this->getSlug();
$path = "/post/{$id}/{$slug}/";
$domain = $this->getBlog()->getDomain();
return (string)id(new PhutilURI('http://'.$domain))
->setPath($path);
}
public function getInternalLiveURI() {
$id = $this->getID();
$slug = $this->getSlug();
$blog_id = $this->getBlog()->getID();
return "/phame/live/{$blog_id}/post/{$id}/{$slug}/";
}
public function getViewURI() {
$id = $this->getID();
$slug = $this->getSlug();
return "/phame/post/view/{$id}/{$slug}/";
}
public function getBestURI($is_live, $is_external) {
if ($is_live) {
if ($is_external) {
return $this->getExternalLiveURI();
} else {
return $this->getInternalLiveURI();
}
} else {
return $this->getViewURI();
}
}
public function getEditURI() {
return '/phame/post/edit/'.$this->getID().'/';
}
public function isDraft() {
return ($this->getVisibility() == PhameConstants::VISIBILITY_DRAFT);
}
public function isArchived() {
return ($this->getVisibility() == PhameConstants::VISIBILITY_ARCHIVED);
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'configData' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'title' => 'text255',
'subtitle' => 'text64',
'phameTitle' => 'sort64?',
'visibility' => 'uint32',
'mailKey' => 'bytes20',
'headerImagePHID' => 'phid?',
// T6203/NULLABILITY
// These seem like they should always be non-null?
'blogPHID' => 'phid?',
'body' => 'text?',
'configData' => 'text?',
// T6203/NULLABILITY
// This one probably should be nullable?
'datePublished' => 'epoch',
'interactPolicy' => 'policy',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'bloggerPosts' => array(
'columns' => array(
'bloggerPHID',
'visibility',
'datePublished',
'id',
),
),
),
) + parent::getConfiguration();
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPhamePostPHIDType::TYPECONST);
}
public function getSlug() {
return PhabricatorSlug::normalizeProjectSlug($this->getTitle());
}
public function getHeaderImageURI() {
return $this->getHeaderImageFile()->getBestURI();
}
public function attachHeaderImageFile(PhabricatorFile $file) {
$this->headerImageFile = $file;
return $this;
}
public function getHeaderImageFile() {
return $this->assertAttached($this->headerImageFile);
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
PhabricatorPolicyCapability::CAN_INTERACT,
);
}
public function getPolicy($capability) {
// Draft and archived posts are visible only to the author and other
// users who can edit the blog. Published posts are visible to whoever
// the blog is visible to.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if (!$this->isDraft() && !$this->isArchived() && $this->getBlog()) {
return $this->getBlog()->getViewPolicy();
} else if ($this->getBlog()) {
return $this->getBlog()->getEditPolicy();
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
break;
case PhabricatorPolicyCapability::CAN_EDIT:
if ($this->getBlog()) {
return $this->getBlog()->getEditPolicy();
} else {
return PhabricatorPolicies::POLICY_NOONE;
}
case PhabricatorPolicyCapability::CAN_INTERACT:
return $this->getInteractPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $user) {
// A blog post's author can always view it.
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
case PhabricatorPolicyCapability::CAN_EDIT:
return ($user->getPHID() == $this->getBloggerPHID());
case PhabricatorPolicyCapability::CAN_INTERACT:
return false;
}
}
public function describeAutomaticCapability($capability) {
return pht('The author of a blog post can always view and edit it.');
}
/* -( PhabricatorMarkupInterface Implementation )-------------------------- */
public function getMarkupFieldKey($field) {
$content = $this->getMarkupText($field);
return PhabricatorMarkupEngine::digestRemarkupContent($this, $content);
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newPhameMarkupEngine();
}
public function getMarkupText($field) {
switch ($field) {
case self::MARKUP_FIELD_BODY:
return $this->getBody();
}
+ return null;
}
public function didMarkupText(
$field,
$output,
PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getPHID();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhamePostEditor();
}
public function getApplicationTransactionTemplate() {
return new PhamePostTransaction();
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getBloggerPHID(),
);
}
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->bloggerPHID == $phid);
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public function getFieldSpecificationsForConduit() {
return array(
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('title')
->setType('string')
->setDescription(pht('Title of the post.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('slug')
->setType('string')
->setDescription(pht('Slug for the post.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('blogPHID')
->setType('phid')
->setDescription(pht('PHID of the blog that the post belongs to.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('authorPHID')
->setType('phid')
->setDescription(pht('PHID of the author of the post.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('body')
->setType('string')
->setDescription(pht('Body of the post.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('datePublished')
->setType('epoch?')
->setDescription(pht('Publish date, if the post has been published.')),
);
}
public function getFieldValuesForConduit() {
if ($this->isDraft()) {
$date_published = null;
} else if ($this->isArchived()) {
$date_published = null;
} else {
$date_published = (int)$this->getDatePublished();
}
return array(
'title' => $this->getTitle(),
'slug' => $this->getSlug(),
'blogPHID' => $this->getBlogPHID(),
'authorPHID' => $this->getBloggerPHID(),
'body' => $this->getBody(),
'datePublished' => $date_published,
);
}
public function getConduitSearchAttachments() {
return array();
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhamePostFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhamePostFerretEngine();
}
/* -( PhabricatorEditEngineLockableInterface )----------------------------- */
public function newEditEngineLock() {
return new PhamePostEditEngineLock();
}
}
diff --git a/src/infrastructure/daemon/workers/PhabricatorWorker.php b/src/infrastructure/daemon/workers/PhabricatorWorker.php
index c62fde447f..2b1e650485 100644
--- a/src/infrastructure/daemon/workers/PhabricatorWorker.php
+++ b/src/infrastructure/daemon/workers/PhabricatorWorker.php
@@ -1,329 +1,328 @@
<?php
/**
* @task config Configuring Retries and Failures
*/
abstract class PhabricatorWorker extends Phobject {
private $data;
private static $runAllTasksInProcess = false;
private $queuedTasks = array();
private $currentWorkerTask;
// NOTE: Lower priority numbers execute first. The priority numbers have to
// have the same ordering that IDs do (lowest first) so MySQL can use a
// multipart key across both of them efficiently.
const PRIORITY_ALERTS = 1000;
const PRIORITY_DEFAULT = 2000;
const PRIORITY_COMMIT = 2500;
const PRIORITY_BULK = 3000;
const PRIORITY_INDEX = 3500;
const PRIORITY_IMPORT = 4000;
/**
* Special owner indicating that the task has yielded.
*/
const YIELD_OWNER = '(yield)';
/* -( Configuring Retries and Failures )----------------------------------- */
/**
* Return the number of seconds this worker needs hold a lease on the task for
* while it performs work. For most tasks you can leave this at `null`, which
* will give you a default lease (currently 2 hours).
*
* For tasks which may take a very long time to complete, you should return
* an upper bound on the amount of time the task may require.
*
* @return int|null Number of seconds this task needs to remain leased for,
* or null for a default lease.
*
* @task config
*/
public function getRequiredLeaseTime() {
return null;
}
/**
* Return the maximum number of times this task may be retried before it is
* considered permanently failed. By default, tasks retry indefinitely. You
* can throw a @{class:PhabricatorWorkerPermanentFailureException} to cause an
* immediate permanent failure.
*
* @return int|null Number of times the task will retry before permanent
* failure. Return `null` to retry indefinitely.
*
* @task config
*/
public function getMaximumRetryCount() {
return null;
}
/**
* Return the number of seconds a task should wait after a failure before
* retrying. For most tasks you can leave this at `null`, which will give you
* a short default retry period (currently 60 seconds).
*
* @param PhabricatorWorkerTask The task itself. This object is probably
* useful mostly to examine the failure count
* if you want to implement staggered retries,
* or to examine the execution exception if
* you want to react to different failures in
* different ways.
* @return int|null Number of seconds to wait between retries,
* or null for a default retry period
* (currently 60 seconds).
*
* @task config
*/
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
return null;
}
public function setCurrentWorkerTask(PhabricatorWorkerTask $task) {
$this->currentWorkerTask = $task;
return $this;
}
public function getCurrentWorkerTask() {
return $this->currentWorkerTask;
}
public function getCurrentWorkerTaskID() {
$task = $this->getCurrentWorkerTask();
if (!$task) {
return null;
}
return $task->getID();
}
abstract protected function doWork();
final public function __construct($data) {
$this->data = $data;
}
final protected function getTaskData() {
return $this->data;
}
final protected function getTaskDataValue($key, $default = null) {
$data = $this->getTaskData();
if (!is_array($data)) {
throw new PhabricatorWorkerPermanentFailureException(
pht('Expected task data to be a dictionary.'));
}
return idx($data, $key, $default);
}
final public function executeTask() {
$this->doWork();
}
final public static function scheduleTask(
$task_class,
$data,
$options = array()) {
PhutilTypeSpec::checkMap(
$options,
array(
'priority' => 'optional int|null',
'objectPHID' => 'optional string|null',
'containerPHID' => 'optional string|null',
'delayUntil' => 'optional int|null',
));
$priority = idx($options, 'priority');
if ($priority === null) {
$priority = self::PRIORITY_DEFAULT;
}
$object_phid = idx($options, 'objectPHID');
$container_phid = idx($options, 'containerPHID');
$task = id(new PhabricatorWorkerActiveTask())
->setTaskClass($task_class)
->setData($data)
->setPriority($priority)
->setObjectPHID($object_phid)
->setContainerPHID($container_phid);
$delay = idx($options, 'delayUntil');
if ($delay) {
$task->setLeaseExpires($delay);
}
if (self::$runAllTasksInProcess) {
// Do the work in-process.
$worker = newv($task_class, array($data));
while (true) {
try {
$worker->executeTask();
$worker->flushTaskQueue();
$task_result = PhabricatorWorkerArchiveTask::RESULT_SUCCESS;
break;
} catch (PhabricatorWorkerPermanentFailureException $ex) {
$proxy = new PhutilProxyException(
pht(
'In-process task ("%s") failed permanently.',
$task_class),
$ex);
phlog($proxy);
$task_result = PhabricatorWorkerArchiveTask::RESULT_FAILURE;
break;
} catch (PhabricatorWorkerYieldException $ex) {
phlog(
pht(
'In-process task "%s" yielded for %s seconds, sleeping...',
$task_class,
$ex->getDuration()));
sleep($ex->getDuration());
}
}
// Now, save a task row and immediately archive it so we can return an
// object with a valid ID.
$task->openTransaction();
$task->save();
$archived = $task->archiveTask($task_result, 0);
$task->saveTransaction();
return $archived;
} else {
$task->save();
return $task;
}
}
public function renderForDisplay(PhabricatorUser $viewer) {
return null;
}
/**
* Set this flag to execute scheduled tasks synchronously, in the same
* process. This is useful for debugging, and otherwise dramatically worse
* in every way imaginable.
*/
public static function setRunAllTasksInProcess($all) {
self::$runAllTasksInProcess = $all;
}
final protected function log($pattern /* , ... */) {
$console = PhutilConsole::getConsole();
$argv = func_get_args();
call_user_func_array(array($console, 'writeLog'), $argv);
return $this;
}
/**
* Queue a task to be executed after this one succeeds.
*
* The followup task will be queued only if this task completes cleanly.
*
* @param string Task class to queue.
* @param array Data for the followup task.
* @param array Options for the followup task.
* @return this
*/
final protected function queueTask(
$class,
array $data,
array $options = array()) {
$this->queuedTasks[] = array($class, $data, $options);
return $this;
}
/**
* Get tasks queued as followups by @{method:queueTask}.
*
* @return list<tuple<string, wild, int|null>> Queued task specifications.
*/
final protected function getQueuedTasks() {
return $this->queuedTasks;
}
/**
* Schedule any queued tasks, then empty the task queue.
*
* By default, the queue is flushed only if a task succeeds. You can call
* this method to force the queue to flush before failing (for example, if
* you are using queues to improve locking behavior).
*
* @param map<string, wild> Optional default options.
- * @return this
*/
final public function flushTaskQueue($defaults = array()) {
foreach ($this->getQueuedTasks() as $task) {
list($class, $data, $options) = $task;
$options = $options + $defaults;
self::scheduleTask($class, $data, $options);
}
$this->queuedTasks = array();
}
/**
* Awaken tasks that have yielded.
*
* Reschedules the specified tasks if they are currently queued in a yielded,
* unleased, unretried state so they'll execute sooner. This can let the
* queue avoid unnecessary waits.
*
* This method does not provide any assurances about when these tasks will
* execute, or even guarantee that it will have any effect at all.
*
* @param list<id> List of task IDs to try to awaken.
* @return void
*/
final public static function awakenTaskIDs(array $ids) {
if (!$ids) {
return;
}
$table = new PhabricatorWorkerActiveTask();
$conn_w = $table->establishConnection('w');
// NOTE: At least for now, we're keeping these tasks yielded, just
// pretending that they threw a shorter yield than they really did.
// Overlap the windows here to handle minor client/server time differences
// and because it's likely correct to push these tasks to the head of their
// respective priorities. There is a good chance they are ready to execute.
$window = phutil_units('1 hour in seconds');
$epoch_ago = (PhabricatorTime::getNow() - $window);
queryfx(
$conn_w,
'UPDATE %T SET leaseExpires = %d
WHERE id IN (%Ld)
AND leaseOwner = %s
AND leaseExpires > %d
AND failureCount = 0',
$table->getTableName(),
$epoch_ago,
$ids,
self::YIELD_OWNER,
$epoch_ago);
}
protected function newContentSource() {
return PhabricatorContentSource::newForSource(
PhabricatorDaemonContentSource::SOURCECONST);
}
}
diff --git a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php
index 588b5267b4..d019c7d0fe 100644
--- a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php
+++ b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php
@@ -1,406 +1,405 @@
<?php
/**
* Add and remove edges between objects. You can use
* @{class:PhabricatorEdgeQuery} to load object edges. For more information
* on edges, see @{article:Using Edges}.
*
* Edges are not directly policy aware, and this editor makes low-level changes
* below the policy layer.
*
* name=Adding Edges
* $src = $earth_phid;
* $type = PhabricatorEdgeConfig::TYPE_BODY_HAS_SATELLITE;
* $dst = $moon_phid;
*
* id(new PhabricatorEdgeEditor())
* ->addEdge($src, $type, $dst)
* ->save();
*
* @task edit Editing Edges
* @task cycles Cycle Prevention
* @task internal Internals
*/
final class PhabricatorEdgeEditor extends Phobject {
private $addEdges = array();
private $remEdges = array();
private $openTransactions = array();
/* -( Editing Edges )------------------------------------------------------ */
/**
* Add a new edge (possibly also adding its inverse). Changes take effect when
* you call @{method:save}. If the edge already exists, it will not be
* overwritten, but if data is attached to the edge it will be updated.
* Removals queued with @{method:removeEdge} are executed before
* adds, so the effect of removing and adding the same edge is to overwrite
* any existing edge.
*
* The `$options` parameter accepts these values:
*
* - `data` Optional, data to write onto the edge.
* - `inverse_data` Optional, data to write on the inverse edge. If not
* provided, `data` will be written.
*
* @param phid Source object PHID.
* @param const Edge type constant.
* @param phid Destination object PHID.
* @param map Options map (see documentation).
* @return this
*
* @task edit
*/
public function addEdge($src, $type, $dst, array $options = array()) {
foreach ($this->buildEdgeSpecs($src, $type, $dst, $options) as $spec) {
$this->addEdges[] = $spec;
}
return $this;
}
/**
* Remove an edge (possibly also removing its inverse). Changes take effect
* when you call @{method:save}. If an edge does not exist, the removal
* will be ignored. Edges are added after edges are removed, so the effect of
* a remove plus an add is to overwrite.
*
* @param phid Source object PHID.
* @param const Edge type constant.
* @param phid Destination object PHID.
* @return this
*
* @task edit
*/
public function removeEdge($src, $type, $dst) {
foreach ($this->buildEdgeSpecs($src, $type, $dst) as $spec) {
$this->remEdges[] = $spec;
}
return $this;
}
/**
* Apply edge additions and removals queued by @{method:addEdge} and
* @{method:removeEdge}. Note that transactions are opened, all additions and
* removals are executed, and then transactions are saved. Thus, in some cases
* it may be slightly more efficient to perform multiple edit operations
* (e.g., adds followed by removals) if their outcomes are not dependent,
* since transactions will not be held open as long.
*
- * @return this
* @task edit
*/
public function save() {
$cycle_types = $this->getPreventCyclesEdgeTypes();
$locks = array();
$caught = null;
try {
// NOTE: We write edge data first, before doing any transactions, since
// it's OK if we just leave it hanging out in space unattached to
// anything.
$this->writeEdgeData();
// If we're going to perform cycle detection, lock the edge type before
// doing edits.
if ($cycle_types) {
$src_phids = ipull($this->addEdges, 'src');
foreach ($cycle_types as $cycle_type) {
$key = 'edge.cycle:'.$cycle_type;
$locks[] = PhabricatorGlobalLock::newLock($key)->lock(15);
}
}
static $id = 0;
$id++;
// NOTE: Removes first, then adds, so that "remove + add" is a useful
// operation meaning "overwrite".
$this->executeRemoves();
$this->executeAdds();
foreach ($cycle_types as $cycle_type) {
$this->detectCycles($src_phids, $cycle_type);
}
$this->saveTransactions();
} catch (Exception $ex) {
$caught = $ex;
}
if ($caught) {
$this->killTransactions();
}
foreach ($locks as $lock) {
$lock->unlock();
}
if ($caught) {
throw $caught;
}
}
/* -( Internals )---------------------------------------------------------- */
/**
* Build the specification for an edge operation, and possibly build its
* inverse as well.
*
* @task internal
*/
private function buildEdgeSpecs($src, $type, $dst, array $options = array()) {
$data = array();
if (!empty($options['data'])) {
$data['data'] = $options['data'];
}
$src_type = phid_get_type($src);
$dst_type = phid_get_type($dst);
$specs = array();
$specs[] = array(
'src' => $src,
'src_type' => $src_type,
'dst' => $dst,
'dst_type' => $dst_type,
'type' => $type,
'data' => $data,
);
$type_obj = PhabricatorEdgeType::getByConstant($type);
$inverse = $type_obj->getInverseEdgeConstant();
if ($inverse !== null) {
// If `inverse_data` is set, overwrite the edge data. Normally, just
// write the same data to the inverse edge.
if (array_key_exists('inverse_data', $options)) {
$data['data'] = $options['inverse_data'];
}
$specs[] = array(
'src' => $dst,
'src_type' => $dst_type,
'dst' => $src,
'dst_type' => $src_type,
'type' => $inverse,
'data' => $data,
);
}
return $specs;
}
/**
* Write edge data.
*
* @task internal
*/
private function writeEdgeData() {
$adds = $this->addEdges;
$writes = array();
foreach ($adds as $key => $edge) {
if ($edge['data']) {
$writes[] = array($key, $edge['src_type'], json_encode($edge['data']));
}
}
foreach ($writes as $write) {
list($key, $src_type, $data) = $write;
$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
queryfx(
$conn_w,
'INSERT INTO %T (data) VALUES (%s)',
PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA,
$data);
$this->addEdges[$key]['data_id'] = $conn_w->getInsertID();
}
}
/**
* Add queued edges.
*
* @task internal
*/
private function executeAdds() {
$adds = $this->addEdges;
$adds = igroup($adds, 'src_type');
// Assign stable sequence numbers to each edge, so we have a consistent
// ordering across edges by source and type.
foreach ($adds as $src_type => $edges) {
$edges_by_src = igroup($edges, 'src');
foreach ($edges_by_src as $src => $src_edges) {
$seq = 0;
foreach ($src_edges as $key => $edge) {
$src_edges[$key]['seq'] = $seq++;
$src_edges[$key]['dateCreated'] = time();
}
$edges_by_src[$src] = $src_edges;
}
$adds[$src_type] = array_mergev($edges_by_src);
}
$inserts = array();
foreach ($adds as $src_type => $edges) {
$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
$sql = array();
foreach ($edges as $edge) {
$sql[] = qsprintf(
$conn_w,
'(%s, %d, %s, %d, %d, %nd)',
$edge['src'],
$edge['type'],
$edge['dst'],
$edge['dateCreated'],
$edge['seq'],
idx($edge, 'data_id'));
}
$inserts[] = array($conn_w, $sql);
}
foreach ($inserts as $insert) {
list($conn_w, $sql) = $insert;
$conn_w->openTransaction();
$this->openTransactions[] = $conn_w;
foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) {
queryfx(
$conn_w,
'INSERT INTO %T (src, type, dst, dateCreated, seq, dataID)
VALUES %LQ ON DUPLICATE KEY UPDATE dataID = VALUES(dataID)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
$chunk);
}
}
}
/**
* Remove queued edges.
*
* @task internal
*/
private function executeRemoves() {
$rems = $this->remEdges;
$rems = igroup($rems, 'src_type');
$deletes = array();
foreach ($rems as $src_type => $edges) {
$conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w');
$sql = array();
foreach ($edges as $edge) {
$sql[] = qsprintf(
$conn_w,
'(src = %s AND type = %d AND dst = %s)',
$edge['src'],
$edge['type'],
$edge['dst']);
}
$deletes[] = array($conn_w, $sql);
}
foreach ($deletes as $delete) {
list($conn_w, $sql) = $delete;
$conn_w->openTransaction();
$this->openTransactions[] = $conn_w;
foreach (array_chunk($sql, 256) as $chunk) {
queryfx(
$conn_w,
'DELETE FROM %T WHERE %LO',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
$chunk);
}
}
}
/**
* Save open transactions.
*
* @task internal
*/
private function saveTransactions() {
foreach ($this->openTransactions as $key => $conn_w) {
$conn_w->saveTransaction();
unset($this->openTransactions[$key]);
}
}
private function killTransactions() {
foreach ($this->openTransactions as $key => $conn_w) {
$conn_w->killTransaction();
unset($this->openTransactions[$key]);
}
}
/* -( Cycle Prevention )--------------------------------------------------- */
/**
* Get a list of all edge types which are being added, and which we should
* prevent cycles on.
*
* @return list<const> List of edge types which should have cycles prevented.
* @task cycle
*/
private function getPreventCyclesEdgeTypes() {
$edge_types = array();
foreach ($this->addEdges as $edge) {
$edge_types[$edge['type']] = true;
}
foreach ($edge_types as $type => $ignored) {
$type_obj = PhabricatorEdgeType::getByConstant($type);
if (!$type_obj->shouldPreventCycles()) {
unset($edge_types[$type]);
}
}
return array_keys($edge_types);
}
/**
* Detect graph cycles of a given edge type. If the edit introduces a cycle,
* a @{class:PhabricatorEdgeCycleException} is thrown with details.
*
* @return void
* @task cycle
*/
private function detectCycles(array $phids, $edge_type) {
// For simplicity, we just seed the graph with the affected nodes rather
// than seeding it with their edges. To do this, we just add synthetic
// edges from an imaginary '<seed>' node to the known edges.
$graph = id(new PhabricatorEdgeGraph())
->setEdgeType($edge_type)
->addNodes(
array(
'<seed>' => $phids,
))
->loadGraph();
foreach ($phids as $phid) {
$cycle = $graph->detectCycles($phid);
if ($cycle) {
throw new PhabricatorEdgeCycleException($edge_type, $cycle);
}
}
}
}
diff --git a/support/startup/PhabricatorStartup.php b/support/startup/PhabricatorStartup.php
index 10c6295587..2ae5993dfb 100644
--- a/support/startup/PhabricatorStartup.php
+++ b/support/startup/PhabricatorStartup.php
@@ -1,792 +1,791 @@
<?php
/**
* Handle request startup, before loading the environment or libraries. This
* class bootstraps the request state up to the point where we can enter
* Phabricator code.
*
* NOTE: This class MUST NOT have any dependencies. It runs before libraries
* load.
*
* Rate Limiting
* =============
*
* Phabricator limits the rate at which clients can request pages, and issues
* HTTP 429 "Too Many Requests" responses if clients request too many pages too
* quickly. Although this is not a complete defense against high-volume attacks,
* it can protect an install against aggressive crawlers, security scanners,
* and some types of malicious activity.
*
* To perform rate limiting, each page increments a score counter for the
* requesting user's IP. The page can give the IP more points for an expensive
* request, or fewer for an authetnicated request.
*
* Score counters are kept in buckets, and writes move to a new bucket every
* minute. After a few minutes (defined by @{method:getRateLimitBucketCount}),
* the oldest bucket is discarded. This provides a simple mechanism for keeping
* track of scores without needing to store, access, or read very much data.
*
* Users are allowed to accumulate up to 1000 points per minute, averaged across
* all of the tracked buckets.
*
* @task info Accessing Request Information
* @task hook Startup Hooks
* @task apocalypse In Case Of Apocalypse
* @task validation Validation
* @task ratelimit Rate Limiting
* @task phases Startup Phase Timers
* @task request-path Request Path
*/
final class PhabricatorStartup {
private static $startTime;
private static $debugTimeLimit;
private static $accessLog;
private static $capturingOutput;
private static $rawInput;
private static $oldMemoryLimit;
private static $phases;
private static $limits = array();
private static $requestPath;
/* -( Accessing Request Information )-------------------------------------- */
/**
* @task info
*/
public static function getStartTime() {
return self::$startTime;
}
/**
* @task info
*/
public static function getMicrosecondsSinceStart() {
// This is the same as "phutil_microseconds_since()", but we may not have
// loaded libraries yet.
return (int)(1000000 * (microtime(true) - self::getStartTime()));
}
/**
* @task info
*/
public static function setAccessLog($access_log) {
self::$accessLog = $access_log;
}
/**
* @task info
*/
public static function getRawInput() {
if (self::$rawInput === null) {
$stream = new AphrontRequestStream();
if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
$encoding = trim($_SERVER['HTTP_CONTENT_ENCODING']);
$stream->setEncoding($encoding);
}
$input = '';
do {
$bytes = $stream->readData();
if ($bytes === null) {
break;
}
$input .= $bytes;
} while (true);
self::$rawInput = $input;
}
return self::$rawInput;
}
/* -( Startup Hooks )------------------------------------------------------ */
/**
* @param float Request start time, from `microtime(true)`.
* @task hook
*/
public static function didStartup($start_time) {
self::$startTime = $start_time;
self::$phases = array();
self::$accessLog = null;
self::$requestPath = null;
static $registered;
if (!$registered) {
// NOTE: This protects us against multiple calls to didStartup() in the
// same request, but also against repeated requests to the same
// interpreter state, which we may implement in the future.
register_shutdown_function(array(__CLASS__, 'didShutdown'));
$registered = true;
}
self::setupPHP();
self::verifyPHP();
// If we've made it this far, the environment isn't completely broken so
// we can switch over to relying on our own exception recovery mechanisms.
ini_set('display_errors', 0);
self::connectRateLimits();
self::normalizeInput();
self::readRequestPath();
self::beginOutputCapture();
}
/**
* @task hook
*/
public static function didShutdown() {
// Disconnect any active rate limits before we shut down. If we don't do
// this, requests which exit early will lock a slot in any active
// connection limits, and won't count for rate limits.
self::disconnectRateLimits(array());
$event = error_get_last();
if (!$event) {
return;
}
switch ($event['type']) {
case E_ERROR:
case E_PARSE:
case E_COMPILE_ERROR:
break;
default:
return;
}
$msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n";
if ($event) {
// Even though we should be emitting this as text-plain, escape things
// just to be sure since we can't really be sure what the program state
// is when we get here.
$msg .= htmlspecialchars(
$event['message']."\n\n".$event['file'].':'.$event['line'],
ENT_QUOTES,
'UTF-8');
}
// flip dem tables
$msg .= "\n\n\n";
$msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf".
"\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20".
"\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb";
self::didFatal($msg);
}
public static function loadCoreLibraries() {
$phabricator_root = dirname(dirname(dirname(__FILE__)));
$libraries_root = dirname($phabricator_root);
$root = null;
if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
$root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
}
ini_set(
'include_path',
$libraries_root.PATH_SEPARATOR.ini_get('include_path'));
$ok = @include_once $root.'arcanist/src/init/init-library.php';
if (!$ok) {
self::didFatal(
'Unable to load the "Arcanist" library. Put "arcanist/" next to '.
'"phorge/" on disk.');
}
// Load Phabricator itself using the absolute path, so we never end up doing
// anything surprising (loading index.php and libraries from different
// directories).
phutil_load_library($phabricator_root.'/src');
}
/* -( Output Capture )----------------------------------------------------- */
public static function beginOutputCapture() {
if (self::$capturingOutput) {
self::didFatal('Already capturing output!');
}
self::$capturingOutput = true;
ob_start();
}
public static function endOutputCapture() {
if (!self::$capturingOutput) {
return null;
}
self::$capturingOutput = false;
return ob_get_clean();
}
/* -( Debug Time Limit )--------------------------------------------------- */
/**
* Set a time limit (in seconds) for the current script. After time expires,
* the script fatals.
*
* This works like `max_execution_time`, but prints out a useful stack trace
* when the time limit expires. This is primarily intended to make it easier
* to debug pages which hang by allowing extraction of a stack trace: set a
* short debug limit, then use the trace to figure out what's happening.
*
* The limit is implemented with a tick function, so enabling it implies
* some accounting overhead.
*
* @param int Time limit in seconds.
* @return void
*/
public static function setDebugTimeLimit($limit) {
self::$debugTimeLimit = $limit;
static $initialized = false;
if (!$initialized) {
declare(ticks=1);
register_tick_function(array(__CLASS__, 'onDebugTick'));
$initialized = true;
}
}
/**
* Callback tick function used by @{method:setDebugTimeLimit}.
*
* Fatals with a useful stack trace after the time limit expires.
*
* @return void
*/
public static function onDebugTick() {
$limit = self::$debugTimeLimit;
if (!$limit) {
return;
}
$elapsed = (microtime(true) - self::getStartTime());
if ($elapsed > $limit) {
$frames = array();
foreach (debug_backtrace() as $frame) {
$file = isset($frame['file']) ? $frame['file'] : '-';
$file = basename($file);
$line = isset($frame['line']) ? $frame['line'] : '-';
$class = isset($frame['class']) ? $frame['class'].'->' : null;
$func = isset($frame['function']) ? $frame['function'].'()' : '?';
$frames[] = "{$file}:{$line} {$class}{$func}";
}
self::didFatal(
"Request aborted by debug time limit after {$limit} seconds.\n\n".
"STACK TRACE\n".
implode("\n", $frames));
}
}
/* -( In Case of Apocalypse )---------------------------------------------- */
/**
* Fatal the request completely in response to an exception, sending a plain
* text message to the client. Calls @{method:didFatal} internally.
*
* @param string Brief description of the exception context, like
* `"Rendering Exception"`.
* @param Throwable The exception itself.
* @param bool True if it's okay to show the exception's stack trace
* to the user. The trace will always be logged.
- * @return exit This method **does not return**.
*
* @task apocalypse
*/
public static function didEncounterFatalException(
$note,
$ex,
$show_trace) {
$message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage();
$full_message = $message;
$full_message .= "\n\n";
$full_message .= $ex->getTraceAsString();
if ($show_trace) {
$message = $full_message;
}
self::didFatal($message, $full_message);
}
/**
* Fatal the request completely, sending a plain text message to the client.
*
* @param string Plain text message to send to the client.
* @param string Plain text message to send to the error log. If not
* provided, the client message is used. You can pass a more
* detailed message here (e.g., with stack traces) to avoid
* showing it to users.
* @return exit This method **does not return**.
*
* @task apocalypse
*/
public static function didFatal($message, $log_message = null) {
if ($log_message === null) {
$log_message = $message;
}
self::endOutputCapture();
$access_log = self::$accessLog;
if ($access_log) {
// We may end up here before the access log is initialized, e.g. from
// verifyPHP().
$access_log->setData(
array(
'c' => 500,
));
$access_log->write();
}
header(
'Content-Type: text/plain; charset=utf-8',
$replace = true,
$http_error = 500);
error_log($log_message);
echo $message."\n";
exit(1);
}
/* -( Validation )--------------------------------------------------------- */
/**
* @task validation
*/
private static function setupPHP() {
error_reporting(E_ALL | E_STRICT);
self::$oldMemoryLimit = ini_get('memory_limit');
ini_set('memory_limit', -1);
// If we have libxml, disable the incredibly dangerous entity loader.
// PHP 8 deprecates this function and disables this by default; remove once
// PHP 7 is no longer supported or a future version has removed the function
// entirely.
if (function_exists('libxml_disable_entity_loader')) {
@libxml_disable_entity_loader(true);
}
// See T13060. If the locale for this process (the parent process) is not
// a UTF-8 locale we can encounter problems when launching subprocesses
// which receive UTF-8 parameters in their command line argument list.
@setlocale(LC_ALL, 'en_US.UTF-8');
$config_map = array(
// See PHI1894. Keep "args" in exception backtraces.
'zend.exception_ignore_args' => 0,
// See T13100. We'd like the regex engine to fail, rather than segfault,
// if handed a pathological regular expression.
'pcre.backtrack_limit' => 10000,
'pcre.recusion_limit' => 10000,
// NOTE: Arcanist applies a similar set of startup options for CLI
// environments in "init-script.php". Changes here may also be
// appropriate to apply there.
);
foreach ($config_map as $config_key => $config_value) {
ini_set($config_key, $config_value);
}
}
/**
* @task validation
*/
public static function getOldMemoryLimit() {
return self::$oldMemoryLimit;
}
/**
* @task validation
*/
private static function normalizeInput() {
// Replace superglobals with unfiltered versions, disrespect php.ini (we
// filter ourselves).
// NOTE: We don't filter INPUT_SERVER because we don't want to overwrite
// changes made in "preamble.php".
// NOTE: WE don't filter INPUT_POST because we may be constructing it
// lazily if "enable_post_data_reading" is disabled.
$filter = array(
INPUT_GET,
INPUT_ENV,
INPUT_COOKIE,
);
foreach ($filter as $type) {
$filtered = filter_input_array($type, FILTER_UNSAFE_RAW);
if (!is_array($filtered)) {
continue;
}
switch ($type) {
case INPUT_GET:
$_GET = array_merge($_GET, $filtered);
break;
case INPUT_COOKIE:
$_COOKIE = array_merge($_COOKIE, $filtered);
break;
case INPUT_ENV;
$env = array_merge($_ENV, $filtered);
$_ENV = self::filterEnvSuperglobal($env);
break;
}
}
self::rebuildRequest();
}
/**
* @task validation
*/
public static function rebuildRequest() {
// Rebuild $_REQUEST, respecting order declared in ".ini" files.
$order = ini_get('request_order');
if (!$order) {
$order = ini_get('variables_order');
}
if (!$order) {
// $_REQUEST will be empty, so leave it alone.
return;
}
$_REQUEST = array();
for ($ii = 0; $ii < strlen($order); $ii++) {
switch ($order[$ii]) {
case 'G':
$_REQUEST = array_merge($_REQUEST, $_GET);
break;
case 'P':
$_REQUEST = array_merge($_REQUEST, $_POST);
break;
case 'C':
$_REQUEST = array_merge($_REQUEST, $_COOKIE);
break;
default:
// $_ENV and $_SERVER never go into $_REQUEST.
break;
}
}
}
/**
* Adjust `$_ENV` before execution.
*
* Adjustments here primarily impact the environment as seen by subprocesses.
* The environment is forwarded explicitly by @{class:ExecFuture}.
*
* @param map<string, wild> Input `$_ENV`.
* @return map<string, string> Suitable `$_ENV`.
* @task validation
*/
private static function filterEnvSuperglobal(array $env) {
// In some configurations, we may get "argc" and "argv" set in $_ENV.
// These are not real environmental variables, and "argv" may have an array
// value which can not be forwarded to subprocesses. Remove these from the
// environment if they are present.
unset($env['argc']);
unset($env['argv']);
return $env;
}
/**
* @task validation
*/
private static function verifyPHP() {
$required_version = '5.2.3';
if (version_compare(PHP_VERSION, $required_version) < 0) {
self::didFatal(
"You are running PHP version '".PHP_VERSION."', which is older than ".
"the minimum version, '{$required_version}'. Update to at least ".
"'{$required_version}'.");
}
if (function_exists('get_magic_quotes_gpc')) {
if (@get_magic_quotes_gpc()) {
self::didFatal(
'Your server is configured with the PHP language feature '.
'"magic_quotes_gpc" enabled.'.
"\n\n".
'This feature is "highly discouraged" by PHP\'s developers, and '.
'has been removed entirely in PHP8.'.
"\n\n".
'You must disable "magic_quotes_gpc" to run Phabricator. Consult '.
'the PHP manual for instructions.');
}
}
if (extension_loaded('apc')) {
$apc_version = phpversion('apc');
$known_bad = array(
'3.1.14' => true,
'3.1.15' => true,
'3.1.15-dev' => true,
);
if (isset($known_bad[$apc_version])) {
self::didFatal(
"You have APC {$apc_version} installed. This version of APC is ".
"known to be bad, and does not work with Phabricator (it will ".
"cause Phabricator to fatal unrecoverably with nonsense errors). ".
"Downgrade to version 3.1.13.");
}
}
if (isset($_SERVER['HTTP_PROXY'])) {
self::didFatal(
'This HTTP request included a "Proxy:" header, poisoning the '.
'environment (CVE-2016-5385 / httpoxy). Declining to process this '.
'request. For details, see: https://secure.phabricator.com/T11359');
}
}
/**
* @task request-path
*/
private static function readRequestPath() {
// See T13575. The request path may be provided in:
//
// - the "$_GET" parameter "__path__" (normal for Apache and nginx); or
// - the "$_SERVER" parameter "REQUEST_URI" (normal for the PHP builtin
// webserver).
//
// Locate it wherever it is, and store it for later use. Note that writing
// to "$_REQUEST" here won't always work, because later code may rebuild
// "$_REQUEST" from other sources.
if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) {
self::setRequestPath($_REQUEST['__path__']);
return;
}
// Compatibility with PHP 5.4+ built-in web server.
if (php_sapi_name() == 'cli-server') {
$path = parse_url($_SERVER['REQUEST_URI']);
self::setRequestPath($path['path']);
return;
}
if (!isset($_REQUEST['__path__'])) {
self::didFatal(
"Request parameter '__path__' is not set. Your rewrite rules ".
"are not configured correctly.");
}
if (!strlen($_REQUEST['__path__'])) {
self::didFatal(
"Request parameter '__path__' is set, but empty. Your rewrite rules ".
"are not configured correctly. The '__path__' should always ".
"begin with a '/'.");
}
}
/**
* @task request-path
*/
public static function getRequestPath() {
$path = self::$requestPath;
if ($path === null) {
self::didFatal(
'Request attempted to access request path, but no request path is '.
'available for this request. You may be calling web request code '.
'from a non-request context, or your webserver may not be passing '.
'a request path to Phabricator in a format that it understands.');
}
return $path;
}
/**
* @task request-path
*/
public static function setRequestPath($path) {
self::$requestPath = $path;
}
/* -( Rate Limiting )------------------------------------------------------ */
/**
* Add a new client limits.
*
* @param PhabricatorClientLimit New limit.
* @return PhabricatorClientLimit The limit.
*/
public static function addRateLimit(PhabricatorClientLimit $limit) {
self::$limits[] = $limit;
return $limit;
}
/**
* Apply configured rate limits.
*
* If any limit is exceeded, this method terminates the request.
*
* @return void
* @task ratelimit
*/
private static function connectRateLimits() {
$limits = self::$limits;
$reason = null;
$connected = array();
foreach ($limits as $limit) {
$reason = $limit->didConnect();
$connected[] = $limit;
if ($reason !== null) {
break;
}
}
// If we're killing the request here, disconnect any limits that we
// connected to try to keep the accounting straight.
if ($reason !== null) {
foreach ($connected as $limit) {
$limit->didDisconnect(array());
}
self::didRateLimit($reason);
}
}
/**
* Tear down rate limiting and allow limits to score the request.
*
* @param map<string, wild> Additional, freeform request state.
* @return void
* @task ratelimit
*/
public static function disconnectRateLimits(array $request_state) {
$limits = self::$limits;
// Remove all limits before disconnecting them so this works properly if
// it runs twice. (We run this automatically as a shutdown handler.)
self::$limits = array();
foreach ($limits as $limit) {
$limit->didDisconnect($request_state);
}
}
/**
* Emit an HTTP 429 "Too Many Requests" response (indicating that the user
* has exceeded application rate limits) and exit.
*
* @return exit This method **does not return**.
* @task ratelimit
*/
private static function didRateLimit($reason) {
header(
'Content-Type: text/plain; charset=utf-8',
$replace = true,
$http_error = 429);
echo $reason;
exit(1);
}
/* -( Startup Timers )----------------------------------------------------- */
/**
* Record the beginning of a new startup phase.
*
* For phases which occur before @{class:PhabricatorStartup} loads, save the
* time and record it with @{method:recordStartupPhase} after the class is
* available.
*
* @param string Phase name.
* @task phases
*/
public static function beginStartupPhase($phase) {
self::recordStartupPhase($phase, microtime(true));
}
/**
* Record the start time of a previously executed startup phase.
*
* For startup phases which occur after @{class:PhabricatorStartup} loads,
* use @{method:beginStartupPhase} instead. This method can be used to
* record a time before the class loads, then hand it over once the class
* becomes available.
*
* @param string Phase name.
* @param float Phase start time, from `microtime(true)`.
* @task phases
*/
public static function recordStartupPhase($phase, $time) {
self::$phases[$phase] = $time;
}
/**
* Get information about startup phase timings.
*
* Sometimes, performance problems can occur before we start the profiler.
* Since the profiler can't examine these phases, it isn't useful in
* understanding their performance costs.
*
* Instead, the startup process marks when it enters various phases using
* @{method:beginStartupPhase}. A later call to this method can retrieve this
* information, which can be examined to gain greater insight into where
* time was spent. The output is still crude, but better than nothing.
*
* @task phases
*/
public static function getPhases() {
return self::$phases;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Apr 28, 4:45 PM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
108068
Default Alt Text
(92 KB)

Event Timeline