Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php
index 48004a521f..95534c048d 100644
--- a/src/aphront/AphrontRequest.php
+++ b/src/aphront/AphrontRequest.php
@@ -1,909 +1,918 @@
<?php
/**
* @task data Accessing Request Data
* @task cookie Managing Cookies
* @task cluster Working With a Phabricator Cluster
*/
final class AphrontRequest extends Phobject {
// NOTE: These magic request-type parameters are automatically included in
// certain requests (e.g., by phabricator_form(), JX.Request,
// JX.Workflow, and ConduitClient) and help us figure out what sort of
// response the client expects.
const TYPE_AJAX = '__ajax__';
const TYPE_FORM = '__form__';
const TYPE_CONDUIT = '__conduit__';
const TYPE_WORKFLOW = '__wflow__';
const TYPE_CONTINUE = '__continue__';
const TYPE_PREVIEW = '__preview__';
const TYPE_HISEC = '__hisec__';
const TYPE_QUICKSAND = '__quicksand__';
private $host;
private $path;
private $requestData;
private $user;
private $applicationConfiguration;
private $site;
private $controller;
private $uriData = array();
private $cookiePrefix;
public function __construct($host, $path) {
$this->host = $host;
$this->path = $path;
}
public function setURIMap(array $uri_data) {
$this->uriData = $uri_data;
return $this;
}
public function getURIMap() {
return $this->uriData;
}
public function getURIData($key, $default = null) {
return idx($this->uriData, $key, $default);
}
/**
* Read line range parameter data from the request.
*
* Applications like Paste, Diffusion, and Harbormaster use "$12-14" in the
* URI to allow users to link to particular lines.
*
* @param string URI data key to pull line range information from.
* @param int|null Maximum length of the range.
* @return null|pair<int, int> Null, or beginning and end of the range.
*/
public function getURILineRange($key, $limit) {
$range = $this->getURIData($key);
return self::parseURILineRange($range, $limit);
}
public static function parseURILineRange($range, $limit) {
if (!strlen($range)) {
return null;
}
$range = explode('-', $range, 2);
foreach ($range as $key => $value) {
$value = (int)$value;
if (!$value) {
// If either value is "0", discard the range.
return null;
}
$range[$key] = $value;
}
// If the range is like "$10", treat it like "$10-10".
if (count($range) == 1) {
$range[] = head($range);
}
// If the range is "$7-5", treat it like "$5-7".
if ($range[1] < $range[0]) {
$range = array_reverse($range);
}
// If the user specified something like "$1-999999999" and we have a limit,
// clamp it to a more reasonable range.
if ($limit !== null) {
if ($range[1] - $range[0] > $limit) {
$range[1] = $range[0] + $limit;
}
}
return $range;
}
public function setApplicationConfiguration(
$application_configuration) {
$this->applicationConfiguration = $application_configuration;
return $this;
}
public function getApplicationConfiguration() {
return $this->applicationConfiguration;
}
public function setPath($path) {
$this->path = $path;
return $this;
}
public function getPath() {
return $this->path;
}
public function getHost() {
// The "Host" header may include a port number, or may be a malicious
// header in the form "realdomain.com:ignored@evil.com". Invoke the full
// parser to extract the real domain correctly. See here for coverage of
// a similar issue in Django:
//
// https://www.djangoproject.com/weblog/2012/oct/17/security/
$uri = new PhutilURI('http://'.$this->host);
return $uri->getDomain();
}
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;
}
/* -( Accessing Request Data )--------------------------------------------- */
/**
* @task data
*/
public function setRequestData(array $request_data) {
$this->requestData = $request_data;
return $this;
}
/**
* @task data
*/
public function getRequestData() {
return $this->requestData;
}
/**
* @task data
*/
public function getInt($name, $default = null) {
if (isset($this->requestData[$name])) {
// Converting from array to int is "undefined". Don't rely on whatever
// PHP decides to do.
if (is_array($this->requestData[$name])) {
return $default;
}
return (int)$this->requestData[$name];
} else {
return $default;
}
}
/**
* @task data
*/
public function getBool($name, $default = null) {
if (isset($this->requestData[$name])) {
if ($this->requestData[$name] === 'true') {
return true;
} else if ($this->requestData[$name] === 'false') {
return false;
} else {
return (bool)$this->requestData[$name];
}
} else {
return $default;
}
}
/**
* @task data
*/
public function getStr($name, $default = null) {
if (isset($this->requestData[$name])) {
$str = (string)$this->requestData[$name];
// Normalize newline craziness.
$str = str_replace(
array("\r\n", "\r"),
array("\n", "\n"),
$str);
return $str;
} else {
return $default;
}
}
/**
* @task data
*/
public function getArr($name, $default = array()) {
if (isset($this->requestData[$name]) &&
is_array($this->requestData[$name])) {
return $this->requestData[$name];
} else {
return $default;
}
}
/**
* @task data
*/
public function getStrList($name, $default = array()) {
if (!isset($this->requestData[$name])) {
return $default;
}
$list = $this->getStr($name);
$list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
return $list;
}
/**
* @task data
*/
public function getExists($name) {
return array_key_exists($name, $this->requestData);
}
public function getFileExists($name) {
return isset($_FILES[$name]) &&
(idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
}
public function isHTTPGet() {
return ($_SERVER['REQUEST_METHOD'] == 'GET');
}
public function isHTTPPost() {
return ($_SERVER['REQUEST_METHOD'] == 'POST');
}
public function isAjax() {
return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand();
}
public function isWorkflow() {
return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand();
}
public function isQuicksand() {
return $this->getExists(self::TYPE_QUICKSAND);
}
public function isConduit() {
return $this->getExists(self::TYPE_CONDUIT);
}
public static function getCSRFTokenName() {
return '__csrf__';
}
public static function getCSRFHeaderName() {
return 'X-Phabricator-Csrf';
}
public static function getViaHeaderName() {
return 'X-Phabricator-Via';
}
public function validateCSRF() {
$token_name = self::getCSRFTokenName();
$token = $this->getStr($token_name);
// No token in the request, check the HTTP header which is added for Ajax
// requests.
if (empty($token)) {
$token = self::getHTTPHeader(self::getCSRFHeaderName());
}
$valid = $this->getUser()->validateCSRFToken($token);
if (!$valid) {
// Add some diagnostic details so we can figure out if some CSRF issues
// are JS problems or people accessing Ajax URIs directly with their
// browsers.
$info = array();
$info[] = pht(
'You are trying to save some data to Phabricator, but the request '.
'your browser made included an incorrect token. Reload the page '.
'and try again. You may need to clear your cookies.');
if ($this->isAjax()) {
$info[] = pht('This was an Ajax request.');
} else {
$info[] = pht('This was a Web request.');
}
if ($token) {
$info[] = pht('This request had an invalid CSRF token.');
} else {
$info[] = pht('This request had no CSRF token.');
}
// Give a more detailed explanation of how to avoid the exception
// in developer mode.
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
// TODO: Clean this up, see T1921.
$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 ".
"mechanisms.\n\n".
"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)');
}
$message = implode("\n", $info);
// 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 AphrontMalformedRequestException(
pht('Invalid Request (CSRF)'),
$message,
true);
}
return true;
}
public function isFormPost() {
$post = $this->getExists(self::TYPE_FORM) &&
!$this->getExists(self::TYPE_HISEC) &&
$this->isHTTPPost();
if (!$post) {
return false;
}
return $this->validateCSRF();
}
+ public function hasCSRF() {
+ try {
+ $this->validateCSRF();
+ return true;
+ } catch (AphrontMalformedRequestException $ex) {
+ return false;
+ }
+ }
+
public function isFormOrHisecPost() {
$post = $this->getExists(self::TYPE_FORM) &&
$this->isHTTPPost();
if (!$post) {
return false;
}
return $this->validateCSRF();
}
public function setCookiePrefix($prefix) {
$this->cookiePrefix = $prefix;
return $this;
}
private function getPrefixedCookieName($name) {
if (strlen($this->cookiePrefix)) {
return $this->cookiePrefix.'_'.$name;
} else {
return $name;
}
}
public function getCookie($name, $default = null) {
$name = $this->getPrefixedCookieName($name);
$value = idx($_COOKIE, $name, $default);
// Internally, PHP deletes cookies by setting them to the value 'deleted'
// with an expiration date in the past.
// At least in Safari, the browser may send this cookie anyway in some
// circumstances. After logging out, the 302'd GET to /login/ consistently
// includes deleted cookies on my local install. If a cookie value is
// literally 'deleted', pretend it does not exist.
if ($value === 'deleted') {
return null;
}
return $value;
}
public function clearCookie($name) {
$this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
unset($_COOKIE[$name]);
}
/**
* Get the domain which cookies should be set on for this request, or null
* if the request does not correspond to a valid cookie domain.
*
* @return PhutilURI|null Domain URI, or null if no valid domain exists.
*
* @task cookie
*/
private function getCookieDomainURI() {
if (PhabricatorEnv::getEnvConfig('security.require-https') &&
!$this->isHTTPS()) {
return null;
}
$host = $this->getHost();
// If there's no base domain configured, just use whatever the request
// domain is. This makes setup easier, and we'll tell administrators to
// configure a base domain during the setup process.
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
if (!strlen($base_uri)) {
return new PhutilURI('http://'.$host.'/');
}
$alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
$allowed_uris = array_merge(
array($base_uri),
$alternates);
foreach ($allowed_uris as $allowed_uri) {
$uri = new PhutilURI($allowed_uri);
if ($uri->getDomain() == $host) {
return $uri;
}
}
return null;
}
/**
* Determine if security policy rules will allow cookies to be set when
* responding to the request.
*
* @return bool True if setCookie() will succeed. If this method returns
* false, setCookie() will throw.
*
* @task cookie
*/
public function canSetCookies() {
return (bool)$this->getCookieDomainURI();
}
/**
* Set a cookie which does not expire for a long time.
*
* To set a temporary cookie, see @{method:setTemporaryCookie}.
*
* @param string Cookie name.
* @param string Cookie value.
* @return this
* @task cookie
*/
public function setCookie($name, $value) {
$far_future = time() + (60 * 60 * 24 * 365 * 5);
return $this->setCookieWithExpiration($name, $value, $far_future);
}
/**
* Set a cookie which expires soon.
*
* To set a durable cookie, see @{method:setCookie}.
*
* @param string Cookie name.
* @param string Cookie value.
* @return this
* @task cookie
*/
public function setTemporaryCookie($name, $value) {
return $this->setCookieWithExpiration($name, $value, 0);
}
/**
* Set a cookie with a given expiration policy.
*
* @param string Cookie name.
* @param string Cookie value.
* @param int Epoch timestamp for cookie expiration.
* @return this
* @task cookie
*/
private function setCookieWithExpiration(
$name,
$value,
$expire) {
$is_secure = false;
$base_domain_uri = $this->getCookieDomainURI();
if (!$base_domain_uri) {
$configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$accessed_as = $this->getHost();
throw new AphrontMalformedRequestException(
pht('Bad Host Header'),
pht(
'This Phabricator install is configured as "%s", but you are '.
'using the domain name "%s" to access a page which is trying to '.
'set a cookie. Access Phabricator on the configured primary '.
'domain or a configured alternate domain. Phabricator will not '.
'set cookies on other domains for security reasons.',
$configured_as,
$accessed_as),
true);
}
$base_domain = $base_domain_uri->getDomain();
$is_secure = ($base_domain_uri->getProtocol() == 'https');
$name = $this->getPrefixedCookieName($name);
if (php_sapi_name() == 'cli') {
// Do nothing, to avoid triggering "Cannot modify header information"
// warnings.
// TODO: This is effectively a test for whether we're running in a unit
// test or not. Move this actual call to HTTPSink?
} else {
setcookie(
$name,
$value,
$expire,
$path = '/',
$base_domain,
$is_secure,
$http_only = true);
}
$_COOKIE[$name] = $value;
return $this;
}
public function setUser($user) {
$this->user = $user;
return $this;
}
public function getUser() {
return $this->user;
}
public function getViewer() {
return $this->user;
}
public function getRequestURI() {
$uri_path = phutil_escape_uri($this->getPath());
$uri_query = idx($_SERVER, 'QUERY_STRING', '');
return id(new PhutilURI($uri_path.'?'.$uri_query))
->removeQueryParam('__path__');
}
public function getAbsoluteRequestURI() {
$uri = $this->getRequestURI();
$uri->setDomain($this->getHost());
if ($this->isHTTPS()) {
$protocol = 'https';
} else {
$protocol = 'http';
}
$uri->setProtocol($protocol);
// If the request used a nonstandard port, preserve it while building the
// absolute URI.
// First, get the default port for the request protocol.
$default_port = id(new PhutilURI($protocol.'://example.com/'))
->getPortWithProtocolDefault();
// NOTE: See note in getHost() about malicious "Host" headers. This
// construction defuses some obscure potential attacks.
$port = id(new PhutilURI($protocol.'://'.$this->host))
->getPort();
if (($port !== null) && ($port !== $default_port)) {
$uri->setPort($port);
}
return $uri;
}
public function isDialogFormPost() {
return $this->isFormPost() && $this->getStr('__dialog__');
}
public function getRemoteAddress() {
$address = PhabricatorEnv::getRemoteAddress();
if (!$address) {
return null;
}
return $address->getAddress();
}
public function isHTTPS() {
if (empty($_SERVER['HTTPS'])) {
return false;
}
if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
return false;
}
return true;
}
public function isContinueRequest() {
return $this->isFormPost() && $this->getStr('__continue__');
}
public function isPreviewRequest() {
return $this->isFormPost() && $this->getStr('__preview__');
}
/**
* Get application request parameters in a flattened form suitable for
* inclusion in an HTTP request, excluding parameters with special meanings.
* This is primarily useful if you want to ask the user for more input and
* then resubmit their request.
*
* @return dict<string, string> Original request parameters.
*/
public function getPassthroughRequestParameters($include_quicksand = false) {
return self::flattenData(
$this->getPassthroughRequestData($include_quicksand));
}
/**
* Get request data other than "magic" parameters.
*
* @return dict<string, wild> Request data, with magic filtered out.
*/
public function getPassthroughRequestData($include_quicksand = false) {
$data = $this->getRequestData();
// Remove magic parameters like __dialog__ and __ajax__.
foreach ($data as $key => $value) {
if ($include_quicksand && $key == self::TYPE_QUICKSAND) {
continue;
}
if (!strncmp($key, '__', 2)) {
unset($data[$key]);
}
}
return $data;
}
/**
* Flatten an array of key-value pairs (possibly including arrays as values)
* into a list of key-value pairs suitable for submitting via HTTP request
* (with arrays flattened).
*
* @param dict<string, wild> Data to flatten.
* @return dict<string, string> Flat data suitable for inclusion in an HTTP
* request.
*/
public static function flattenData(array $data) {
$result = array();
foreach ($data as $key => $value) {
if (is_array($value)) {
foreach (self::flattenData($value) as $fkey => $fvalue) {
$fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
$result[$key.$fkey] = $fvalue;
}
} else {
$result[$key] = (string)$value;
}
}
ksort($result);
return $result;
}
/**
* Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
*
* This function accepts a canonical header name, like `"Accept-Encoding"`,
* and looks up the appropriate value in `$_SERVER` (in this case,
* `"HTTP_ACCEPT_ENCODING"`).
*
* @param string Canonical header name, like `"Accept-Encoding"`.
* @param wild Default value to return if header is not present.
* @param array? Read this instead of `$_SERVER`.
* @return string|wild Header value if present, or `$default` if not.
*/
public static function getHTTPHeader($name, $default = null, $data = null) {
// PHP mangles HTTP headers by uppercasing them and replacing hyphens with
// underscores, then prepending 'HTTP_'.
$php_index = strtoupper($name);
$php_index = str_replace('-', '_', $php_index);
$try_names = array();
$try_names[] = 'HTTP_'.$php_index;
if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
// These headers may be available under alternate names. See
// http://www.php.net/manual/en/reserved.variables.server.php#110763
$try_names[] = $php_index;
}
if ($data === null) {
$data = $_SERVER;
}
foreach ($try_names as $try_name) {
if (array_key_exists($try_name, $data)) {
return $data[$try_name];
}
}
return $default;
}
/* -( Working With a Phabricator Cluster )--------------------------------- */
/**
* Is this a proxied request originating from within the Phabricator cluster?
*
* IMPORTANT: This means the request is dangerous!
*
* These requests are **more dangerous** than normal requests (they can not
* be safely proxied, because proxying them may cause a loop). Cluster
* requests are not guaranteed to come from a trusted source, and should
* never be treated as safer than normal requests. They are strictly less
* safe.
*/
public function isProxiedClusterRequest() {
return (bool)self::getHTTPHeader('X-Phabricator-Cluster');
}
/**
* Build a new @{class:HTTPSFuture} which proxies this request to another
* node in the cluster.
*
* IMPORTANT: This is very dangerous!
*
* The future forwards authentication information present in the request.
* Proxied requests must only be sent to trusted hosts. (We attempt to
* enforce this.)
*
* This is not a general-purpose proxying method; it is a specialized
* method with niche applications and severe security implications.
*
* @param string URI identifying the host we are proxying the request to.
* @return HTTPSFuture New proxy future.
*
* @phutil-external-symbol class PhabricatorStartup
*/
public function newClusterProxyFuture($uri) {
$uri = new PhutilURI($uri);
$domain = $uri->getDomain();
$ip = gethostbyname($domain);
if (!$ip) {
throw new Exception(
pht(
'Unable to resolve domain "%s"!',
$domain));
}
if (!PhabricatorEnv::isClusterAddress($ip)) {
throw new Exception(
pht(
'Refusing to proxy a request to IP address ("%s") which is not '.
'in the cluster address block (this address was derived by '.
'resolving the domain "%s").',
$ip,
$domain));
}
$uri->setPath($this->getPath());
$uri->removeAllQueryParams();
foreach (self::flattenData($_GET) as $query_key => $query_value) {
$uri->appendQueryParam($query_key, $query_value);
}
$input = PhabricatorStartup::getRawInput();
$future = id(new HTTPSFuture($uri))
->addHeader('Host', self::getHost())
->addHeader('X-Phabricator-Cluster', true)
->setMethod($_SERVER['REQUEST_METHOD'])
->write($input);
if (isset($_SERVER['PHP_AUTH_USER'])) {
$future->setHTTPBasicAuthCredentials(
$_SERVER['PHP_AUTH_USER'],
new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', '')));
}
$headers = array();
$seen = array();
// NOTE: apache_request_headers() might provide a nicer way to do this,
// but isn't available under FCGI until PHP 5.4.0.
foreach ($_SERVER as $key => $value) {
if (!preg_match('/^HTTP_/', $key)) {
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;
}
if ($should_forward) {
$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;
}
}
diff --git a/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelTabsController.php b/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelTabsController.php
index 0703332eac..2b36d3f8a2 100644
--- a/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelTabsController.php
+++ b/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelTabsController.php
@@ -1,354 +1,437 @@
<?php
final class PhabricatorDashboardPanelTabsController
extends PhabricatorDashboardController {
private $contextObject;
private function setContextObject($context_object) {
$this->contextObject = $context_object;
return $this;
}
private function getContextObject() {
return $this->contextObject;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$panel = id(new PhabricatorDashboardPanelQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$panel) {
return new Aphront404Response();
}
$tabs_type = id(new PhabricatorDashboardTabsPanelType())
->getPanelTypeKey();
// This controller may only be used to edit tab panels.
$panel_type = $panel->getPanelType();
if ($panel_type !== $tabs_type) {
return new Aphront404Response();
}
$op = $request->getURIData('op');
$after = $request->getStr('after');
if (!strlen($after)) {
$after = null;
}
$target = $request->getStr('target');
if (!strlen($target)) {
$target = null;
}
$impl = $panel->getImplementation();
$config = $impl->getPanelConfiguration($panel);
$cancel_uri = $panel->getURI();
if ($after !== null) {
$found = false;
foreach ($config as $key => $spec) {
if ((string)$key === $after) {
$found = true;
break;
}
}
if (!$found) {
return $this->newDialog()
->setTitle(pht('Adjacent Tab Not Found'))
->appendParagraph(
pht(
'Adjacent tab ("%s") was not found on this panel. It may have '.
'been removed.',
$after))
->addCancelButton($cancel_uri);
}
}
if ($target !== null) {
$found = false;
foreach ($config as $key => $spec) {
if ((string)$key === $target) {
$found = true;
break;
}
}
if (!$found) {
return $this->newDialog()
->setTitle(pht('Target Tab Not Found'))
->appendParagraph(
pht(
'Target tab ("%s") was not found on this panel. It may have '.
'been removed.',
$target))
->addCancelButton($cancel_uri);
}
}
// Tab panels may be edited from the panel page, or from the context of
// a dashboard. If we're editing from a dashboard, we want to redirect
// back to the dashboard after making changes.
$context_phid = $request->getStr('contextPHID');
$context = null;
if (strlen($context_phid)) {
$context = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($context_phid))
->executeOne();
if (!$context) {
return new Aphront404Response();
}
switch (phid_get_type($context_phid)) {
case PhabricatorDashboardDashboardPHIDType::TYPECONST:
$cancel_uri = $context->getURI();
break;
case PhabricatorDashboardPanelPHIDType::TYPECONST:
$cancel_uri = $context->getURI();
break;
default:
return $this->newDialog()
->setTitle(pht('Context Object Unsupported'))
->appendParagraph(
pht(
'Context object ("%s") has unsupported type. Panels should '.
'be rendered from the context of a dashboard or another '.
'panel.',
$context_phid))
->addCancelButton($cancel_uri);
}
$this->setContextObject($context);
}
switch ($op) {
case 'add':
return $this->handleAddOperation($panel, $after, $cancel_uri);
case 'remove':
return $this->handleRemoveOperation($panel, $target, $cancel_uri);
case 'move':
- break;
+ return $this->handleMoveOperation($panel, $target, $after, $cancel_uri);
case 'rename':
return $this->handleRenameOperation($panel, $target, $cancel_uri);
}
}
private function handleAddOperation(
PhabricatorDashboardPanel $panel,
$after,
$cancel_uri) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$panel_phid = null;
$errors = array();
if ($request->isFormPost()) {
$panel_phid = $request->getArr('panelPHID');
$panel_phid = head($panel_phid);
$add_panel = id(new PhabricatorDashboardPanelQuery())
->setViewer($viewer)
->withPHIDs(array($panel_phid))
->executeOne();
if (!$add_panel) {
$errors[] = pht('You must select a valid panel.');
}
if (!$errors) {
$add_panel_config = array(
'name' => null,
'panelID' => $add_panel->getID(),
);
$add_panel_key = Filesystem::readRandomCharacters(12);
$impl = $panel->getImplementation();
$old_config = $impl->getPanelConfiguration($panel);
$new_config = array();
if ($after === null) {
$new_config = $old_config;
$new_config[] = $add_panel_config;
} else {
foreach ($old_config as $key => $value) {
$new_config[$key] = $value;
if ((string)$key === $after) {
$new_config[$add_panel_key] = $add_panel_config;
}
}
}
$xactions = array();
$xactions[] = $panel->getApplicationTransactionTemplate()
->setTransactionType(
PhabricatorDashboardTabsPanelTabsTransaction::TRANSACTIONTYPE)
->setNewValue($new_config);
$editor = id(new PhabricatorDashboardPanelTransactionEditor())
->setContentSourceFromRequest($request)
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$editor->applyTransactions($panel, $xactions);
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
}
if ($panel_phid) {
$v_panel = array($panel_phid);
} else {
$v_panel = array();
}
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorDashboardPanelDatasource())
->setLimit(1)
->setName('panelPHID')
->setLabel(pht('Panel'))
->setValue($v_panel));
return $this->newEditDialog()
->setTitle(pht('Choose Dashboard Panel'))
->setErrors($errors)
->addHiddenInput('after', $after)
->appendForm($form)
->addCancelButton($cancel_uri)
->addSubmitButton(pht('Add Panel'));
}
private function handleRemoveOperation(
PhabricatorDashboardPanel $panel,
$target,
$cancel_uri) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$panel_phid = null;
$errors = array();
if ($request->isFormPost()) {
$impl = $panel->getImplementation();
$old_config = $impl->getPanelConfiguration($panel);
$new_config = $this->removePanel($old_config, $target);
$this->writePanelConfig($panel, $new_config);
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
return $this->newEditDialog()
->setTitle(pht('Remove tab?'))
->addHiddenInput('target', $target)
->appendParagraph(pht('Really remove this tab?'))
->addCancelButton($cancel_uri)
->addSubmitButton(pht('Remove Tab'));
}
private function handleRenameOperation(
PhabricatorDashboardPanel $panel,
$target,
$cancel_uri) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$impl = $panel->getImplementation();
$old_config = $impl->getPanelConfiguration($panel);
$spec = $old_config[$target];
$name = idx($spec, 'name');
if ($request->isFormPost()) {
$name = $request->getStr('name');
$new_config = $this->renamePanel($old_config, $target, $name);
$this->writePanelConfig($panel, $new_config);
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormTextControl())
->setValue($name)
->setName('name')
->setLabel(pht('Tab Name')));
return $this->newEditDialog()
->setTitle(pht('Rename Panel'))
->addHiddenInput('target', $target)
->appendForm($form)
->addCancelButton($cancel_uri)
->addSubmitButton(pht('Rename Tab'));
}
+ private function handleMoveOperation(
+ PhabricatorDashboardPanel $panel,
+ $target,
+ $after,
+ $cancel_uri) {
+ $request = $this->getRequest();
+ $viewer = $this->getViewer();
+
+ $move = $request->getStr('move');
+
+ $impl = $panel->getImplementation();
+ $old_config = $impl->getPanelConfiguration($panel);
+
+ $is_next = ($move === 'next');
+ if ($target === $after) {
+ return $this->newDialog()
+ ->setTitle(pht('Impossible!'))
+ ->appendParagraph(
+ pht(
+ 'You can not move a tab relative to itself.'))
+ ->addCancelButton($cancel_uri);
+ } else if ($is_next && ((string)last_key($old_config) === $target)) {
+ return $this->newDialog()
+ ->setTitle(pht('Impossible!'))
+ ->appendParagraph(
+ pht(
+ 'This is already the last tab. It can not move any farther to '.
+ 'the right.'))
+ ->addCancelButton($cancel_uri);
+ } else if ((string)head_key($old_config) === $target) {
+ return $this->newDialog()
+ ->setTitle(pht('Impossible!'))
+ ->appendParagraph(
+ pht(
+ 'This is already the first tab. It can not move any farther to '.
+ 'the left.'))
+ ->addCancelButton($cancel_uri);
+ }
+
+ if ($request->hasCSRF()) {
+ $new_config = array();
+ foreach ($old_config as $old_key => $old_spec) {
+ $old_key = (string)$old_key;
+
+ $is_after = ($old_key === $after);
+
+ if (!$is_after) {
+ if ($old_key === $target) {
+ continue;
+ }
+ }
+
+ if ($is_after && !$is_next) {
+ $new_config[$target] = $old_config[$target];
+ }
+
+ $new_config[$old_key] = $old_spec;
+
+ if ($is_after && $is_next) {
+ $new_config[$target] = $old_config[$target];
+ }
+ }
+
+ $this->writePanelConfig($panel, $new_config);
+
+ return id(new AphrontRedirectResponse())->setURI($cancel_uri);
+ }
+
+ if ($is_next) {
+ $prompt = pht('Move this tab to the right?');
+ } else {
+ $prompt = pht('Move this tab to the left?');
+ }
+
+ return $this->newEditDialog()
+ ->setTitle(pht('Move Tab'))
+ ->addHiddenInput('target', $target)
+ ->addHiddenInput('after', $after)
+ ->addHiddenInput('move', $move)
+ ->appendParagraph($prompt)
+ ->addCancelButton($cancel_uri)
+ ->addSubmitButton(pht('Move Tab'));
+ }
private function writePanelConfig(
PhabricatorDashboardPanel $panel,
array $config) {
$request = $this->getRequest();
$viewer = $this->getViewer();
$xactions = array();
$xactions[] = $panel->getApplicationTransactionTemplate()
->setTransactionType(
PhabricatorDashboardTabsPanelTabsTransaction::TRANSACTIONTYPE)
->setNewValue($config);
$editor = id(new PhabricatorDashboardPanelTransactionEditor())
->setContentSourceFromRequest($request)
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
return $editor->applyTransactions($panel, $xactions);
}
private function removePanel(array $config, $target) {
$result = array();
foreach ($config as $key => $panel_spec) {
if ((string)$key === $target) {
continue;
}
$result[$key] = $panel_spec;
}
return $result;
}
private function renamePanel(array $config, $target, $name) {
$config[$target]['name'] = $name;
return $config;
}
protected function newEditDialog() {
$dialog = $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FORM);
$context = $this->getContextObject();
if ($context) {
$dialog->addHiddenInput('contextPHID', $context->getPHID());
}
return $dialog;
}
}
diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php
index 682cc11d78..91c6d17833 100644
--- a/src/applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php
+++ b/src/applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php
@@ -1,312 +1,357 @@
<?php
final class PhabricatorDashboardTabsPanelType
extends PhabricatorDashboardPanelType {
public function getPanelTypeKey() {
return 'tabs';
}
public function getPanelTypeName() {
return pht('Tab Panel');
}
public function getIcon() {
return 'fa-columns';
}
public function getPanelTypeDescription() {
return pht('Use tabs to switch between several other panels.');
}
protected function newEditEngineFields(PhabricatorDashboardPanel $panel) {
return array();
}
public function shouldRenderAsync() {
// The actual tab panel itself is cheap to render.
return false;
}
public function getPanelConfiguration(PhabricatorDashboardPanel $panel) {
$config = $panel->getProperty('config');
if (!is_array($config)) {
// NOTE: The older version of this panel stored raw JSON.
try {
$config = phutil_json_decode($config);
} catch (PhutilJSONParserException $ex) {
$config = array();
}
}
return $config;
}
public function renderPanelContent(
PhabricatorUser $viewer,
PhabricatorDashboardPanel $panel,
PhabricatorDashboardPanelRenderingEngine $engine) {
$is_edit = $engine->isEditMode();
$config = $this->getPanelConfiguration($panel);
$context_object = $engine->getContextObject();
if (!$context_object) {
$context_object = $panel;
}
$context_phid = $context_object->getPHID();
$list = id(new PHUIListView())
->setType(PHUIListView::NAVBAR_LIST);
$ids = ipull($config, 'panelID');
if ($ids) {
$panels = id(new PhabricatorDashboardPanelQuery())
->setViewer($viewer)
->withIDs($ids)
->execute();
} else {
$panels = array();
}
$id = $panel->getID();
$add_uri = urisprintf('/dashboard/panel/tabs/%d/add/', $id);
$add_uri = id(new PhutilURI($add_uri))
->replaceQueryParam('contextPHID', $context_phid);
$remove_uri = urisprintf('/dashboard/panel/tabs/%d/remove/', $id);
$remove_uri = id(new PhutilURI($remove_uri))
->replaceQueryParam('contextPHID', $context_phid);
$rename_uri = urisprintf('/dashboard/panel/tabs/%d/rename/', $id);
$rename_uri = id(new PhutilURI($rename_uri))
->replaceQueryParam('contextPHID', $context_phid);
$selected = 0;
- $last_idx = null;
+ $key_list = array_keys($config);
+
+ $next_keys = array();
+ $prev_keys = array();
+ for ($ii = 0; $ii < count($key_list); $ii++) {
+ $next_keys[$key_list[$ii]] = idx($key_list, $ii + 1);
+ $prev_keys[$key_list[$ii]] = idx($key_list, $ii - 1);
+ }
+
foreach ($config as $idx => $tab_spec) {
$panel_id = idx($tab_spec, 'panelID');
$subpanel = idx($panels, $panel_id);
$name = idx($tab_spec, 'name');
if (!strlen($name)) {
if ($subpanel) {
$name = $subpanel->getName();
}
}
if (!strlen($name)) {
$name = pht('Unnamed Tab');
}
$tab_view = id(new PHUIListItemView())
->setHref('#')
->setSelected((string)$idx === (string)$selected)
->addSigil('dashboard-tab-panel-tab')
->setMetadata(array('panelKey' => $idx))
->setName($name);
if ($is_edit) {
$dropdown_menu = id(new PhabricatorActionListView())
->setViewer($viewer);
$remove_tab_uri = id(clone $remove_uri)
->replaceQueryParam('target', $idx);
$rename_tab_uri = id(clone $rename_uri)
->replaceQueryParam('target', $idx);
if ($subpanel) {
$details_uri = $subpanel->getURI();
} else {
$details_uri = null;
}
$edit_uri = urisprintf(
'/dashboard/panel/edit/%d/',
$panel_id);
if ($subpanel) {
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$subpanel,
PhabricatorPolicyCapability::CAN_EDIT);
} else {
$can_edit = false;
}
$dropdown_menu->addAction(
id(new PhabricatorActionView())
->setName(pht('Rename Tab'))
->setIcon('fa-pencil')
->setHref($rename_tab_uri)
->setWorkflow(true));
+ $move_uri = urisprintf('/dashboard/panel/tabs/%d/move/', $id);
+
+ $prev_key = $prev_keys[$idx];
+ $prev_params = array(
+ 'target' => $idx,
+ 'after' => $prev_key,
+ 'move' => 'prev',
+ 'contextPHID' => $context_phid,
+ );
+ $prev_uri = new PhutilURI($move_uri, $prev_params);
+
+ $next_key = $next_keys[$idx];
+ $next_params = array(
+ 'target' => $idx,
+ 'after' => $next_key,
+ 'move' => 'next',
+ 'contextPHID' => $context_phid,
+ );
+ $next_uri = new PhutilURI($move_uri, $next_params);
+
+ $dropdown_menu->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Move Tab Left'))
+ ->setIcon('fa-chevron-left')
+ ->setHref($prev_uri)
+ ->setWorkflow(true)
+ ->setDisabled(($prev_key === null) || !$can_edit));
+
+ $dropdown_menu->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Move Tab Right'))
+ ->setIcon('fa-chevron-right')
+ ->setHref($next_uri)
+ ->setWorkflow(true)
+ ->setDisabled(($next_key === null) || !$can_edit));
+
$dropdown_menu->addAction(
id(new PhabricatorActionView())
->setName(pht('Remove Tab'))
->setIcon('fa-times')
->setHref($remove_tab_uri)
->setWorkflow(true));
$dropdown_menu->addAction(
id(new PhabricatorActionView())
->setType(PhabricatorActionView::TYPE_DIVIDER));
$dropdown_menu->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Panel'))
->setIcon('fa-pencil')
->setHref($edit_uri)
->setWorkflow(true)
->setDisabled(!$can_edit));
$dropdown_menu->addAction(
id(new PhabricatorActionView())
->setName(pht('View Panel Details'))
->setIcon('fa-window-maximize')
->setHref($details_uri)
->setDisabled(!$subpanel));
$tab_view
->setActionIcon('fa-caret-down', '#')
->setDropdownMenu($dropdown_menu);
}
$list->addMenuItem($tab_view);
-
- $last_idx = $idx;
}
+
if ($is_edit) {
$actions = id(new PhabricatorActionListView())
->setViewer($viewer);
$add_last_uri = clone $add_uri;
+
+ $last_idx = last_key($config);
if ($last_idx) {
$add_last_uri->replaceQueryParam('after', $last_idx);
}
$actions->addAction(
id(new PhabricatorActionView())
->setName(pht('Add Existing Panel'))
->setIcon('fa-window-maximize')
->setHref($add_last_uri)
->setWorkflow(true));
$list->addMenuItem(
id(new PHUIListItemView())
->setHref('#')
->setSelected(false)
->setName(pht('Add Tab...'))
->setDropdownMenu($actions));
}
$parent_phids = $engine->getParentPanelPHIDs();
$parent_phids[] = $panel->getPHID();
// TODO: Currently, we'll load all the panels on page load. It would be
// vaguely nice to load hidden panels only when the user selects them.
// TODO: Maybe we should persist which panel the user selected, so it
// remains selected across page loads.
$content = array();
$panel_list = array();
$no_headers = PhabricatorDashboardPanelRenderingEngine::HEADER_MODE_NONE;
foreach ($config as $idx => $tab_spec) {
$panel_id = idx($tab_spec, 'panelID');
$subpanel = idx($panels, $panel_id);
if ($subpanel) {
$panel_content = id(new PhabricatorDashboardPanelRenderingEngine())
->setViewer($viewer)
->setEnableAsyncRendering(true)
->setContextObject($context_object)
->setParentPanelPHIDs($parent_phids)
->setPanel($subpanel)
->setPanelPHID($subpanel->getPHID())
->setHeaderMode($no_headers)
->renderPanel();
} else {
$panel_content = pht('(Invalid Panel)');
}
$content_id = celerity_generate_unique_node_id();
$content[] = phutil_tag(
'div',
array(
'id' => $content_id,
'style' => ($idx == $selected) ? null : 'display: none',
),
$panel_content);
$panel_list[] = array(
'panelKey' => (string)$idx,
'panelContentID' => $content_id,
);
}
if (!$content) {
if ($is_edit) {
$message = pht(
'This tab panel does not have any tabs yet. Use "Add Tab..." to '.
'create or place a tab.');
} else {
$message = pht(
'This tab panel does not have any tabs yet.');
}
$content = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NODATA)
->setErrors(
array(
$message,
));
$content = id(new PHUIBoxView())
->addClass('mlt mlb')
->appendChild($content);
}
Javelin::initBehavior('dashboard-tab-panel');
return javelin_tag(
'div',
array(
'sigil' => 'dashboard-tab-panel-container',
'meta' => array(
'panels' => $panel_list,
),
),
array(
$list,
$content,
));
}
public function getSubpanelPHIDs(PhabricatorDashboardPanel $panel) {
$config = $this->getPanelConfiguration($panel);
$panel_ids = array();
foreach ($config as $tab_key => $tab_spec) {
$panel_ids[] = $tab_spec['panelID'];
}
if ($panel_ids) {
$panels = id(new PhabricatorDashboardPanelQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs($panel_ids)
->execute();
} else {
$panels = array();
}
return mpull($panels, 'getPHID');
}
}
diff --git a/src/applications/notification/controller/PhabricatorNotificationClearController.php b/src/applications/notification/controller/PhabricatorNotificationClearController.php
index 1d6c7f7b97..6712591e12 100644
--- a/src/applications/notification/controller/PhabricatorNotificationClearController.php
+++ b/src/applications/notification/controller/PhabricatorNotificationClearController.php
@@ -1,68 +1,63 @@
<?php
final class PhabricatorNotificationClearController
extends PhabricatorNotificationController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$chrono_key = $request->getStr('chronoKey');
if ($request->isDialogFormPost()) {
$should_clear = true;
} else {
- try {
- $request->validateCSRF();
- $should_clear = true;
- } catch (AphrontMalformedRequestException $ex) {
- $should_clear = false;
- }
+ $should_clear = $request->hasCSRF();
}
if ($should_clear) {
$table = new PhabricatorFeedStoryNotification();
queryfx(
$table->establishConnection('w'),
'UPDATE %T SET hasViewed = 1 '.
'WHERE userPHID = %s AND hasViewed = 0 and chronologicalKey <= %s',
$table->getTableName(),
$viewer->getPHID(),
$chrono_key);
PhabricatorUserCache::clearCache(
PhabricatorUserNotificationCountCacheType::KEY_COUNT,
$viewer->getPHID());
return id(new AphrontReloadResponse())
->setURI('/notification/');
}
$dialog = new AphrontDialogView();
$dialog->setUser($viewer);
$dialog->addCancelButton('/notification/');
if ($chrono_key) {
$dialog->setTitle(pht('Really mark all notifications as read?'));
$dialog->addHiddenInput('chronoKey', $chrono_key);
$is_serious =
PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$dialog->appendChild(
pht(
'All unread notifications will be marked as read. You can not '.
'undo this action.'));
} else {
$dialog->appendChild(
pht(
"You can't ignore your problems forever, you know."));
}
$dialog->addSubmitButton(pht('Mark All Read'));
} else {
$dialog->setTitle(pht('No notifications to mark as read.'));
$dialog->appendChild(pht('You have no unread notifications.'));
}
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Mar 14, 3:42 PM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
72002
Default Alt Text
(51 KB)

Event Timeline