mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-01 03:02:43 +01:00
c896aeb62e
Summary: Apply various linter fixes. Test Plan: Unit tests + eyeballing. Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: hach-que, Korvin, epriestley Differential Revision: https://secure.phabricator.com/D12390
763 lines
21 KiB
PHP
763 lines
21 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @task data Accessing Request Data
|
|
* @task cookie Managing Cookies
|
|
* @task cluster Working With a Phabricator Cluster
|
|
*/
|
|
final class AphrontRequest {
|
|
|
|
// NOTE: These magic request-type parameters are automatically included in
|
|
// certain requests (e.g., by phabricator_form(), JX.Request,
|
|
// JX.Workflow, and ConduitClient) and help us figure out what sort of
|
|
// response the client expects.
|
|
|
|
const TYPE_AJAX = '__ajax__';
|
|
const TYPE_FORM = '__form__';
|
|
const TYPE_CONDUIT = '__conduit__';
|
|
const TYPE_WORKFLOW = '__wflow__';
|
|
const TYPE_CONTINUE = '__continue__';
|
|
const TYPE_PREVIEW = '__preview__';
|
|
const TYPE_HISEC = '__hisec__';
|
|
const TYPE_QUICKSAND = '__quicksand__';
|
|
|
|
private $host;
|
|
private $path;
|
|
private $requestData;
|
|
private $user;
|
|
private $applicationConfiguration;
|
|
private $uriData;
|
|
|
|
public function __construct($host, $path) {
|
|
$this->host = $host;
|
|
$this->path = $path;
|
|
}
|
|
|
|
public function setURIMap(array $uri_data) {
|
|
$this->uriData = $uri_data;
|
|
return $this;
|
|
}
|
|
|
|
public function getURIMap() {
|
|
return $this->uriData;
|
|
}
|
|
|
|
public function getURIData($key, $default = null) {
|
|
return idx($this->uriData, $key, $default);
|
|
}
|
|
|
|
public function setApplicationConfiguration(
|
|
$application_configuration) {
|
|
$this->applicationConfiguration = $application_configuration;
|
|
return $this;
|
|
}
|
|
|
|
public function getApplicationConfiguration() {
|
|
return $this->applicationConfiguration;
|
|
}
|
|
|
|
public function setPath($path) {
|
|
$this->path = $path;
|
|
return $this;
|
|
}
|
|
|
|
public function getPath() {
|
|
return $this->path;
|
|
}
|
|
|
|
public function getHost() {
|
|
// The "Host" header may include a port number, or may be a malicious
|
|
// header in the form "realdomain.com:ignored@evil.com". Invoke the full
|
|
// parser to extract the real domain correctly. See here for coverage of
|
|
// a similar issue in Django:
|
|
//
|
|
// https://www.djangoproject.com/weblog/2012/oct/17/security/
|
|
$uri = new PhutilURI('http://'.$this->host);
|
|
return $uri->getDomain();
|
|
}
|
|
|
|
|
|
/* -( Accessing Request Data )--------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* @task data
|
|
*/
|
|
public function setRequestData(array $request_data) {
|
|
$this->requestData = $request_data;
|
|
return $this;
|
|
}
|
|
|
|
|
|
/**
|
|
* @task data
|
|
*/
|
|
public function getRequestData() {
|
|
return $this->requestData;
|
|
}
|
|
|
|
|
|
/**
|
|
* @task data
|
|
*/
|
|
public function getInt($name, $default = null) {
|
|
if (isset($this->requestData[$name])) {
|
|
return (int)$this->requestData[$name];
|
|
} else {
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @task data
|
|
*/
|
|
public function getBool($name, $default = null) {
|
|
if (isset($this->requestData[$name])) {
|
|
if ($this->requestData[$name] === 'true') {
|
|
return true;
|
|
} else if ($this->requestData[$name] === 'false') {
|
|
return false;
|
|
} else {
|
|
return (bool)$this->requestData[$name];
|
|
}
|
|
} else {
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @task data
|
|
*/
|
|
public function getStr($name, $default = null) {
|
|
if (isset($this->requestData[$name])) {
|
|
$str = (string)$this->requestData[$name];
|
|
// Normalize newline craziness.
|
|
$str = str_replace(
|
|
array("\r\n", "\r"),
|
|
array("\n", "\n"),
|
|
$str);
|
|
return $str;
|
|
} else {
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @task data
|
|
*/
|
|
public function getArr($name, $default = array()) {
|
|
if (isset($this->requestData[$name]) &&
|
|
is_array($this->requestData[$name])) {
|
|
return $this->requestData[$name];
|
|
} else {
|
|
return $default;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @task data
|
|
*/
|
|
public function getStrList($name, $default = array()) {
|
|
if (!isset($this->requestData[$name])) {
|
|
return $default;
|
|
}
|
|
$list = $this->getStr($name);
|
|
$list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
|
|
return $list;
|
|
}
|
|
|
|
|
|
/**
|
|
* @task data
|
|
*/
|
|
public function getExists($name) {
|
|
return array_key_exists($name, $this->requestData);
|
|
}
|
|
|
|
public function getFileExists($name) {
|
|
return isset($_FILES[$name]) &&
|
|
(idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
|
|
}
|
|
|
|
public function isHTTPGet() {
|
|
return ($_SERVER['REQUEST_METHOD'] == 'GET');
|
|
}
|
|
|
|
public function isHTTPPost() {
|
|
return ($_SERVER['REQUEST_METHOD'] == 'POST');
|
|
}
|
|
|
|
public function isAjax() {
|
|
return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand();
|
|
}
|
|
|
|
public function isWorkflow() {
|
|
return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand();
|
|
}
|
|
|
|
public function isQuicksand() {
|
|
return $this->getExists(self::TYPE_QUICKSAND);
|
|
}
|
|
|
|
public function isConduit() {
|
|
return $this->getExists(self::TYPE_CONDUIT);
|
|
}
|
|
|
|
public static function getCSRFTokenName() {
|
|
return '__csrf__';
|
|
}
|
|
|
|
public static function getCSRFHeaderName() {
|
|
return 'X-Phabricator-Csrf';
|
|
}
|
|
|
|
public function validateCSRF() {
|
|
$token_name = self::getCSRFTokenName();
|
|
$token = $this->getStr($token_name);
|
|
|
|
// No token in the request, check the HTTP header which is added for Ajax
|
|
// requests.
|
|
if (empty($token)) {
|
|
$token = self::getHTTPHeader(self::getCSRFHeaderName());
|
|
}
|
|
|
|
$valid = $this->getUser()->validateCSRFToken($token);
|
|
if (!$valid) {
|
|
|
|
// Add some diagnostic details so we can figure out if some CSRF issues
|
|
// are JS problems or people accessing Ajax URIs directly with their
|
|
// browsers.
|
|
$more_info = array();
|
|
|
|
if ($this->isAjax()) {
|
|
$more_info[] = pht('This was an Ajax request.');
|
|
} else {
|
|
$more_info[] = pht('This was a Web request.');
|
|
}
|
|
|
|
if ($token) {
|
|
$more_info[] = pht('This request had an invalid CSRF token.');
|
|
} else {
|
|
$more_info[] = pht('This request had no CSRF token.');
|
|
}
|
|
|
|
// Give a more detailed explanation of how to avoid the exception
|
|
// in developer mode.
|
|
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
|
|
// TODO: Clean this up, see T1921.
|
|
$more_info[] =
|
|
"To avoid this error, use phabricator_form() to construct forms. ".
|
|
"If you are already using phabricator_form(), make sure the form ".
|
|
"'action' uses a relative URI (i.e., begins with a '/'). Forms ".
|
|
"using absolute URIs do not include CSRF tokens, to prevent ".
|
|
"leaking tokens to external sites.\n\n".
|
|
"If this page performs writes which do not require CSRF ".
|
|
"protection (usually, filling caches or logging), you can use ".
|
|
"AphrontWriteGuard::beginScopedUnguardedWrites() to temporarily ".
|
|
"bypass CSRF protection while writing. You should use this only ".
|
|
"for writes which can not be protected with normal CSRF ".
|
|
"mechanisms.\n\n".
|
|
"Some UI elements (like PhabricatorActionListView) also have ".
|
|
"methods which will allow you to render links as forms (like ".
|
|
"setRenderAsForm(true)).";
|
|
}
|
|
|
|
// This should only be able to happen if you load a form, pull your
|
|
// internet for 6 hours, and then reconnect and immediately submit,
|
|
// but give the user some indication of what happened since the workflow
|
|
// is incredibly confusing otherwise.
|
|
throw new AphrontCSRFException(
|
|
pht(
|
|
"You are trying to save some data to Phabricator, but the request ".
|
|
"your browser made included an incorrect token. Reload the page ".
|
|
"and try again. You may need to clear your cookies.\n\n%s",
|
|
implode("\n", $more_info)));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function isFormPost() {
|
|
$post = $this->getExists(self::TYPE_FORM) &&
|
|
!$this->getExists(self::TYPE_HISEC) &&
|
|
$this->isHTTPPost();
|
|
|
|
if (!$post) {
|
|
return false;
|
|
}
|
|
|
|
return $this->validateCSRF();
|
|
}
|
|
|
|
public function isFormOrHisecPost() {
|
|
$post = $this->getExists(self::TYPE_FORM) &&
|
|
$this->isHTTPPost();
|
|
|
|
if (!$post) {
|
|
return false;
|
|
}
|
|
|
|
return $this->validateCSRF();
|
|
}
|
|
|
|
|
|
public function setCookiePrefix($prefix) {
|
|
$this->cookiePrefix = $prefix;
|
|
return $this;
|
|
}
|
|
|
|
private function getPrefixedCookieName($name) {
|
|
if (strlen($this->cookiePrefix)) {
|
|
return $this->cookiePrefix.'_'.$name;
|
|
} else {
|
|
return $name;
|
|
}
|
|
}
|
|
|
|
public function getCookie($name, $default = null) {
|
|
$name = $this->getPrefixedCookieName($name);
|
|
$value = idx($_COOKIE, $name, $default);
|
|
|
|
// Internally, PHP deletes cookies by setting them to the value 'deleted'
|
|
// with an expiration date in the past.
|
|
|
|
// At least in Safari, the browser may send this cookie anyway in some
|
|
// circumstances. After logging out, the 302'd GET to /login/ consistently
|
|
// includes deleted cookies on my local install. If a cookie value is
|
|
// literally 'deleted', pretend it does not exist.
|
|
|
|
if ($value === 'deleted') {
|
|
return null;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
public function clearCookie($name) {
|
|
$this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
|
|
unset($_COOKIE[$name]);
|
|
}
|
|
|
|
/**
|
|
* Get the domain which cookies should be set on for this request, or null
|
|
* if the request does not correspond to a valid cookie domain.
|
|
*
|
|
* @return PhutilURI|null Domain URI, or null if no valid domain exists.
|
|
*
|
|
* @task cookie
|
|
*/
|
|
private function getCookieDomainURI() {
|
|
if (PhabricatorEnv::getEnvConfig('security.require-https') &&
|
|
!$this->isHTTPS()) {
|
|
return null;
|
|
}
|
|
|
|
$host = $this->getHost();
|
|
|
|
// If there's no base domain configured, just use whatever the request
|
|
// domain is. This makes setup easier, and we'll tell administrators to
|
|
// configure a base domain during the setup process.
|
|
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
|
|
if (!strlen($base_uri)) {
|
|
return new PhutilURI('http://'.$host.'/');
|
|
}
|
|
|
|
$alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
|
|
$allowed_uris = array_merge(
|
|
array($base_uri),
|
|
$alternates);
|
|
|
|
foreach ($allowed_uris as $allowed_uri) {
|
|
$uri = new PhutilURI($allowed_uri);
|
|
if ($uri->getDomain() == $host) {
|
|
return $uri;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Determine if security policy rules will allow cookies to be set when
|
|
* responding to the request.
|
|
*
|
|
* @return bool True if setCookie() will succeed. If this method returns
|
|
* false, setCookie() will throw.
|
|
*
|
|
* @task cookie
|
|
*/
|
|
public function canSetCookies() {
|
|
return (bool)$this->getCookieDomainURI();
|
|
}
|
|
|
|
|
|
/**
|
|
* Set a cookie which does not expire for a long time.
|
|
*
|
|
* To set a temporary cookie, see @{method:setTemporaryCookie}.
|
|
*
|
|
* @param string Cookie name.
|
|
* @param string Cookie value.
|
|
* @return this
|
|
* @task cookie
|
|
*/
|
|
public function setCookie($name, $value) {
|
|
$far_future = time() + (60 * 60 * 24 * 365 * 5);
|
|
return $this->setCookieWithExpiration($name, $value, $far_future);
|
|
}
|
|
|
|
|
|
/**
|
|
* Set a cookie which expires soon.
|
|
*
|
|
* To set a durable cookie, see @{method:setCookie}.
|
|
*
|
|
* @param string Cookie name.
|
|
* @param string Cookie value.
|
|
* @return this
|
|
* @task cookie
|
|
*/
|
|
public function setTemporaryCookie($name, $value) {
|
|
return $this->setCookieWithExpiration($name, $value, 0);
|
|
}
|
|
|
|
|
|
/**
|
|
* Set a cookie with a given expiration policy.
|
|
*
|
|
* @param string Cookie name.
|
|
* @param string Cookie value.
|
|
* @param int Epoch timestamp for cookie expiration.
|
|
* @return this
|
|
* @task cookie
|
|
*/
|
|
private function setCookieWithExpiration(
|
|
$name,
|
|
$value,
|
|
$expire) {
|
|
|
|
$is_secure = false;
|
|
|
|
$base_domain_uri = $this->getCookieDomainURI();
|
|
if (!$base_domain_uri) {
|
|
$configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
|
|
$accessed_as = $this->getHost();
|
|
|
|
throw new Exception(
|
|
pht(
|
|
'This Phabricator install is configured as "%s", but you are '.
|
|
'using the domain name "%s" to access a page which is trying to '.
|
|
'set a cookie. Acccess Phabricator on the configured primary '.
|
|
'domain or a configured alternate domain. Phabricator will not '.
|
|
'set cookies on other domains for security reasons.',
|
|
$configured_as,
|
|
$accessed_as));
|
|
}
|
|
|
|
$base_domain = $base_domain_uri->getDomain();
|
|
$is_secure = ($base_domain_uri->getProtocol() == 'https');
|
|
|
|
$name = $this->getPrefixedCookieName($name);
|
|
|
|
if (php_sapi_name() == 'cli') {
|
|
// Do nothing, to avoid triggering "Cannot modify header information"
|
|
// warnings.
|
|
|
|
// TODO: This is effectively a test for whether we're running in a unit
|
|
// test or not. Move this actual call to HTTPSink?
|
|
} else {
|
|
setcookie(
|
|
$name,
|
|
$value,
|
|
$expire,
|
|
$path = '/',
|
|
$base_domain,
|
|
$is_secure,
|
|
$http_only = true);
|
|
}
|
|
|
|
$_COOKIE[$name] = $value;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setUser($user) {
|
|
$this->user = $user;
|
|
return $this;
|
|
}
|
|
|
|
public function getUser() {
|
|
return $this->user;
|
|
}
|
|
|
|
public function getViewer() {
|
|
return $this->user;
|
|
}
|
|
|
|
public function getRequestURI() {
|
|
$get = $_GET;
|
|
unset($get['__path__']);
|
|
$path = phutil_escape_uri($this->getPath());
|
|
return id(new PhutilURI($path))->setQueryParams($get);
|
|
}
|
|
|
|
public function isDialogFormPost() {
|
|
return $this->isFormPost() && $this->getStr('__dialog__');
|
|
}
|
|
|
|
public function getRemoteAddr() {
|
|
return $_SERVER['REMOTE_ADDR'];
|
|
}
|
|
|
|
public function isHTTPS() {
|
|
if (empty($_SERVER['HTTPS'])) {
|
|
return false;
|
|
}
|
|
if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public function isContinueRequest() {
|
|
return $this->isFormPost() && $this->getStr('__continue__');
|
|
}
|
|
|
|
public function isPreviewRequest() {
|
|
return $this->isFormPost() && $this->getStr('__preview__');
|
|
}
|
|
|
|
/**
|
|
* Get application request parameters in a flattened form suitable for
|
|
* inclusion in an HTTP request, excluding parameters with special meanings.
|
|
* This is primarily useful if you want to ask the user for more input and
|
|
* then resubmit their request.
|
|
*
|
|
* @return dict<string, string> Original request parameters.
|
|
*/
|
|
public function getPassthroughRequestParameters($include_quicksand = false) {
|
|
return self::flattenData(
|
|
$this->getPassthroughRequestData($include_quicksand));
|
|
}
|
|
|
|
/**
|
|
* Get request data other than "magic" parameters.
|
|
*
|
|
* @return dict<string, wild> Request data, with magic filtered out.
|
|
*/
|
|
public function getPassthroughRequestData($include_quicksand = false) {
|
|
$data = $this->getRequestData();
|
|
|
|
// Remove magic parameters like __dialog__ and __ajax__.
|
|
foreach ($data as $key => $value) {
|
|
if ($include_quicksand && $key == self::TYPE_QUICKSAND) {
|
|
continue;
|
|
}
|
|
if (!strncmp($key, '__', 2)) {
|
|
unset($data[$key]);
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
|
|
/**
|
|
* Flatten an array of key-value pairs (possibly including arrays as values)
|
|
* into a list of key-value pairs suitable for submitting via HTTP request
|
|
* (with arrays flattened).
|
|
*
|
|
* @param dict<string, wild> Data to flatten.
|
|
* @return dict<string, string> Flat data suitable for inclusion in an HTTP
|
|
* request.
|
|
*/
|
|
public static function flattenData(array $data) {
|
|
$result = array();
|
|
foreach ($data as $key => $value) {
|
|
if (is_array($value)) {
|
|
foreach (self::flattenData($value) as $fkey => $fvalue) {
|
|
$fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
|
|
$result[$key.$fkey] = $fvalue;
|
|
}
|
|
} else {
|
|
$result[$key] = (string)$value;
|
|
}
|
|
}
|
|
|
|
ksort($result);
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
|
|
*
|
|
* This function accepts a canonical header name, like `"Accept-Encoding"`,
|
|
* and looks up the appropriate value in `$_SERVER` (in this case,
|
|
* `"HTTP_ACCEPT_ENCODING"`).
|
|
*
|
|
* @param string Canonical header name, like `"Accept-Encoding"`.
|
|
* @param wild Default value to return if header is not present.
|
|
* @param array? Read this instead of `$_SERVER`.
|
|
* @return string|wild Header value if present, or `$default` if not.
|
|
*/
|
|
public static function getHTTPHeader($name, $default = null, $data = null) {
|
|
// PHP mangles HTTP headers by uppercasing them and replacing hyphens with
|
|
// underscores, then prepending 'HTTP_'.
|
|
$php_index = strtoupper($name);
|
|
$php_index = str_replace('-', '_', $php_index);
|
|
|
|
$try_names = array();
|
|
|
|
$try_names[] = 'HTTP_'.$php_index;
|
|
if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
|
|
// These headers may be available under alternate names. See
|
|
// http://www.php.net/manual/en/reserved.variables.server.php#110763
|
|
$try_names[] = $php_index;
|
|
}
|
|
|
|
if ($data === null) {
|
|
$data = $_SERVER;
|
|
}
|
|
|
|
foreach ($try_names as $try_name) {
|
|
if (array_key_exists($try_name, $data)) {
|
|
return $data[$try_name];
|
|
}
|
|
}
|
|
|
|
return $default;
|
|
}
|
|
|
|
|
|
/* -( Working With a Phabricator Cluster )--------------------------------- */
|
|
|
|
|
|
/**
|
|
* Is this a proxied request originating from within the Phabricator cluster?
|
|
*
|
|
* IMPORTANT: This means the request is dangerous!
|
|
*
|
|
* These requests are **more dangerous** than normal requests (they can not
|
|
* be safely proxied, because proxying them may cause a loop). Cluster
|
|
* requests are not guaranteed to come from a trusted source, and should
|
|
* never be treated as safer than normal requests. They are strictly less
|
|
* safe.
|
|
*/
|
|
public function isProxiedClusterRequest() {
|
|
return (bool)self::getHTTPHeader('X-Phabricator-Cluster');
|
|
}
|
|
|
|
|
|
/**
|
|
* Build a new @{class:HTTPSFuture} which proxies this request to another
|
|
* node in the cluster.
|
|
*
|
|
* IMPORTANT: This is very dangerous!
|
|
*
|
|
* The future forwards authentication information present in the request.
|
|
* Proxied requests must only be sent to trusted hosts. (We attempt to
|
|
* enforce this.)
|
|
*
|
|
* This is not a general-purpose proxying method; it is a specialized
|
|
* method with niche applications and severe security implications.
|
|
*
|
|
* @param string URI identifying the host we are proxying the request to.
|
|
* @return HTTPSFuture New proxy future.
|
|
*
|
|
* @phutil-external-symbol class PhabricatorStartup
|
|
*/
|
|
public function newClusterProxyFuture($uri) {
|
|
$uri = new PhutilURI($uri);
|
|
|
|
$domain = $uri->getDomain();
|
|
$ip = gethostbyname($domain);
|
|
if (!$ip) {
|
|
throw new Exception(
|
|
pht(
|
|
'Unable to resolve domain "%s"!',
|
|
$domain));
|
|
}
|
|
|
|
if (!PhabricatorEnv::isClusterAddress($ip)) {
|
|
throw new Exception(
|
|
pht(
|
|
'Refusing to proxy a request to IP address ("%s") which is not '.
|
|
'in the cluster address block (this address was derived by '.
|
|
'resolving the domain "%s").',
|
|
$ip,
|
|
$domain));
|
|
}
|
|
|
|
$uri->setPath($this->getPath());
|
|
$uri->setQueryParams(self::flattenData($_GET));
|
|
|
|
$input = PhabricatorStartup::getRawInput();
|
|
|
|
$future = id(new HTTPSFuture($uri))
|
|
->addHeader('Host', self::getHost())
|
|
->addHeader('X-Phabricator-Cluster', true)
|
|
->setMethod($_SERVER['REQUEST_METHOD'])
|
|
->write($input);
|
|
|
|
if (isset($_SERVER['PHP_AUTH_USER'])) {
|
|
$future->setHTTPBasicAuthCredentials(
|
|
$_SERVER['PHP_AUTH_USER'],
|
|
new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', '')));
|
|
}
|
|
|
|
$headers = array();
|
|
$seen = array();
|
|
|
|
// NOTE: apache_request_headers() might provide a nicer way to do this,
|
|
// but isn't available under FCGI until PHP 5.4.0.
|
|
foreach ($_SERVER as $key => $value) {
|
|
if (preg_match('/^HTTP_/', $key)) {
|
|
// Unmangle the header as best we can.
|
|
$key = str_replace('_', ' ', $key);
|
|
$key = strtolower($key);
|
|
$key = ucwords($key);
|
|
$key = str_replace(' ', '-', $key);
|
|
|
|
$headers[] = array($key, $value);
|
|
$seen[$key] = true;
|
|
}
|
|
}
|
|
|
|
// In some situations, this may not be mapped into the HTTP_X constants.
|
|
// CONTENT_LENGTH is similarly affected, but we trust cURL to take care
|
|
// of that if it matters, since we're handing off a request body.
|
|
if (empty($seen['Content-Type'])) {
|
|
if (isset($_SERVER['CONTENT_TYPE'])) {
|
|
$headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']);
|
|
}
|
|
}
|
|
|
|
foreach ($headers as $header) {
|
|
list($key, $value) = $header;
|
|
switch ($key) {
|
|
case 'Host':
|
|
case 'Authorization':
|
|
// Don't forward these headers, we've already handled them elsewhere.
|
|
unset($headers[$key]);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach ($headers as $header) {
|
|
list($key, $value) = $header;
|
|
$future->addHeader($key, $value);
|
|
}
|
|
|
|
return $future;
|
|
}
|
|
|
|
|
|
}
|