2011-01-16 22:51:39 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
2015-01-27 23:51:09 +01:00
|
|
|
* @task data Accessing Request Data
|
|
|
|
* @task cookie Managing Cookies
|
|
|
|
* @task cluster Working With a Phabricator Cluster
|
2011-01-16 22:51:39 +01:00
|
|
|
*/
|
2015-06-15 10:02:26 +02:00
|
|
|
final class AphrontRequest extends Phobject {
|
2011-01-16 22:51:39 +01:00
|
|
|
|
2012-05-07 15:17:00 +02:00
|
|
|
// NOTE: These magic request-type parameters are automatically included in
|
2013-02-13 23:50:15 +01:00
|
|
|
// certain requests (e.g., by phabricator_form(), JX.Request,
|
2012-05-07 15:17:00 +02:00
|
|
|
// JX.Workflow, and ConduitClient) and help us figure out what sort of
|
|
|
|
// response the client expects.
|
|
|
|
|
2011-01-16 22:51:39 +01:00
|
|
|
const TYPE_AJAX = '__ajax__';
|
|
|
|
const TYPE_FORM = '__form__';
|
2012-01-15 20:06:13 +01:00
|
|
|
const TYPE_CONDUIT = '__conduit__';
|
2012-05-07 15:17:00 +02:00
|
|
|
const TYPE_WORKFLOW = '__wflow__';
|
Add passthru to AphrontRequest
Summary:
For transaction interfaces, I want to prompt the user when they take an action that has no effect, e.g.:
Action Has No Effect
You can not close this task, because someone else has already closed it.
Do you want to post your comment anyway?
[Cancel] [Post Comment]
We already do this for Differential, but it's all hard-coded. T912 is an open task for fixing this for Maniphest.
To do this in a general way, I want to embed the entire request in the dialog as hidden inputs, then add a "__continue__" key and resubmit the form. The endpoint will read this key the second time through and apply what effects it can (e.g., just post a comment).
This adds a mechanism for getting all the request data, minus "magic" like __dialog__ and __csrf__. We need to jump through some hoops because of how PHP encodes arrays.
Test Plan: Ran unit tests, built "no effect" dialogs on top of this.
Reviewers: btrahan, vrana
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T912, T2104
Differential Revision: https://secure.phabricator.com/D4158
2012-12-12 02:27:02 +01:00
|
|
|
const TYPE_CONTINUE = '__continue__';
|
2012-12-21 14:51:33 +01:00
|
|
|
const TYPE_PREVIEW = '__preview__';
|
2014-04-28 02:31:11 +02:00
|
|
|
const TYPE_HISEC = '__hisec__';
|
Quicksand, an ignoble successor to Quickling
Summary:
Ref T2086. Ref T7014. With the persistent column, there is significant value in retaining chrome state through navigation events, because the user may have a lot of state in the chat window (scroll position, text selection, room juggling, partially entered text, etc). We can do this by capturing navigation events and faking them with Javascript.
(This can also improve performance, albeit slightly, and I believe there are better approaches to tackle performance any problems which exist with the chrome in many cases).
At Facebook, this system was "Photostream" in photos and then "Quickling" in general, and the technical cost of the system was //staggering//. I am loathe to pursue it again. However:
- Browsers are less junky now, and we target a smaller set of browsers. A large part of the technical cost of Quickling was the high complexity of emulating nagivation events in IE, where we needed to navigate a hidden iframe to make history entries. All desktop browsers which we might want to use this system on support the History API (although this prototype does not yet implement it).
- Javelin and Phabricator's architecture are much cleaner than Facebook's was. A large part of the technical cost of Quickling was inconsistency, inlined `onclick` handlers, and general lack of coordination and abstraction. We will have //some// of this, but "correctly written" behaviors are mostly immune to it by design, and many of Javelin's architectural decisions were influenced by desire to avoid issues we encountered building this stuff for Facebook.
- Some of the primitives which Quickling required (like loading resources over Ajax) have existed in a stable state in our codebase for a year or more, and adoption of these primitives was trivial and uneventful (vs a huge production at Facebook).
- My hubris is bolstered by recent success with WebSockets and JX.Scrollbar, both of which I would have assessed as infeasibly complex to develop in this project a few years ago.
To these points, the developer cost to prototype Photostream was several weeks; the developer cost to prototype this was a bit less than an hour. It is plausible to me that implementing and maintaining this system really will be hundreds of times less complex than it was at Facebook.
Test Plan:
My plan for this and D11497 is:
- Get them in master.
- Some secret key / relatively-hidden preference activates the column.
- Quicksand activates //only// when the column is open.
- We can use column + quicksand for a long period of time (i.e., over the course of Conpherence v2 development) and hammer out the long tail of issues.
- When it derps up, you just hide the column and you're good to go.
Reviewers: btrahan, chad
Reviewed By: chad
Subscribers: epriestley
Maniphest Tasks: T2086, T7014
Differential Revision: https://secure.phabricator.com/D11507
2015-01-27 23:52:09 +01:00
|
|
|
const TYPE_QUICKSAND = '__quicksand__';
|
2011-01-16 22:51:39 +01:00
|
|
|
|
|
|
|
private $host;
|
|
|
|
private $path;
|
|
|
|
private $requestData;
|
2011-01-26 22:21:12 +01:00
|
|
|
private $user;
|
2011-02-02 22:48:52 +01:00
|
|
|
private $applicationConfiguration;
|
2015-08-31 13:01:01 +02:00
|
|
|
private $site;
|
|
|
|
private $controller;
|
2015-10-26 22:15:45 +01:00
|
|
|
private $uriData = array();
|
2015-06-15 10:02:26 +02:00
|
|
|
private $cookiePrefix;
|
2011-01-31 20:55:26 +01:00
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function __construct($host, $path) {
|
2011-02-02 22:48:52 +01:00
|
|
|
$this->host = $host;
|
|
|
|
$this->path = $path;
|
2011-01-31 20:55:26 +01:00
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function setURIMap(array $uri_data) {
|
Decouple some aspects of request routing and construction
Summary:
Ref T5702. This is a forward-looking change which provides some very broad API improvements but does not implement them. In particular:
- Controllers no longer require `$request` to construct. This is mostly for T5702, directly, but simplifies things in general. Instead, we call `setRequest()` before using a controller. Only a small number of sites activate controllers, so this is less code overall, and more consistent with most constructors not having any parameters or effects.
- `$request` now offers `getURIData($key, ...)`. This is an alternate way of accessing `$data` which is currently only available on `willProcessRequest(array $data)`. Almost all controllers which implement this method do so in order to read one or two things out of the URI data. Instead, let them just read this data directly when processing the request.
- Introduce `handleRequest(AphrontRequest $request)` and deprecate (very softly) `processRequest()`. The majority of `processRequest()` calls begin `$request = $this->getRequest()`, which is avoided with the more practical signature.
- Provide `getViewer()` on `$request`, and a convenience `getViewer()` on `$controller`. This fixes `$viewer = $request->getUser();` into `$viewer = $request->getViewer();`, and converts the `$request + $viewer` two-liner into a single `$this->getViewer()`.
Test Plan:
- Browsed around in general.
- Hit special controllers (redirect, 404).
- Hit AuditList controller (uses new style).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5702
Differential Revision: https://secure.phabricator.com/D10698
2014-10-17 14:01:40 +02:00
|
|
|
$this->uriData = $uri_data;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getURIMap() {
|
Decouple some aspects of request routing and construction
Summary:
Ref T5702. This is a forward-looking change which provides some very broad API improvements but does not implement them. In particular:
- Controllers no longer require `$request` to construct. This is mostly for T5702, directly, but simplifies things in general. Instead, we call `setRequest()` before using a controller. Only a small number of sites activate controllers, so this is less code overall, and more consistent with most constructors not having any parameters or effects.
- `$request` now offers `getURIData($key, ...)`. This is an alternate way of accessing `$data` which is currently only available on `willProcessRequest(array $data)`. Almost all controllers which implement this method do so in order to read one or two things out of the URI data. Instead, let them just read this data directly when processing the request.
- Introduce `handleRequest(AphrontRequest $request)` and deprecate (very softly) `processRequest()`. The majority of `processRequest()` calls begin `$request = $this->getRequest()`, which is avoided with the more practical signature.
- Provide `getViewer()` on `$request`, and a convenience `getViewer()` on `$controller`. This fixes `$viewer = $request->getUser();` into `$viewer = $request->getViewer();`, and converts the `$request + $viewer` two-liner into a single `$this->getViewer()`.
Test Plan:
- Browsed around in general.
- Hit special controllers (redirect, 404).
- Hit AuditList controller (uses new style).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5702
Differential Revision: https://secure.phabricator.com/D10698
2014-10-17 14:01:40 +02:00
|
|
|
return $this->uriData;
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getURIData($key, $default = null) {
|
Decouple some aspects of request routing and construction
Summary:
Ref T5702. This is a forward-looking change which provides some very broad API improvements but does not implement them. In particular:
- Controllers no longer require `$request` to construct. This is mostly for T5702, directly, but simplifies things in general. Instead, we call `setRequest()` before using a controller. Only a small number of sites activate controllers, so this is less code overall, and more consistent with most constructors not having any parameters or effects.
- `$request` now offers `getURIData($key, ...)`. This is an alternate way of accessing `$data` which is currently only available on `willProcessRequest(array $data)`. Almost all controllers which implement this method do so in order to read one or two things out of the URI data. Instead, let them just read this data directly when processing the request.
- Introduce `handleRequest(AphrontRequest $request)` and deprecate (very softly) `processRequest()`. The majority of `processRequest()` calls begin `$request = $this->getRequest()`, which is avoided with the more practical signature.
- Provide `getViewer()` on `$request`, and a convenience `getViewer()` on `$controller`. This fixes `$viewer = $request->getUser();` into `$viewer = $request->getViewer();`, and converts the `$request + $viewer` two-liner into a single `$this->getViewer()`.
Test Plan:
- Browsed around in general.
- Hit special controllers (redirect, 404).
- Hit AuditList controller (uses new style).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5702
Differential Revision: https://secure.phabricator.com/D10698
2014-10-17 14:01:40 +02:00
|
|
|
return idx($this->uriData, $key, $default);
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function setApplicationConfiguration(
|
2011-02-02 22:48:52 +01:00
|
|
|
$application_configuration) {
|
|
|
|
$this->applicationConfiguration = $application_configuration;
|
|
|
|
return $this;
|
2011-01-31 20:55:26 +01:00
|
|
|
}
|
2011-01-16 22:51:39 +01:00
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getApplicationConfiguration() {
|
2011-02-02 22:48:52 +01:00
|
|
|
return $this->applicationConfiguration;
|
2011-01-16 22:51:39 +01:00
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function setPath($path) {
|
Move skins toward modularization
Summary:
Two high-level things happening here:
- We no longer ever need to put meta-UI (content creation, editing, notices, etc.) on live blog views, since this is all in Phame now. I pulled this out.
- On the other hand, I pushed more routing/control logic into Skins and made the root skin a Controller instead of a View. This simplifies some of the code above skins, and the theory behind this is that it gives us greater flexibility to, e.g., put a glue layer between Phame and Wordpress templates or whatever else, and allows skins to handle routing and thus add pages like "About" or "Bio".
- I added a basic skin below the root skin which is more like the old root skin and has standard rendering hooks.
- "Ten Eleven" is a play on the popular (default?) Wordpress themes called "Twenty Ten", "Twenty Eleven" and "Twenty Twelve".
Test Plan: Viewed live blog and live posts. They aren't pretty, but they don't have extraneous resources.
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T1373
Differential Revision: https://secure.phabricator.com/D3714
2012-10-17 17:36:25 +02:00
|
|
|
$this->path = $path;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getPath() {
|
2012-01-04 06:57:45 +01:00
|
|
|
return $this->path;
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getHost() {
|
2012-10-22 19:49:06 +02:00
|
|
|
// 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();
|
2012-01-04 06:57:45 +01:00
|
|
|
}
|
|
|
|
|
2015-08-31 13:01:01 +02:00
|
|
|
public function setSite(AphrontSite $site) {
|
|
|
|
$this->site = $site;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSite() {
|
|
|
|
return $this->site;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function setController(AphrontController $controller) {
|
|
|
|
$this->controller = $controller;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getController() {
|
|
|
|
return $this->controller;
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
|
|
|
|
/* -( Accessing Request Data )--------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
public function setRequestData(array $request_data) {
|
2011-01-16 22:51:39 +01:00
|
|
|
$this->requestData = $request_data;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getRequestData() {
|
2011-12-08 06:41:33 +01:00
|
|
|
return $this->requestData;
|
|
|
|
}
|
|
|
|
|
2011-01-16 22:51:39 +01:00
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getInt($name, $default = null) {
|
2011-01-16 22:51:39 +01:00
|
|
|
if (isset($this->requestData[$name])) {
|
2016-01-02 14:16:10 +01:00
|
|
|
// Converting from array to int is "undefined". Don't rely on whatever
|
|
|
|
// PHP decides to do.
|
|
|
|
if (is_array($this->requestData[$name])) {
|
|
|
|
return $default;
|
|
|
|
}
|
2011-01-16 22:51:39 +01:00
|
|
|
return (int)$this->requestData[$name];
|
|
|
|
} else {
|
|
|
|
return $default;
|
Fix "reply" link in Differential
Summary:
Fixes the issue caused by rPa0af5b66437719dba6136579c051982ab275e6a0. Prior to
that patch, isCommentInNewFile() returned $comment->getIsNewFile(). While this
was often the wrong value, it came from the database and was the integer 1 if
true.
After the patch, the function returns 'true' as a boolean, which is passed to JS
and then back to PHP, interpreted as an integer, and evaluates to 0.
To avoid this issue in general, provide an isBool() method on AphrontRequest
which interprets this correctly.
I will also revert the revert of rPa0af5b66437719dba6136579c051982ab275e6a0 when
I land this.
Test Plan:
Clicked "reply" on the right hand side of a diff, got a right-hand-side inline
comment.
Reviewed By: rm
Reviewers: tuomaspelkonen, jungejason, aran, rm
CC: simpkins, aran, epriestley, rm
Differential Revision: 250
2011-05-07 19:42:40 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getBool($name, $default = null) {
|
Fix "reply" link in Differential
Summary:
Fixes the issue caused by rPa0af5b66437719dba6136579c051982ab275e6a0. Prior to
that patch, isCommentInNewFile() returned $comment->getIsNewFile(). While this
was often the wrong value, it came from the database and was the integer 1 if
true.
After the patch, the function returns 'true' as a boolean, which is passed to JS
and then back to PHP, interpreted as an integer, and evaluates to 0.
To avoid this issue in general, provide an isBool() method on AphrontRequest
which interprets this correctly.
I will also revert the revert of rPa0af5b66437719dba6136579c051982ab275e6a0 when
I land this.
Test Plan:
Clicked "reply" on the right hand side of a diff, got a right-hand-side inline
comment.
Reviewed By: rm
Reviewers: tuomaspelkonen, jungejason, aran, rm
CC: simpkins, aran, epriestley, rm
Differential Revision: 250
2011-05-07 19:42:40 +02:00
|
|
|
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;
|
2011-01-16 22:51:39 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getStr($name, $default = null) {
|
2011-01-16 22:51:39 +01:00
|
|
|
if (isset($this->requestData[$name])) {
|
2011-02-05 21:20:18 +01:00
|
|
|
$str = (string)$this->requestData[$name];
|
|
|
|
// Normalize newline craziness.
|
|
|
|
$str = str_replace(
|
|
|
|
array("\r\n", "\r"),
|
|
|
|
array("\n", "\n"),
|
|
|
|
$str);
|
|
|
|
return $str;
|
2011-01-16 22:51:39 +01:00
|
|
|
} else {
|
|
|
|
return $default;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getArr($name, $default = array()) {
|
2011-01-16 22:51:39 +01:00
|
|
|
if (isset($this->requestData[$name]) &&
|
|
|
|
is_array($this->requestData[$name])) {
|
|
|
|
return $this->requestData[$name];
|
|
|
|
} else {
|
|
|
|
return $default;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-04 06:57:45 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getStrList($name, $default = array()) {
|
2012-01-04 06:57:45 +01:00
|
|
|
if (!isset($this->requestData[$name])) {
|
|
|
|
return $default;
|
|
|
|
}
|
|
|
|
$list = $this->getStr($name);
|
2012-09-21 00:23:38 +02:00
|
|
|
$list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
|
2012-01-04 06:57:45 +01:00
|
|
|
return $list;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task data
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getExists($name) {
|
2011-01-16 22:51:39 +01:00
|
|
|
return array_key_exists($name, $this->requestData);
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getFileExists($name) {
|
Modernize Macro application
Summary: Adds feed, email, notifications, comments, partial editing, subscriptions, enable/disable, flags and crumbs to Macro.
Test Plan:
{F26839}
{F26840}
{F26841}
{F26842}
{F26843}
{F26844}
{F26845}
Reviewers: vrana, btrahan, chad
Reviewed By: vrana
CC: aran
Maniphest Tasks: T2157, T175, T2104
Differential Revision: https://secure.phabricator.com/D4141
2012-12-11 23:01:03 +01:00
|
|
|
return isset($_FILES[$name]) &&
|
|
|
|
(idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function isHTTPGet() {
|
Allow construction of ApplicationSearch queries with GET
Summary:
Ref T3775 (discussion here). Ref T2625.
T3775 presents two problems:
# Existing tools which linked to `/differential/active/epriestley/` (that is, put a username in the URL) can't generate search links now.
# Humans can't edit the URL anymore, either.
I think (1) is an actual issue, and this fixes it. I think (2) is pretty fluff, and this doesn't really try to fix it, although it probably improves it.
The fix for (1) is:
- Provide a helper to read a parameter containing either a list of user PHIDs or a list of usernames, so `/?users[]=PHID-USER-xyz` (from a tokenizer) and `/?users=alincoln,htaft` (from an external program) are equivalent inputs.
- Rename all the form parameters to be more digestable (`authorPHIDs` -> `authors`). Almost all of them were in this form already anyway. This just gives us `?users=alincoln` instead of `userPHIDs=alincoln`.
- Inside ApplicationSearch, if a request has no query associated with it but does have query parameters, build a query from the request instead of issuing the user's default query. Basically, this means that `/differential/` runs the default query, while `/differential/?users=x` runs a custom query.
Test Plan: {F56612}
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2625, T3775
Differential Revision: https://secure.phabricator.com/D6840
2013-08-29 20:52:29 +02:00
|
|
|
return ($_SERVER['REQUEST_METHOD'] == 'GET');
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function isHTTPPost() {
|
2011-01-16 22:51:39 +01:00
|
|
|
return ($_SERVER['REQUEST_METHOD'] == 'POST');
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function isAjax() {
|
2015-03-28 23:45:52 +01:00
|
|
|
return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand();
|
2011-01-16 22:51:39 +01:00
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function isWorkflow() {
|
2015-03-28 23:45:52 +01:00
|
|
|
return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand();
|
2012-05-07 15:17:00 +02:00
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function isQuicksand() {
|
Quicksand, an ignoble successor to Quickling
Summary:
Ref T2086. Ref T7014. With the persistent column, there is significant value in retaining chrome state through navigation events, because the user may have a lot of state in the chat window (scroll position, text selection, room juggling, partially entered text, etc). We can do this by capturing navigation events and faking them with Javascript.
(This can also improve performance, albeit slightly, and I believe there are better approaches to tackle performance any problems which exist with the chrome in many cases).
At Facebook, this system was "Photostream" in photos and then "Quickling" in general, and the technical cost of the system was //staggering//. I am loathe to pursue it again. However:
- Browsers are less junky now, and we target a smaller set of browsers. A large part of the technical cost of Quickling was the high complexity of emulating nagivation events in IE, where we needed to navigate a hidden iframe to make history entries. All desktop browsers which we might want to use this system on support the History API (although this prototype does not yet implement it).
- Javelin and Phabricator's architecture are much cleaner than Facebook's was. A large part of the technical cost of Quickling was inconsistency, inlined `onclick` handlers, and general lack of coordination and abstraction. We will have //some// of this, but "correctly written" behaviors are mostly immune to it by design, and many of Javelin's architectural decisions were influenced by desire to avoid issues we encountered building this stuff for Facebook.
- Some of the primitives which Quickling required (like loading resources over Ajax) have existed in a stable state in our codebase for a year or more, and adoption of these primitives was trivial and uneventful (vs a huge production at Facebook).
- My hubris is bolstered by recent success with WebSockets and JX.Scrollbar, both of which I would have assessed as infeasibly complex to develop in this project a few years ago.
To these points, the developer cost to prototype Photostream was several weeks; the developer cost to prototype this was a bit less than an hour. It is plausible to me that implementing and maintaining this system really will be hundreds of times less complex than it was at Facebook.
Test Plan:
My plan for this and D11497 is:
- Get them in master.
- Some secret key / relatively-hidden preference activates the column.
- Quicksand activates //only// when the column is open.
- We can use column + quicksand for a long period of time (i.e., over the course of Conpherence v2 development) and hammer out the long tail of issues.
- When it derps up, you just hide the column and you're good to go.
Reviewers: btrahan, chad
Reviewed By: chad
Subscribers: epriestley
Maniphest Tasks: T2086, T7014
Differential Revision: https://secure.phabricator.com/D11507
2015-01-27 23:52:09 +01:00
|
|
|
return $this->getExists(self::TYPE_QUICKSAND);
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function isConduit() {
|
2012-01-15 20:06:13 +01:00
|
|
|
return $this->getExists(self::TYPE_CONDUIT);
|
|
|
|
}
|
|
|
|
|
Prevent CSRF uploads via /file/dropupload/
Summary:
We don't currently validate CSRF tokens on this workflow. This allows an
attacker to upload arbitrary files on the user's behalf. Although I believe the
tight list of servable mime-types means that's more or less the end of the
attack, this is still a vulnerability.
In the long term, the right solution is probably to pass CSRF tokens on all Ajax
requests in an HTTP header (or just a GET param) or something like that.
However, this endpoint is unique and this is the quickest and most direct way to
close the hole.
Test Plan:
- Drop-uploaded files to Files, Maniphest, Phriction and Differential.
- Modified CSRF vaidator to use __csrf__.'x' and verified uploads and form
submissions don't work.
Reviewers: andrewjcg, aran, jungejason, tuomaspelkonen, erling
Commenters: andrewjcg, pedram
CC: aran, epriestley, andrewjcg, pedram
Differential Revision: 758
2011-08-02 05:23:01 +02:00
|
|
|
public static function getCSRFTokenName() {
|
|
|
|
return '__csrf__';
|
|
|
|
}
|
Fix conservative CSRF token cycling limit
Summary:
We currently cycle CSRF tokens every hour and check for the last two valid ones.
This means that a form could go stale in as little as an hour, and is certainly
stale after two.
When a stale form is submitted, you basically get a terrible heisen-state where
some of your data might persist if you're lucky but more likely it all just
vanishes. The .js file below outlines some more details.
This is a pretty terrible UX and we don't need to be as conservative about CSRF
validation as we're being. Remedy this problem by:
- Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are
valid for at least 6 hours, and for as long as 7).
- Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected
to the internet are valid indefinitely).
- Showing the user an explicit message about what went wrong when CSRF
validation fails so the experience is less bewildering.
They should now only be able to submit with a bad CSRF token if:
- They load a page, disconnect from the internet for 7 hours, reconnect, and
submit the form within 55 minutes; or
- They are actually the victim of a CSRF attack.
We could eventually fix the first one by tracking reconnects, which might be
"free" once the notification server gets built. It will probably never be an
issue in practice.
Test Plan:
- Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15
seconds, got the CSRF exception.
- Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15
seconds, got a clean form post.
- Added debugging code the the csrf refresh to make sure it was doing sensible
things (pulling different tokens, finding all the inputs).
Reviewed By: aran
Reviewers: tuomaspelkonen, jungejason, aran
CC: aran, epriestley
Differential Revision: 660
2011-07-13 23:05:18 +02:00
|
|
|
|
Prevent CSRF uploads via /file/dropupload/
Summary:
We don't currently validate CSRF tokens on this workflow. This allows an
attacker to upload arbitrary files on the user's behalf. Although I believe the
tight list of servable mime-types means that's more or less the end of the
attack, this is still a vulnerability.
In the long term, the right solution is probably to pass CSRF tokens on all Ajax
requests in an HTTP header (or just a GET param) or something like that.
However, this endpoint is unique and this is the quickest and most direct way to
close the hole.
Test Plan:
- Drop-uploaded files to Files, Maniphest, Phriction and Differential.
- Modified CSRF vaidator to use __csrf__.'x' and verified uploads and form
submissions don't work.
Reviewers: andrewjcg, aran, jungejason, tuomaspelkonen, erling
Commenters: andrewjcg, pedram
CC: aran, epriestley, andrewjcg, pedram
Differential Revision: 758
2011-08-02 05:23:01 +02:00
|
|
|
public static function getCSRFHeaderName() {
|
|
|
|
return 'X-Phabricator-Csrf';
|
|
|
|
}
|
|
|
|
|
When logged-out users hit a "Login Required" dialog, try to choose a better "next" URI
Summary:
Ref T10004. After a user logs in, we send them to the "next" URI cookie if there is one, but currently don't always do a very good job of selecting a "next" URI, especially if they tried to do something with a dialog before being asked to log in.
In particular, if a logged-out user clicks an action like "Edit Blocking Tasks" on a Maniphest task, the default behavior is to send them to the standalone page for that dialog after they log in. This can be pretty confusing.
See T2691 and D6416 for earlier efforts here. At that time, we added a mechanism to //manually// override the default behavior, and fixed the most common links. This worked, but I'd like to fix the //default// beahvior so we don't need to remember to `setObjectURI()` correctly all over the place.
ApplicationEditor has also introduced new cases which are more difficult to get right. While we could get them right by using the override and being careful about things, this also motivates fixing the default behavior.
Finally, we have better tools for fixing the default behavior now than we did in 2013.
Instead of using manual overrides, have JS include an "X-Phabricator-Via" header in Ajax requests. This is basically like a referrer header, and will contain the page the user's browser is on.
In essentially every case, this should be a very good place (and often the best place) to send them after login. For all pages currently using `setObjectURI()`, it should produce the same behavior by default.
I'll remove the `setObjectURI()` mechanism in the next diff.
Test Plan: Clicked various workflow actions while logged out, saw "next" get set to a reasonable value, was redirected to a sensible, non-confusing page after login (the page with whatever button I clicked on it).
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T10004
Differential Revision: https://secure.phabricator.com/D14804
2015-12-17 15:10:04 +01:00
|
|
|
public static function getViaHeaderName() {
|
|
|
|
return 'X-Phabricator-Via';
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function validateCSRF() {
|
Prevent CSRF uploads via /file/dropupload/
Summary:
We don't currently validate CSRF tokens on this workflow. This allows an
attacker to upload arbitrary files on the user's behalf. Although I believe the
tight list of servable mime-types means that's more or less the end of the
attack, this is still a vulnerability.
In the long term, the right solution is probably to pass CSRF tokens on all Ajax
requests in an HTTP header (or just a GET param) or something like that.
However, this endpoint is unique and this is the quickest and most direct way to
close the hole.
Test Plan:
- Drop-uploaded files to Files, Maniphest, Phriction and Differential.
- Modified CSRF vaidator to use __csrf__.'x' and verified uploads and form
submissions don't work.
Reviewers: andrewjcg, aran, jungejason, tuomaspelkonen, erling
Commenters: andrewjcg, pedram
CC: aran, epriestley, andrewjcg, pedram
Differential Revision: 758
2011-08-02 05:23:01 +02:00
|
|
|
$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)) {
|
2013-02-10 00:01:57 +01:00
|
|
|
$token = self::getHTTPHeader(self::getCSRFHeaderName());
|
Fix conservative CSRF token cycling limit
Summary:
We currently cycle CSRF tokens every hour and check for the last two valid ones.
This means that a form could go stale in as little as an hour, and is certainly
stale after two.
When a stale form is submitted, you basically get a terrible heisen-state where
some of your data might persist if you're lucky but more likely it all just
vanishes. The .js file below outlines some more details.
This is a pretty terrible UX and we don't need to be as conservative about CSRF
validation as we're being. Remedy this problem by:
- Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are
valid for at least 6 hours, and for as long as 7).
- Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected
to the internet are valid indefinitely).
- Showing the user an explicit message about what went wrong when CSRF
validation fails so the experience is less bewildering.
They should now only be able to submit with a bad CSRF token if:
- They load a page, disconnect from the internet for 7 hours, reconnect, and
submit the form within 55 minutes; or
- They are actually the victim of a CSRF attack.
We could eventually fix the first one by tracking reconnects, which might be
"free" once the notification server gets built. It will probably never be an
issue in practice.
Test Plan:
- Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15
seconds, got the CSRF exception.
- Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15
seconds, got a clean form post.
- Added debugging code the the csrf refresh to make sure it was doing sensible
things (pulling different tokens, finding all the inputs).
Reviewed By: aran
Reviewers: tuomaspelkonen, jungejason, aran
CC: aran, epriestley
Differential Revision: 660
2011-07-13 23:05:18 +02:00
|
|
|
}
|
|
|
|
|
Prevent CSRF uploads via /file/dropupload/
Summary:
We don't currently validate CSRF tokens on this workflow. This allows an
attacker to upload arbitrary files on the user's behalf. Although I believe the
tight list of servable mime-types means that's more or less the end of the
attack, this is still a vulnerability.
In the long term, the right solution is probably to pass CSRF tokens on all Ajax
requests in an HTTP header (or just a GET param) or something like that.
However, this endpoint is unique and this is the quickest and most direct way to
close the hole.
Test Plan:
- Drop-uploaded files to Files, Maniphest, Phriction and Differential.
- Modified CSRF vaidator to use __csrf__.'x' and verified uploads and form
submissions don't work.
Reviewers: andrewjcg, aran, jungejason, tuomaspelkonen, erling
Commenters: andrewjcg, pedram
CC: aran, epriestley, andrewjcg, pedram
Differential Revision: 758
2011-08-02 05:23:01 +02:00
|
|
|
$valid = $this->getUser()->validateCSRFToken($token);
|
Fix conservative CSRF token cycling limit
Summary:
We currently cycle CSRF tokens every hour and check for the last two valid ones.
This means that a form could go stale in as little as an hour, and is certainly
stale after two.
When a stale form is submitted, you basically get a terrible heisen-state where
some of your data might persist if you're lucky but more likely it all just
vanishes. The .js file below outlines some more details.
This is a pretty terrible UX and we don't need to be as conservative about CSRF
validation as we're being. Remedy this problem by:
- Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are
valid for at least 6 hours, and for as long as 7).
- Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected
to the internet are valid indefinitely).
- Showing the user an explicit message about what went wrong when CSRF
validation fails so the experience is less bewildering.
They should now only be able to submit with a bad CSRF token if:
- They load a page, disconnect from the internet for 7 hours, reconnect, and
submit the form within 55 minutes; or
- They are actually the victim of a CSRF attack.
We could eventually fix the first one by tracking reconnects, which might be
"free" once the notification server gets built. It will probably never be an
issue in practice.
Test Plan:
- Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15
seconds, got the CSRF exception.
- Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15
seconds, got a clean form post.
- Added debugging code the the csrf refresh to make sure it was doing sensible
things (pulling different tokens, finding all the inputs).
Reviewed By: aran
Reviewers: tuomaspelkonen, jungejason, aran
CC: aran, epriestley
Differential Revision: 660
2011-07-13 23:05:18 +02:00
|
|
|
if (!$valid) {
|
2011-09-01 18:29:33 +02:00
|
|
|
|
|
|
|
// 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.
|
2014-01-23 23:03:54 +01:00
|
|
|
$more_info = array();
|
|
|
|
|
|
|
|
if ($this->isAjax()) {
|
|
|
|
$more_info[] = pht('This was an Ajax request.');
|
2011-09-01 18:29:33 +02:00
|
|
|
} else {
|
2014-01-23 23:03:54 +01:00
|
|
|
$more_info[] = pht('This was a Web request.');
|
2011-09-01 18:29:33 +02:00
|
|
|
}
|
|
|
|
|
2014-01-23 23:03:54 +01:00
|
|
|
if ($token) {
|
|
|
|
$more_info[] = pht('This request had an invalid CSRF token.');
|
2011-09-01 18:29:33 +02:00
|
|
|
} else {
|
2014-01-23 23:03:54 +01:00
|
|
|
$more_info[] = pht('This request had no CSRF token.');
|
2011-09-01 18:29:33 +02:00
|
|
|
}
|
|
|
|
|
2013-02-01 18:34:06 +01:00
|
|
|
// Give a more detailed explanation of how to avoid the exception
|
|
|
|
// in developer mode.
|
|
|
|
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
|
2014-01-23 23:03:54 +01:00
|
|
|
// TODO: Clean this up, see T1921.
|
2015-05-22 09:27:56 +02:00
|
|
|
$more_info[] = pht(
|
|
|
|
"To avoid this error, use %s to construct forms. If you are already ".
|
|
|
|
"using %s, make sure the form 'action' uses a relative URI (i.e., ".
|
|
|
|
"begins with a '%s'). 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 %s to ".
|
|
|
|
"temporarily bypass CSRF protection while writing. You should use ".
|
|
|
|
"this only for writes which can not be protected with normal CSRF ".
|
2014-06-10 01:03:58 +02:00
|
|
|
"mechanisms.\n\n".
|
2015-05-22 09:27:56 +02:00
|
|
|
"Some UI elements (like %s) also have methods which will allow you ".
|
|
|
|
"to render links as forms (like %s).",
|
|
|
|
'phabricator_form()',
|
|
|
|
'phabricator_form()',
|
|
|
|
'/',
|
|
|
|
'AphrontWriteGuard::beginScopedUnguardedWrites()',
|
|
|
|
'PhabricatorActionListView',
|
|
|
|
'setRenderAsForm(true)');
|
2013-02-01 18:34:06 +01:00
|
|
|
}
|
|
|
|
|
Fix conservative CSRF token cycling limit
Summary:
We currently cycle CSRF tokens every hour and check for the last two valid ones.
This means that a form could go stale in as little as an hour, and is certainly
stale after two.
When a stale form is submitted, you basically get a terrible heisen-state where
some of your data might persist if you're lucky but more likely it all just
vanishes. The .js file below outlines some more details.
This is a pretty terrible UX and we don't need to be as conservative about CSRF
validation as we're being. Remedy this problem by:
- Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are
valid for at least 6 hours, and for as long as 7).
- Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected
to the internet are valid indefinitely).
- Showing the user an explicit message about what went wrong when CSRF
validation fails so the experience is less bewildering.
They should now only be able to submit with a bad CSRF token if:
- They load a page, disconnect from the internet for 7 hours, reconnect, and
submit the form within 55 minutes; or
- They are actually the victim of a CSRF attack.
We could eventually fix the first one by tracking reconnects, which might be
"free" once the notification server gets built. It will probably never be an
issue in practice.
Test Plan:
- Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15
seconds, got the CSRF exception.
- Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15
seconds, got a clean form post.
- Added debugging code the the csrf refresh to make sure it was doing sensible
things (pulling different tokens, finding all the inputs).
Reviewed By: aran
Reviewers: tuomaspelkonen, jungejason, aran
CC: aran, epriestley
Differential Revision: 660
2011-07-13 23:05:18 +02:00
|
|
|
// 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(
|
2014-01-23 23:03:54 +01:00
|
|
|
pht(
|
2015-05-22 09:27:56 +02:00
|
|
|
'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".
|
|
|
|
implode("\n", $more_info));
|
Fix conservative CSRF token cycling limit
Summary:
We currently cycle CSRF tokens every hour and check for the last two valid ones.
This means that a form could go stale in as little as an hour, and is certainly
stale after two.
When a stale form is submitted, you basically get a terrible heisen-state where
some of your data might persist if you're lucky but more likely it all just
vanishes. The .js file below outlines some more details.
This is a pretty terrible UX and we don't need to be as conservative about CSRF
validation as we're being. Remedy this problem by:
- Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are
valid for at least 6 hours, and for as long as 7).
- Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected
to the internet are valid indefinitely).
- Showing the user an explicit message about what went wrong when CSRF
validation fails so the experience is less bewildering.
They should now only be able to submit with a bad CSRF token if:
- They load a page, disconnect from the internet for 7 hours, reconnect, and
submit the form within 55 minutes; or
- They are actually the victim of a CSRF attack.
We could eventually fix the first one by tracking reconnects, which might be
"free" once the notification server gets built. It will probably never be an
issue in practice.
Test Plan:
- Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15
seconds, got the CSRF exception.
- Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15
seconds, got a clean form post.
- Added debugging code the the csrf refresh to make sure it was doing sensible
things (pulling different tokens, finding all the inputs).
Reviewed By: aran
Reviewers: tuomaspelkonen, jungejason, aran
CC: aran, epriestley
Differential Revision: 660
2011-07-13 23:05:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2011-01-16 22:51:39 +01:00
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function isFormPost() {
|
Prevent CSRF uploads via /file/dropupload/
Summary:
We don't currently validate CSRF tokens on this workflow. This allows an
attacker to upload arbitrary files on the user's behalf. Although I believe the
tight list of servable mime-types means that's more or less the end of the
attack, this is still a vulnerability.
In the long term, the right solution is probably to pass CSRF tokens on all Ajax
requests in an HTTP header (or just a GET param) or something like that.
However, this endpoint is unique and this is the quickest and most direct way to
close the hole.
Test Plan:
- Drop-uploaded files to Files, Maniphest, Phriction and Differential.
- Modified CSRF vaidator to use __csrf__.'x' and verified uploads and form
submissions don't work.
Reviewers: andrewjcg, aran, jungejason, tuomaspelkonen, erling
Commenters: andrewjcg, pedram
CC: aran, epriestley, andrewjcg, pedram
Differential Revision: 758
2011-08-02 05:23:01 +02:00
|
|
|
$post = $this->getExists(self::TYPE_FORM) &&
|
2014-04-28 02:31:11 +02:00
|
|
|
!$this->getExists(self::TYPE_HISEC) &&
|
Prevent CSRF uploads via /file/dropupload/
Summary:
We don't currently validate CSRF tokens on this workflow. This allows an
attacker to upload arbitrary files on the user's behalf. Although I believe the
tight list of servable mime-types means that's more or less the end of the
attack, this is still a vulnerability.
In the long term, the right solution is probably to pass CSRF tokens on all Ajax
requests in an HTTP header (or just a GET param) or something like that.
However, this endpoint is unique and this is the quickest and most direct way to
close the hole.
Test Plan:
- Drop-uploaded files to Files, Maniphest, Phriction and Differential.
- Modified CSRF vaidator to use __csrf__.'x' and verified uploads and form
submissions don't work.
Reviewers: andrewjcg, aran, jungejason, tuomaspelkonen, erling
Commenters: andrewjcg, pedram
CC: aran, epriestley, andrewjcg, pedram
Differential Revision: 758
2011-08-02 05:23:01 +02:00
|
|
|
$this->isHTTPPost();
|
|
|
|
|
|
|
|
if (!$post) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->validateCSRF();
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function isFormOrHisecPost() {
|
2014-06-29 15:16:48 +02:00
|
|
|
$post = $this->getExists(self::TYPE_FORM) &&
|
|
|
|
$this->isHTTPPost();
|
|
|
|
|
|
|
|
if (!$post) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->validateCSRF();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function setCookiePrefix($prefix) {
|
2014-01-17 17:07:57 +01:00
|
|
|
$this->cookiePrefix = $prefix;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
private function getPrefixedCookieName($name) {
|
2014-01-17 17:07:57 +01:00
|
|
|
if (strlen($this->cookiePrefix)) {
|
|
|
|
return $this->cookiePrefix.'_'.$name;
|
|
|
|
} else {
|
|
|
|
return $name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getCookie($name, $default = null) {
|
2014-01-17 17:07:57 +01:00
|
|
|
$name = $this->getPrefixedCookieName($name);
|
2014-03-14 22:33:31 +01:00
|
|
|
$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;
|
2011-01-26 22:21:12 +01:00
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function clearCookie($name) {
|
2014-03-14 22:33:31 +01:00
|
|
|
$this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
|
2013-06-16 19:18:56 +02:00
|
|
|
unset($_COOKIE[$name]);
|
2011-01-26 22:21:12 +01:00
|
|
|
}
|
|
|
|
|
Don't try to set anonymous session cookie on CDN/file domain
Summary:
Ref T2380. If an install has a CDN domain configured, but does not list it as an alternate domain (which is standard/correct, but not incredibly common, see T2380), we'll currently try to set anonymous cookies on it. These will correctly fail security rules.
Instead, don't try to set these cookies.
I missed this in testing yesterday because I have a file domain, but I also have it configured as an alternate domain, which allows cookies to be set. Generally, domain management is due for some refactoring.
Test Plan: Set file domain but not as an alternate, logged out, nuked file domain cookies, reloaded page. No error after patch.
Reviewers: btrahan, csilvers
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2380
Differential Revision: https://secure.phabricator.com/D8057
2014-01-24 21:29:03 +01:00
|
|
|
/**
|
|
|
|
* 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() {
|
2014-02-15 02:20:46 +01:00
|
|
|
if (PhabricatorEnv::getEnvConfig('security.require-https') &&
|
|
|
|
!$this->isHTTPS()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
Don't try to set anonymous session cookie on CDN/file domain
Summary:
Ref T2380. If an install has a CDN domain configured, but does not list it as an alternate domain (which is standard/correct, but not incredibly common, see T2380), we'll currently try to set anonymous cookies on it. These will correctly fail security rules.
Instead, don't try to set these cookies.
I missed this in testing yesterday because I have a file domain, but I also have it configured as an alternate domain, which allows cookies to be set. Generally, domain management is due for some refactoring.
Test Plan: Set file domain but not as an alternate, logged out, nuked file domain cookies, reloaded page. No error after patch.
Reviewers: btrahan, csilvers
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2380
Differential Revision: https://secure.phabricator.com/D8057
2014-01-24 21:29:03 +01:00
|
|
|
$host = $this->getHost();
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
|
Don't try to set anonymous session cookie on CDN/file domain
Summary:
Ref T2380. If an install has a CDN domain configured, but does not list it as an alternate domain (which is standard/correct, but not incredibly common, see T2380), we'll currently try to set anonymous cookies on it. These will correctly fail security rules.
Instead, don't try to set these cookies.
I missed this in testing yesterday because I have a file domain, but I also have it configured as an alternate domain, which allows cookies to be set. Generally, domain management is due for some refactoring.
Test Plan: Set file domain but not as an alternate, logged out, nuked file domain cookies, reloaded page. No error after patch.
Reviewers: btrahan, csilvers
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2380
Differential Revision: https://secure.phabricator.com/D8057
2014-01-24 21:29:03 +01:00
|
|
|
// 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.
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
|
Don't try to set anonymous session cookie on CDN/file domain
Summary:
Ref T2380. If an install has a CDN domain configured, but does not list it as an alternate domain (which is standard/correct, but not incredibly common, see T2380), we'll currently try to set anonymous cookies on it. These will correctly fail security rules.
Instead, don't try to set these cookies.
I missed this in testing yesterday because I have a file domain, but I also have it configured as an alternate domain, which allows cookies to be set. Generally, domain management is due for some refactoring.
Test Plan: Set file domain but not as an alternate, logged out, nuked file domain cookies, reloaded page. No error after patch.
Reviewers: btrahan, csilvers
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2380
Differential Revision: https://secure.phabricator.com/D8057
2014-01-24 21:29:03 +01:00
|
|
|
if (!strlen($base_uri)) {
|
|
|
|
return new PhutilURI('http://'.$host.'/');
|
|
|
|
}
|
|
|
|
|
|
|
|
$alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
|
|
|
|
$allowed_uris = array_merge(
|
|
|
|
array($base_uri),
|
|
|
|
$alternates);
|
2013-07-22 21:21:08 +02:00
|
|
|
|
Don't try to set anonymous session cookie on CDN/file domain
Summary:
Ref T2380. If an install has a CDN domain configured, but does not list it as an alternate domain (which is standard/correct, but not incredibly common, see T2380), we'll currently try to set anonymous cookies on it. These will correctly fail security rules.
Instead, don't try to set these cookies.
I missed this in testing yesterday because I have a file domain, but I also have it configured as an alternate domain, which allows cookies to be set. Generally, domain management is due for some refactoring.
Test Plan: Set file domain but not as an alternate, logged out, nuked file domain cookies, reloaded page. No error after patch.
Reviewers: btrahan, csilvers
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2380
Differential Revision: https://secure.phabricator.com/D8057
2014-01-24 21:29:03 +01:00
|
|
|
foreach ($allowed_uris as $allowed_uri) {
|
|
|
|
$uri = new PhutilURI($allowed_uri);
|
|
|
|
if ($uri->getDomain() == $host) {
|
|
|
|
return $uri;
|
2013-01-22 22:57:02 +01:00
|
|
|
}
|
Don't try to set anonymous session cookie on CDN/file domain
Summary:
Ref T2380. If an install has a CDN domain configured, but does not list it as an alternate domain (which is standard/correct, but not incredibly common, see T2380), we'll currently try to set anonymous cookies on it. These will correctly fail security rules.
Instead, don't try to set these cookies.
I missed this in testing yesterday because I have a file domain, but I also have it configured as an alternate domain, which allows cookies to be set. Generally, domain management is due for some refactoring.
Test Plan: Set file domain but not as an alternate, logged out, nuked file domain cookies, reloaded page. No error after patch.
Reviewers: btrahan, csilvers
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2380
Differential Revision: https://secure.phabricator.com/D8057
2014-01-24 21:29:03 +01:00
|
|
|
}
|
2013-01-22 22:57:02 +01:00
|
|
|
|
Don't try to set anonymous session cookie on CDN/file domain
Summary:
Ref T2380. If an install has a CDN domain configured, but does not list it as an alternate domain (which is standard/correct, but not incredibly common, see T2380), we'll currently try to set anonymous cookies on it. These will correctly fail security rules.
Instead, don't try to set these cookies.
I missed this in testing yesterday because I have a file domain, but I also have it configured as an alternate domain, which allows cookies to be set. Generally, domain management is due for some refactoring.
Test Plan: Set file domain but not as an alternate, logged out, nuked file domain cookies, reloaded page. No error after patch.
Reviewers: btrahan, csilvers
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2380
Differential Revision: https://secure.phabricator.com/D8057
2014-01-24 21:29:03 +01:00
|
|
|
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
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
public function canSetCookies() {
|
Don't try to set anonymous session cookie on CDN/file domain
Summary:
Ref T2380. If an install has a CDN domain configured, but does not list it as an alternate domain (which is standard/correct, but not incredibly common, see T2380), we'll currently try to set anonymous cookies on it. These will correctly fail security rules.
Instead, don't try to set these cookies.
I missed this in testing yesterday because I have a file domain, but I also have it configured as an alternate domain, which allows cookies to be set. Generally, domain management is due for some refactoring.
Test Plan: Set file domain but not as an alternate, logged out, nuked file domain cookies, reloaded page. No error after patch.
Reviewers: btrahan, csilvers
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2380
Differential Revision: https://secure.phabricator.com/D8057
2014-01-24 21:29:03 +01:00
|
|
|
return (bool)$this->getCookieDomainURI();
|
|
|
|
}
|
|
|
|
|
2014-03-14 22:33:31 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
public function setCookie($name, $value) {
|
2014-03-14 22:33:31 +01:00
|
|
|
$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
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
public function setTemporaryCookie($name, $value) {
|
2014-03-14 22:33:31 +01:00
|
|
|
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
|
|
|
|
*/
|
2015-05-19 23:06:07 +02:00
|
|
|
private function setCookieWithExpiration(
|
2014-03-14 22:33:31 +01:00
|
|
|
$name,
|
|
|
|
$value,
|
|
|
|
$expire) {
|
Don't try to set anonymous session cookie on CDN/file domain
Summary:
Ref T2380. If an install has a CDN domain configured, but does not list it as an alternate domain (which is standard/correct, but not incredibly common, see T2380), we'll currently try to set anonymous cookies on it. These will correctly fail security rules.
Instead, don't try to set these cookies.
I missed this in testing yesterday because I have a file domain, but I also have it configured as an alternate domain, which allows cookies to be set. Generally, domain management is due for some refactoring.
Test Plan: Set file domain but not as an alternate, logged out, nuked file domain cookies, reloaded page. No error after patch.
Reviewers: btrahan, csilvers
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2380
Differential Revision: https://secure.phabricator.com/D8057
2014-01-24 21:29:03 +01:00
|
|
|
|
|
|
|
$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 '.
|
2016-04-30 18:04:28 +02:00
|
|
|
'set a cookie. Access Phabricator on the configured primary '.
|
2014-02-05 20:02:41 +01:00
|
|
|
'domain or a configured alternate domain. Phabricator will not '.
|
Don't try to set anonymous session cookie on CDN/file domain
Summary:
Ref T2380. If an install has a CDN domain configured, but does not list it as an alternate domain (which is standard/correct, but not incredibly common, see T2380), we'll currently try to set anonymous cookies on it. These will correctly fail security rules.
Instead, don't try to set these cookies.
I missed this in testing yesterday because I have a file domain, but I also have it configured as an alternate domain, which allows cookies to be set. Generally, domain management is due for some refactoring.
Test Plan: Set file domain but not as an alternate, logged out, nuked file domain cookies, reloaded page. No error after patch.
Reviewers: btrahan, csilvers
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2380
Differential Revision: https://secure.phabricator.com/D8057
2014-01-24 21:29:03 +01:00
|
|
|
'set cookies on other domains for security reasons.',
|
|
|
|
$configured_as,
|
|
|
|
$accessed_as));
|
Provide a setting which forces all file views to be served from an alternate
domain
Summary:
See D758, D759.
- Provide a strongly recommended setting which permits configuration of an
alternate domain.
- Lock cookies down better: set them on the exact domain, and use SSL-only if
the configuration is HTTPS.
- Prevent Phabriator from setting cookies on other domains.
This assumes D759 will land, it is not effective without that change.
Test Plan:
- Attempted to login from a different domain and was rejected.
- Logged out, logged back in normally.
- Put install in setup mode and verified it revealed a warning.
- Configured an alterate domain.
- Tried to view an image with an old URI, got a 400.
- Went to /files/ and verified links rendered to the alternate domain.
- Viewed an alternate domain file.
- Tried to view an alternate domain file without the secret key, got a 404.
Reviewers: andrewjcg, erling, aran, tuomaspelkonen, jungejason, codeblock
CC: aran
Differential Revision: 760
2011-08-02 07:24:00 +02:00
|
|
|
}
|
|
|
|
|
Don't try to set anonymous session cookie on CDN/file domain
Summary:
Ref T2380. If an install has a CDN domain configured, but does not list it as an alternate domain (which is standard/correct, but not incredibly common, see T2380), we'll currently try to set anonymous cookies on it. These will correctly fail security rules.
Instead, don't try to set these cookies.
I missed this in testing yesterday because I have a file domain, but I also have it configured as an alternate domain, which allows cookies to be set. Generally, domain management is due for some refactoring.
Test Plan: Set file domain but not as an alternate, logged out, nuked file domain cookies, reloaded page. No error after patch.
Reviewers: btrahan, csilvers
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2380
Differential Revision: https://secure.phabricator.com/D8057
2014-01-24 21:29:03 +01:00
|
|
|
$base_domain = $base_domain_uri->getDomain();
|
|
|
|
$is_secure = ($base_domain_uri->getProtocol() == 'https');
|
|
|
|
|
2014-01-17 17:07:57 +01:00
|
|
|
$name = $this->getPrefixedCookieName($name);
|
2013-10-04 04:05:47 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2012-04-03 03:35:09 +02:00
|
|
|
|
2013-06-16 19:18:56 +02:00
|
|
|
$_COOKIE[$name] = $value;
|
|
|
|
|
2012-04-03 03:35:09 +02:00
|
|
|
return $this;
|
2011-01-26 22:21:12 +01:00
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function setUser($user) {
|
2011-01-26 22:21:12 +01:00
|
|
|
$this->user = $user;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getUser() {
|
2011-01-26 22:21:12 +01:00
|
|
|
return $this->user;
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getViewer() {
|
Decouple some aspects of request routing and construction
Summary:
Ref T5702. This is a forward-looking change which provides some very broad API improvements but does not implement them. In particular:
- Controllers no longer require `$request` to construct. This is mostly for T5702, directly, but simplifies things in general. Instead, we call `setRequest()` before using a controller. Only a small number of sites activate controllers, so this is less code overall, and more consistent with most constructors not having any parameters or effects.
- `$request` now offers `getURIData($key, ...)`. This is an alternate way of accessing `$data` which is currently only available on `willProcessRequest(array $data)`. Almost all controllers which implement this method do so in order to read one or two things out of the URI data. Instead, let them just read this data directly when processing the request.
- Introduce `handleRequest(AphrontRequest $request)` and deprecate (very softly) `processRequest()`. The majority of `processRequest()` calls begin `$request = $this->getRequest()`, which is avoided with the more practical signature.
- Provide `getViewer()` on `$request`, and a convenience `getViewer()` on `$controller`. This fixes `$viewer = $request->getUser();` into `$viewer = $request->getViewer();`, and converts the `$request + $viewer` two-liner into a single `$this->getViewer()`.
Test Plan:
- Browsed around in general.
- Hit special controllers (redirect, 404).
- Hit AuditList controller (uses new style).
Reviewers: btrahan
Reviewed By: btrahan
Subscribers: epriestley
Maniphest Tasks: T5702
Differential Revision: https://secure.phabricator.com/D10698
2014-10-17 14:01:40 +02:00
|
|
|
return $this->user;
|
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function getRequestURI() {
|
2011-02-06 01:43:28 +01:00
|
|
|
$get = $_GET;
|
|
|
|
unset($get['__path__']);
|
2012-03-28 01:53:47 +02:00
|
|
|
$path = phutil_escape_uri($this->getPath());
|
|
|
|
return id(new PhutilURI($path))->setQueryParams($get);
|
2011-02-06 01:43:28 +01:00
|
|
|
}
|
|
|
|
|
2015-05-19 23:06:07 +02:00
|
|
|
public function isDialogFormPost() {
|
2011-02-06 07:36:21 +01:00
|
|
|
return $this->isFormPost() && $this->getStr('__dialog__');
|
|
|
|
}
|
|
|
|
|
2016-02-02 18:59:49 +01:00
|
|
|
public function getRemoteAddress() {
|
|
|
|
$address = $_SERVER['REMOTE_ADDR'];
|
|
|
|
if (!strlen($address)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return substr($address, 0, 64);
|
Track content sources (email, web, conduit, mobile) for replies
Summary:
When an object is updated, record the content source for the update. This mostly
isn't terribly useful but one concrete thing I want to do with it is let admins
audit via-email replies more easily since there are a bunch of options which let
you do hyjinx if you intentionally configure them insecurely. I think having a
little more auditability around this feature is generally good. At some point
I'm going to turn this into a link admins can click to see details.
It also allows us to see how frequently different mechanisms are used, and lets
you see if someone is at their desk or on a mobile or whatever, at least
indirectly.
The "tablet" and "mobile" sources are currently unused but I figured I'd throw
them in anyway. SMS support should definitely happen at some point.
Not 100% sure about the design for this, I might change it to plain text at some
point.
Test Plan: Updated objects and saw update sources rendered.
Reviewers: jungejason, tuomaspelkonen, aran
Reviewed By: jungejason
CC: aran, epriestley, jungejason
Differential Revision: 844
2011-08-22 19:25:45 +02:00
|
|
|
}
|
|
|
|
|
2012-09-04 18:56:30 +02:00
|
|
|
public function isHTTPS() {
|
|
|
|
if (empty($_SERVER['HTTPS'])) {
|
|
|
|
return false;
|
|
|
|
}
|
2014-06-09 20:36:49 +02:00
|
|
|
if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
|
2012-09-04 18:56:30 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
Add passthru to AphrontRequest
Summary:
For transaction interfaces, I want to prompt the user when they take an action that has no effect, e.g.:
Action Has No Effect
You can not close this task, because someone else has already closed it.
Do you want to post your comment anyway?
[Cancel] [Post Comment]
We already do this for Differential, but it's all hard-coded. T912 is an open task for fixing this for Maniphest.
To do this in a general way, I want to embed the entire request in the dialog as hidden inputs, then add a "__continue__" key and resubmit the form. The endpoint will read this key the second time through and apply what effects it can (e.g., just post a comment).
This adds a mechanism for getting all the request data, minus "magic" like __dialog__ and __csrf__. We need to jump through some hoops because of how PHP encodes arrays.
Test Plan: Ran unit tests, built "no effect" dialogs on top of this.
Reviewers: btrahan, vrana
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T912, T2104
Differential Revision: https://secure.phabricator.com/D4158
2012-12-12 02:27:02 +01:00
|
|
|
public function isContinueRequest() {
|
|
|
|
return $this->isFormPost() && $this->getStr('__continue__');
|
|
|
|
}
|
|
|
|
|
2012-12-21 14:51:33 +01:00
|
|
|
public function isPreviewRequest() {
|
|
|
|
return $this->isFormPost() && $this->getStr('__preview__');
|
|
|
|
}
|
|
|
|
|
Add passthru to AphrontRequest
Summary:
For transaction interfaces, I want to prompt the user when they take an action that has no effect, e.g.:
Action Has No Effect
You can not close this task, because someone else has already closed it.
Do you want to post your comment anyway?
[Cancel] [Post Comment]
We already do this for Differential, but it's all hard-coded. T912 is an open task for fixing this for Maniphest.
To do this in a general way, I want to embed the entire request in the dialog as hidden inputs, then add a "__continue__" key and resubmit the form. The endpoint will read this key the second time through and apply what effects it can (e.g., just post a comment).
This adds a mechanism for getting all the request data, minus "magic" like __dialog__ and __csrf__. We need to jump through some hoops because of how PHP encodes arrays.
Test Plan: Ran unit tests, built "no effect" dialogs on top of this.
Reviewers: btrahan, vrana
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T912, T2104
Differential Revision: https://secure.phabricator.com/D4158
2012-12-12 02:27:02 +01:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2015-04-24 01:37:56 +02:00
|
|
|
public function getPassthroughRequestParameters($include_quicksand = false) {
|
|
|
|
return self::flattenData(
|
|
|
|
$this->getPassthroughRequestData($include_quicksand));
|
2012-12-21 14:57:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get request data other than "magic" parameters.
|
|
|
|
*
|
|
|
|
* @return dict<string, wild> Request data, with magic filtered out.
|
|
|
|
*/
|
2015-04-24 01:37:56 +02:00
|
|
|
public function getPassthroughRequestData($include_quicksand = false) {
|
2012-12-21 14:57:14 +01:00
|
|
|
$data = $this->getRequestData();
|
Add passthru to AphrontRequest
Summary:
For transaction interfaces, I want to prompt the user when they take an action that has no effect, e.g.:
Action Has No Effect
You can not close this task, because someone else has already closed it.
Do you want to post your comment anyway?
[Cancel] [Post Comment]
We already do this for Differential, but it's all hard-coded. T912 is an open task for fixing this for Maniphest.
To do this in a general way, I want to embed the entire request in the dialog as hidden inputs, then add a "__continue__" key and resubmit the form. The endpoint will read this key the second time through and apply what effects it can (e.g., just post a comment).
This adds a mechanism for getting all the request data, minus "magic" like __dialog__ and __csrf__. We need to jump through some hoops because of how PHP encodes arrays.
Test Plan: Ran unit tests, built "no effect" dialogs on top of this.
Reviewers: btrahan, vrana
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T912, T2104
Differential Revision: https://secure.phabricator.com/D4158
2012-12-12 02:27:02 +01:00
|
|
|
|
|
|
|
// Remove magic parameters like __dialog__ and __ajax__.
|
|
|
|
foreach ($data as $key => $value) {
|
2015-04-24 01:37:56 +02:00
|
|
|
if ($include_quicksand && $key == self::TYPE_QUICKSAND) {
|
|
|
|
continue;
|
|
|
|
}
|
Allow construction of ApplicationSearch queries with GET
Summary:
Ref T3775 (discussion here). Ref T2625.
T3775 presents two problems:
# Existing tools which linked to `/differential/active/epriestley/` (that is, put a username in the URL) can't generate search links now.
# Humans can't edit the URL anymore, either.
I think (1) is an actual issue, and this fixes it. I think (2) is pretty fluff, and this doesn't really try to fix it, although it probably improves it.
The fix for (1) is:
- Provide a helper to read a parameter containing either a list of user PHIDs or a list of usernames, so `/?users[]=PHID-USER-xyz` (from a tokenizer) and `/?users=alincoln,htaft` (from an external program) are equivalent inputs.
- Rename all the form parameters to be more digestable (`authorPHIDs` -> `authors`). Almost all of them were in this form already anyway. This just gives us `?users=alincoln` instead of `userPHIDs=alincoln`.
- Inside ApplicationSearch, if a request has no query associated with it but does have query parameters, build a query from the request instead of issuing the user's default query. Basically, this means that `/differential/` runs the default query, while `/differential/?users=x` runs a custom query.
Test Plan: {F56612}
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2625, T3775
Differential Revision: https://secure.phabricator.com/D6840
2013-08-29 20:52:29 +02:00
|
|
|
if (!strncmp($key, '__', 2)) {
|
Add passthru to AphrontRequest
Summary:
For transaction interfaces, I want to prompt the user when they take an action that has no effect, e.g.:
Action Has No Effect
You can not close this task, because someone else has already closed it.
Do you want to post your comment anyway?
[Cancel] [Post Comment]
We already do this for Differential, but it's all hard-coded. T912 is an open task for fixing this for Maniphest.
To do this in a general way, I want to embed the entire request in the dialog as hidden inputs, then add a "__continue__" key and resubmit the form. The endpoint will read this key the second time through and apply what effects it can (e.g., just post a comment).
This adds a mechanism for getting all the request data, minus "magic" like __dialog__ and __csrf__. We need to jump through some hoops because of how PHP encodes arrays.
Test Plan: Ran unit tests, built "no effect" dialogs on top of this.
Reviewers: btrahan, vrana
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T912, T2104
Differential Revision: https://secure.phabricator.com/D4158
2012-12-12 02:27:02 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-11-11 21:17:34 +01:00
|
|
|
/**
|
|
|
|
* 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) {
|
2013-02-10 00:01:57 +01:00
|
|
|
// 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);
|
|
|
|
|
2013-11-11 21:17:34 +01:00
|
|
|
$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;
|
2013-02-10 00:01:57 +01:00
|
|
|
}
|
|
|
|
|
2015-01-27 23:51:09 +01:00
|
|
|
|
|
|
|
/* -( 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() {
|
2015-05-13 22:50:28 +02:00
|
|
|
return (bool)self::getHTTPHeader('X-Phabricator-Cluster');
|
2015-01-27 23:51:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2016-04-09 12:39:17 +02:00
|
|
|
if (!preg_match('/^HTTP_/', $key)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unmangle the header as best we can.
|
|
|
|
$key = substr($key, strlen('HTTP_'));
|
|
|
|
$key = str_replace('_', ' ', $key);
|
|
|
|
$key = strtolower($key);
|
|
|
|
$key = ucwords($key);
|
|
|
|
$key = str_replace(' ', '-', $key);
|
|
|
|
|
|
|
|
// By default, do not forward headers.
|
|
|
|
$should_forward = false;
|
|
|
|
|
|
|
|
// Forward "X-Hgarg-..." headers.
|
|
|
|
if (preg_match('/^X-Hgarg-/', $key)) {
|
|
|
|
$should_forward = true;
|
|
|
|
}
|
2015-01-27 23:51:09 +01:00
|
|
|
|
2016-04-09 12:39:17 +02:00
|
|
|
if ($should_forward) {
|
2015-01-27 23:51:09 +01:00
|
|
|
$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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-01-16 22:51:39 +01:00
|
|
|
}
|