mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-27 06:58:17 +01:00
5c71da8cdb
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
289 lines
8.8 KiB
PHP
289 lines
8.8 KiB
PHP
<?php
|
|
|
|
/**
|
|
* NOTE: Do not extend this!
|
|
*
|
|
* @concrete-extensible
|
|
*/
|
|
class AphrontDefaultApplicationConfiguration
|
|
extends AphrontApplicationConfiguration {
|
|
|
|
public function __construct() {}
|
|
|
|
public function getApplicationName() {
|
|
return 'aphront-default';
|
|
}
|
|
|
|
/**
|
|
* @phutil-external-symbol class PhabricatorStartup
|
|
*/
|
|
public function buildRequest() {
|
|
$parser = new PhutilQueryStringParser();
|
|
$data = array();
|
|
|
|
// If the request has "multipart/form-data" content, we can't use
|
|
// PhutilQueryStringParser to parse it, and the raw data supposedly is not
|
|
// available anyway (according to the PHP documentation, "php://input" is
|
|
// not available for "multipart/form-data" requests). However, it is
|
|
// available at least some of the time (see T3673), so double check that
|
|
// we aren't trying to parse data we won't be able to parse correctly by
|
|
// examining the Content-Type header.
|
|
$content_type = idx($_SERVER, 'CONTENT_TYPE');
|
|
$is_form_data = preg_match('@^multipart/form-data@i', $content_type);
|
|
|
|
$raw_input = PhabricatorStartup::getRawInput();
|
|
if (strlen($raw_input) && !$is_form_data) {
|
|
$data += $parser->parseQueryString($raw_input);
|
|
} else if ($_POST) {
|
|
$data += $_POST;
|
|
}
|
|
|
|
$data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));
|
|
|
|
$cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix');
|
|
|
|
$request = new AphrontRequest($this->getHost(), $this->getPath());
|
|
$request->setRequestData($data);
|
|
$request->setApplicationConfiguration($this);
|
|
$request->setCookiePrefix($cookie_prefix);
|
|
|
|
return $request;
|
|
}
|
|
|
|
public function handleException(Exception $ex) {
|
|
$request = $this->getRequest();
|
|
|
|
// For Conduit requests, return a Conduit response.
|
|
if ($request->isConduit()) {
|
|
$response = new ConduitAPIResponse();
|
|
$response->setErrorCode(get_class($ex));
|
|
$response->setErrorInfo($ex->getMessage());
|
|
|
|
return id(new AphrontJSONResponse())
|
|
->setAddJSONShield(false)
|
|
->setContent($response->toDictionary());
|
|
}
|
|
|
|
// For non-workflow requests, return a Ajax response.
|
|
if ($request->isAjax() && !$request->isWorkflow()) {
|
|
// Log these; they don't get shown on the client and can be difficult
|
|
// to debug.
|
|
phlog($ex);
|
|
|
|
$response = new AphrontAjaxResponse();
|
|
$response->setError(
|
|
array(
|
|
'code' => get_class($ex),
|
|
'info' => $ex->getMessage(),
|
|
));
|
|
return $response;
|
|
}
|
|
|
|
$user = $request->getUser();
|
|
if (!$user) {
|
|
// If we hit an exception very early, we won't have a user.
|
|
$user = new PhabricatorUser();
|
|
}
|
|
|
|
if ($ex instanceof PhabricatorSystemActionRateLimitException) {
|
|
$dialog = id(new AphrontDialogView())
|
|
->setTitle(pht('Slow Down!'))
|
|
->setUser($user)
|
|
->setErrors(array(pht('You are being rate limited.')))
|
|
->appendParagraph($ex->getMessage())
|
|
->appendParagraph($ex->getRateExplanation())
|
|
->addCancelButton('/', pht('Okaaaaaaaaaaaaaay...'));
|
|
|
|
$response = new AphrontDialogResponse();
|
|
$response->setDialog($dialog);
|
|
return $response;
|
|
}
|
|
|
|
if ($ex instanceof PhabricatorAuthHighSecurityRequiredException) {
|
|
|
|
$form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm(
|
|
$ex->getFactors(),
|
|
$ex->getFactorValidationResults(),
|
|
$user,
|
|
$request);
|
|
|
|
$dialog = id(new AphrontDialogView())
|
|
->setUser($user)
|
|
->setTitle(pht('Entering High Security'))
|
|
->setShortTitle(pht('Security Checkpoint'))
|
|
->setWidth(AphrontDialogView::WIDTH_FORM)
|
|
->addHiddenInput(AphrontRequest::TYPE_HISEC, true)
|
|
->setErrors(
|
|
array(
|
|
pht(
|
|
'You are taking an action which requires you to enter '.
|
|
'high security.'),
|
|
))
|
|
->appendParagraph(
|
|
pht(
|
|
'High security mode helps protect your account from security '.
|
|
'threats, like session theft or someone messing with your stuff '.
|
|
'while you\'re grabbing a coffee. To enter high security mode, '.
|
|
'confirm your credentials.'))
|
|
->appendChild($form->buildLayoutView())
|
|
->appendParagraph(
|
|
pht(
|
|
'Your account will remain in high security mode for a short '.
|
|
'period of time. When you are finished taking sensitive '.
|
|
'actions, you should leave high security.'))
|
|
->setSubmitURI($request->getPath())
|
|
->addCancelButton($ex->getCancelURI())
|
|
->addSubmitButton(pht('Enter High Security'));
|
|
|
|
foreach ($request->getPassthroughRequestParameters() as $key => $value) {
|
|
$dialog->addHiddenInput($key, $value);
|
|
}
|
|
|
|
$response = new AphrontDialogResponse();
|
|
$response->setDialog($dialog);
|
|
return $response;
|
|
}
|
|
|
|
if ($ex instanceof PhabricatorPolicyException) {
|
|
if (!$user->isLoggedIn()) {
|
|
// If the user isn't logged in, just give them a login form. This is
|
|
// probably a generally more useful response than a policy dialog that
|
|
// they have to click through to get a login form.
|
|
//
|
|
// Possibly we should add a header here like "you need to login to see
|
|
// the thing you are trying to look at".
|
|
$login_controller = new PhabricatorAuthStartController();
|
|
$login_controller->setRequest($request);
|
|
|
|
$auth_app_class = 'PhabricatorAuthApplication';
|
|
$auth_app = PhabricatorApplication::getByClass($auth_app_class);
|
|
$login_controller->setCurrentApplication($auth_app);
|
|
|
|
return $login_controller->handleRequest($request);
|
|
}
|
|
|
|
$list = $ex->getMoreInfo();
|
|
foreach ($list as $key => $item) {
|
|
$list[$key] = phutil_tag('li', array(), $item);
|
|
}
|
|
if ($list) {
|
|
$list = phutil_tag('ul', array(), $list);
|
|
}
|
|
|
|
$content = array(
|
|
phutil_tag(
|
|
'div',
|
|
array(
|
|
'class' => 'aphront-policy-rejection',
|
|
),
|
|
$ex->getRejection()),
|
|
phutil_tag(
|
|
'div',
|
|
array(
|
|
'class' => 'aphront-capability-details',
|
|
),
|
|
pht('Users with the "%s" capability:', $ex->getCapabilityName())),
|
|
$list,
|
|
);
|
|
|
|
$dialog = new AphrontDialogView();
|
|
$dialog
|
|
->setTitle($ex->getTitle())
|
|
->setClass('aphront-access-dialog')
|
|
->setUser($user)
|
|
->appendChild($content);
|
|
|
|
if ($this->getRequest()->isAjax()) {
|
|
$dialog->addCancelButton('/', pht('Close'));
|
|
} else {
|
|
$dialog->addCancelButton('/', pht('OK'));
|
|
}
|
|
|
|
$response = new AphrontDialogResponse();
|
|
$response->setDialog($dialog);
|
|
return $response;
|
|
}
|
|
|
|
if ($ex instanceof AphrontUsageException) {
|
|
$error = new AphrontErrorView();
|
|
$error->setTitle($ex->getTitle());
|
|
$error->appendChild($ex->getMessage());
|
|
|
|
$view = new PhabricatorStandardPageView();
|
|
$view->setRequest($this->getRequest());
|
|
$view->appendChild($error);
|
|
|
|
$response = new AphrontWebpageResponse();
|
|
$response->setContent($view->render());
|
|
$response->setHTTPResponseCode(500);
|
|
|
|
return $response;
|
|
}
|
|
|
|
// Always log the unhandled exception.
|
|
phlog($ex);
|
|
|
|
$class = get_class($ex);
|
|
$message = $ex->getMessage();
|
|
|
|
if ($ex instanceof AphrontSchemaQueryException) {
|
|
$message .=
|
|
"\n\n".
|
|
"NOTE: This usually indicates that the MySQL schema has not been ".
|
|
"properly upgraded. Run 'bin/storage upgrade' to ensure your ".
|
|
"schema is up to date.";
|
|
}
|
|
|
|
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
|
|
$trace = id(new AphrontStackTraceView())
|
|
->setUser($user)
|
|
->setTrace($ex->getTrace());
|
|
} else {
|
|
$trace = null;
|
|
}
|
|
|
|
$content = phutil_tag(
|
|
'div',
|
|
array('class' => 'aphront-unhandled-exception'),
|
|
array(
|
|
phutil_tag('div', array('class' => 'exception-message'), $message),
|
|
$trace,
|
|
));
|
|
|
|
$dialog = new AphrontDialogView();
|
|
$dialog
|
|
->setTitle('Unhandled Exception ("'.$class.'")')
|
|
->setClass('aphront-exception-dialog')
|
|
->setUser($user)
|
|
->appendChild($content);
|
|
|
|
if ($this->getRequest()->isAjax()) {
|
|
$dialog->addCancelButton('/', 'Close');
|
|
}
|
|
|
|
$response = new AphrontDialogResponse();
|
|
$response->setDialog($dialog);
|
|
$response->setHTTPResponseCode(500);
|
|
|
|
return $response;
|
|
}
|
|
|
|
public function willSendResponse(AphrontResponse $response) {
|
|
return $response;
|
|
}
|
|
|
|
public function build404Controller() {
|
|
return array(new Phabricator404Controller(), array());
|
|
}
|
|
|
|
public function buildRedirectController($uri, $external) {
|
|
return array(
|
|
new PhabricatorRedirectController(),
|
|
array(
|
|
'uri' => $uri,
|
|
'external' => $external,
|
|
),
|
|
);
|
|
}
|
|
|
|
}
|