diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index 38eb5a6ffc..46d1266b08 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -1,913 +1,913 @@ 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 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 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() { $request_uri = idx($_SERVER, 'REQUEST_URI', '/'); $uri = new PhutilURI($request_uri); - $uri->setQueryParam('__path__', null); + $uri->removeQueryParam('__path__'); $path = phutil_escape_uri($this->getPath()); $uri->setPath($path); return $uri; } 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 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 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 Data to flatten. * @return dict 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/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php index 7fad62a509..918b3a4ad9 100644 --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -1,209 +1,209 @@ getViewer(); $properties = $object->getAlmanacProperties(); $this->requireResource('almanac-css'); Javelin::initBehavior('phabricator-tooltips', array()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); $properties = $object->getAlmanacProperties(); $icon_builtin = id(new PHUIIconView()) ->setIcon('fa-circle') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Builtin Property'), 'align' => 'E', )); $icon_custom = id(new PHUIIconView()) ->setIcon('fa-circle-o grey') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Custom Property'), 'align' => 'E', )); $builtins = $object->getAlmanacPropertyFieldSpecifications(); $defaults = mpull($builtins, 'getValueForTransaction'); // Sort fields so builtin fields appear first, then fields are ordered // alphabetically. $properties = msort($properties, 'getFieldName'); $head = array(); $tail = array(); foreach ($properties as $property) { $key = $property->getFieldName(); if (isset($builtins[$key])) { $head[$key] = $property; } else { $tail[$key] = $property; } } $properties = $head + $tail; $delete_base = $this->getApplicationURI('property/delete/'); $edit_base = $this->getApplicationURI('property/update/'); $rows = array(); foreach ($properties as $key => $property) { $value = $property->getFieldValue(); $is_builtin = isset($builtins[$key]); $is_persistent = (bool)$property->getID(); $params = array( 'key' => $key, 'objectPHID' => $object->getPHID(), ); $delete_uri = new PhutilURI($delete_base, $params); $edit_uri = new PhutilURI($edit_base, $params); $delete = javelin_tag( 'a', array( 'class' => (($can_edit && $is_persistent) ? 'button button-grey small' : 'button button-grey small disabled'), 'sigil' => 'workflow', 'href' => $delete_uri, ), $is_builtin ? pht('Reset') : pht('Delete')); $default = idx($defaults, $key); $is_default = ($default !== null && $default === $value); $display_value = PhabricatorConfigJSON::prettyPrintJSON($value); if ($is_default) { $display_value = phutil_tag( 'span', array( 'class' => 'almanac-default-property-value', ), $display_value); } $display_key = $key; if ($can_edit) { $display_key = javelin_tag( 'a', array( 'href' => $edit_uri, 'sigil' => 'workflow', ), $display_key); } $rows[] = array( ($is_builtin ? $icon_builtin : $icon_custom), $display_key, $display_value, $delete, ); } $table = id(new AphrontTableView($rows)) ->setNoDataString(pht('No properties.')) ->setHeaders( array( null, pht('Name'), pht('Value'), null, )) ->setColumnClasses( array( null, null, 'wide', 'action', )); $phid = $object->getPHID(); $add_uri = id(new PhutilURI($edit_base)) - ->setQueryParam('objectPHID', $object->getPHID()); + ->replaceQueryParam('objectPHID', $object->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); $add_button = id(new PHUIButtonView()) ->setTag('a') ->setHref($add_uri) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setText(pht('Add Property')) ->setIcon('fa-plus'); $header = id(new PHUIHeaderView()) ->setHeader(pht('Properties')) ->addActionLink($add_button); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table); } protected function addClusterMessage( $positive, $negative) { $can_manage = $this->hasApplicationCapability( AlmanacManageClusterServicesCapability::CAPABILITY); $doc_link = phutil_tag( 'a', array( 'href' => PhabricatorEnv::getDoclink( 'Clustering Introduction'), 'target' => '_blank', ), pht('Learn More')); if ($can_manage) { $severity = PHUIInfoView::SEVERITY_NOTICE; $message = $positive; } else { $severity = PHUIInfoView::SEVERITY_WARNING; $message = $negative; } $icon = id(new PHUIIconView()) ->setIcon('fa-sitemap'); return id(new PHUIInfoView()) ->setSeverity($severity) ->setErrors( array( array($icon, ' ', $message, ' ', $doc_link), )); } protected function getPropertyDeleteURI($object) { return null; } protected function getPropertyUpdateURI($object) { return null; } } diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php index e81301218c..cda56d34b1 100644 --- a/src/applications/auth/controller/PhabricatorAuthController.php +++ b/src/applications/auth/controller/PhabricatorAuthController.php @@ -1,289 +1,289 @@ setTitle($title); $view->setErrors($messages); return $this->newPage() ->setTitle($title) ->appendChild($view); } /** * Returns true if this install is newly setup (i.e., there are no user * accounts yet). In this case, we enter a special mode to permit creation * of the first account form the web UI. */ protected function isFirstTimeSetup() { // If there are any auth providers, this isn't first time setup, even if // we don't have accounts. if (PhabricatorAuthProvider::getAllEnabledProviders()) { return false; } // Otherwise, check if there are any user accounts. If not, we're in first // time setup. $any_users = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->setLimit(1) ->execute(); return !$any_users; } /** * Log a user into a web session and return an @{class:AphrontResponse} which * corresponds to continuing the login process. * * Normally, this is a redirect to the validation controller which makes sure * the user's cookies are set. However, event listeners can intercept this * event and do something else if they prefer. * * @param PhabricatorUser User to log the viewer in as. * @param bool True to issue a full session immediately, bypassing MFA. * @return AphrontResponse Response which continues the login process. */ protected function loginUser( PhabricatorUser $user, $force_full_session = false) { $response = $this->buildLoginValidateResponse($user); $session_type = PhabricatorAuthSession::TYPE_WEB; if ($force_full_session) { $partial_session = false; } else { $partial_session = true; } $session_key = id(new PhabricatorAuthSessionEngine()) ->establishSession($session_type, $user->getPHID(), $partial_session); // NOTE: We allow disabled users to login and roadblock them later, so // there's no check for users being disabled here. $request = $this->getRequest(); $request->setCookie( PhabricatorCookies::COOKIE_USERNAME, $user->getUsername()); $request->setCookie( PhabricatorCookies::COOKIE_SESSION, $session_key); $this->clearRegistrationCookies(); return $response; } protected function clearRegistrationCookies() { $request = $this->getRequest(); // Clear the registration key. $request->clearCookie(PhabricatorCookies::COOKIE_REGISTRATION); // Clear the client ID / OAuth state key. $request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID); // Clear the invite cookie. $request->clearCookie(PhabricatorCookies::COOKIE_INVITE); } private function buildLoginValidateResponse(PhabricatorUser $user) { $validate_uri = new PhutilURI($this->getApplicationURI('validate/')); - $validate_uri->setQueryParam('expect', $user->getUsername()); + $validate_uri->replaceQueryParam('expect', $user->getUsername()); return id(new AphrontRedirectResponse())->setURI((string)$validate_uri); } protected function renderError($message) { return $this->renderErrorPage( pht('Authentication Error'), array( $message, )); } protected function loadAccountForRegistrationOrLinking($account_key) { $request = $this->getRequest(); $viewer = $request->getUser(); $account = null; $provider = null; $response = null; if (!$account_key) { $response = $this->renderError( pht('Request did not include account key.')); return array($account, $provider, $response); } // NOTE: We're using the omnipotent user because the actual user may not // be logged in yet, and because we want to tailor an error message to // distinguish between "not usable" and "does not exist". We do explicit // checks later on to make sure this account is valid for the intended // operation. This requires edit permission for completeness and consistency // but it won't actually be meaningfully checked because we're using the // omnipotent user. $account = id(new PhabricatorExternalAccountQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withAccountSecrets(array($account_key)) ->needImages(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$account) { $response = $this->renderError(pht('No valid linkable account.')); return array($account, $provider, $response); } if ($account->getUserPHID()) { if ($account->getUserPHID() != $viewer->getPHID()) { $response = $this->renderError( pht( 'The account you are attempting to register or link is already '. 'linked to another user.')); } else { $response = $this->renderError( pht( 'The account you are attempting to link is already linked '. 'to your account.')); } return array($account, $provider, $response); } $registration_key = $request->getCookie( PhabricatorCookies::COOKIE_REGISTRATION); // NOTE: This registration key check is not strictly necessary, because // we're only creating new accounts, not linking existing accounts. It // might be more hassle than it is worth, especially for email. // // The attack this prevents is getting to the registration screen, then // copy/pasting the URL and getting someone else to click it and complete // the process. They end up with an account bound to credentials you // control. This doesn't really let you do anything meaningful, though, // since you could have simply completed the process yourself. if (!$registration_key) { $response = $this->renderError( pht( 'Your browser did not submit a registration key with the request. '. 'You must use the same browser to begin and complete registration. '. 'Check that cookies are enabled and try again.')); return array($account, $provider, $response); } // We store the digest of the key rather than the key itself to prevent a // theoretical attacker with read-only access to the database from // hijacking registration sessions. $actual = $account->getProperty('registrationKey'); $expect = PhabricatorHash::weakDigest($registration_key); if (!phutil_hashes_are_identical($actual, $expect)) { $response = $this->renderError( pht( 'Your browser submitted a different registration key than the one '. 'associated with this account. You may need to clear your cookies.')); return array($account, $provider, $response); } $other_account = id(new PhabricatorExternalAccount())->loadAllWhere( 'accountType = %s AND accountDomain = %s AND accountID = %s AND id != %d', $account->getAccountType(), $account->getAccountDomain(), $account->getAccountID(), $account->getID()); if ($other_account) { $response = $this->renderError( pht( 'The account you are attempting to register with already belongs '. 'to another user.')); return array($account, $provider, $response); } $config = $account->getProviderConfig(); if (!$config->getIsEnabled()) { $response = $this->renderError( pht( 'The account you are attempting to register with uses a disabled '. 'authentication provider ("%s"). An administrator may have '. 'recently disabled this provider.', $config->getDisplayName())); return array($account, $provider, $response); } $provider = $config->getProvider(); return array($account, $provider, null); } protected function loadInvite() { $invite_cookie = PhabricatorCookies::COOKIE_INVITE; $invite_code = $this->getRequest()->getCookie($invite_cookie); if (!$invite_code) { return null; } $engine = id(new PhabricatorAuthInviteEngine()) ->setViewer($this->getViewer()) ->setUserHasConfirmedVerify(true); try { return $engine->processInviteCode($invite_code); } catch (Exception $ex) { // If this fails for any reason, just drop the invite. In normal // circumstances, we gave them a detailed explanation of any error // before they jumped into this workflow. return null; } } protected function renderInviteHeader(PhabricatorAuthInvite $invite) { $viewer = $this->getViewer(); // Since the user hasn't registered yet, they may not be able to see other // user accounts. Load the inviting user with the omnipotent viewer. $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); $invite_author = id(new PhabricatorPeopleQuery()) ->setViewer($omnipotent_viewer) ->withPHIDs(array($invite->getAuthorPHID())) ->needProfileImage(true) ->executeOne(); // If we can't load the author for some reason, just drop this message. // We lose the value of contextualizing things without author details. if (!$invite_author) { return null; } $invite_item = id(new PHUIObjectItemView()) ->setHeader(pht('Welcome to Phabricator!')) ->setImageURI($invite_author->getProfileImageURI()) ->addAttribute( pht( '%s has invited you to join Phabricator.', $invite_author->getFullName())); $invite_list = id(new PHUIObjectItemListView()) ->addItem($invite_item) ->setFlush(true); return id(new PHUIBoxView()) ->addMargin(PHUI::MARGIN_LARGE) ->appendChild($invite_list); } } diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index 0b823098d7..72cbbea5a8 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -1,361 +1,361 @@ getUser(); if ($viewer->isLoggedIn()) { // Kick the user home if they are already logged in. return id(new AphrontRedirectResponse())->setURI('/'); } if ($request->isAjax()) { return $this->processAjaxRequest(); } if ($request->isConduit()) { return $this->processConduitRequest(); } // If the user gets this far, they aren't logged in, so if they have a // user session token we can conclude that it's invalid: if it was valid, // they'd have been logged in above and never made it here. Try to clear // it and warn the user they may need to nuke their cookies. $session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); $did_clear = $request->getStr('cleared'); if (strlen($session_token)) { $kind = PhabricatorAuthSessionEngine::getSessionKindFromToken( $session_token); switch ($kind) { case PhabricatorAuthSessionEngine::KIND_ANONYMOUS: // If this is an anonymous session. It's expected that they won't // be logged in, so we can just continue. break; default: // The session cookie is invalid, so try to clear it. $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME); $request->clearCookie(PhabricatorCookies::COOKIE_SESSION); // We've previously tried to clear the cookie but we ended up back // here, so it didn't work. Hard fatal instead of trying again. if ($did_clear) { return $this->renderError( pht( 'Your login session is invalid, and clearing the session '. 'cookie was unsuccessful. Try clearing your browser cookies.')); } $redirect_uri = $request->getRequestURI(); - $redirect_uri->setQueryParam('cleared', 1); + $redirect_uri->replaceQueryParam('cleared', 1); return id(new AphrontRedirectResponse())->setURI($redirect_uri); } } // If we just cleared the session cookie and it worked, clean up after // ourselves by redirecting to get rid of the "cleared" parameter. The // the workflow will continue normally. if ($did_clear) { $redirect_uri = $request->getRequestURI(); - $redirect_uri->setQueryParam('cleared', null); + $redirect_uri->removeQueryParam('cleared'); return id(new AphrontRedirectResponse())->setURI($redirect_uri); } $providers = PhabricatorAuthProvider::getAllEnabledProviders(); foreach ($providers as $key => $provider) { if (!$provider->shouldAllowLogin()) { unset($providers[$key]); } } $configs = array(); foreach ($providers as $provider) { $configs[] = $provider->getProviderConfig(); } if (!$providers) { if ($this->isFirstTimeSetup()) { // If this is a fresh install, let the user register their admin // account. return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('/register/')); } return $this->renderError( pht( 'This Phabricator install is not configured with any enabled '. 'authentication providers which can be used to log in. If you '. 'have accidentally locked yourself out by disabling all providers, '. 'you can use `%s` to recover access to an account.', 'phabricator/bin/auth recover ')); } $next_uri = $request->getStr('next'); if (!strlen($next_uri)) { if ($this->getDelegatingController()) { // Only set a next URI from the request path if this controller was // delegated to, which happens when a user tries to view a page which // requires them to login. // If this controller handled the request directly, we're on the main // login page, and never want to redirect the user back here after they // login. $next_uri = (string)$this->getRequest()->getRequestURI(); } } if (!$request->isFormPost()) { if (strlen($next_uri)) { PhabricatorCookies::setNextURICookie($request, $next_uri); } PhabricatorCookies::setClientIDCookie($request); } $auto_response = $this->tryAutoLogin($providers); if ($auto_response) { return $auto_response; } $invite = $this->loadInvite(); $not_buttons = array(); $are_buttons = array(); $providers = msort($providers, 'getLoginOrder'); foreach ($providers as $provider) { if ($invite) { $form = $provider->buildInviteForm($this); } else { $form = $provider->buildLoginForm($this); } if ($provider->isLoginFormAButton()) { $are_buttons[] = $form; } else { $not_buttons[] = $form; } } $out = array(); $out[] = $not_buttons; if ($are_buttons) { require_celerity_resource('auth-css'); foreach ($are_buttons as $key => $button) { $are_buttons[$key] = phutil_tag( 'div', array( 'class' => 'phabricator-login-button mmb', ), $button); } // If we only have one button, add a second pretend button so that we // always have two columns. This makes it easier to get the alignments // looking reasonable. if (count($are_buttons) == 1) { $are_buttons[] = null; } $button_columns = id(new AphrontMultiColumnView()) ->setFluidLayout(true); $are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2)); foreach ($are_buttons as $column) { $button_columns->addColumn($column); } $out[] = phutil_tag( 'div', array( 'class' => 'phabricator-login-buttons', ), $button_columns); } $invite_message = null; if ($invite) { $invite_message = $this->renderInviteHeader($invite); } $custom_message = $this->newCustomStartMessage(); $email_login = $this->newEmailLoginView($configs); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Login')); $crumbs->setBorder(true); $title = pht('Login'); $view = array( $invite_message, $custom_message, $out, $email_login, ); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } private function processAjaxRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); // We end up here if the user clicks a workflow link that they need to // login to use. We give them a dialog saying "You need to login...". if ($request->isDialogFormPost()) { return id(new AphrontRedirectResponse())->setURI( $request->getRequestURI()); } // Often, users end up here by clicking a disabled action link in the UI // (for example, they might click "Edit Subtasks" on a Maniphest task // page). After they log in we want to send them back to that main object // page if we can, since it's confusing to end up on a standalone page with // only a dialog (particularly if that dialog is another error, // like a policy exception). $via_header = AphrontRequest::getViaHeaderName(); $via_uri = AphrontRequest::getHTTPHeader($via_header); if (strlen($via_uri)) { PhabricatorCookies::setNextURICookie($request, $via_uri, $force = true); } return $this->newDialog() ->setTitle(pht('Login Required')) ->appendParagraph(pht('You must log in to take this action.')) ->addSubmitButton(pht('Log In')) ->addCancelButton('/'); } private function processConduitRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); // A common source of errors in Conduit client configuration is getting // the request path wrong. The client will end up here, so make some // effort to give them a comprehensible error message. $request_path = $this->getRequest()->getPath(); $conduit_path = '/api/'; $example_path = '/api/conduit.ping'; $message = pht( 'ERROR: You are making a Conduit API request to "%s", but the correct '. 'HTTP request path to use in order to access a COnduit method is "%s" '. '(for example, "%s"). Check your configuration.', $request_path, $conduit_path, $example_path); return id(new AphrontPlainTextResponse())->setContent($message); } protected function renderError($message) { return $this->renderErrorPage( pht('Authentication Failure'), array($message)); } private function tryAutoLogin(array $providers) { $request = $this->getRequest(); // If the user just logged out, don't immediately log them in again. if ($request->getURIData('loggedout')) { return null; } // If we have more than one provider, we can't autologin because we // don't know which one the user wants. if (count($providers) != 1) { return null; } $provider = head($providers); if (!$provider->supportsAutoLogin()) { return null; } $config = $provider->getProviderConfig(); if (!$config->getShouldAutoLogin()) { return null; } $auto_uri = $provider->getAutoLoginURI($request); return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI($auto_uri); } private function newCustomStartMessage() { $viewer = $this->getViewer(); $text = PhabricatorAuthMessage::loadMessageText( $viewer, PhabricatorAuthLoginMessageType::MESSAGEKEY); if (!strlen($text)) { return null; } $remarkup_view = new PHUIRemarkupView($viewer, $text); return phutil_tag( 'div', array( 'class' => 'auth-custom-message', ), $remarkup_view); } private function newEmailLoginView(array $configs) { assert_instances_of($configs, 'PhabricatorAuthProviderConfig'); // Check if password auth is enabled. If it is, the password login form // renders a "Forgot password?" link, so we don't need to provide a // supplemental link. $has_password = false; foreach ($configs as $config) { $provider = $config->getProvider(); if ($provider instanceof PhabricatorPasswordAuthProvider) { $has_password = true; } } if ($has_password) { return null; } $view = array( pht('Trouble logging in?'), ' ', phutil_tag( 'a', array( 'href' => '/login/email/', ), pht('Send a login link to your email address.')), ); return phutil_tag( 'div', array( 'class' => 'auth-custom-message', ), $view); } } diff --git a/src/applications/auth/controller/config/PhabricatorAuthNewController.php b/src/applications/auth/controller/config/PhabricatorAuthNewController.php index c8fd0ad8a5..770c43208d 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthNewController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthNewController.php @@ -1,74 +1,74 @@ requireApplicationCapability( AuthManageProvidersCapability::CAPABILITY); $viewer = $this->getViewer(); $cancel_uri = $this->getApplicationURI(); $providers = PhabricatorAuthProvider::getAllBaseProviders(); $configured = PhabricatorAuthProvider::getAllProviders(); $configured_classes = array(); foreach ($configured as $configured_provider) { $configured_classes[get_class($configured_provider)] = true; } // Sort providers by login order, and move disabled providers to the // bottom. $providers = msort($providers, 'getLoginOrder'); $providers = array_diff_key($providers, $configured_classes) + $providers; $menu = id(new PHUIObjectItemListView()) ->setViewer($viewer) ->setBig(true) ->setFlush(true); foreach ($providers as $provider_key => $provider) { $provider_class = get_class($provider); $provider_uri = id(new PhutilURI('/config/edit/')) - ->setQueryParam('provider', $provider_class); + ->replaceQueryParam('provider', $provider_class); $provider_uri = $this->getApplicationURI($provider_uri); $already_exists = isset($configured_classes[get_class($provider)]); $item = id(new PHUIObjectItemView()) ->setHeader($provider->getNameForCreate()) ->setImageIcon($provider->newIconView()) ->addAttribute($provider->getDescriptionForCreate()); if (!$already_exists) { $item ->setHref($provider_uri) ->setClickable(true); } else { $item->setDisabled(true); } if ($already_exists) { $messages = array(); $messages[] = pht('You already have a provider of this type.'); $info = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors($messages); $item->appendChild($info); } $menu->addItem($item); } return $this->newDialog() ->setTitle(pht('Add Auth Provider')) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendChild($menu) ->addCancelButton($cancel_uri); } } diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php index a8d87e2ead..a1636396ac 100644 --- a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php @@ -1,80 +1,80 @@ requireApplicationCapability( AuthManageProvidersCapability::CAPABILITY); $engine = id(new PhabricatorAuthFactorProviderEditEngine()) ->setController($this); $id = $request->getURIData('id'); if (!$id) { $factor_key = $request->getStr('providerFactorKey'); $map = PhabricatorAuthFactor::getAllFactors(); $factor = idx($map, $factor_key); if (!$factor) { return $this->buildFactorSelectionResponse(); } $engine ->addContextParameter('providerFactorKey', $factor_key) ->setProviderFactor($factor); } return $engine->buildResponse(); } private function buildFactorSelectionResponse() { $request = $this->getRequest(); $viewer = $this->getViewer(); $cancel_uri = $this->getApplicationURI('mfa/'); $factors = PhabricatorAuthFactor::getAllFactors(); $menu = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setBig(true) ->setFlush(true); $factors = msortv($factors, 'newSortVector'); foreach ($factors as $factor_key => $factor) { $factor_uri = id(new PhutilURI('/mfa/edit/')) - ->setQueryParam('providerFactorKey', $factor_key); + ->replaceQueryParam('providerFactorKey', $factor_key); $factor_uri = $this->getApplicationURI($factor_uri); $is_enabled = $factor->canCreateNewProvider(); $item = id(new PHUIObjectItemView()) ->setHeader($factor->getFactorName()) ->setImageIcon($factor->newIconView()) ->addAttribute($factor->getFactorCreateHelp()); if ($is_enabled) { $item ->setHref($factor_uri) ->setClickable(true); } else { $item->setDisabled(true); } $create_description = $factor->getProviderCreateDescription(); if ($create_description) { $item->appendChild($create_description); } $menu->addItem($item); } return $this->newDialog() ->setTitle(pht('Choose Provider Type')) ->appendChild($menu) ->addCancelButton($cancel_uri); } } diff --git a/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php b/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php index d9fb5d013b..d49a447df6 100644 --- a/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php +++ b/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php @@ -1,56 +1,56 @@ getViewer(); if ($viewer->isLoggedIn()) { return array(); } $controller = $this->getController(); if ($controller instanceof PhabricatorAuthController) { // Don't show the "Login" item on auth controllers, since they're // generally all related to logging in anyway. return array(); } return array( $this->buildLoginMenu(), ); } private function buildLoginMenu() { $controller = $this->getController(); $uri = new PhutilURI('/auth/start/'); if ($controller) { $path = $controller->getRequest()->getPath(); - $uri->setQueryParam('next', $path); + $uri->replaceQueryParam('next', $path); } return id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Log In')) ->setHref($uri) ->setNoCSS(true) ->addClass('phabricator-core-login-button'); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php index 9c07d371d0..6d92f00a97 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php @@ -1,295 +1,295 @@ getViewer(); $import = id(new PhabricatorCalendarImportQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) ->executeOne(); if (!$import) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( pht('Imports'), '/calendar/import/'); $crumbs->addTextCrumb(pht('Import %d', $import->getID())); $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $import, new PhabricatorCalendarImportTransactionQuery()); $timeline->setShouldTerminate(true); $header = $this->buildHeaderView($import); $curtain = $this->buildCurtain($import); $details = $this->buildPropertySection($import); $log_messages = $this->buildLogMessages($import); $imported_events = $this->buildImportedEvents($import); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setMainColumn( array( $log_messages, $imported_events, $timeline, )) ->setCurtain($curtain) ->addPropertySection(pht('Details'), $details); $page_title = pht( 'Import %d %s', $import->getID(), $import->getDisplayName()); return $this->newPage() ->setTitle($page_title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($import->getPHID())) ->appendChild($view); } private function buildHeaderView( PhabricatorCalendarImport $import) { $viewer = $this->getViewer(); $id = $import->getID(); if ($import->getIsDisabled()) { $icon = 'fa-ban'; $color = 'red'; $status = pht('Disabled'); } else { $icon = 'fa-check'; $color = 'bluegrey'; $status = pht('Active'); } $header = id(new PHUIHeaderView()) ->setViewer($viewer) ->setHeader($import->getDisplayName()) ->setStatus($icon, $color, $status) ->setPolicyObject($import); return $header; } private function buildCurtain(PhabricatorCalendarImport $import) { $viewer = $this->getViewer(); $id = $import->getID(); $curtain = $this->newCurtainView($import); $engine = $import->getEngine(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $import, PhabricatorPolicyCapability::CAN_EDIT); $edit_uri = "import/edit/{$id}/"; $edit_uri = $this->getApplicationURI($edit_uri); $can_disable = ($can_edit && $engine->canDisable($viewer, $import)); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Import')) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref($edit_uri)); $reload_uri = "import/reload/{$id}/"; $reload_uri = $this->getApplicationURI($reload_uri); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Reload Import')) ->setIcon('fa-refresh') ->setDisabled(!$can_edit) ->setWorkflow(true) ->setHref($reload_uri)); $disable_uri = "import/disable/{$id}/"; $disable_uri = $this->getApplicationURI($disable_uri); if ($import->getIsDisabled()) { $disable_name = pht('Enable Import'); $disable_icon = 'fa-check'; } else { $disable_name = pht('Disable Import'); $disable_icon = 'fa-ban'; } $curtain->addAction( id(new PhabricatorActionView()) ->setName($disable_name) ->setIcon($disable_icon) ->setDisabled(!$can_disable) ->setWorkflow(true) ->setHref($disable_uri)); if ($can_edit) { $can_delete = $engine->canDeleteAnyEvents($viewer, $import); } else { $can_delete = false; } $delete_uri = "import/delete/{$id}/"; $delete_uri = $this->getApplicationURI($delete_uri); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete Imported Events')) ->setIcon('fa-times') ->setDisabled(!$can_delete) ->setWorkflow(true) ->setHref($delete_uri)); return $curtain; } private function buildPropertySection( PhabricatorCalendarImport $import) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setViewer($viewer); $engine = $import->getEngine(); $properties->addProperty( pht('Source Type'), $engine->getImportEngineTypeName()); if ($import->getIsDisabled()) { $auto_updates = phutil_tag('em', array(), pht('Import Disabled')); $has_trigger = false; } else { $frequency = $import->getTriggerFrequency(); $frequency_map = PhabricatorCalendarImport::getTriggerFrequencyMap(); $frequency_names = ipull($frequency_map, 'name'); $auto_updates = idx($frequency_names, $frequency, $frequency); if ($frequency == PhabricatorCalendarImport::FREQUENCY_ONCE) { $has_trigger = false; $auto_updates = phutil_tag('em', array(), $auto_updates); } else { $has_trigger = true; } } $properties->addProperty( pht('Automatic Updates'), $auto_updates); if ($has_trigger) { $trigger = id(new PhabricatorWorkerTriggerQuery()) ->setViewer($viewer) ->withPHIDs(array($import->getTriggerPHID())) ->needEvents(true) ->executeOne(); if (!$trigger) { $next_trigger = phutil_tag('em', array(), pht('Invalid Trigger')); } else { $now = PhabricatorTime::getNow(); $next_epoch = $trigger->getNextEventPrediction(); $next_trigger = pht( '%s (%s)', phabricator_datetime($next_epoch, $viewer), phutil_format_relative_time($next_epoch - $now)); } $properties->addProperty( pht('Next Update'), $next_trigger); } $engine->appendImportProperties( $viewer, $import, $properties); return $properties; } private function buildLogMessages(PhabricatorCalendarImport $import) { $viewer = $this->getViewer(); $logs = id(new PhabricatorCalendarImportLogQuery()) ->setViewer($viewer) ->withImportPHIDs(array($import->getPHID())) ->setLimit(25) ->execute(); $logs_view = id(new PhabricatorCalendarImportLogView()) ->setViewer($viewer) ->setLogs($logs); $all_uri = $this->getApplicationURI('import/log/'); $all_uri = (string)id(new PhutilURI($all_uri)) - ->setQueryParam('importSourcePHID', $import->getPHID()); + ->replaceQueryParam('importSourcePHID', $import->getPHID()); $all_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('View All')) ->setIcon('fa-search') ->setHref($all_uri); $header = id(new PHUIHeaderView()) ->setHeader(pht('Log Messages')) ->addActionLink($all_button); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($logs_view); } private function buildImportedEvents(PhabricatorCalendarImport $import) { $viewer = $this->getViewer(); $engine = id(new PhabricatorCalendarEventSearchEngine()) ->setViewer($viewer); $saved = $engine->newSavedQuery() ->setParameter('importSourcePHIDs', array($import->getPHID())); $pager = $engine->newPagerForSavedQuery($saved); $pager->setPageSize(25); $query = $engine->buildQueryFromSavedQuery($saved); $results = $engine->executeQuery($query, $pager); $view = $engine->renderResults($results, $saved); $list = $view->getObjectList(); $list->setNoDataString(pht('No imported events.')); $all_uri = $this->getApplicationURI(); $all_uri = (string)id(new PhutilURI($all_uri)) - ->setQueryParam('importSourcePHID', $import->getPHID()) - ->setQueryParam('display', 'list'); + ->replaceQueryParam('importSourcePHID', $import->getPHID()) + ->replaceQueryParam('display', 'list'); $all_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('View All')) ->setIcon('fa-search') ->setHref($all_uri); $header = id(new PHUIHeaderView()) ->setHeader(pht('Imported Events')) ->addActionLink($all_button); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList($list); } } diff --git a/src/applications/config/check/PhabricatorWebServerSetupCheck.php b/src/applications/config/check/PhabricatorWebServerSetupCheck.php index 398ebd6376..8f6885e8e8 100644 --- a/src/applications/config/check/PhabricatorWebServerSetupCheck.php +++ b/src/applications/config/check/PhabricatorWebServerSetupCheck.php @@ -1,265 +1,265 @@ newIssue('webserver.pagespeed') ->setName(pht('Disable Pagespeed')) ->setSummary(pht('Pagespeed is enabled, but should be disabled.')) ->setMessage( pht( 'Phabricator received an "X-Mod-Pagespeed" or "X-Page-Speed" '. 'HTTP header on this request, which indicates that you have '. 'enabled "mod_pagespeed" on this server. This module is not '. 'compatible with Phabricator. You should disable it.')); } $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); if (!strlen($base_uri)) { // If `phabricator.base-uri` is not set then we can't really do // anything. return; } $expect_user = 'alincoln'; $expect_pass = 'hunter2'; $send_path = '/test-%252A/'; $expect_path = '/test-%2A/'; $expect_key = 'duck-sound'; $expect_value = 'quack'; $base_uri = id(new PhutilURI($base_uri)) ->setPath($send_path) - ->setQueryParam($expect_key, $expect_value); + ->replaceQueryParam($expect_key, $expect_value); $self_future = id(new HTTPSFuture($base_uri)) ->addHeader('X-Phabricator-SelfCheck', 1) ->addHeader('Accept-Encoding', 'gzip') ->setHTTPBasicAuthCredentials( $expect_user, new PhutilOpaqueEnvelope($expect_pass)) ->setTimeout(5); // Make a request to the metadata service available on EC2 instances, // to test if we're running on a T2 instance in AWS so we can warn that // this is a bad idea. Outside of AWS, this request will just fail. $ec2_uri = 'http://169.254.169.254/latest/meta-data/instance-type'; $ec2_future = id(new HTTPSFuture($ec2_uri)) ->setTimeout(1); $futures = array( $self_future, $ec2_future, ); $futures = new FutureIterator($futures); foreach ($futures as $future) { // Just resolve the futures here. } try { list($body) = $ec2_future->resolvex(); $body = trim($body); if (preg_match('/^t2/', $body)) { $message = pht( 'Phabricator appears to be installed on a very small EC2 instance '. '(of class "%s") with burstable CPU. This is strongly discouraged. '. 'Phabricator regularly needs CPU, and these instances are often '. 'choked to death by CPU throttling. Use an instance with a normal '. 'CPU instead.', $body); $this->newIssue('ec2.burstable') ->setName(pht('Installed on Burstable CPU Instance')) ->setSummary( pht( 'Do not install Phabricator on an instance class with '. 'burstable CPU.')) ->setMessage($message); } } catch (Exception $ex) { // If this fails, just continue. We're probably not running in EC2. } try { list($body, $headers) = $self_future->resolvex(); } catch (Exception $ex) { // If this fails for whatever reason, just ignore it. Hopefully, the // error is obvious and the user can correct it on their own, but we // can't do much to offer diagnostic advice. return; } if (BaseHTTPFuture::getHeader($headers, 'Content-Encoding') != 'gzip') { $message = pht( 'Phabricator sent itself a request with "Accept-Encoding: gzip", '. 'but received an uncompressed response.'. "\n\n". 'This may indicate that your webserver is not configured to '. 'compress responses. If so, you should enable compression. '. 'Compression can dramatically improve performance, especially '. 'for clients with less bandwidth.'); $this->newIssue('webserver.gzip') ->setName(pht('GZip Compression May Not Be Enabled')) ->setSummary(pht('Your webserver may have compression disabled.')) ->setMessage($message); } else { if (function_exists('gzdecode')) { $body = gzdecode($body); } else { $body = null; } if (!$body) { // For now, just bail if we can't decode the response. // This might need to use the stronger magic in "AphrontRequestStream" // to decode more reliably. return; } } $structure = null; $caught = null; $extra_whitespace = ($body !== trim($body)); if (!$extra_whitespace) { try { $structure = phutil_json_decode($body); } catch (Exception $ex) { $caught = $ex; } } if (!$structure) { if ($extra_whitespace) { $message = pht( 'Phabricator sent itself a test request and expected to get a bare '. 'JSON response back, but the response had extra whitespace at '. 'the beginning or end.'. "\n\n". 'This usually means you have edited a file and left whitespace '. 'characters before the opening %s tag, or after a closing %s tag. '. 'Remove any leading whitespace, and prefer to omit closing tags.', phutil_tag('tt', array(), '')); } else { $short = id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(1024) ->truncateString($body); $message = pht( 'Phabricator sent itself a test request with the '. '"X-Phabricator-SelfCheck" header and expected to get a valid JSON '. 'response back. Instead, the response begins:'. "\n\n". '%s'. "\n\n". 'Something is misconfigured or otherwise mangling responses.', phutil_tag('pre', array(), $short)); } $this->newIssue('webserver.mangle') ->setName(pht('Mangled Webserver Response')) ->setSummary(pht('Your webserver produced an unexpected response.')) ->setMessage($message); // We can't run the other checks if we could not decode the response. return; } $actual_user = idx($structure, 'user'); $actual_pass = idx($structure, 'pass'); if (($expect_user != $actual_user) || ($actual_pass != $expect_pass)) { $message = pht( 'Phabricator sent itself a test request with an "Authorization" HTTP '. 'header, and expected those credentials to be transmitted. However, '. 'they were absent or incorrect when received. Phabricator sent '. 'username "%s" with password "%s"; received username "%s" and '. 'password "%s".'. "\n\n". 'Your webserver may not be configured to forward HTTP basic '. 'authentication. If you plan to use basic authentication (for '. 'example, to access repositories) you should reconfigure it.', $expect_user, $expect_pass, $actual_user, $actual_pass); $this->newIssue('webserver.basic-auth') ->setName(pht('HTTP Basic Auth Not Configured')) ->setSummary(pht('Your webserver is not forwarding credentials.')) ->setMessage($message); } $actual_path = idx($structure, 'path'); if ($expect_path != $actual_path) { $message = pht( 'Phabricator sent itself a test request with an unusual path, to '. 'test if your webserver is rewriting paths correctly. The path was '. 'not transmitted correctly.'. "\n\n". 'Phabricator sent a request to path "%s", and expected the webserver '. 'to decode and rewrite that path so that it received a request for '. '"%s". However, it received a request for "%s" instead.'. "\n\n". 'Verify that your rewrite rules are configured correctly, following '. 'the instructions in the documentation. If path encoding is not '. 'working properly you will be unable to access files with unusual '. 'names in repositories, among other issues.'. "\n\n". '(This problem can be caused by a missing "B" in your RewriteRule.)', $send_path, $expect_path, $actual_path); $this->newIssue('webserver.rewrites') ->setName(pht('HTTP Path Rewriting Incorrect')) ->setSummary(pht('Your webserver is rewriting paths improperly.')) ->setMessage($message); } $actual_key = pht(''); $actual_value = pht(''); foreach (idx($structure, 'params', array()) as $pair) { if (idx($pair, 'name') == $expect_key) { $actual_key = idx($pair, 'name'); $actual_value = idx($pair, 'value'); break; } } if (($expect_key !== $actual_key) || ($expect_value !== $actual_value)) { $message = pht( 'Phabricator sent itself a test request with an HTTP GET parameter, '. 'but the parameter was not transmitted. Sent "%s" with value "%s", '. 'got "%s" with value "%s".'. "\n\n". 'Your webserver is configured incorrectly and large parts of '. 'Phabricator will not work until this issue is corrected.'. "\n\n". '(This problem can be caused by a missing "QSA" in your RewriteRule.)', $expect_key, $expect_value, $actual_key, $actual_value); $this->newIssue('webserver.parameters') ->setName(pht('HTTP Parameters Not Transmitting')) ->setSummary( pht('Your webserver is not handling GET parameters properly.')) ->setMessage($message); } } } diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 996417a307..357d07631a 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -1,211 +1,211 @@ getUser(); $conpherence_id = $request->getURIData('id'); if (!$conpherence_id) { return new Aphront404Response(); } $query = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) ->needProfileImage(true) ->needTransactions(true) ->setTransactionLimit($this->getMainQueryLimit()); $before_transaction_id = $request->getInt('oldest_transaction_id'); $after_transaction_id = $request->getInt('newest_transaction_id'); $old_message_id = $request->getURIData('messageID'); if ($before_transaction_id && ($old_message_id || $after_transaction_id)) { throw new Aphront400Response(); } if ($old_message_id && $after_transaction_id) { throw new Aphront400Response(); } $marker_type = 'older'; if ($before_transaction_id) { $query ->setBeforeTransactionID($before_transaction_id); } if ($old_message_id) { $marker_type = 'olderandnewer'; $query ->setAfterTransactionID($old_message_id - 1); } if ($after_transaction_id) { $marker_type = 'newer'; $query ->setAfterTransactionID($after_transaction_id); } $conpherence = $query->executeOne(); if (!$conpherence) { return new Aphront404Response(); } $this->setConpherence($conpherence); $participant = $conpherence->getParticipantIfExists($user->getPHID()); $theme = ConpherenceRoomSettings::COLOR_LIGHT; if ($participant) { $settings = $participant->getSettings(); $theme = idx($settings, 'theme', ConpherenceRoomSettings::COLOR_LIGHT); if (!$participant->isUpToDate($conpherence)) { $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites(); $participant->markUpToDate($conpherence); $user->clearCacheData(PhabricatorUserMessageCountCacheType::KEY_COUNT); unset($write_guard); } } $data = ConpherenceTransactionRenderer::renderTransactions( $user, $conpherence, $marker_type); $messages = ConpherenceTransactionRenderer::renderMessagePaneContent( $data['transactions'], $data['oldest_transaction_id'], $data['newest_transaction_id']); if ($before_transaction_id || $after_transaction_id) { $header = null; $form = null; $content = array('transactions' => $messages); } else { $header = $this->buildHeaderPaneContent($conpherence); $search = $this->buildSearchForm(); $form = $this->renderFormContent(); $content = array( 'header' => $header, 'search' => $search, 'transactions' => $messages, 'form' => $form, ); } $d_data = $conpherence->getDisplayData($user); $content['title'] = $title = $d_data['title']; if ($request->isAjax()) { $dropdown_query = id(new AphlictDropdownDataQuery()) ->setViewer($user); $dropdown_query->execute(); $content['threadID'] = $conpherence->getID(); $content['threadPHID'] = $conpherence->getPHID(); $content['latestTransactionID'] = $data['latest_transaction_id']; $content['canEdit'] = PhabricatorPolicyFilter::hasCapability( $user, $conpherence, PhabricatorPolicyCapability::CAN_EDIT); $content['aphlictDropdownData'] = array( $dropdown_query->getNotificationData(), $dropdown_query->getConpherenceData(), ); return id(new AphrontAjaxResponse())->setContent($content); } $layout = id(new ConpherenceLayoutView()) ->setUser($user) ->setBaseURI($this->getApplicationURI()) ->setThread($conpherence) ->setHeader($header) ->setSearch($search) ->setMessages($messages) ->setReplyForm($form) ->setTheme($theme) ->setLatestTransactionID($data['latest_transaction_id']) ->setRole('thread'); $participating = $conpherence->getParticipantIfExists($user->getPHID()); if (!$user->isLoggedIn()) { $layout->addClass('conpherence-no-pontificate'); } return $this->newPage() ->setTitle($title) ->setPageObjectPHIDs(array($conpherence->getPHID())) ->appendChild($layout); } private function renderFormContent() { $conpherence = $this->getConpherence(); $user = $this->getRequest()->getUser(); $participating = $conpherence->getParticipantIfExists($user->getPHID()); $draft = PhabricatorDraft::newFromUserAndKey( $user, $conpherence->getPHID()); $update_uri = $this->getApplicationURI('update/'.$conpherence->getID().'/'); if ($user->isLoggedIn()) { $this->initBehavior('conpherence-pontificate'); if ($participating) { $action = ConpherenceUpdateActions::MESSAGE; $status = new PhabricatorNotificationStatusView(); } else { $action = ConpherenceUpdateActions::JOIN_ROOM; $status = pht('Sending a message will also join the room.'); } $form = id(new AphrontFormView()) ->setUser($user) ->setAction($update_uri) ->addSigil('conpherence-pontificate') ->setWorkflow(true) ->addHiddenInput('action', $action) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($user) ->setName('text') ->setSendOnEnter(true) ->setValue($draft->getDraft())); $status_view = phutil_tag( 'div', array( 'class' => 'conpherence-room-status', 'id' => 'conpherence-room-status', ), $status); $view = phutil_tag_div( 'pontificate-container', array($form, $status_view)); return $view; } else { // user not logged in so give them a login button. $login_href = id(new PhutilURI('/auth/start/')) - ->setQueryParam('next', '/'.$conpherence->getMonogram()); + ->replaceQueryParam('next', '/'.$conpherence->getMonogram()); return id(new PHUIFormLayoutView()) ->addClass('login-to-participate') ->appendInstructions(pht('Log in to join this room and participate.')) ->appendChild( id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Log In to Participate')) ->setHref((string)$login_href)); } } private function getMainQueryLimit() { $request = $this->getRequest(); $base_limit = ConpherenceThreadQuery::TRANSACTION_LIMIT; if ($request->getURIData('messageID')) { $base_limit = $base_limit - self::OLDER_FETCH_LIMIT; } return $base_limit; } } diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php index dfe328933a..fc62c4d5cb 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -1,357 +1,357 @@ dashboardID = $id; return $this; } public function getDashboardID() { return $this->dashboardID; } public function setHeaderMode($header_mode) { $this->headerMode = $header_mode; return $this; } public function getHeaderMode() { return $this->headerMode; } /** * Allow the engine to render the panel via Ajax. */ public function setEnableAsyncRendering($enable) { $this->enableAsyncRendering = $enable; return $this; } public function setParentPanelPHIDs(array $parents) { $this->parentPanelPHIDs = $parents; return $this; } public function getParentPanelPHIDs() { return $this->parentPanelPHIDs; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setPanel(PhabricatorDashboardPanel $panel) { $this->panel = $panel; return $this; } public function setMovable($movable) { $this->movable = $movable; return $this; } public function getMovable() { return $this->movable; } public function getPanel() { return $this->panel; } public function setPanelPHID($panel_phid) { $this->panelPHID = $panel_phid; return $this; } public function getPanelPHID() { return $this->panelPHID; } public function renderPanel() { $panel = $this->getPanel(); if (!$panel) { return $this->renderErrorPanel( pht('Missing or Restricted Panel'), pht( 'This panel does not exist, or you do not have permission '. 'to see it.')); } $panel_type = $panel->getImplementation(); if (!$panel_type) { return $this->renderErrorPanel( $panel->getName(), pht( 'This panel has type "%s", but that panel type is not known to '. 'Phabricator.', $panel->getPanelType())); } try { $this->detectRenderingCycle($panel); if ($this->enableAsyncRendering) { if ($panel_type->shouldRenderAsync()) { return $this->renderAsyncPanel(); } } return $this->renderNormalPanel(); } catch (Exception $ex) { return $this->renderErrorPanel( $panel->getName(), pht( '%s: %s', phutil_tag('strong', array(), get_class($ex)), $ex->getMessage())); } } private function renderNormalPanel() { $panel = $this->getPanel(); $panel_type = $panel->getImplementation(); $content = $panel_type->renderPanelContent( $this->getViewer(), $panel, $this); $header = $this->renderPanelHeader(); return $this->renderPanelDiv( $content, $header); } private function renderAsyncPanel() { $panel = $this->getPanel(); $panel_id = celerity_generate_unique_node_id(); $dashboard_id = $this->getDashboardID(); Javelin::initBehavior( 'dashboard-async-panel', array( 'panelID' => $panel_id, 'parentPanelPHIDs' => $this->getParentPanelPHIDs(), 'headerMode' => $this->getHeaderMode(), 'dashboardID' => $dashboard_id, 'uri' => '/dashboard/panel/render/'.$panel->getID().'/', )); $header = $this->renderPanelHeader(); $content = id(new PHUIPropertyListView()) ->addTextContent(pht('Loading...')); return $this->renderPanelDiv( $content, $header, $panel_id); } private function renderErrorPanel($title, $body) { switch ($this->getHeaderMode()) { case self::HEADER_MODE_NONE: $header = null; break; case self::HEADER_MODE_EDIT: $header = id(new PHUIHeaderView()) ->setHeader($title); $header = $this->addPanelHeaderActions($header); break; case self::HEADER_MODE_NORMAL: default: $header = id(new PHUIHeaderView()) ->setHeader($title); break; } $icon = id(new PHUIIconView()) ->setIcon('fa-warning red msr'); $content = id(new PHUIBoxView()) ->addClass('dashboard-box') ->addMargin(PHUI::MARGIN_MEDIUM) ->appendChild($icon) ->appendChild($body); return $this->renderPanelDiv( $content, $header); } private function renderPanelDiv( $content, $header = null, $id = null) { require_celerity_resource('phabricator-dashboard-css'); $panel = $this->getPanel(); if (!$id) { $id = celerity_generate_unique_node_id(); } $box = new PHUIObjectBoxView(); $interface = 'PhabricatorApplicationSearchResultView'; if ($content instanceof $interface) { if ($content->getObjectList()) { $box->setObjectList($content->getObjectList()); } if ($content->getTable()) { $box->setTable($content->getTable()); } if ($content->getContent()) { $box->appendChild($content->getContent()); } } else { $box->appendChild($content); } $box ->setHeader($header) ->setID($id) ->addClass('dashboard-box') ->addSigil('dashboard-panel'); if ($this->getMovable()) { $box->addSigil('panel-movable'); } if ($panel) { $box->setMetadata( array( 'objectPHID' => $panel->getPHID(), )); } return phutil_tag_div('dashboard-pane', $box); } private function renderPanelHeader() { $panel = $this->getPanel(); switch ($this->getHeaderMode()) { case self::HEADER_MODE_NONE: $header = null; break; case self::HEADER_MODE_EDIT: $header = id(new PHUIHeaderView()) ->setHeader($panel->getName()); $header = $this->addPanelHeaderActions($header); break; case self::HEADER_MODE_NORMAL: default: $header = id(new PHUIHeaderView()) ->setHeader($panel->getName()); $panel_type = $panel->getImplementation(); $header = $panel_type->adjustPanelHeader( $this->getViewer(), $panel, $this, $header); break; } return $header; } private function addPanelHeaderActions( PHUIHeaderView $header) { $panel = $this->getPanel(); $dashboard_id = $this->getDashboardID(); if ($panel) { $panel_id = $panel->getID(); $edit_uri = "/dashboard/panel/edit/{$panel_id}/"; $edit_uri = new PhutilURI($edit_uri); if ($dashboard_id) { - $edit_uri->setQueryParam('dashboardID', $dashboard_id); + $edit_uri->replaceQueryParam('dashboardID', $dashboard_id); } $action_edit = id(new PHUIIconView()) ->setIcon('fa-pencil') ->setWorkflow(true) ->setHref((string)$edit_uri); $header->addActionItem($action_edit); } if ($dashboard_id) { $panel_phid = $this->getPanelPHID(); $remove_uri = "/dashboard/removepanel/{$dashboard_id}/"; $remove_uri = id(new PhutilURI($remove_uri)) - ->setQueryParam('panelPHID', $panel_phid); + ->replaceQueryParam('panelPHID', $panel_phid); $action_remove = id(new PHUIIconView()) ->setIcon('fa-trash-o') ->setHref((string)$remove_uri) ->setWorkflow(true); $header->addActionItem($action_remove); } return $header; } /** * Detect graph cycles in panels, and deeply nested panels. * * This method throws if the current rendering stack is too deep or contains * a cycle. This can happen if you embed layout panels inside each other, * build a big stack of panels, or embed a panel in remarkup inside another * panel. Generally, all of this stuff is ridiculous and we just want to * shut it down. * * @param PhabricatorDashboardPanel Panel being rendered. * @return void */ private function detectRenderingCycle(PhabricatorDashboardPanel $panel) { if ($this->parentPanelPHIDs === null) { throw new PhutilInvalidStateException('setParentPanelPHIDs'); } $max_depth = 4; if (count($this->parentPanelPHIDs) >= $max_depth) { throw new Exception( pht( 'To render more than %s levels of panels nested inside other '. 'panels, purchase a subscription to Phabricator Gold.', new PhutilNumber($max_depth))); } if (in_array($panel->getPHID(), $this->parentPanelPHIDs)) { throw new Exception( pht( 'You awake in a twisting maze of mirrors, all alike. '. 'You are likely to be eaten by a graph cycle. '. 'Should you escape alive, you resolve to be more careful about '. 'putting dashboard panels inside themselves.')); } } } diff --git a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php index ba6aace971..9f6481c05b 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php @@ -1,147 +1,147 @@ viewer = $viewer; return $this; } public function setDashboard(PhabricatorDashboard $dashboard) { $this->dashboard = $dashboard; return $this; } public function setArrangeMode($mode) { $this->arrangeMode = $mode; return $this; } public function renderDashboard() { require_celerity_resource('phabricator-dashboard-css'); $dashboard = $this->dashboard; $viewer = $this->viewer; $layout_config = $dashboard->getLayoutConfigObject(); $panel_grid_locations = $layout_config->getPanelLocations(); $panels = mpull($dashboard->getPanels(), null, 'getPHID'); $dashboard_id = celerity_generate_unique_node_id(); $result = id(new AphrontMultiColumnView()) ->setID($dashboard_id) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_LARGE); if ($this->arrangeMode) { $h_mode = PhabricatorDashboardPanelRenderingEngine::HEADER_MODE_EDIT; } else { $h_mode = PhabricatorDashboardPanelRenderingEngine::HEADER_MODE_NORMAL; } foreach ($panel_grid_locations as $column => $panel_column_locations) { $panel_phids = $panel_column_locations; // TODO: This list may contain duplicates when the dashboard itself // does not? Perhaps this is related to T10612. For now, just unique // the list before moving on. $panel_phids = array_unique($panel_phids); $column_result = array(); foreach ($panel_phids as $panel_phid) { $panel_engine = id(new PhabricatorDashboardPanelRenderingEngine()) ->setViewer($viewer) ->setDashboardID($dashboard->getID()) ->setEnableAsyncRendering(true) ->setPanelPHID($panel_phid) ->setParentPanelPHIDs(array()) ->setHeaderMode($h_mode); $panel = idx($panels, $panel_phid); if ($panel) { $panel_engine->setPanel($panel); } $column_result[] = $panel_engine->renderPanel(); } $column_class = $layout_config->getColumnClass( $column, $this->arrangeMode); if ($this->arrangeMode) { $column_result[] = $this->renderAddPanelPlaceHolder($column); $column_result[] = $this->renderAddPanelUI($column); } $result->addColumn( $column_result, $column_class, $sigil = 'dashboard-column', $metadata = array('columnID' => $column)); } if ($this->arrangeMode) { Javelin::initBehavior( 'dashboard-move-panels', array( 'dashboardID' => $dashboard_id, 'moveURI' => '/dashboard/movepanel/'.$dashboard->getID().'/', )); } $view = id(new PHUIBoxView()) ->addClass('dashboard-view') ->appendChild($result); return $view; } private function renderAddPanelPlaceHolder($column) { $dashboard = $this->dashboard; $panels = $dashboard->getPanels(); return javelin_tag( 'span', array( 'sigil' => 'workflow', 'class' => 'drag-ghost dashboard-panel-placeholder', ), pht('This column does not have any panels yet.')); } private function renderAddPanelUI($column) { $dashboard_id = $this->dashboard->getID(); $create_uri = id(new PhutilURI('/dashboard/panel/create/')) - ->setQueryParam('dashboardID', $dashboard_id) - ->setQueryParam('column', $column); + ->replaceQueryParam('dashboardID', $dashboard_id) + ->replaceQueryParam('column', $column); $add_uri = id(new PhutilURI('/dashboard/addpanel/'.$dashboard_id.'/')) - ->setQueryParam('column', $column); + ->replaceQueryParam('column', $column); $create_button = id(new PHUIButtonView()) ->setTag('a') ->setHref($create_uri) ->setWorkflow(true) ->setText(pht('Create Panel')) ->addClass(PHUI::MARGIN_MEDIUM); $add_button = id(new PHUIButtonView()) ->setTag('a') ->setHref($add_uri) ->setWorkflow(true) ->setText(pht('Add Existing Panel')) ->addClass(PHUI::MARGIN_MEDIUM); return phutil_tag( 'div', array( 'style' => 'text-align: center;', ), array( $create_button, $add_button, )); } } diff --git a/src/applications/differential/controller/DifferentialDiffCreateController.php b/src/applications/differential/controller/DifferentialDiffCreateController.php index 284edf49d3..9aaf407e28 100644 --- a/src/applications/differential/controller/DifferentialDiffCreateController.php +++ b/src/applications/differential/controller/DifferentialDiffCreateController.php @@ -1,212 +1,212 @@ getViewer(); // If we're on the "Update Diff" workflow, load the revision we're going // to update. $revision = null; $revision_id = $request->getURIData('revisionID'); if ($revision_id) { $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($revision_id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$revision) { return new Aphront404Response(); } } $diff = null; // This object is just for policy stuff $diff_object = DifferentialDiff::initializeNewDiff($viewer); $repository_phid = null; $errors = array(); $e_diff = null; $e_file = null; $validation_exception = null; if ($request->isFormPost()) { $repository_tokenizer = $request->getArr( id(new DifferentialRepositoryField())->getFieldKey()); if ($repository_tokenizer) { $repository_phid = reset($repository_tokenizer); } if ($request->getFileExists('diff-file')) { $diff = PhabricatorFile::readUploadedFileData($_FILES['diff-file']); } else { $diff = $request->getStr('diff'); } if (!strlen($diff)) { $errors[] = pht( 'You can not create an empty diff. Paste a diff or upload a '. 'file containing a diff.'); $e_diff = pht('Required'); $e_file = pht('Required'); } if (!$errors) { try { $call = new ConduitCall( 'differential.createrawdiff', array( 'diff' => $diff, 'repositoryPHID' => $repository_phid, 'viewPolicy' => $request->getStr('viewPolicy'), )); $call->setUser($viewer); $result = $call->execute(); $diff_id = $result['id']; $uri = $this->getApplicationURI("diff/{$diff_id}/"); $uri = new PhutilURI($uri); if ($revision) { - $uri->setQueryParam('revisionID', $revision->getID()); + $uri->replaceQueryParam('revisionID', $revision->getID()); } return id(new AphrontRedirectResponse())->setURI($uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; } } } $form = new AphrontFormView(); $arcanist_href = PhabricatorEnv::getDoclink('Arcanist User Guide'); $arcanist_link = phutil_tag( 'a', array( 'href' => $arcanist_href, 'target' => '_blank', ), pht('Learn More')); $cancel_uri = $this->getApplicationURI(); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($diff_object) ->execute(); $info_view = null; if (!$request->isFormPost()) { $info_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setErrors( array( array( pht( 'The best way to create a diff is to use the Arcanist '. 'command-line tool.'), ' ', $arcanist_link, ), pht( 'You can also paste a diff above, or upload a file '. 'containing a diff (for example, from %s, %s or %s).', phutil_tag('tt', array(), 'svn diff'), phutil_tag('tt', array(), 'git diff'), phutil_tag('tt', array(), 'hg diff --git')), )); } if ($revision) { $title = pht('Update Diff'); $header = pht('Update Diff'); $button = pht('Continue'); $header_icon = 'fa-upload'; } else { $title = pht('Create Diff'); $header = pht('Create New Diff'); $button = pht('Create Diff'); $header_icon = 'fa-plus-square'; } $form ->setEncType('multipart/form-data') ->setUser($viewer); if ($revision) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Updating Revision')) ->setValue($viewer->renderHandle($revision->getPHID()))); } if ($repository_phid) { $repository_value = array($repository_phid); } else { $repository_value = array(); } $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Raw Diff')) ->setName('diff') ->setValue($diff) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setError($e_diff)) ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('Raw Diff From File')) ->setName('diff-file') ->setError($e_file)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setName(id(new DifferentialRepositoryField())->getFieldKey()) ->setLabel(pht('Repository')) ->setDatasource(new DiffusionRepositoryDatasource()) ->setValue($repository_value) ->setLimit(1)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setName('viewPolicy') ->setPolicyObject($diff_object) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($button)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setValidationException($validation_exception) ->setForm($form) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setFormErrors($errors); $crumbs = $this->buildApplicationCrumbs(); if ($revision) { $crumbs->addTextCrumb( $revision->getMonogram(), '/'.$revision->getMonogram()); } $crumbs->addTextCrumb($title); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setFooter(array( $form_box, $info_view, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } } diff --git a/src/applications/fact/controller/PhabricatorFactHomeController.php b/src/applications/fact/controller/PhabricatorFactHomeController.php index 82f6a0905b..56ffe3930b 100644 --- a/src/applications/fact/controller/PhabricatorFactHomeController.php +++ b/src/applications/fact/controller/PhabricatorFactHomeController.php @@ -1,59 +1,59 @@ getViewer(); if ($request->isFormPost()) { $uri = new PhutilURI('/fact/chart/'); - $uri->setQueryParam('y1', $request->getStr('y1')); + $uri->replaceQueryParam('y1', $request->getStr('y1')); return id(new AphrontRedirectResponse())->setURI($uri); } $chart_form = $this->buildChartForm(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Home')); $title = pht('Facts'); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( $chart_form, )); } private function buildChartForm() { $request = $this->getRequest(); $viewer = $request->getUser(); $specs = PhabricatorFact::getAllFacts(); $options = mpull($specs, 'getName', 'getKey'); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Y-Axis')) ->setName('y1') ->setOptions($options)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Plot Chart'))); $panel = new PHUIObjectBoxView(); $panel->setForm($form); $panel->setHeaderText(pht('Plot Chart')); return $panel; } } diff --git a/src/applications/files/controller/PhabricatorFileLightboxController.php b/src/applications/files/controller/PhabricatorFileLightboxController.php index 1f679d621b..59a826dd42 100644 --- a/src/applications/files/controller/PhabricatorFileLightboxController.php +++ b/src/applications/files/controller/PhabricatorFileLightboxController.php @@ -1,109 +1,109 @@ getViewer(); $phid = $request->getURIData('phid'); $comment = $request->getStr('comment'); $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); if (!$file) { return new Aphront404Response(); } if (strlen($comment)) { $xactions = array(); $xactions[] = id(new PhabricatorFileTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new PhabricatorFileTransactionComment()) ->setContent($comment)); $editor = id(new PhabricatorFileEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $editor->applyTransactions($file, $xactions); } $transactions = id(new PhabricatorFileTransactionQuery()) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)); $timeline = $this->buildTransactionTimeline($file, $transactions); $comment_form = $this->renderCommentForm($file); $info = phutil_tag( 'div', array( 'class' => 'phui-comment-panel-header', ), $file->getName()); require_celerity_resource('phui-comment-panel-css'); $content = phutil_tag( 'div', array( 'class' => 'phui-comment-panel', ), array( $info, $timeline, $comment_form, )); return id(new AphrontAjaxResponse()) ->setContent($content); } private function renderCommentForm(PhabricatorFile $file) { $viewer = $this->getViewer(); if (!$viewer->isLoggedIn()) { $login_href = id(new PhutilURI('/auth/start/')) - ->setQueryParam('next', '/'.$file->getMonogram()); + ->replaceQueryParam('next', '/'.$file->getMonogram()); return id(new PHUIFormLayoutView()) ->addClass('phui-comment-panel-empty') ->appendChild( id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Log In to Comment')) ->setHref((string)$login_href)); } $draft = PhabricatorDraft::newFromUserAndKey( $viewer, $file->getPHID()); $post_uri = $this->getApplicationURI('thread/'.$file->getPHID().'/'); $form = id(new AphrontFormView()) ->setUser($viewer) ->setAction($post_uri) ->addSigil('lightbox-comment-form') ->addClass('lightbox-comment-form') ->setWorkflow(true) ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($viewer) ->setName('comment') ->setValue($draft->getDraft())) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Comment'))); $view = phutil_tag_div('phui-comment-panel', $form); return $view; } } diff --git a/src/applications/files/controller/PhabricatorFileTransformListController.php b/src/applications/files/controller/PhabricatorFileTransformListController.php index ab5322fc1a..7b5bc9299d 100644 --- a/src/applications/files/controller/PhabricatorFileTransformListController.php +++ b/src/applications/files/controller/PhabricatorFileTransformListController.php @@ -1,147 +1,147 @@ getViewer(); $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) ->executeOne(); if (!$file) { return new Aphront404Response(); } $monogram = $file->getMonogram(); $xdst = id(new PhabricatorTransformedFile())->loadAllWhere( 'transformedPHID = %s', $file->getPHID()); $dst_rows = array(); foreach ($xdst as $source) { $dst_rows[] = array( $source->getTransform(), $viewer->renderHandle($source->getOriginalPHID()), ); } $dst_table = id(new AphrontTableView($dst_rows)) ->setHeaders( array( pht('Key'), pht('Source'), )) ->setColumnClasses( array( '', 'wide', )) ->setNoDataString( pht( 'This file was not created by transforming another file.')); $xsrc = id(new PhabricatorTransformedFile())->loadAllWhere( 'originalPHID = %s', $file->getPHID()); $xsrc = mpull($xsrc, 'getTransformedPHID', 'getTransform'); $src_rows = array(); $xforms = PhabricatorFileTransform::getAllTransforms(); foreach ($xforms as $xform) { $dst_phid = idx($xsrc, $xform->getTransformKey()); if ($xform->canApplyTransform($file)) { $can_apply = pht('Yes'); $view_href = $file->getURIForTransform($xform); $view_href = new PhutilURI($view_href); - $view_href->setQueryParam('regenerate', 'true'); + $view_href->replaceQueryParam('regenerate', 'true'); $view_text = pht('Regenerate'); $view_link = phutil_tag( 'a', array( 'class' => 'small button button-grey', 'href' => $view_href, ), $view_text); } else { $can_apply = phutil_tag('em', array(), pht('No')); $view_link = phutil_tag('em', array(), pht('None')); } if ($dst_phid) { $dst_link = $viewer->renderHandle($dst_phid); } else { $dst_link = phutil_tag('em', array(), pht('None')); } $src_rows[] = array( $xform->getTransformName(), $xform->getTransformKey(), $can_apply, $dst_link, $view_link, ); } $src_table = id(new AphrontTableView($src_rows)) ->setHeaders( array( pht('Name'), pht('Key'), pht('Supported'), pht('Transform'), pht('View'), )) ->setColumnClasses( array( 'wide', '', '', '', 'action', )); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($monogram, '/'.$monogram); $crumbs->addTextCrumb(pht('Transforms')); $crumbs->setBorder(true); $dst_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('File Sources')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($dst_table); $src_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Available Transforms')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($src_table); $title = pht('%s Transforms', $file->getName()); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setHeaderIcon('fa-arrows-alt'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $dst_box, $src_box, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } } diff --git a/src/applications/files/markup/PhabricatorImageRemarkupRule.php b/src/applications/files/markup/PhabricatorImageRemarkupRule.php index 5d1979ed3c..57ad75bbc5 100644 --- a/src/applications/files/markup/PhabricatorImageRemarkupRule.php +++ b/src/applications/files/markup/PhabricatorImageRemarkupRule.php @@ -1,171 +1,171 @@ isFlatText($matches[0])) { return $matches[0]; } $args = array(); $defaults = array( 'uri' => null, 'alt' => null, 'width' => null, 'height' => null, ); $trimmed_match = trim($matches[2]); if ($this->isURI($trimmed_match)) { $args['uri'] = $trimmed_match; } else { $parser = new PhutilSimpleOptions(); $keys = $parser->parse($trimmed_match); $uri_key = ''; foreach (array('src', 'uri', 'url') as $key) { if (array_key_exists($key, $keys)) { $uri_key = $key; } } if ($uri_key) { $args['uri'] = $keys[$uri_key]; } $args += $keys; } $args += $defaults; if (!strlen($args['uri'])) { return $matches[0]; } // Make sure this is something that looks roughly like a real URI. We'll // validate it more carefully before proxying it, but if whatever the user // has typed isn't even close, just decline to activate the rule behavior. try { $uri = new PhutilURI($args['uri']); if (!strlen($uri->getProtocol())) { return $matches[0]; } $args['uri'] = (string)$uri; } catch (Exception $ex) { return $matches[0]; } $engine = $this->getEngine(); $metadata_key = self::KEY_RULE_EXTERNAL_IMAGE; $metadata = $engine->getTextMetadata($metadata_key, array()); $token = $engine->storeText(''); $metadata[] = array( 'token' => $token, 'args' => $args, ); $engine->setTextMetadata($metadata_key, $metadata); return $token; } public function didMarkupText() { $engine = $this->getEngine(); $metadata_key = self::KEY_RULE_EXTERNAL_IMAGE; $images = $engine->getTextMetadata($metadata_key, array()); $engine->setTextMetadata($metadata_key, array()); if (!$images) { return; } // Look for images we've already successfully fetched that aren't about // to get eaten by the GC. For any we find, we can just emit a normal // "" tag pointing directly to the file. // For files which we don't hit in the cache, we emit a placeholder // instead and use AJAX to actually perform the fetch. $digests = array(); foreach ($images as $image) { $uri = $image['args']['uri']; $digests[] = PhabricatorHash::digestForIndex($uri); } $caches = id(new PhabricatorFileExternalRequest())->loadAllWhere( 'uriIndex IN (%Ls) AND isSuccessful = 1 AND ttl > %d', $digests, PhabricatorTime::getNow() + phutil_units('1 hour in seconds')); $file_phids = array(); foreach ($caches as $cache) { $file_phids[$cache->getFilePHID()] = $cache->getURI(); } $file_map = array(); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array_keys($file_phids)) ->execute(); foreach ($files as $file) { $phid = $file->getPHID(); $file_remote_uri = $file_phids[$phid]; $file_view_uri = $file->getViewURI(); $file_map[$file_remote_uri] = $file_view_uri; } } foreach ($images as $image) { $args = $image['args']; $uri = $args['uri']; $direct_uri = idx($file_map, $uri); if ($direct_uri) { $img = phutil_tag( 'img', array( 'src' => $direct_uri, 'alt' => $args['alt'], 'width' => $args['width'], 'height' => $args['height'], )); } else { $src_uri = id(new PhutilURI('/file/imageproxy/')) - ->setQueryParam('uri', $uri); + ->replaceQueryParam('uri', $uri); $img = id(new PHUIRemarkupImageView()) ->setURI($src_uri) ->setAlt($args['alt']) ->setWidth($args['width']) ->setHeight($args['height']); } $engine->overwriteStoredText($image['token'], $img); } } private function isURI($uri_string) { // Very simple check to make sure it starts with either http or https. // If it does, we'll try to treat it like a valid URI return preg_match('~^https?\:\/\/.*\z~i', $uri_string); } } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 0f96d76b91..ba826e16e8 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -1,605 +1,605 @@ getViewer(); $id = $request->getURIData('id'); $task = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needSubscriberPHIDs(true) ->executeOne(); if (!$task) { return new Aphront404Response(); } $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_VIEW); $field_list ->setViewer($viewer) ->readFieldsFromStorage($task); $edit_engine = id(new ManiphestEditEngine()) ->setViewer($viewer) ->setTargetObject($task); $edge_types = array( ManiphestTaskHasCommitEdgeType::EDGECONST, ManiphestTaskHasRevisionEdgeType::EDGECONST, ManiphestTaskHasMockEdgeType::EDGECONST, PhabricatorObjectMentionedByObjectEdgeType::EDGECONST, PhabricatorObjectMentionsObjectEdgeType::EDGECONST, ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST, ); $phid = $task->getPHID(); $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($phid)) ->withEdgeTypes($edge_types); $edges = idx($query->execute(), $phid); $phids = array_fill_keys($query->getDestinationPHIDs(), true); if ($task->getOwnerPHID()) { $phids[$task->getOwnerPHID()] = true; } $phids[$task->getAuthorPHID()] = true; $phids = array_keys($phids); $handles = $viewer->loadHandles($phids); $timeline = $this->buildTransactionTimeline( $task, new ManiphestTransactionQuery()); $monogram = $task->getMonogram(); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb($monogram) ->setBorder(true); $header = $this->buildHeaderView($task); $details = $this->buildPropertyView($task, $field_list, $edges, $handles); $description = $this->buildDescriptionView($task); $curtain = $this->buildCurtain($task, $edit_engine); $title = pht('%s %s', $monogram, $task->getTitle()); $comment_view = $edit_engine ->buildEditEngineCommentView($task); $timeline->setQuoteRef($monogram); $comment_view->setTransactionTimeline($timeline); $related_tabs = array(); $graph_menu = null; $graph_limit = 100; $task_graph = id(new ManiphestTaskGraph()) ->setViewer($viewer) ->setSeedPHID($task->getPHID()) ->setLimit($graph_limit) ->loadGraph(); if (!$task_graph->isEmpty()) { $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; $parent_map = $task_graph->getEdges($parent_type); $subtask_map = $task_graph->getEdges($subtask_type); $parent_list = idx($parent_map, $task->getPHID(), array()); $subtask_list = idx($subtask_map, $task->getPHID(), array()); $has_parents = (bool)$parent_list; $has_subtasks = (bool)$subtask_list; $search_text = pht('Search...'); // First, get a count of direct parent tasks and subtasks. If there // are too many of these, we just don't draw anything. You can use // the search button to browse tasks with the search UI instead. $direct_count = count($parent_list) + count($subtask_list); if ($direct_count > $graph_limit) { $message = pht( 'Task graph too large to display (this task is directly connected '. 'to more than %s other tasks). Use %s to explore connected tasks.', $graph_limit, phutil_tag('strong', array(), $search_text)); $message = phutil_tag('em', array(), $message); $graph_table = id(new PHUIPropertyListView()) ->addTextContent($message); } else { // If there aren't too many direct tasks, but there are too many total // tasks, we'll only render directly connected tasks. if ($task_graph->isOverLimit()) { $task_graph->setRenderOnlyAdjacentNodes(true); } $graph_table = $task_graph->newGraphTable(); } $parents_uri = urisprintf( '/?subtaskIDs=%d#R', $task->getID()); $parents_uri = $this->getApplicationURI($parents_uri); $subtasks_uri = urisprintf( '/?parentIDs=%d#R', $task->getID()); $subtasks_uri = $this->getApplicationURI($subtasks_uri); $dropdown_menu = id(new PhabricatorActionListView()) ->setViewer($viewer) ->addAction( id(new PhabricatorActionView()) ->setHref($parents_uri) ->setName(pht('Search Parent Tasks')) ->setDisabled(!$has_parents) ->setIcon('fa-chevron-circle-up')) ->addAction( id(new PhabricatorActionView()) ->setHref($subtasks_uri) ->setName(pht('Search Subtasks')) ->setDisabled(!$has_subtasks) ->setIcon('fa-chevron-circle-down')); $graph_menu = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-search') ->setText($search_text) ->setDropdownMenu($dropdown_menu); $related_tabs[] = id(new PHUITabView()) ->setName(pht('Task Graph')) ->setKey('graph') ->appendChild($graph_table); } $related_tabs[] = $this->newMocksTab($task, $query); $related_tabs[] = $this->newMentionsTab($task, $query); $related_tabs[] = $this->newDuplicatesTab($task, $query); $tab_view = null; $related_tabs = array_filter($related_tabs); if ($related_tabs) { $tab_group = new PHUITabGroupView(); foreach ($related_tabs as $tab) { $tab_group->addTab($tab); } $related_header = id(new PHUIHeaderView()) ->setHeader(pht('Related Objects')); if ($graph_menu) { $related_header->addActionLink($graph_menu); } $tab_view = id(new PHUIObjectBoxView()) ->setHeader($related_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addTabGroup($tab_group); } $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn( array( $tab_view, $timeline, $comment_view, )) ->addPropertySection(pht('Description'), $description) ->addPropertySection(pht('Details'), $details); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setPageObjectPHIDs( array( $task->getPHID(), )) ->appendChild($view); } private function buildHeaderView(ManiphestTask $task) { $view = id(new PHUIHeaderView()) ->setHeader($task->getTitle()) ->setUser($this->getRequest()->getUser()) ->setPolicyObject($task); $priority_name = ManiphestTaskPriority::getTaskPriorityName( $task->getPriority()); $priority_color = ManiphestTaskPriority::getTaskPriorityColor( $task->getPriority()); $status = $task->getStatus(); $status_name = ManiphestTaskStatus::renderFullDescription( $status, $priority_name); $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name); $view->setHeaderIcon(ManiphestTaskStatus::getStatusIcon( $task->getStatus()).' '.$priority_color); if (ManiphestTaskPoints::getIsEnabled()) { $points = $task->getPoints(); if ($points !== null) { $points_name = pht('%s %s', $task->getPoints(), ManiphestTaskPoints::getPointsLabel()); $tag = id(new PHUITagView()) ->setName($points_name) ->setColor(PHUITagView::COLOR_BLUE) ->setType(PHUITagView::TYPE_SHADE); $view->addTag($tag); } } $subtype = $task->newSubtypeObject(); if ($subtype && $subtype->hasTagView()) { $subtype_tag = $subtype->newTagView(); $view->addTag($subtype_tag); } return $view; } private function buildCurtain( ManiphestTask $task, PhabricatorEditEngine $edit_engine) { $viewer = $this->getViewer(); $id = $task->getID(); $phid = $task->getPHID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $task, PhabricatorPolicyCapability::CAN_EDIT); $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $task); // We expect a policy dialog if you can't edit the task, and expect a // lock override dialog if you can't interact with it. $workflow_edit = (!$can_edit || !$can_interact); $curtain = $this->newCurtainView($task); $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Task')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("/task/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow($workflow_edit)); $subtype_map = $task->newEditEngineSubtypeMap(); $subtask_options = $subtype_map->getCreateFormsForSubtype( $edit_engine, $task); // If no forms are available, we want to show the user an error. // If one form is available, we take them user directly to the form. // If two or more forms are available, we give the user a choice. // The "subtask" controller handles the first case (no forms) and the // third case (more than one form). In the case of one form, we link // directly to the form. $subtask_uri = "/task/subtask/{$id}/"; $subtask_workflow = true; if (count($subtask_options) == 1) { $subtask_form = head($subtask_options); $form_key = $subtask_form->getIdentifier(); $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) - ->setQueryParam('parent', $id) - ->setQueryParam('template', $id) - ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); + ->replaceQueryParam('parent', $id) + ->replaceQueryParam('template', $id) + ->replaceQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); $subtask_workflow = false; } $subtask_uri = $this->getApplicationURI($subtask_uri); $subtask_item = id(new PhabricatorActionView()) ->setName(pht('Create Subtask')) ->setHref($subtask_uri) ->setIcon('fa-level-down') ->setDisabled(!$subtask_options) ->setWorkflow($subtask_workflow); $relationship_list = PhabricatorObjectRelationshipList::newForObject( $viewer, $task); $submenu_actions = array( $subtask_item, ManiphestTaskHasParentRelationship::RELATIONSHIPKEY, ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY, ManiphestTaskMergeInRelationship::RELATIONSHIPKEY, ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY, ); $task_submenu = $relationship_list->newActionSubmenu($submenu_actions) ->setName(pht('Edit Related Tasks...')) ->setIcon('fa-anchor'); $curtain->addAction($task_submenu); $relationship_submenu = $relationship_list->newActionMenu(); if ($relationship_submenu) { $curtain->addAction($relationship_submenu); } $owner_phid = $task->getOwnerPHID(); $author_phid = $task->getAuthorPHID(); $handles = $viewer->loadHandles(array($owner_phid, $author_phid)); if ($owner_phid) { $image_uri = $handles[$owner_phid]->getImageURI(); $image_href = $handles[$owner_phid]->getURI(); $owner = $viewer->renderHandle($owner_phid)->render(); $content = phutil_tag('strong', array(), $owner); $assigned_to = id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } else { $assigned_to = phutil_tag('em', array(), pht('None')); } $curtain->newPanel() ->setHeaderText(pht('Assigned To')) ->appendChild($assigned_to); $author_uri = $handles[$author_phid]->getImageURI(); $author_href = $handles[$author_phid]->getURI(); $author = $viewer->renderHandle($author_phid)->render(); $content = phutil_tag('strong', array(), $author); $date = phabricator_date($task->getDateCreated(), $viewer); $content = pht('%s, %s', $content, $date); $authored_by = id(new PHUIHeadThingView()) ->setImage($author_uri) ->setImageHref($author_href) ->setContent($content); $curtain->newPanel() ->setHeaderText(pht('Authored By')) ->appendChild($authored_by); return $curtain; } private function buildPropertyView( ManiphestTask $task, PhabricatorCustomFieldList $field_list, array $edges, $handles) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $source = $task->getOriginalEmailSource(); if ($source) { $subject = '[T'.$task->getID().'] '.$task->getTitle(); $view->addProperty( pht('From Email'), phutil_tag( 'a', array( 'href' => 'mailto:'.$source.'?subject='.$subject, ), $source)); } $edge_types = array( ManiphestTaskHasRevisionEdgeType::EDGECONST => pht('Differential Revisions'), ); $revisions_commits = array(); $commit_phids = array_keys( $edges[ManiphestTaskHasCommitEdgeType::EDGECONST]); if ($commit_phids) { $commit_drev = DiffusionCommitHasRevisionEdgeType::EDGECONST; $drev_edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($commit_phids) ->withEdgeTypes(array($commit_drev)) ->execute(); foreach ($commit_phids as $phid) { $revisions_commits[$phid] = $handles->renderHandle($phid) ->setShowHovercard(true) ->setShowStateIcon(true); $revision_phid = key($drev_edges[$phid][$commit_drev]); $revision_handle = $handles->getHandleIfExists($revision_phid); if ($revision_handle) { $task_drev = ManiphestTaskHasRevisionEdgeType::EDGECONST; unset($edges[$task_drev][$revision_phid]); $revisions_commits[$phid] = hsprintf( '%s / %s', $revision_handle->renderHovercardLink($revision_handle->getName()), $revisions_commits[$phid]); } } } foreach ($edge_types as $edge_type => $edge_name) { if (!$edges[$edge_type]) { continue; } $edge_handles = $viewer->loadHandles(array_keys($edges[$edge_type])); $edge_list = $edge_handles->renderList() ->setShowStateIcons(true); $view->addProperty($edge_name, $edge_list); } if ($revisions_commits) { $view->addProperty( pht('Commits'), phutil_implode_html(phutil_tag('br'), $revisions_commits)); } $field_list->appendFieldsToPropertyList( $task, $viewer, $view); if ($view->hasAnyProperties()) { return $view; } return null; } private function buildDescriptionView(ManiphestTask $task) { $viewer = $this->getViewer(); $section = null; $description = $task->getDescription(); if (strlen($description)) { $section = new PHUIPropertyListView(); $section->addTextContent( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), id(new PHUIRemarkupView($viewer, $description)) ->setContextObject($task))); } return $section; } private function newMocksTab( ManiphestTask $task, PhabricatorEdgeQuery $edge_query) { $mock_type = ManiphestTaskHasMockEdgeType::EDGECONST; $mock_phids = $edge_query->getDestinationPHIDs(array(), array($mock_type)); if (!$mock_phids) { return null; } $viewer = $this->getViewer(); $handles = $viewer->loadHandles($mock_phids); // TODO: It would be nice to render this as pinboard-style thumbnails, // similar to "{M123}", instead of a list of links. $view = id(new PHUIPropertyListView()) ->addProperty(pht('Mocks'), $handles->renderList()); return id(new PHUITabView()) ->setName(pht('Mocks')) ->setKey('mocks') ->appendChild($view); } private function newMentionsTab( ManiphestTask $task, PhabricatorEdgeQuery $edge_query) { $in_type = PhabricatorObjectMentionedByObjectEdgeType::EDGECONST; $out_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST; $in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type)); $out_phids = $edge_query->getDestinationPHIDs(array(), array($out_type)); // Filter out any mentioned users from the list. These are not generally // very interesting to show in a relationship summary since they usually // end up as subscribers anyway. $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; foreach ($out_phids as $key => $out_phid) { if (phid_get_type($out_phid) == $user_type) { unset($out_phids[$key]); } } if (!$in_phids && !$out_phids) { return null; } $viewer = $this->getViewer(); $in_handles = $viewer->loadHandles($in_phids); $out_handles = $viewer->loadHandles($out_phids); $in_handles = $this->getCompleteHandles($in_handles); $out_handles = $this->getCompleteHandles($out_handles); if (!count($in_handles) && !count($out_handles)) { return null; } $view = new PHUIPropertyListView(); if (count($in_handles)) { $view->addProperty(pht('Mentioned In'), $in_handles->renderList()); } if (count($out_handles)) { $view->addProperty(pht('Mentioned Here'), $out_handles->renderList()); } return id(new PHUITabView()) ->setName(pht('Mentions')) ->setKey('mentions') ->appendChild($view); } private function newDuplicatesTab( ManiphestTask $task, PhabricatorEdgeQuery $edge_query) { $in_type = ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST; $in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type)); $viewer = $this->getViewer(); $in_handles = $viewer->loadHandles($in_phids); $in_handles = $this->getCompleteHandles($in_handles); $view = new PHUIPropertyListView(); if (!count($in_handles)) { return null; } $view->addProperty( pht('Duplicates Merged Here'), $in_handles->renderList()); return id(new PHUITabView()) ->setName(pht('Duplicates')) ->setKey('duplicates') ->appendChild($view); } private function getCompleteHandles(PhabricatorHandleList $handles) { $phids = array(); foreach ($handles as $phid => $handle) { if (!$handle->isComplete()) { continue; } $phids[] = $phid; } return $handles->newSublist($phids); } } diff --git a/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php b/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php index 5256c1bd26..3105cf661d 100644 --- a/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php +++ b/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php @@ -1,71 +1,71 @@ getViewer(); $id = $request->getURIData('id'); $task = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$task) { return new Aphront404Response(); } $cancel_uri = $task->getURI(); $edit_engine = id(new ManiphestEditEngine()) ->setViewer($viewer) ->setTargetObject($task); $subtype_map = $task->newEditEngineSubtypeMap(); $subtype_options = $subtype_map->getCreateFormsForSubtype( $edit_engine, $task); if (!$subtype_options) { return $this->newDialog() ->setTitle(pht('No Forms')) ->appendParagraph( pht( 'You do not have access to any forms which can be used to '. 'create a subtask.')) ->addCancelButton($cancel_uri, pht('Close')); } $menu = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setBig(true) ->setFlush(true); foreach ($subtype_options as $form_key => $subtype_form) { $subtype_key = $subtype_form->getSubtype(); $subtype = $subtype_map->getSubtype($subtype_key); $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) - ->setQueryParam('parent', $id) - ->setQueryParam('template', $id) - ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); + ->replaceQueryParam('parent', $id) + ->replaceQueryParam('template', $id) + ->replaceQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); $subtask_uri = $this->getApplicationURI($subtask_uri); $item = id(new PHUIObjectItemView()) ->setHeader($subtype_form->getDisplayName()) ->setHref($subtask_uri) ->setClickable(true) ->setImageIcon($subtype->newIconView()) ->addAttribute($subtype->getName()); $menu->addItem($item); } return $this->newDialog() ->setTitle(pht('Choose Subtype')) ->appendChild($menu) ->addCancelButton($cancel_uri); } } diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php index e7128fb850..6bf5daf29b 100644 --- a/src/applications/maniphest/view/ManiphestTaskListView.php +++ b/src/applications/maniphest/view/ManiphestTaskListView.php @@ -1,179 +1,179 @@ tasks = $tasks; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setShowBatchControls($show_batch_controls) { $this->showBatchControls = $show_batch_controls; return $this; } public function setShowSubpriorityControls($show_subpriority_controls) { $this->showSubpriorityControls = $show_subpriority_controls; return $this; } public function setNoDataString($text) { $this->noDataString = $text; return $this; } public function render() { $handles = $this->handles; require_celerity_resource('maniphest-task-summary-css'); $list = new PHUIObjectItemListView(); if ($this->noDataString) { $list->setNoDataString($this->noDataString); } else { $list->setNoDataString(pht('No tasks.')); } $status_map = ManiphestTaskStatus::getTaskStatusMap(); $color_map = ManiphestTaskPriority::getColorMap(); $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); if ($this->showBatchControls) { Javelin::initBehavior('maniphest-list-editor'); } foreach ($this->tasks as $task) { $item = id(new PHUIObjectItemView()) ->setUser($this->getUser()) ->setObject($task) ->setObjectName('T'.$task->getID()) ->setHeader($task->getTitle()) ->setHref('/T'.$task->getID()); if ($task->getOwnerPHID()) { $owner = $handles[$task->getOwnerPHID()]; $item->addByline(pht('Assigned: %s', $owner->renderLink())); } $status = $task->getStatus(); $pri = idx($priority_map, $task->getPriority()); $status_name = idx($status_map, $task->getStatus()); $tooltip = pht('%s, %s', $status_name, $pri); $icon = ManiphestTaskStatus::getStatusIcon($task->getStatus()); $color = idx($color_map, $task->getPriority(), 'grey'); if ($task->isClosed()) { $item->setDisabled(true); $color = 'grey'; } $item->setStatusIcon($icon.' '.$color, $tooltip); if ($task->isClosed()) { $closed_epoch = $task->getClosedEpoch(); // We don't expect a task to be closed without a closed epoch, but // recover if we find one. This can happen with older objects or with // lipsum test data. if (!$closed_epoch) { $closed_epoch = $task->getDateModified(); } $item->addIcon( 'fa-check-square-o grey', phabricator_datetime($closed_epoch, $this->getUser())); } else { $item->addIcon( 'none', phabricator_datetime($task->getDateModified(), $this->getUser())); } if ($this->showSubpriorityControls) { $item->setGrippable(true); } if ($this->showSubpriorityControls || $this->showBatchControls) { $item->addSigil('maniphest-task'); } $subtype = $task->newSubtypeObject(); if ($subtype && $subtype->hasTagView()) { $subtype_tag = $subtype->newTagView() ->setSlimShady(true); $item->addAttribute($subtype_tag); } $project_handles = array_select_keys( $handles, array_reverse($task->getProjectPHIDs())); $item->addAttribute( id(new PHUIHandleTagListView()) ->setLimit(4) ->setNoDataString(pht('No Projects')) ->setSlim(true) ->setHandles($project_handles)); $item->setMetadata( array( 'taskID' => $task->getID(), )); if ($this->showBatchControls) { $href = new PhutilURI('/maniphest/task/edit/'.$task->getID().'/'); if (!$this->showSubpriorityControls) { - $href->setQueryParam('ungrippable', 'true'); + $href->replaceQueryParam('ungrippable', 'true'); } $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-pencil') ->addSigil('maniphest-edit-task') ->setHref($href)); } $list->addItem($item); } return $list; } public static function loadTaskHandles( PhabricatorUser $viewer, array $tasks) { assert_instances_of($tasks, 'ManiphestTask'); $phids = array(); foreach ($tasks as $task) { $assigned_phid = $task->getOwnerPHID(); if ($assigned_phid) { $phids[] = $assigned_phid; } foreach ($task->getProjectPHIDs() as $project_phid) { $phids[] = $project_phid; } } if (!$phids) { return array(); } return id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); } } diff --git a/src/applications/multimeter/controller/MultimeterSampleController.php b/src/applications/multimeter/controller/MultimeterSampleController.php index da09641d22..73c9ba530e 100644 --- a/src/applications/multimeter/controller/MultimeterSampleController.php +++ b/src/applications/multimeter/controller/MultimeterSampleController.php @@ -1,349 +1,349 @@ getViewer(); $group_map = $this->getColumnMap(); $group = explode('.', $request->getStr('group')); $group = array_intersect($group, array_keys($group_map)); $group = array_fuse($group); if (empty($group['type'])) { $group['type'] = 'type'; } $now = PhabricatorTime::getNow(); $ago = ($now - phutil_units('24 hours in seconds')); $table = new MultimeterEvent(); $conn = $table->establishConnection('r'); $where = array(); $where[] = qsprintf( $conn, 'epoch >= %d AND epoch <= %d', $ago, $now); $with = array(); foreach ($group_map as $key => $column) { // Don't let non-admins filter by viewers, this feels a little too // invasive of privacy. if ($key == 'viewer') { if (!$viewer->getIsAdmin()) { continue; } } $with[$key] = $request->getStrList($key); if ($with[$key]) { $where[] = qsprintf( $conn, '%T IN (%Ls)', $column, $with[$key]); } } $data = queryfx_all( $conn, 'SELECT *, count(*) AS N, SUM(sampleRate * resourceCost) AS totalCost, SUM(sampleRate * resourceCost) / SUM(sampleRate) AS averageCost FROM %T WHERE %LA GROUP BY %LC ORDER BY totalCost DESC, MAX(id) DESC LIMIT 100', $table->getTableName(), $where, array_select_keys($group_map, $group)); $this->loadDimensions($data); $phids = array(); foreach ($data as $row) { $viewer_name = $this->getViewerDimension($row['eventViewerID']) ->getName(); $viewer_phid = $this->getEventViewerPHID($viewer_name); if ($viewer_phid) { $phids[] = $viewer_phid; } } $handles = $viewer->loadHandles($phids); $rows = array(); foreach ($data as $row) { if ($row['N'] == 1) { $events_col = $row['id']; } else { $events_col = $this->renderGroupingLink( $group, 'id', pht('%s Event(s)', new PhutilNumber($row['N']))); } if (isset($group['request'])) { $request_col = $row['requestKey']; if (!$with['request']) { $request_col = $this->renderSelectionLink( 'request', $row['requestKey'], $request_col); } } else { $request_col = $this->renderGroupingLink($group, 'request'); } if (isset($group['viewer'])) { if ($viewer->getIsAdmin()) { $viewer_col = $this->getViewerDimension($row['eventViewerID']) ->getName(); $viewer_phid = $this->getEventViewerPHID($viewer_col); if ($viewer_phid) { $viewer_col = $handles[$viewer_phid]->getName(); } if (!$with['viewer']) { $viewer_col = $this->renderSelectionLink( 'viewer', $row['eventViewerID'], $viewer_col); } } else { $viewer_col = phutil_tag('em', array(), pht('(Masked)')); } } else { $viewer_col = $this->renderGroupingLink($group, 'viewer'); } if (isset($group['context'])) { $context_col = $this->getContextDimension($row['eventContextID']) ->getName(); if (!$with['context']) { $context_col = $this->renderSelectionLink( 'context', $row['eventContextID'], $context_col); } } else { $context_col = $this->renderGroupingLink($group, 'context'); } if (isset($group['host'])) { $host_col = $this->getHostDimension($row['eventHostID']) ->getName(); if (!$with['host']) { $host_col = $this->renderSelectionLink( 'host', $row['eventHostID'], $host_col); } } else { $host_col = $this->renderGroupingLink($group, 'host'); } if (isset($group['label'])) { $label_col = $this->getLabelDimension($row['eventLabelID']) ->getName(); if (!$with['label']) { $label_col = $this->renderSelectionLink( 'label', $row['eventLabelID'], $label_col); } } else { $label_col = $this->renderGroupingLink($group, 'label'); } if ($with['type']) { $type_col = MultimeterEvent::getEventTypeName($row['eventType']); } else { $type_col = $this->renderSelectionLink( 'type', $row['eventType'], MultimeterEvent::getEventTypeName($row['eventType'])); } $rows[] = array( $events_col, $request_col, $viewer_col, $context_col, $host_col, $type_col, $label_col, MultimeterEvent::formatResourceCost( $viewer, $row['eventType'], $row['averageCost']), MultimeterEvent::formatResourceCost( $viewer, $row['eventType'], $row['totalCost']), ($row['N'] == 1) ? $row['sampleRate'] : '-', phabricator_datetime($row['epoch'], $viewer), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('ID'), pht('Request'), pht('Viewer'), pht('Context'), pht('Host'), pht('Type'), pht('Label'), pht('Avg'), pht('Cost'), pht('Rate'), pht('Epoch'), )) ->setColumnClasses( array( null, null, null, null, null, null, 'wide', 'n', 'n', 'n', null, )); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Samples')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( pht('Samples'), $this->getGroupURI(array(), true)); $crumbs->setBorder(true); $crumb_map = array( 'host' => pht('By Host'), 'context' => pht('By Context'), 'viewer' => pht('By Viewer'), 'request' => pht('By Request'), 'label' => pht('By Label'), 'id' => pht('By ID'), ); $parts = array(); foreach ($group as $item) { if ($item == 'type') { continue; } $parts[$item] = $item; $crumbs->addTextCrumb( idx($crumb_map, $item, $item), $this->getGroupURI($parts, true)); } $header = id(new PHUIHeaderView()) ->setHeader( pht( 'Samples (%s - %s)', phabricator_datetime($ago, $viewer), phabricator_datetime($now, $viewer))) ->setHeaderIcon('fa-motorcycle'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($box); return $this->newPage() ->setTitle(pht('Samples')) ->setCrumbs($crumbs) ->appendChild($view); } private function renderGroupingLink(array $group, $key, $name = null) { $group[] = $key; $uri = $this->getGroupURI($group); if ($name === null) { $name = pht('(All)'); } return phutil_tag( 'a', array( 'href' => $uri, 'style' => 'font-weight: bold', ), $name); } private function getGroupURI(array $group, $wipe = false) { unset($group['type']); $uri = clone $this->getRequest()->getRequestURI(); $group = implode('.', $group); if (!strlen($group)) { $group = null; } - $uri->setQueryParam('group', $group); + $uri->replaceQueryParam('group', $group); if ($wipe) { foreach ($this->getColumnMap() as $key => $column) { - $uri->setQueryParam($key, null); + $uri->removeQueryParam($key); } } return $uri; } private function renderSelectionLink($key, $value, $link_text) { $value = (array)$value; $uri = clone $this->getRequest()->getRequestURI(); - $uri->setQueryParam($key, implode(',', $value)); + $uri->replaceQueryParam($key, implode(',', $value)); return phutil_tag( 'a', array( 'href' => $uri, ), $link_text); } private function getColumnMap() { return array( 'type' => 'eventType', 'host' => 'eventHostID', 'context' => 'eventContextID', 'viewer' => 'eventViewerID', 'request' => 'requestKey', 'label' => 'eventLabelID', 'id' => 'id', ); } private function getEventViewerPHID($viewer_name) { if (!strncmp($viewer_name, 'user.', 5)) { return substr($viewer_name, 5); } return null; } } diff --git a/src/applications/notification/client/PhabricatorNotificationServerRef.php b/src/applications/notification/client/PhabricatorNotificationServerRef.php index b183221eee..46d03a5c3a 100644 --- a/src/applications/notification/client/PhabricatorNotificationServerRef.php +++ b/src/applications/notification/client/PhabricatorNotificationServerRef.php @@ -1,234 +1,234 @@ type = $type; return $this; } public function getType() { return $this->type; } public function setHost($host) { $this->host = $host; return $this; } public function getHost() { return $this->host; } public function setPort($port) { $this->port = $port; return $this; } public function getPort() { return $this->port; } public function setProtocol($protocol) { $this->protocol = $protocol; return $this; } public function getProtocol() { return $this->protocol; } public function setPath($path) { $this->path = $path; return $this; } public function getPath() { return $this->path; } public function setIsDisabled($is_disabled) { $this->isDisabled = $is_disabled; return $this; } public function getIsDisabled() { return $this->isDisabled; } public static function getLiveServers() { $cache = PhabricatorCaches::getRequestCache(); $refs = $cache->getKey(self::KEY_REFS); if (!$refs) { $refs = self::newRefs(); $cache->setKey(self::KEY_REFS, $refs); } return $refs; } public static function newRefs() { $configs = PhabricatorEnv::getEnvConfig('notification.servers'); $refs = array(); foreach ($configs as $config) { $ref = id(new self()) ->setType($config['type']) ->setHost($config['host']) ->setPort($config['port']) ->setProtocol($config['protocol']) ->setPath(idx($config, 'path')) ->setIsDisabled(idx($config, 'disabled', false)); $refs[] = $ref; } return $refs; } public static function getEnabledServers() { $servers = self::getLiveServers(); foreach ($servers as $key => $server) { if ($server->getIsDisabled()) { unset($servers[$key]); } } return array_values($servers); } public static function getEnabledAdminServers() { $servers = self::getEnabledServers(); foreach ($servers as $key => $server) { if (!$server->isAdminServer()) { unset($servers[$key]); } } return array_values($servers); } public static function getEnabledClientServers($with_protocol) { $servers = self::getEnabledServers(); foreach ($servers as $key => $server) { if ($server->isAdminServer()) { unset($servers[$key]); continue; } $protocol = $server->getProtocol(); if ($protocol != $with_protocol) { unset($servers[$key]); continue; } } return array_values($servers); } public function isAdminServer() { return ($this->type == 'admin'); } public function getURI($to_path = null) { $full_path = rtrim($this->getPath(), '/').'/'.ltrim($to_path, '/'); $uri = id(new PhutilURI('http://'.$this->getHost())) ->setProtocol($this->getProtocol()) ->setPort($this->getPort()) ->setPath($full_path); $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if (strlen($instance)) { - $uri->setQueryParam('instance', $instance); + $uri->replaceQueryParam('instance', $instance); } return $uri; } public function getWebsocketURI($to_path = null) { $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if (strlen($instance)) { $to_path = $to_path.'~'.$instance.'/'; } $uri = $this->getURI($to_path); if ($this->getProtocol() == 'https') { $uri->setProtocol('wss'); } else { $uri->setProtocol('ws'); } return $uri; } public function testClient() { if ($this->isAdminServer()) { throw new Exception( pht('Unable to test client on an admin server!')); } $server_uri = $this->getURI(); try { id(new HTTPSFuture($server_uri)) ->setTimeout(2) ->resolvex(); } catch (HTTPFutureHTTPResponseStatus $ex) { // This is what we expect when things are working correctly. if ($ex->getStatusCode() == 501) { return true; } throw $ex; } throw new Exception( pht('Got HTTP 200, but expected HTTP 501 (WebSocket Upgrade)!')); } public function loadServerStatus() { if (!$this->isAdminServer()) { throw new Exception( pht( 'Unable to load server status: this is not an admin server!')); } $server_uri = $this->getURI('/status/'); list($body) = id(new HTTPSFuture($server_uri)) ->setTimeout(2) ->resolvex(); return phutil_json_decode($body); } public function postMessage(array $data) { if (!$this->isAdminServer()) { throw new Exception( pht('Unable to post message: this is not an admin server!')); } $server_uri = $this->getURI('/'); $payload = phutil_json_encode($data); id(new HTTPSFuture($server_uri, $payload)) ->setMethod('POST') ->setTimeout(2) ->resolvex(); } } diff --git a/src/applications/notification/controller/PhabricatorNotificationPanelController.php b/src/applications/notification/controller/PhabricatorNotificationPanelController.php index 1e956a60ea..5991e8db7b 100644 --- a/src/applications/notification/controller/PhabricatorNotificationPanelController.php +++ b/src/applications/notification/controller/PhabricatorNotificationPanelController.php @@ -1,163 +1,163 @@ getViewer(); $unread_count = $viewer->getUnreadNotificationCount(); $warning = $this->prunePhantomNotifications($unread_count); $query = id(new PhabricatorNotificationQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->setLimit(10); $stories = $query->execute(); $clear_ui_class = 'phabricator-notification-clear-all'; $clear_uri = id(new PhutilURI('/notification/clear/')); if ($stories) { $builder = id(new PhabricatorNotificationBuilder($stories)) ->setUser($viewer); $notifications_view = $builder->buildView(); $content = $notifications_view->render(); - $clear_uri->setQueryParam( + $clear_uri->replaceQueryParam( 'chronoKey', head($stories)->getChronologicalKey()); } else { $content = phutil_tag_div( 'phabricator-notification no-notifications', pht('You have no notifications.')); $clear_ui_class .= ' disabled'; } $clear_ui = javelin_tag( 'a', array( 'sigil' => 'workflow', 'href' => (string)$clear_uri, 'class' => $clear_ui_class, ), pht('Mark All Read')); $notifications_link = phutil_tag( 'a', array( 'href' => '/notification/', ), pht('Notifications')); $connection_status = new PhabricatorNotificationStatusView(); $connection_ui = phutil_tag( 'div', array( 'class' => 'phabricator-notification-footer', ), $connection_status); $header = phutil_tag( 'div', array( 'class' => 'phabricator-notification-header', ), array( $notifications_link, $clear_ui, )); $content = hsprintf( '%s%s%s%s', $header, $warning, $content, $connection_ui); $json = array( 'content' => $content, 'number' => (int)$unread_count, ); return id(new AphrontAjaxResponse())->setContent($json); } private function prunePhantomNotifications($unread_count) { // See T8953. If you have an unread notification about an object you // do not have permission to view, it isn't possible to clear it by // visiting the object. Identify these notifications and mark them as // read. $viewer = $this->getViewer(); if (!$unread_count) { return null; } $table = new PhabricatorFeedStoryNotification(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT chronologicalKey, primaryObjectPHID FROM %T WHERE userPHID = %s AND hasViewed = 0', $table->getTableName(), $viewer->getPHID()); if (!$rows) { return null; } $map = array(); foreach ($rows as $row) { $map[$row['primaryObjectPHID']][] = $row['chronologicalKey']; } $handles = $viewer->loadHandles(array_keys($map)); $purge_keys = array(); foreach ($handles as $handle) { $phid = $handle->getPHID(); if ($handle->isComplete()) { continue; } foreach ($map[$phid] as $chronological_key) { $purge_keys[] = $chronological_key; } } if (!$purge_keys) { return null; } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $conn = $table->establishConnection('w'); queryfx( $conn, 'UPDATE %T SET hasViewed = 1 WHERE userPHID = %s AND chronologicalKey IN (%Ls)', $table->getTableName(), $viewer->getPHID(), $purge_keys); PhabricatorUserCache::clearCache( PhabricatorUserNotificationCountCacheType::KEY_COUNT, $viewer->getPHID()); unset($unguarded); return phutil_tag( 'div', array( 'class' => 'phabricator-notification phabricator-notification-warning', ), pht( '%s notification(s) about objects which no longer exist or which '. 'you can no longer see were discarded.', phutil_count($purge_keys))); } } diff --git a/src/applications/notification/query/PhabricatorNotificationSearchEngine.php b/src/applications/notification/query/PhabricatorNotificationSearchEngine.php index 0ee7327bfc..c7e1998333 100644 --- a/src/applications/notification/query/PhabricatorNotificationSearchEngine.php +++ b/src/applications/notification/query/PhabricatorNotificationSearchEngine.php @@ -1,141 +1,141 @@ setParameter( 'unread', $this->readBoolFromRequest($request, 'unread')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorNotificationQuery()) ->withUserPHIDs(array($this->requireViewer()->getPHID())); if ($saved->getParameter('unread')) { $query->withUnread(true); } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $unread = $saved->getParameter('unread'); $form->appendChild( id(new AphrontFormCheckboxControl()) ->setLabel(pht('Unread')) ->addCheckbox( 'unread', 1, pht('Show only unread notifications.'), $unread)); } protected function getURI($path) { return '/notification/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'all' => pht('All Notifications'), 'unread' => pht('Unread Notifications'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; case 'unread': return $query->setParameter('unread', true); } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $notifications, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($notifications, 'PhabricatorFeedStory'); $viewer = $this->requireViewer(); $image = id(new PHUIIconView()) ->setIcon('fa-bell-o'); $button = id(new PHUIButtonView()) ->setTag('a') ->addSigil('workflow') ->setColor(PHUIButtonView::GREY) ->setIcon($image) ->setText(pht('Mark All Read')); switch ($query->getQueryKey()) { case 'unread': $header = pht('Unread Notifications'); $no_data = pht('You have no unread notifications.'); break; default: $header = pht('Notifications'); $no_data = pht('You have no notifications.'); break; } $clear_uri = id(new PhutilURI('/notification/clear/')); if ($notifications) { $builder = id(new PhabricatorNotificationBuilder($notifications)) ->setUser($viewer); $view = $builder->buildView(); - $clear_uri->setQueryParam( + $clear_uri->replaceQueryParam( 'chronoKey', head($notifications)->getChronologicalKey()); } else { $view = phutil_tag_div( 'phabricator-notification no-notifications', $no_data); $button->setDisabled(true); } $button->setHref((string)$clear_uri); $view = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_MEDIUM) ->addClass('phabricator-notification-list') ->appendChild($view); $result = new PhabricatorApplicationSearchResultView(); $result->addAction($button); $result->setContent($view); return $result; } public function shouldUseOffsetPaging() { return true; } } diff --git a/src/applications/oauthserver/PhabricatorOAuthResponse.php b/src/applications/oauthserver/PhabricatorOAuthResponse.php index 62c0fc9821..e0fca827b4 100644 --- a/src/applications/oauthserver/PhabricatorOAuthResponse.php +++ b/src/applications/oauthserver/PhabricatorOAuthResponse.php @@ -1,106 +1,106 @@ state; } public function setState($state) { $this->state = $state; return $this; } private function getContent() { return $this->content; } public function setContent($content) { $this->content = $content; return $this; } private function getClientURI() { return $this->clientURI; } public function setClientURI(PhutilURI $uri) { $this->setHTTPResponseCode(302); $this->clientURI = $uri; return $this; } private function getFullURI() { $base_uri = $this->getClientURI(); $query_params = $this->buildResponseDict(); foreach ($query_params as $key => $value) { - $base_uri->setQueryParam($key, $value); + $base_uri->replaceQueryParam($key, $value); } return $base_uri; } private function getError() { return $this->error; } public function setError($error) { // errors sometimes redirect to the client (302) but otherwise // the spec says all code 400 if (!$this->getClientURI()) { $this->setHTTPResponseCode(400); } $this->error = $error; return $this; } private function getErrorDescription() { return $this->errorDescription; } public function setErrorDescription($error_description) { $this->errorDescription = $error_description; return $this; } public function __construct() { $this->setHTTPResponseCode(200); // assume the best } public function getHeaders() { $headers = array( array('Content-Type', 'application/json'), ); if ($this->getClientURI()) { $headers[] = array('Location', $this->getFullURI()); } // TODO -- T844 set headers with X-Auth-Scopes, etc $headers = array_merge(parent::getHeaders(), $headers); return $headers; } private function buildResponseDict() { if ($this->getError()) { $content = array( 'error' => $this->getError(), 'error_description' => $this->getErrorDescription(), ); $this->setContent($content); } $content = $this->getContent(); if (!$content) { return ''; } if ($this->getState()) { $content['state'] = $this->getState(); } return $content; } public function buildResponseString() { return $this->encodeJSONForHTTPResponse($this->buildResponseDict()); } } diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php index 745be3e820..2b454e00ef 100644 --- a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php +++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php @@ -1,316 +1,316 @@ getViewer(); $server = new PhabricatorOAuthServer(); $client_phid = $request->getStr('client_id'); $redirect_uri = $request->getStr('redirect_uri'); $response_type = $request->getStr('response_type'); // state is an opaque value the client sent us for their own purposes // we just need to send it right back to them in the response! $state = $request->getStr('state'); if (!$client_phid) { return $this->buildErrorResponse( 'invalid_request', pht('Malformed Request'), pht( 'Required parameter %s was not present in the request.', phutil_tag('strong', array(), 'client_id'))); } // We require that users must be able to see an OAuth application // in order to authorize it. This allows an application's visibility // policy to be used to restrict authorized users. try { $client = id(new PhabricatorOAuthServerClientQuery()) ->setViewer($viewer) ->withPHIDs(array($client_phid)) ->executeOne(); } catch (PhabricatorPolicyException $ex) { $ex->setContext(self::CONTEXT_AUTHORIZE); throw $ex; } $server->setUser($viewer); $is_authorized = false; $authorization = null; $uri = null; $name = null; // one giant try / catch around all the exciting database stuff so we // can return a 'server_error' response if something goes wrong! try { if (!$client) { return $this->buildErrorResponse( 'invalid_request', pht('Invalid Client Application'), pht( 'Request parameter %s does not specify a valid client application.', phutil_tag('strong', array(), 'client_id'))); } if ($client->getIsDisabled()) { return $this->buildErrorResponse( 'invalid_request', pht('Application Disabled'), pht( 'The %s OAuth application has been disabled.', phutil_tag('strong', array(), 'client_id'))); } $name = $client->getName(); $server->setClient($client); if ($redirect_uri) { $client_uri = new PhutilURI($client->getRedirectURI()); $redirect_uri = new PhutilURI($redirect_uri); if (!($server->validateSecondaryRedirectURI($redirect_uri, $client_uri))) { return $this->buildErrorResponse( 'invalid_request', pht('Invalid Redirect URI'), pht( 'Request parameter %s specifies an invalid redirect URI. '. 'The redirect URI must be a fully-qualified domain with no '. 'fragments, and must have the same domain and at least '. 'the same query parameters as the redirect URI the client '. 'registered.', phutil_tag('strong', array(), 'redirect_uri'))); } $uri = $redirect_uri; } else { $uri = new PhutilURI($client->getRedirectURI()); } if (empty($response_type)) { return $this->buildErrorResponse( 'invalid_request', pht('Invalid Response Type'), pht( 'Required request parameter %s is missing.', phutil_tag('strong', array(), 'response_type'))); } if ($response_type != 'code') { return $this->buildErrorResponse( 'unsupported_response_type', pht('Unsupported Response Type'), pht( 'Request parameter %s specifies an unsupported response type. '. 'Valid response types are: %s.', phutil_tag('strong', array(), 'response_type'), implode(', ', array('code')))); } $requested_scope = $request->getStrList('scope'); $requested_scope = array_fuse($requested_scope); $scope = PhabricatorOAuthServerScope::filterScope($requested_scope); // NOTE: We're always requiring a confirmation dialog to redirect. // Partly this is a general defense against redirect attacks, and // partly this shakes off anchors in the URI (which are not shaken // by 302'ing). $auth_info = $server->userHasAuthorizedClient($scope); list($is_authorized, $authorization) = $auth_info; if ($request->isFormPost()) { if ($authorization) { $authorization->setScope($scope)->save(); } else { $authorization = $server->authorizeClient($scope); } $is_authorized = true; } } catch (Exception $e) { return $this->buildErrorResponse( 'server_error', pht('Server Error'), pht( 'The authorization server encountered an unexpected condition '. 'which prevented it from fulfilling the request.')); } // When we reach this part of the controller, we can be in two states: // // 1. The user has not authorized the application yet. We want to // give them an "Authorize this application?" dialog. // 2. The user has authorized the application. We want to give them // a "Confirm Login" dialog. if ($is_authorized) { // The second case is simpler, so handle it first. The user either // authorized the application previously, or has just authorized the // application. Show them a confirm dialog with a normal link back to // the application. This shakes anchors from the URI. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $auth_code = $server->generateAuthorizationCode($uri); unset($unguarded); $full_uri = $this->addQueryParams( $uri, array( 'code' => $auth_code->getCode(), 'scope' => $authorization->getScopeString(), 'state' => $state, )); if ($client->getIsTrusted()) { // NOTE: See T13099. We currently emit a "Content-Security-Policy" // which includes a narrow "form-action". At the time of writing, // Chrome applies "form-action" to redirects following form submission. // This can lead to a situation where a user enters the OAuth workflow // and is prompted for MFA. When they submit an MFA response, the form // can redirect here, and Chrome will block the "Location" redirect. // To avoid this, render an interstitial. We only actually need to do // this in Chrome (but do it everywhere for consistency) and only need // to do it if the request is a redirect after a form submission (but // we can't tell if it is or not). Javelin::initBehavior( 'redirect', array( 'uri' => (string)$full_uri, )); return $this->newDialog() ->setTitle(pht('Authenticate: %s', $name)) ->appendParagraph( pht( 'Authorization for "%s" confirmed, redirecting...', phutil_tag('strong', array(), $name))) ->addCancelButton((string)$full_uri, pht('Continue')); } // TODO: It would be nice to give the user more options here, like // reviewing permissions, canceling the authorization, or aborting // the workflow. $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Authenticate: %s', $name)) ->appendParagraph( pht( 'This application ("%s") is authorized to use your Phabricator '. 'credentials. Continue to complete the authentication workflow.', phutil_tag('strong', array(), $name))) ->addCancelButton((string)$full_uri, pht('Continue to Application')); return id(new AphrontDialogResponse())->setDialog($dialog); } // Here, we're confirming authorization for the application. if ($authorization) { $missing_scope = array_diff_key($scope, $authorization->getScope()); } else { $missing_scope = $scope; } $form = id(new AphrontFormView()) ->addHiddenInput('client_id', $client_phid) ->addHiddenInput('redirect_uri', $redirect_uri) ->addHiddenInput('response_type', $response_type) ->addHiddenInput('state', $state) ->addHiddenInput('scope', $request->getStr('scope')) ->setUser($viewer); $cancel_msg = pht('The user declined to authorize this application.'); $cancel_uri = $this->addQueryParams( $uri, array( 'error' => 'access_denied', 'error_description' => $cancel_msg, )); $dialog = $this->newDialog() ->setShortTitle(pht('Authorize Access')) ->setTitle(pht('Authorize "%s"?', $name)) ->setSubmitURI($request->getRequestURI()->getPath()) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendParagraph( pht( 'Do you want to authorize the external application "%s" to '. 'access your Phabricator account data, including your primary '. 'email address?', phutil_tag('strong', array(), $name))) ->appendForm($form) ->addSubmitButton(pht('Authorize Access')) ->addCancelButton((string)$cancel_uri, pht('Do Not Authorize')); if ($missing_scope) { $dialog->appendParagraph( pht( 'This application has requested these additional permissions. '. 'Authorizing it will grant it the permissions it requests:')); foreach ($missing_scope as $scope_key => $ignored) { // TODO: Once we introduce more scopes, explain them here. } } $unknown_scope = array_diff_key($requested_scope, $scope); if ($unknown_scope) { $dialog->appendParagraph( pht( 'This application also requested additional unrecognized '. 'permissions. These permissions may have existed in an older '. 'version of Phabricator, or may be from a future version of '. 'Phabricator. They will not be granted.')); $unknown_form = id(new AphrontFormView()) ->setViewer($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Unknown Scope')) ->setValue(implode(', ', array_keys($unknown_scope))) ->setDisabled(true)); $dialog->appendForm($unknown_form); } return $dialog; } private function buildErrorResponse($code, $title, $message) { $viewer = $this->getRequest()->getUser(); return $this->newDialog() ->setTitle(pht('OAuth: %s', $title)) ->appendParagraph($message) ->appendParagraph( pht('OAuth Error Code: %s', phutil_tag('tt', array(), $code))) ->addCancelButton('/', pht('Alas!')); } private function addQueryParams(PhutilURI $uri, array $params) { $full_uri = clone $uri; foreach ($params as $key => $value) { if (strlen($value)) { - $full_uri->setQueryParam($key, $value); + $full_uri->replaceQueryParam($key, $value); } } return $full_uri; } } diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index 99645c4a91..786de07cfd 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -1,223 +1,223 @@ commentFormID = $comment_form_id; return $this; } public function getCommentFormID() { return $this->commentFormID; } public function setRequestURI(PhutilURI $request_uri) { $this->requestURI = $request_uri; return $this; } public function getRequestURI() { return $this->requestURI; } public function setImageID($image_id) { $this->imageID = $image_id; return $this; } public function getImageID() { return $this->imageID; } public function setMock(PholioMock $mock) { $this->mock = $mock; return $this; } public function getMock() { return $this->mock; } public function __construct() { $this->panelID = celerity_generate_unique_node_id(); $this->viewportID = celerity_generate_unique_node_id(); } public function getBehaviorConfig() { if (!$this->getMock()) { throw new PhutilInvalidStateException('setMock'); } if ($this->behaviorConfig === null) { $this->behaviorConfig = $this->calculateBehaviorConfig(); } return $this->behaviorConfig; } private function calculateBehaviorConfig() { $mock = $this->getMock(); // TODO: We could maybe do a better job with tailoring this, which is the // image shown on the review stage. $viewer = $this->getUser(); $default = PhabricatorFile::loadBuiltin($viewer, 'image-100x100.png'); $images = array(); $current_set = 0; foreach ($mock->getImages() as $image) { $file = $image->getFile(); $metadata = $file->getMetadata(); $x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH); $y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT); $is_obs = (bool)$image->getIsObsolete(); if (!$is_obs) { $current_set++; } $description = $image->getDescription(); if (strlen($description)) { $description = new PHUIRemarkupView($viewer, $description); } $history_uri = '/pholio/image/history/'.$image->getID().'/'; $images[] = array( 'id' => $image->getID(), 'fullURI' => $file->getBestURI(), 'stageURI' => ($file->isViewableImage() ? $file->getBestURI() : $default->getBestURI()), 'pageURI' => $this->getImagePageURI($image, $mock), 'downloadURI' => $file->getDownloadURI(), 'historyURI' => $history_uri, 'width' => $x, 'height' => $y, 'title' => $image->getName(), 'descriptionMarkup' => $description, 'isObsolete' => (bool)$image->getIsObsolete(), 'isImage' => $file->isViewableImage(), 'isViewable' => $file->isViewableInBrowser(), ); } $ids = mpull($mock->getActiveImages(), null, 'getID'); if ($this->imageID && isset($ids[$this->imageID])) { $selected_id = $this->imageID; } else { $selected_id = head_key($ids); } $navsequence = array(); foreach ($mock->getActiveImages() as $image) { $navsequence[] = $image->getID(); } $full_icon = array( javelin_tag('span', array('aural' => true), pht('View Raw File')), id(new PHUIIconView())->setIcon('fa-file-image-o'), ); $download_icon = array( javelin_tag('span', array('aural' => true), pht('Download File')), id(new PHUIIconView())->setIcon('fa-download'), ); $login_uri = id(new PhutilURI('/login/')) - ->setQueryParam('next', (string)$this->getRequestURI()); + ->replaceQueryParam('next', (string)$this->getRequestURI()); $config = array( 'mockID' => $mock->getID(), 'panelID' => $this->panelID, 'viewportID' => $this->viewportID, 'commentFormID' => $this->getCommentFormID(), 'images' => $images, 'selectedID' => $selected_id, 'loggedIn' => $this->getUser()->isLoggedIn(), 'logInLink' => (string)$login_uri, 'navsequence' => $navsequence, 'fullIcon' => hsprintf('%s', $full_icon), 'downloadIcon' => hsprintf('%s', $download_icon), 'currentSetSize' => $current_set, ); return $config; } public function render() { if (!$this->getMock()) { throw new PhutilInvalidStateException('setMock'); } $mock = $this->getMock(); require_celerity_resource('javelin-behavior-pholio-mock-view'); $panel_id = $this->panelID; $viewport_id = $this->viewportID; $config = $this->getBehaviorConfig(); Javelin::initBehavior( 'pholio-mock-view', $this->getBehaviorConfig()); $mock_wrapper = javelin_tag( 'div', array( 'id' => $this->viewportID, 'sigil' => 'mock-viewport', 'class' => 'pholio-mock-image-viewport', ), ''); $image_header = javelin_tag( 'div', array( 'id' => 'mock-image-header', 'class' => 'pholio-mock-image-header', ), ''); $mock_wrapper = javelin_tag( 'div', array( 'id' => $this->panelID, 'sigil' => 'mock-panel touchable', 'class' => 'pholio-mock-image-panel', ), array( $image_header, $mock_wrapper, )); $inline_comments_holder = javelin_tag( 'div', array( 'id' => 'mock-image-description', 'sigil' => 'mock-image-description', 'class' => 'mock-image-description', ), ''); return phutil_tag( 'div', array( 'class' => 'pholio-mock-image-container', 'id' => 'pholio-mock-image-container', ), array($mock_wrapper, $inline_comments_holder)); } private function getImagePageURI(PholioImage $image, PholioMock $mock) { $uri = '/M'.$mock->getID().'/'.$image->getID().'/'; return $uri; } } diff --git a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php b/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php index 847fbf5716..c068862631 100644 --- a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php +++ b/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php @@ -1,303 +1,303 @@ getViewer(); $account_id = $request->getURIData('accountID'); $account = id(new PhortuneAccountQuery()) ->setViewer($viewer) ->withIDs(array($account_id)) ->executeOne(); if (!$account) { return new Aphront404Response(); } $account_id = $account->getID(); $merchant = id(new PhortuneMerchantQuery()) ->setViewer($viewer) ->withIDs(array($request->getInt('merchantID'))) ->executeOne(); if (!$merchant) { return new Aphront404Response(); } $cart_id = $request->getInt('cartID'); $subscription_id = $request->getInt('subscriptionID'); if ($cart_id) { $cancel_uri = $this->getApplicationURI("cart/{$cart_id}/checkout/"); } else if ($subscription_id) { $cancel_uri = $this->getApplicationURI( "{$account_id}/subscription/edit/{$subscription_id}/"); } else { $cancel_uri = $this->getApplicationURI($account->getID().'/'); } $providers = $this->loadCreatePaymentMethodProvidersForMerchant($merchant); if (!$providers) { throw new Exception( pht( 'There are no payment providers enabled that can add payment '. 'methods.')); } if (count($providers) == 1) { // If there's only one provider, always choose it. $provider_id = head_key($providers); } else { $provider_id = $request->getInt('providerID'); if (empty($providers[$provider_id])) { $choices = array(); foreach ($providers as $provider) { $choices[] = $this->renderSelectProvider($provider); } $content = phutil_tag( 'div', array( 'class' => 'phortune-payment-method-list', ), $choices); return $this->newDialog() ->setRenderDialogAsDiv(true) ->setTitle(pht('Add Payment Method')) ->appendParagraph(pht('Choose a payment method to add:')) ->appendChild($content) ->addCancelButton($cancel_uri); } } $provider = $providers[$provider_id]; $errors = array(); $display_exception = null; if ($request->isFormPost() && $request->getBool('isProviderForm')) { $method = id(new PhortunePaymentMethod()) ->setAccountPHID($account->getPHID()) ->setAuthorPHID($viewer->getPHID()) ->setMerchantPHID($merchant->getPHID()) ->setProviderPHID($provider->getProviderConfig()->getPHID()) ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE); // Limit the rate at which you can attempt to add payment methods. This // is intended as a line of defense against using Phortune to validate a // large list of stolen credit card numbers. PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), new PhortuneAddPaymentMethodAction(), 1); if (!$errors) { $errors = $this->processClientErrors( $provider, $request->getStr('errors')); } if (!$errors) { $client_token_raw = $request->getStr('token'); $client_token = null; try { $client_token = phutil_json_decode($client_token_raw); } catch (PhutilJSONParserException $ex) { $errors[] = pht( 'There was an error decoding token information submitted by the '. 'client. Expected a JSON-encoded token dictionary, received: %s.', nonempty($client_token_raw, pht('nothing'))); } if (!$provider->validateCreatePaymentMethodToken($client_token)) { $errors[] = pht( 'There was an error with the payment token submitted by the '. 'client. Expected a valid dictionary, received: %s.', $client_token_raw); } if (!$errors) { try { $provider->createPaymentMethodFromRequest( $request, $method, $client_token); } catch (PhortuneDisplayException $exception) { $display_exception = $exception; } catch (Exception $ex) { $errors = array( pht('There was an error adding this payment method:'), $ex->getMessage(), ); } } } if (!$errors && !$display_exception) { $method->save(); // If we added this method on a cart flow, return to the cart to // check out. if ($cart_id) { $next_uri = $this->getApplicationURI( "cart/{$cart_id}/checkout/?paymentMethodID=".$method->getID()); } else if ($subscription_id) { $next_uri = new PhutilURI($cancel_uri); - $next_uri->setQueryParam('added', true); + $next_uri->replaceQueryParam('added', true); } else { $account_uri = $this->getApplicationURI($account->getID().'/'); $next_uri = new PhutilURI($account_uri); $next_uri->setFragment('payment'); } return id(new AphrontRedirectResponse())->setURI($next_uri); } else { if ($display_exception) { $dialog_body = $display_exception->getView(); } else { $dialog_body = id(new PHUIInfoView()) ->setErrors($errors); } return $this->newDialog() ->setTitle(pht('Error Adding Payment Method')) ->appendChild($dialog_body) ->addCancelButton($request->getRequestURI()); } } $form = $provider->renderCreatePaymentMethodForm($request, $errors); $form ->setUser($viewer) ->setAction($request->getRequestURI()) ->setWorkflow(true) ->addHiddenInput('providerID', $provider_id) ->addHiddenInput('cartID', $request->getInt('cartID')) ->addHiddenInput('subscriptionID', $request->getInt('subscriptionID')) ->addHiddenInput('isProviderForm', true) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Add Payment Method')) ->addCancelButton($cancel_uri)); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Method')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Add Payment Method')); $crumbs->setBorder(true); $header = id(new PHUIHeaderView()) ->setHeader(pht('Add Payment Method')) ->setHeaderIcon('fa-plus-square'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $box, )); return $this->newPage() ->setTitle($provider->getPaymentMethodDescription()) ->setCrumbs($crumbs) ->appendChild($view); } private function renderSelectProvider( PhortunePaymentProvider $provider) { $request = $this->getRequest(); $viewer = $request->getUser(); $description = $provider->getPaymentMethodDescription(); $icon_uri = $provider->getPaymentMethodIcon(); $details = $provider->getPaymentMethodProviderDescription(); $this->requireResource('phortune-css'); $icon = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) ->setSpriteIcon($provider->getPaymentMethodIcon()); $button = id(new PHUIButtonView()) ->setSize(PHUIButtonView::BIG) ->setColor(PHUIButtonView::GREY) ->setIcon($icon) ->setText($description) ->setSubtext($details) ->setMetadata(array('disableWorkflow' => true)); $form = id(new AphrontFormView()) ->setUser($viewer) ->setAction($request->getRequestURI()) ->addHiddenInput('providerID', $provider->getProviderConfig()->getID()) ->appendChild($button); return $form; } private function processClientErrors( PhortunePaymentProvider $provider, $client_errors_raw) { $errors = array(); $client_errors = null; try { $client_errors = phutil_json_decode($client_errors_raw); } catch (PhutilJSONParserException $ex) { $errors[] = pht( 'There was an error decoding error information submitted by the '. 'client. Expected a JSON-encoded list of error codes, received: %s.', nonempty($client_errors_raw, pht('nothing'))); } foreach (array_unique($client_errors) as $key => $client_error) { $client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode( $client_error); } foreach (array_unique($client_errors) as $client_error) { switch ($client_error) { case PhortuneErrCode::ERR_CC_INVALID_NUMBER: $message = pht( 'The card number you entered is not a valid card number. Check '. 'that you entered it correctly.'); break; case PhortuneErrCode::ERR_CC_INVALID_CVC: $message = pht( 'The CVC code you entered is not a valid CVC code. Check that '. 'you entered it correctly. The CVC code is a 3-digit or 4-digit '. 'numeric code which usually appears on the back of the card.'); break; case PhortuneErrCode::ERR_CC_INVALID_EXPIRY: $message = pht( 'The card expiration date is not a valid expiration date. Check '. 'that you entered it correctly. You can not add an expired card '. 'as a payment method.'); break; default: $message = $provider->getCreatePaymentMethodErrorMessage( $client_error); if (!$message) { $message = pht( "There was an unexpected error ('%s') processing payment ". "information.", $client_error); phlog($message); } break; } $errors[$client_error] = $message; } return $errors; } } diff --git a/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php b/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php index e7287f3d29..04367a88a0 100644 --- a/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php +++ b/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php @@ -1,185 +1,185 @@ getViewer(); $added = $request->getBool('added'); $subscription = id(new PhortuneSubscriptionQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$subscription) { return new Aphront404Response(); } id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $subscription->getURI()); $merchant = $subscription->getMerchant(); $account = $subscription->getAccount(); $title = pht('Subscription: %s', $subscription->getSubscriptionName()); $header = id(new PHUIHeaderView()) ->setHeader($subscription->getSubscriptionName()); $view_uri = $subscription->getURI(); $valid_methods = id(new PhortunePaymentMethodQuery()) ->setViewer($viewer) ->withAccountPHIDs(array($account->getPHID())) ->withStatuses( array( PhortunePaymentMethod::STATUS_ACTIVE, )) ->withMerchantPHIDs(array($merchant->getPHID())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); $valid_methods = mpull($valid_methods, null, 'getPHID'); $current_phid = $subscription->getDefaultPaymentMethodPHID(); $e_method = null; if ($current_phid && empty($valid_methods[$current_phid])) { $e_method = pht('Needs Update'); } $errors = array(); if ($request->isFormPost()) { $default_method_phid = $request->getStr('defaultPaymentMethodPHID'); if (!$default_method_phid) { $default_method_phid = null; $e_method = null; } else if (empty($valid_methods[$default_method_phid])) { $e_method = pht('Invalid'); if ($default_method_phid == $current_phid) { $errors[] = pht( 'This subscription is configured to autopay with a payment method '. 'that has been deleted. Choose a valid payment method or disable '. 'autopay.'); } else { $errors[] = pht('You must select a valid default payment method.'); } } // TODO: We should use transactions here, and move the validation logic // inside the Editor. if (!$errors) { $subscription->setDefaultPaymentMethodPHID($default_method_phid); $subscription->save(); return id(new AphrontRedirectResponse()) ->setURI($view_uri); } } // Add the option to disable autopay. $disable_options = array( '' => pht('(Disable Autopay)'), ); // Don't require the user to make a valid selection if the current method // has become invalid. if ($current_phid && empty($valid_methods[$current_phid])) { $current_options = array( $current_phid => pht(''), ); } else { $current_options = array(); } // Add any available options. $valid_options = mpull($valid_methods, 'getFullDisplayName', 'getPHID'); $options = $disable_options + $current_options + $valid_options; $crumbs = $this->buildApplicationCrumbs(); $this->addAccountCrumb($crumbs, $account); $crumbs->addTextCrumb( pht('Subscription %d', $subscription->getID()), $view_uri); $crumbs->addTextCrumb(pht('Edit')); $crumbs->setBorder(true); $uri = $this->getApplicationURI($account->getID().'/card/new/'); $uri = new PhutilURI($uri); - $uri->setQueryParam('merchantID', $merchant->getID()); - $uri->setQueryParam('subscriptionID', $subscription->getID()); + $uri->replaceQueryParam('merchantID', $merchant->getID()); + $uri->replaceQueryParam('subscriptionID', $subscription->getID()); $add_method_button = phutil_tag( 'a', array( 'href' => $uri, 'class' => 'button button-grey', ), pht('Add Payment Method...')); $radio = id(new AphrontFormRadioButtonControl()) ->setName('defaultPaymentMethodPHID') ->setLabel(pht('Autopay With')) ->setValue($current_phid) ->setError($e_method); foreach ($options as $key => $value) { $radio->addButton($key, $value, null); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild($radio) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($add_method_button)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Changes')) ->addCancelButton($view_uri)); $box = id(new PHUIObjectBoxView()) ->setUser($viewer) ->setHeaderText(pht('Subscription')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setFormErrors($errors) ->appendChild($form); if ($added) { $info_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_SUCCESS) ->appendChild(pht('Payment method has been successfully added.')); $box->setInfoView($info_view); } $header = id(new PHUIHeaderView()) ->setHeader(pht('Edit %s', $subscription->getSubscriptionName())) ->setHeaderIcon('fa-pencil'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $box, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($view); } } diff --git a/src/applications/ponder/view/PonderAddAnswerView.php b/src/applications/ponder/view/PonderAddAnswerView.php index 43bfd0d6ba..20c52dac8e 100644 --- a/src/applications/ponder/view/PonderAddAnswerView.php +++ b/src/applications/ponder/view/PonderAddAnswerView.php @@ -1,90 +1,90 @@ question = $question; return $this; } public function setActionURI($uri) { $this->actionURI = $uri; return $this; } public function render() { $question = $this->question; $viewer = $this->getViewer(); $authors = mpull($question->getAnswers(), null, 'getAuthorPHID'); if (isset($authors[$viewer->getPHID()])) { $view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->setTitle(pht('Already Answered')) ->appendChild( pht( 'You have already answered this question. You can not answer '. 'twice, but you can edit your existing answer.')); return phutil_tag_div('ponder-add-answer-view', $view); } $info_panel = null; if ($question->getStatus() != PonderQuestionStatus::STATUS_OPEN) { $info_panel = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild( pht( 'This question has been marked as closed, but you can still leave a new answer.')); } $box_style = null; $header = id(new PHUIHeaderView()) ->setHeader(pht('New Answer')) ->addClass('ponder-add-answer-header'); $form = new AphrontFormView(); $form ->setViewer($viewer) ->setAction($this->actionURI) ->setWorkflow(true) ->addHiddenInput('question_id', $question->getID()) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('answer') ->setLabel(pht('Answer')) ->setError(true) ->setID('answer-content') ->setViewer($viewer)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Add Answer'))); if (!$viewer->isLoggedIn()) { $login_href = id(new PhutilURI('/auth/start/')) - ->setQueryParam('next', '/Q'.$question->getID()); + ->replaceQueryParam('next', '/Q'.$question->getID()); $form = id(new PHUIFormLayoutView()) ->addClass('login-to-participate') ->appendChild( id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Log In to Answer')) ->setHref((string)$login_href)); } $box = id(new PHUIObjectBoxView()) ->appendChild($form) ->setHeaderText('Answer') ->addClass('ponder-add-answer-view'); if ($info_panel) { $box->setInfoView($info_panel); } return array($header, $box); } } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 15e0c5d075..ee239035da 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -1,1372 +1,1376 @@ getUser(); $response = $this->loadProject(); if ($response) { return $response; } $project = $this->getProject(); $this->readRequestState(); $board_uri = $this->getApplicationURI('board/'.$project->getID().'/'); $search_engine = id(new ManiphestTaskSearchEngine()) ->setViewer($viewer) ->setBaseURI($board_uri) ->setIsBoardView(true); if ($request->isFormPost() && !$request->getBool('initialize') && !$request->getStr('move') && !$request->getStr('queryColumnID')) { $saved = $search_engine->buildSavedQueryFromRequest($request); $search_engine->saveQuery($saved); $filter_form = id(new AphrontFormView()) ->setUser($viewer); $search_engine->buildSearchForm($filter_form, $saved); if ($search_engine->getErrors()) { return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle(pht('Advanced Filter')) ->appendChild($filter_form->buildLayoutView()) ->setErrors($search_engine->getErrors()) ->setSubmitURI($board_uri) ->addSubmitButton(pht('Apply Filter')) ->addCancelButton($board_uri); } return id(new AphrontRedirectResponse())->setURI( $this->getURIWithState( $search_engine->getQueryResultsPageURI($saved->getQueryKey()))); } $query_key = $this->getDefaultFilter($project); $request_query = $request->getStr('filter'); if (strlen($request_query)) { $query_key = $request_query; } $uri_query = $request->getURIData('queryKey'); if (strlen($uri_query)) { $query_key = $uri_query; } $this->queryKey = $query_key; $custom_query = null; if ($search_engine->isBuiltinQuery($query_key)) { $saved = $search_engine->buildSavedQueryFromBuiltin($query_key); } else { $saved = id(new PhabricatorSavedQueryQuery()) ->setViewer($viewer) ->withQueryKeys(array($query_key)) ->executeOne(); if (!$saved) { return new Aphront404Response(); } $custom_query = $saved; } if ($request->getURIData('filter')) { $filter_form = id(new AphrontFormView()) ->setUser($viewer); $search_engine->buildSearchForm($filter_form, $saved); return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle(pht('Advanced Filter')) ->appendChild($filter_form->buildLayoutView()) ->setSubmitURI($board_uri) ->addSubmitButton(pht('Apply Filter')) ->addCancelButton($board_uri); } $task_query = $search_engine->buildQueryFromSavedQuery($saved); $select_phids = array($project->getPHID()); if ($project->getHasSubprojects() || $project->getHasMilestones()) { $descendants = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withAncestorProjectPHIDs($select_phids) ->execute(); foreach ($descendants as $descendant) { $select_phids[] = $descendant->getPHID(); } } $tasks = $task_query ->withEdgeLogicPHIDs( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, PhabricatorQueryConstraint::OPERATOR_ANCESTOR, array($select_phids)) ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) ->setViewer($viewer) ->execute(); $tasks = mpull($tasks, null, 'getPHID'); $board_phid = $project->getPHID(); // Regardless of display order, pass tasks to the layout engine in ID order // so layout is consistent. $board_tasks = msort($tasks, 'getID'); $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) ->setBoardPHIDs(array($board_phid)) ->setObjectPHIDs(array_keys($board_tasks)) ->setFetchAllBoards(true) ->executeLayout(); $columns = $layout_engine->getColumns($board_phid); if (!$columns || !$project->getHasWorkboard()) { $has_normal_columns = false; foreach ($columns as $column) { if (!$column->getProxyPHID()) { $has_normal_columns = true; break; } } $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); if (!$has_normal_columns) { if (!$can_edit) { $content = $this->buildNoAccessContent($project); } else { $content = $this->buildInitializeContent($project); } } else { if (!$can_edit) { $content = $this->buildDisabledContent($project); } else { $content = $this->buildEnableContent($project); } } if ($content instanceof AphrontResponse) { return $content; } $nav = $this->getProfileMenu(); $nav->selectFilter(PhabricatorProject::ITEM_WORKBOARD); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Workboard')); return $this->newPage() ->setTitle( array( $project->getDisplayName(), pht('Workboard'), )) ->setNavigation($nav) ->setCrumbs($crumbs) ->appendChild($content); } // If the user wants to turn a particular column into a query, build an // apropriate filter and redirect them to the query results page. $query_column_id = $request->getInt('queryColumnID'); if ($query_column_id) { $column_id_map = mpull($columns, null, 'getID'); $query_column = idx($column_id_map, $query_column_id); if (!$query_column) { return new Aphront404Response(); } // Create a saved query to combine the active filter on the workboard // with the column filter. If the user currently has constraints on the // board, we want to add a new column or project constraint, not // completely replace the constraints. $saved_query = $saved->newCopy(); if ($query_column->getProxyPHID()) { $project_phids = $saved_query->getParameter('projectPHIDs'); if (!$project_phids) { $project_phids = array(); } $project_phids[] = $query_column->getProxyPHID(); $saved_query->setParameter('projectPHIDs', $project_phids); } else { $saved_query->setParameter( 'columnPHIDs', array($query_column->getPHID())); } $search_engine = id(new ManiphestTaskSearchEngine()) ->setViewer($viewer); $search_engine->saveQuery($saved_query); $query_key = $saved_query->getQueryKey(); $query_uri = new PhutilURI("/maniphest/query/{$query_key}/#R"); return id(new AphrontRedirectResponse()) ->setURI($query_uri); } $task_can_edit_map = id(new PhabricatorPolicyFilter()) ->setViewer($viewer) ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) ->apply($tasks); // If this is a batch edit, select the editable tasks in the chosen column // and ship the user into the batch editor. $batch_edit = $request->getStr('batch'); if ($batch_edit) { if ($batch_edit !== self::BATCH_EDIT_ALL) { $column_id_map = mpull($columns, null, 'getID'); $batch_column = idx($column_id_map, $batch_edit); if (!$batch_column) { return new Aphront404Response(); } $batch_task_phids = $layout_engine->getColumnObjectPHIDs( $board_phid, $batch_column->getPHID()); foreach ($batch_task_phids as $key => $batch_task_phid) { if (empty($task_can_edit_map[$batch_task_phid])) { unset($batch_task_phids[$key]); } } $batch_tasks = array_select_keys($tasks, $batch_task_phids); } else { $batch_tasks = $task_can_edit_map; } if (!$batch_tasks) { $cancel_uri = $this->getURIWithState($board_uri); return $this->newDialog() ->setTitle(pht('No Editable Tasks')) ->appendParagraph( pht( 'The selected column contains no visible tasks which you '. 'have permission to edit.')) ->addCancelButton($board_uri); } // Create a saved query to hold the working set. This allows us to get // around URI length limitations with a long "?ids=..." query string. // For details, see T10268. $search_engine = id(new ManiphestTaskSearchEngine()) ->setViewer($viewer); $saved_query = $search_engine->newSavedQuery(); $saved_query->setParameter('ids', mpull($batch_tasks, 'getID')); $search_engine->saveQuery($saved_query); $query_key = $saved_query->getQueryKey(); $bulk_uri = new PhutilURI("/maniphest/bulk/query/{$query_key}/"); - $bulk_uri->setQueryParam('board', $this->id); + $bulk_uri->replaceQueryParam('board', $this->id); return id(new AphrontRedirectResponse()) ->setURI($bulk_uri); } $move_id = $request->getStr('move'); if (strlen($move_id)) { $column_id_map = mpull($columns, null, 'getID'); $move_column = idx($column_id_map, $move_id); if (!$move_column) { return new Aphront404Response(); } $move_task_phids = $layout_engine->getColumnObjectPHIDs( $board_phid, $move_column->getPHID()); foreach ($move_task_phids as $key => $move_task_phid) { if (empty($task_can_edit_map[$move_task_phid])) { unset($move_task_phids[$key]); } } $move_tasks = array_select_keys($tasks, $move_task_phids); $cancel_uri = $this->getURIWithState($board_uri); if (!$move_tasks) { return $this->newDialog() ->setTitle(pht('No Movable Tasks')) ->appendParagraph( pht( 'The selected column contains no visible tasks which you '. 'have permission to move.')) ->addCancelButton($cancel_uri); } $move_project_phid = $project->getPHID(); $move_column_phid = null; $move_project = null; $move_column = null; $columns = null; $errors = array(); if ($request->isFormPost()) { $move_project_phid = head($request->getArr('moveProjectPHID')); if (!$move_project_phid) { $move_project_phid = $request->getStr('moveProjectPHID'); } if (!$move_project_phid) { if ($request->getBool('hasProject')) { $errors[] = pht('Choose a project to move tasks to.'); } } else { $target_project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withPHIDs(array($move_project_phid)) ->executeOne(); if (!$target_project) { $errors[] = pht('You must choose a valid project.'); } else if (!$project->getHasWorkboard()) { $errors[] = pht( 'You must choose a project with a workboard.'); } else { $move_project = $target_project; } } if ($move_project) { $move_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) ->setBoardPHIDs(array($move_project->getPHID())) ->setFetchAllBoards(true) ->executeLayout(); $columns = $move_engine->getColumns($move_project->getPHID()); $columns = mpull($columns, null, 'getPHID'); foreach ($columns as $key => $column) { if ($column->isHidden()) { unset($columns[$key]); } } $move_column_phid = $request->getStr('moveColumnPHID'); if (!$move_column_phid) { if ($request->getBool('hasColumn')) { $errors[] = pht('Choose a column to move tasks to.'); } } else { if (empty($columns[$move_column_phid])) { $errors[] = pht( 'Choose a valid column on the target workboard to move '. 'tasks to.'); } else if ($columns[$move_column_phid]->getID() == $move_id) { $errors[] = pht( 'You can not move tasks from a column to itself.'); } else { $move_column = $columns[$move_column_phid]; } } } } if ($move_column && $move_project) { foreach ($move_tasks as $move_task) { $xactions = array(); // If we're switching projects, get out of the old project first // and move to the new project. if ($move_project->getID() != $project->getID()) { $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue( 'edge:type', PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) ->setNewValue( array( '-' => array( $project->getPHID() => $project->getPHID(), ), '+' => array( $move_project->getPHID() => $move_project->getPHID(), ), )); } $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) ->setNewValue( array( array( 'columnPHID' => $move_column->getPHID(), ), )); $editor = id(new ManiphestTransactionEditor()) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $editor->applyTransactions($move_task, $xactions); } return id(new AphrontRedirectResponse()) ->setURI($cancel_uri); } if ($move_project) { $column_form = id(new AphrontFormView()) ->setViewer($viewer) ->appendControl( id(new AphrontFormSelectControl()) ->setName('moveColumnPHID') ->setLabel(pht('Move to Column')) ->setValue($move_column_phid) ->setOptions(mpull($columns, 'getDisplayName', 'getPHID'))); return $this->newDialog() ->setTitle(pht('Move Tasks')) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setErrors($errors) ->addHiddenInput('move', $move_id) ->addHiddenInput('moveProjectPHID', $move_project->getPHID()) ->addHiddenInput('hasColumn', true) ->addHiddenInput('hasProject', true) ->appendParagraph( pht( 'Choose a column on the %s workboard to move tasks to:', $viewer->renderHandle($move_project->getPHID()))) ->appendForm($column_form) ->addSubmitButton(pht('Move Tasks')) ->addCancelButton($cancel_uri); } if ($move_project_phid) { $move_project_phid_value = array($move_project_phid); } else { $move_project_phid_value = array(); } $project_form = id(new AphrontFormView()) ->setViewer($viewer) ->appendControl( id(new AphrontFormTokenizerControl()) ->setName('moveProjectPHID') ->setLimit(1) ->setLabel(pht('Move to Project')) ->setValue($move_project_phid_value) ->setDatasource(new PhabricatorProjectDatasource())); return $this->newDialog() ->setTitle(pht('Move Tasks')) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setErrors($errors) ->addHiddenInput('move', $move_id) ->addHiddenInput('hasProject', true) ->appendForm($project_form) ->addSubmitButton(pht('Continue')) ->addCancelButton($cancel_uri); } $board_id = celerity_generate_unique_node_id(); $board = id(new PHUIWorkboardView()) ->setUser($viewer) ->setID($board_id) ->addSigil('jx-workboard') ->setMetadata( array( 'boardPHID' => $project->getPHID(), )); $visible_columns = array(); $column_phids = array(); $visible_phids = array(); foreach ($columns as $column) { if (!$this->showHidden) { if ($column->isHidden()) { continue; } } $proxy = $column->getProxy(); if ($proxy && !$proxy->isMilestone()) { // TODO: For now, don't show subproject columns because we can't // handle tasks with multiple positions yet. continue; } $task_phids = $layout_engine->getColumnObjectPHIDs( $board_phid, $column->getPHID()); $column_tasks = array_select_keys($tasks, $task_phids); // If we aren't using "natural" order, reorder the column by the original // query order. if ($this->sortKey != PhabricatorProjectColumn::ORDER_NATURAL) { $column_tasks = array_select_keys($column_tasks, array_keys($tasks)); } $column_phid = $column->getPHID(); $visible_columns[$column_phid] = $column; $column_phids[$column_phid] = $column_tasks; foreach ($column_tasks as $phid => $task) { $visible_phids[$phid] = $phid; } } $rendering_engine = id(new PhabricatorBoardRenderingEngine()) ->setViewer($viewer) ->setObjects(array_select_keys($tasks, $visible_phids)) ->setEditMap($task_can_edit_map) ->setExcludedProjectPHIDs($select_phids); $templates = array(); $column_maps = array(); $all_tasks = array(); foreach ($visible_columns as $column_phid => $column) { $column_tasks = $column_phids[$column_phid]; $panel = id(new PHUIWorkpanelView()) ->setHeader($column->getDisplayName()) ->setSubHeader($column->getDisplayType()) ->addSigil('workpanel'); $proxy = $column->getProxy(); if ($proxy) { $proxy_id = $proxy->getID(); $href = $this->getApplicationURI("view/{$proxy_id}/"); $panel->setHref($href); } $header_icon = $column->getHeaderIcon(); if ($header_icon) { $panel->setHeaderIcon($header_icon); } $display_class = $column->getDisplayClass(); if ($display_class) { $panel->addClass($display_class); } if ($column->isHidden()) { $panel->addClass('project-panel-hidden'); } $column_menu = $this->buildColumnMenu($project, $column); $panel->addHeaderAction($column_menu); $count_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setColor(PHUITagView::COLOR_BLUE) ->addSigil('column-points') ->setName( javelin_tag( 'span', array( 'sigil' => 'column-points-content', ), pht('-'))) ->setStyle('display: none'); $panel->setHeaderTag($count_tag); $cards = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setFlush(true) ->setAllowEmptyList(true) ->addSigil('project-column') ->setItemClass('phui-workcard') ->setMetadata( array( 'columnPHID' => $column->getPHID(), 'pointLimit' => $column->getPointLimit(), )); foreach ($column_tasks as $task) { $object_phid = $task->getPHID(); $card = $rendering_engine->renderCard($object_phid); $templates[$object_phid] = hsprintf('%s', $card->getItem()); $column_maps[$column_phid][] = $object_phid; $all_tasks[$object_phid] = $task; } $panel->setCards($cards); $board->addPanel($panel); } $behavior_config = array( 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'uploadURI' => '/file/dropupload/', 'coverURI' => $this->getApplicationURI('cover/'), 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(), 'boardPHID' => $project->getPHID(), 'order' => $this->sortKey, 'templateMap' => $templates, 'columnMaps' => $column_maps, 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'), 'propertyMaps' => mpull($all_tasks, 'getWorkboardProperties'), 'boardID' => $board_id, 'projectPHID' => $project->getPHID(), ); $this->initBehavior('project-boards', $behavior_config); $sort_menu = $this->buildSortMenu( $viewer, $project, $this->sortKey); $filter_menu = $this->buildFilterMenu( $viewer, $project, $custom_query, $search_engine, $query_key); $manage_menu = $this->buildManageMenu($project, $this->showHidden); $header_link = phutil_tag( 'a', array( 'href' => $this->getApplicationURI('profile/'.$project->getID().'/'), ), $project->getName()); $board_box = id(new PHUIBoxView()) ->appendChild($board) ->addClass('project-board-wrapper'); $nav = $this->getProfileMenu(); $divider = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_DIVIDER); $fullscreen = $this->buildFullscreenMenu(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Workboard')); $crumbs->setBorder(true); $crumbs->addAction($sort_menu); $crumbs->addAction($filter_menu); $crumbs->addAction($divider); $crumbs->addAction($manage_menu); $crumbs->addAction($fullscreen); $page = $this->newPage() ->setTitle( array( $project->getDisplayName(), pht('Workboard'), )) ->setPageObjectPHIDs(array($project->getPHID())) ->setShowFooter(false) ->setNavigation($nav) ->setCrumbs($crumbs) ->addQuicksandConfig( array( 'boardConfig' => $behavior_config, )) ->appendChild( array( $board_box, )); $background = $project->getDisplayWorkboardBackgroundColor(); require_celerity_resource('phui-workboard-color-css'); if ($background !== null) { $background_color_class = "phui-workboard-{$background}"; $page->addClass('phui-workboard-color'); $page->addClass($background_color_class); } else { $page->addClass('phui-workboard-no-color'); } return $page; } private function readRequestState() { $request = $this->getRequest(); $project = $this->getProject(); $this->showHidden = $request->getBool('hidden'); $this->id = $project->getID(); $sort_key = $this->getDefaultSort($project); $request_sort = $request->getStr('order'); if ($this->isValidSort($request_sort)) { $sort_key = $request_sort; } $this->sortKey = $sort_key; } private function getDefaultSort(PhabricatorProject $project) { $default_sort = $project->getDefaultWorkboardSort(); if ($this->isValidSort($default_sort)) { return $default_sort; } return PhabricatorProjectColumn::DEFAULT_ORDER; } private function getDefaultFilter(PhabricatorProject $project) { $default_filter = $project->getDefaultWorkboardFilter(); if (strlen($default_filter)) { return $default_filter; } return 'open'; } private function isValidSort($sort) { switch ($sort) { case PhabricatorProjectColumn::ORDER_NATURAL: case PhabricatorProjectColumn::ORDER_PRIORITY: return true; } return false; } private function buildSortMenu( PhabricatorUser $viewer, PhabricatorProject $project, $sort_key) { $sort_icon = id(new PHUIIconView()) ->setIcon('fa-sort-amount-asc bluegrey'); $named = array( PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'), PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'), ); $base_uri = $this->getURIWithState(); $items = array(); foreach ($named as $key => $name) { $is_selected = ($key == $sort_key); if ($is_selected) { $active_order = $name; } $item = id(new PhabricatorActionView()) ->setIcon('fa-sort-amount-asc') ->setSelected($is_selected) ->setName($name); $uri = $base_uri->alter('order', $key); $item->setHref($uri); $items[] = $item; } $id = $project->getID(); $save_uri = "default/{$id}/sort/"; $save_uri = $this->getApplicationURI($save_uri); $save_uri = $this->getURIWithState($save_uri, $force = true); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $items[] = id(new PhabricatorActionView()) ->setIcon('fa-floppy-o') ->setName(pht('Save as Default')) ->setHref($save_uri) ->setWorkflow(true) ->setDisabled(!$can_edit); $sort_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($items as $item) { $sort_menu->addAction($item); } $sort_button = id(new PHUIListItemView()) ->setName($active_order) ->setIcon('fa-sort-amount-asc') ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( array( 'items' => hsprintf('%s', $sort_menu), )); return $sort_button; } private function buildFilterMenu( PhabricatorUser $viewer, PhabricatorProject $project, $custom_query, PhabricatorApplicationSearchEngine $engine, $query_key) { $named = array( 'open' => pht('Open Tasks'), 'all' => pht('All Tasks'), ); if ($viewer->isLoggedIn()) { $named['assigned'] = pht('Assigned to Me'); } if ($custom_query) { $named[$custom_query->getQueryKey()] = pht('Custom Filter'); } $items = array(); foreach ($named as $key => $name) { $is_selected = ($key == $query_key); if ($is_selected) { $active_filter = $name; } $is_custom = false; if ($custom_query) { $is_custom = ($key == $custom_query->getQueryKey()); } $item = id(new PhabricatorActionView()) ->setIcon('fa-search') ->setSelected($is_selected) ->setName($name); if ($is_custom) { $uri = $this->getApplicationURI( 'board/'.$this->id.'/filter/query/'.$key.'/'); $item->setWorkflow(true); } else { $uri = $engine->getQueryResultsPageURI($key); } $uri = $this->getURIWithState($uri) - ->setQueryParam('filter', null); + ->removeQueryParam('filter'); $item->setHref($uri); $items[] = $item; } $id = $project->getID(); $filter_uri = $this->getApplicationURI("board/{$id}/filter/"); $filter_uri = $this->getURIWithState($filter_uri, $force = true); $items[] = id(new PhabricatorActionView()) ->setIcon('fa-cog') ->setHref($filter_uri) ->setWorkflow(true) ->setName(pht('Advanced Filter...')); $save_uri = "default/{$id}/filter/"; $save_uri = $this->getApplicationURI($save_uri); $save_uri = $this->getURIWithState($save_uri, $force = true); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $items[] = id(new PhabricatorActionView()) ->setIcon('fa-floppy-o') ->setName(pht('Save as Default')) ->setHref($save_uri) ->setWorkflow(true) ->setDisabled(!$can_edit); $filter_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($items as $item) { $filter_menu->addAction($item); } $filter_button = id(new PHUIListItemView()) ->setName($active_filter) ->setIcon('fa-search') ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( array( 'items' => hsprintf('%s', $filter_menu), )); return $filter_button; } private function buildManageMenu( PhabricatorProject $project, $show_hidden) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $project->getID(); $manage_uri = $this->getApplicationURI("board/{$id}/manage/"); $add_uri = $this->getApplicationURI("board/{$id}/edit/"); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $manage_items = array(); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-plus') ->setName(pht('Add Column')) ->setHref($add_uri) ->setDisabled(!$can_edit) ->setWorkflow(true); $reorder_uri = $this->getApplicationURI("board/{$id}/reorder/"); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-exchange') ->setName(pht('Reorder Columns')) ->setHref($reorder_uri) ->setDisabled(!$can_edit) ->setWorkflow(true); if ($show_hidden) { $hidden_uri = $this->getURIWithState() - ->setQueryParam('hidden', null); + ->removeQueryParam('hidden'); $hidden_icon = 'fa-eye-slash'; $hidden_text = pht('Hide Hidden Columns'); } else { $hidden_uri = $this->getURIWithState() - ->setQueryParam('hidden', 'true'); + ->replaceQueryParam('hidden', 'true'); $hidden_icon = 'fa-eye'; $hidden_text = pht('Show Hidden Columns'); } $manage_items[] = id(new PhabricatorActionView()) ->setIcon($hidden_icon) ->setName($hidden_text) ->setHref($hidden_uri); $manage_items[] = id(new PhabricatorActionView()) ->setType(PhabricatorActionView::TYPE_DIVIDER); $background_uri = $this->getApplicationURI("board/{$id}/background/"); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-paint-brush') ->setName(pht('Change Background Color')) ->setHref($background_uri) ->setDisabled(!$can_edit) ->setWorkflow(false); $manage_uri = $this->getApplicationURI("board/{$id}/manage/"); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-gear') ->setName(pht('Manage Workboard')) ->setHref($manage_uri); $batch_edit_uri = $request->getRequestURI(); - $batch_edit_uri->setQueryParam('batch', self::BATCH_EDIT_ALL); + $batch_edit_uri->replaceQueryParam('batch', self::BATCH_EDIT_ALL); $can_batch_edit = PhabricatorPolicyFilter::hasCapability( $viewer, PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), ManiphestBulkEditCapability::CAPABILITY); $manage_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($manage_items as $item) { $manage_menu->addAction($item); } $manage_button = id(new PHUIListItemView()) ->setIcon('fa-cog') ->setHref('#') ->addSigil('boards-dropdown-menu') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Manage'), 'align' => 'S', 'items' => hsprintf('%s', $manage_menu), )); return $manage_button; } private function buildFullscreenMenu() { $up = id(new PHUIListItemView()) ->setIcon('fa-arrows-alt') ->setHref('#') ->addClass('phui-workboard-expand-icon') ->addSigil('jx-toggle-class') ->addSigil('has-tooltip') ->setMetaData(array( 'tip' => pht('Fullscreen'), 'map' => array( 'phabricator-standard-page' => 'phui-workboard-fullscreen', ), )); return $up; } private function buildColumnMenu( PhabricatorProject $project, PhabricatorProjectColumn $column) { $request = $this->getRequest(); $viewer = $request->getUser(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $column_items = array(); if ($column->getProxyPHID()) { $default_phid = $column->getProxyPHID(); } else { $default_phid = $column->getProjectPHID(); } $specs = id(new ManiphestEditEngine()) ->setViewer($viewer) ->newCreateActionSpecifications(array()); foreach ($specs as $spec) { $column_items[] = id(new PhabricatorActionView()) ->setIcon($spec['icon']) ->setName($spec['name']) ->setHref($spec['uri']) ->setDisabled($spec['disabled']) ->addSigil('column-add-task') ->setMetadata( array( 'createURI' => $spec['uri'], 'columnPHID' => $column->getPHID(), 'boardPHID' => $project->getPHID(), 'projectPHID' => $default_phid, )); } if (count($specs) > 1) { $column_items[] = id(new PhabricatorActionView()) ->setType(PhabricatorActionView::TYPE_DIVIDER); } $batch_edit_uri = $request->getRequestURI(); - $batch_edit_uri->setQueryParam('batch', $column->getID()); + $batch_edit_uri->replaceQueryParam('batch', $column->getID()); $can_batch_edit = PhabricatorPolicyFilter::hasCapability( $viewer, PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), ManiphestBulkEditCapability::CAPABILITY); $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-list-ul') ->setName(pht('Bulk Edit Tasks...')) ->setHref($batch_edit_uri) ->setDisabled(!$can_batch_edit); $batch_move_uri = $request->getRequestURI(); - $batch_move_uri->setQueryParam('move', $column->getID()); + $batch_move_uri->replaceQueryParam('move', $column->getID()); $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-arrow-right') ->setName(pht('Move Tasks to Column...')) ->setHref($batch_move_uri) ->setWorkflow(true); $query_uri = $request->getRequestURI(); - $query_uri->setQueryParam('queryColumnID', $column->getID()); + $query_uri->replaceQueryParam('queryColumnID', $column->getID()); $column_items[] = id(new PhabricatorActionView()) ->setName(pht('View as Query')) ->setIcon('fa-search') ->setHref($query_uri); $edit_uri = 'board/'.$this->id.'/edit/'.$column->getID().'/'; $column_items[] = id(new PhabricatorActionView()) ->setName(pht('Edit Column')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI($edit_uri)) ->setDisabled(!$can_edit) ->setWorkflow(true); $can_hide = ($can_edit && !$column->isDefaultColumn()); $hide_uri = 'board/'.$this->id.'/hide/'.$column->getID().'/'; $hide_uri = $this->getApplicationURI($hide_uri); $hide_uri = $this->getURIWithState($hide_uri); if (!$column->isHidden()) { $column_items[] = id(new PhabricatorActionView()) ->setName(pht('Hide Column')) ->setIcon('fa-eye-slash') ->setHref($hide_uri) ->setDisabled(!$can_hide) ->setWorkflow(true); } else { $column_items[] = id(new PhabricatorActionView()) ->setName(pht('Show Column')) ->setIcon('fa-eye') ->setHref($hide_uri) ->setDisabled(!$can_hide) ->setWorkflow(true); } $column_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($column_items as $item) { $column_menu->addAction($item); } $column_button = id(new PHUIIconView()) ->setIcon('fa-caret-down') ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( array( 'items' => hsprintf('%s', $column_menu), )); return $column_button; } /** * Add current state parameters (like order and the visibility of hidden * columns) to a URI. * * This allows actions which toggle or adjust one piece of state to keep * the rest of the board state persistent. If no URI is provided, this method * starts with the request URI. * * @param string|null URI to add state parameters to. * @param bool True to explicitly include all state. * @return PhutilURI URI with state parameters. */ private function getURIWithState($base = null, $force = false) { $project = $this->getProject(); if ($base === null) { $base = $this->getRequest()->getRequestURI(); } $base = new PhutilURI($base); if ($force || ($this->sortKey != $this->getDefaultSort($project))) { - $base->setQueryParam('order', $this->sortKey); + $base->replaceQueryParam('order', $this->sortKey); } else { - $base->setQueryParam('order', null); + $base->removeQueryParam('order'); } if ($force || ($this->queryKey != $this->getDefaultFilter($project))) { - $base->setQueryParam('filter', $this->queryKey); + $base->replaceQueryParam('filter', $this->queryKey); } else { - $base->setQueryParam('filter', null); + $base->removeQueryParam('filter'); } - $base->setQueryParam('hidden', $this->showHidden ? 'true' : null); + if ($this->showHidden) { + $base->replaceQueryParam('hidden', 'true'); + } else { + $base->removeQueryParam('hidden'); + } return $base; } private function buildInitializeContent(PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $this->getViewer(); $type = $request->getStr('initialize-type'); $id = $project->getID(); $profile_uri = $this->getApplicationURI("profile/{$id}/"); $board_uri = $this->getApplicationURI("board/{$id}/"); $import_uri = $this->getApplicationURI("board/{$id}/import/"); $set_default = $request->getBool('default'); if ($set_default) { $this ->getProfileMenuEngine() ->adjustDefault(PhabricatorProject::ITEM_WORKBOARD); } if ($request->isFormPost()) { if ($type == 'backlog-only') { $column = PhabricatorProjectColumn::initializeNewColumn($viewer) ->setSequence(0) ->setProperty('isDefault', true) ->setProjectPHID($project->getPHID()) ->save(); $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType( PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE) ->setNewValue(1); id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse()) ->setURI($board_uri); } else { return id(new AphrontRedirectResponse()) ->setURI($import_uri); } } // TODO: Tailor this UI if the project is already a parent project. We // should not offer options for creating a parent project workboard, since // they can't have their own columns. $new_selector = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Columns')) ->setName('initialize-type') ->setValue('backlog-only') ->addButton( 'backlog-only', pht('New Empty Board'), pht('Create a new board with just a backlog column.')) ->addButton( 'import', pht('Import Columns'), pht('Import board columns from another project.')); $default_checkbox = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Make Default')) ->addCheckbox( 'default', 1, pht('Make the workboard the default view for this project.'), true); $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('initialize', 1) ->appendRemarkupInstructions( pht('The workboard for this project has not been created yet.')) ->appendControl($new_selector) ->appendControl($default_checkbox) ->appendControl( id(new AphrontFormSubmitControl()) ->addCancelButton($profile_uri) ->setValue(pht('Create Workboard'))); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create Workboard')) ->setForm($form); return $box; } private function buildNoAccessContent(PhabricatorProject $project) { $viewer = $this->getViewer(); $id = $project->getID(); $profile_uri = $this->getApplicationURI("profile/{$id}/"); return $this->newDialog() ->setTitle(pht('Unable to Create Workboard')) ->appendParagraph( pht( 'The workboard for this project has not been created yet, '. 'but you do not have permission to create it. Only users '. 'who can edit this project can create a workboard for it.')) ->addCancelButton($profile_uri); } private function buildEnableContent(PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $this->getViewer(); $id = $project->getID(); $profile_uri = $this->getApplicationURI("profile/{$id}/"); $board_uri = $this->getApplicationURI("board/{$id}/"); if ($request->isFormPost()) { $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType( PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE) ->setNewValue(1); id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse()) ->setURI($board_uri); } return $this->newDialog() ->setTitle(pht('Workboard Disabled')) ->addHiddenInput('initialize', 1) ->appendParagraph( pht( 'This workboard has been disabled, but can be restored to its '. 'former glory.')) ->addCancelButton($profile_uri) ->addSubmitButton(pht('Enable Workboard')); } private function buildDisabledContent(PhabricatorProject $project) { $viewer = $this->getViewer(); $id = $project->getID(); $profile_uri = $this->getApplicationURI("profile/{$id}/"); return $this->newDialog() ->setTitle(pht('Workboard Disabled')) ->appendParagraph( pht( 'This workboard has been disabled, and you do not have permission '. 'to enable it. Only users who can edit this project can restore '. 'the workboard.')) ->addCancelButton($profile_uri); } } diff --git a/src/applications/project/controller/PhabricatorProjectColumnHideController.php b/src/applications/project/controller/PhabricatorProjectColumnHideController.php index 1dd5e47ecb..fbda2feb1e 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnHideController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnHideController.php @@ -1,147 +1,147 @@ getViewer(); $id = $request->getURIData('id'); $project_id = $request->getURIData('projectID'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($project_id)) ->executeOne(); if (!$project) { return new Aphront404Response(); } $this->setProject($project); $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$column) { return new Aphront404Response(); } $column_phid = $column->getPHID(); $view_uri = $this->getApplicationURI('/board/'.$project_id.'/'); $view_uri = new PhutilURI($view_uri); foreach ($request->getPassthroughRequestData() as $key => $value) { - $view_uri->setQueryParam($key, $value); + $view_uri->replaceQueryParam($key, $value); } if ($column->isDefaultColumn()) { return $this->newDialog() ->setTitle(pht('Can Not Hide Default Column')) ->appendParagraph( pht('You can not hide the default/backlog column on a board.')) ->addCancelButton($view_uri, pht('Okay')); } $proxy = $column->getProxy(); if ($request->isFormPost()) { if ($proxy) { if ($proxy->isArchived()) { $new_status = PhabricatorProjectStatus::STATUS_ACTIVE; } else { $new_status = PhabricatorProjectStatus::STATUS_ARCHIVED; } $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType( PhabricatorProjectStatusTransaction::TRANSACTIONTYPE) ->setNewValue($new_status); id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($proxy, $xactions); } else { if ($column->isHidden()) { $new_status = PhabricatorProjectColumn::STATUS_ACTIVE; } else { $new_status = PhabricatorProjectColumn::STATUS_HIDDEN; } $type_status = PhabricatorProjectColumnTransaction::TYPE_STATUS; $xactions = array( id(new PhabricatorProjectColumnTransaction()) ->setTransactionType($type_status) ->setNewValue($new_status), ); $editor = id(new PhabricatorProjectColumnTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request) ->applyTransactions($column, $xactions); } return id(new AphrontRedirectResponse())->setURI($view_uri); } if ($proxy) { if ($column->isHidden()) { $title = pht('Activate and Show Column'); $body = pht( 'This column is hidden because it represents an archived '. 'subproject. Do you want to activate the subproject so the '. 'column is visible again?'); $button = pht('Activate Subproject'); } else { $title = pht('Archive and Hide Column'); $body = pht( 'This column is visible because it represents an active '. 'subproject. Do you want to hide the column by archiving the '. 'subproject?'); $button = pht('Archive Subproject'); } } else { if ($column->isHidden()) { $title = pht('Show Column'); $body = pht('Are you sure you want to show this column?'); $button = pht('Show Column'); } else { $title = pht('Hide Column'); $body = pht( 'Are you sure you want to hide this column? It will no longer '. 'appear on the workboard.'); $button = pht('Hide Column'); } } $dialog = $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle($title) ->appendChild($body) ->setDisableWorkflowOnCancel(true) ->addCancelButton($view_uri) ->addSubmitButton($button); foreach ($request->getPassthroughRequestData() as $key => $value) { $dialog->addHiddenInput($key, $value); } return $dialog; } } diff --git a/src/applications/project/controller/PhabricatorProjectDefaultController.php b/src/applications/project/controller/PhabricatorProjectDefaultController.php index 8f42ff9736..2c7a47b2df 100644 --- a/src/applications/project/controller/PhabricatorProjectDefaultController.php +++ b/src/applications/project/controller/PhabricatorProjectDefaultController.php @@ -1,90 +1,90 @@ getViewer(); $project_id = $request->getURIData('projectID'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($project_id)) ->executeOne(); if (!$project) { return new Aphront404Response(); } $this->setProject($project); $target = $request->getURIData('target'); switch ($target) { case 'filter': $title = pht('Set Board Default Filter'); $body = pht( 'Make the current filter the new default filter for this board? '. 'All users will see the new filter as the default when they view '. 'the board.'); $button = pht('Save Default Filter'); $xaction_value = $request->getStr('filter'); $xaction_type = PhabricatorProjectFilterTransaction::TRANSACTIONTYPE; break; case 'sort': $title = pht('Set Board Default Order'); $body = pht( 'Make the current sort order the new default order for this board? '. 'All users will see the new order as the default when they view '. 'the board.'); $button = pht('Save Default Order'); $xaction_value = $request->getStr('order'); $xaction_type = PhabricatorProjectSortTransaction::TRANSACTIONTYPE; break; default: return new Aphront404Response(); } $id = $project->getID(); $view_uri = $this->getApplicationURI("board/{$id}/"); $view_uri = new PhutilURI($view_uri); foreach ($request->getPassthroughRequestData() as $key => $value) { - $view_uri->setQueryParam($key, $value); + $view_uri->replaceQueryParam($key, $value); } if ($request->isFormPost()) { $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType($xaction_type) ->setNewValue($xaction_value); id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); } $dialog = $this->newDialog() ->setTitle($title) ->appendChild($body) ->setDisableWorkflowOnCancel(true) ->addCancelButton($view_uri) ->addSubmitButton($title); foreach ($request->getPassthroughRequestData() as $key => $value) { $dialog->addHiddenInput($key, $value); } return $dialog; } } diff --git a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php index ffd6388284..1835e6f7f9 100644 --- a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php +++ b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php @@ -1,102 +1,102 @@ getURIData('diffRevID'); $viewer = $request->getViewer(); $diff_rev = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($revision_id)) ->executeOne(); if (!$diff_rev) { return new Aphront404Response(); } $this->revision = $diff_rev; $repository = $this->revision->getRepository(); $projects = id(new ReleephProject())->loadAllWhere( 'repositoryPHID = %s AND isActive = 1', $repository->getPHID()); if (!$projects) { throw new Exception( pht( "%s belongs to the '%s' repository, ". "which is not part of any Releeph project!", 'D'.$this->revision->getID(), $repository->getMonogram())); } $branches = id(new ReleephBranch())->loadAllWhere( 'releephProjectID IN (%Ld) AND isActive = 1', mpull($projects, 'getID')); if (!$branches) { throw new Exception(pht( '%s could be in the Releeph project(s) %s, '. 'but this project / none of these projects have open branches.', 'D'.$this->revision->getID(), implode(', ', mpull($projects, 'getName')))); } if (count($branches) === 1) { return id(new AphrontRedirectResponse()) ->setURI($this->buildReleephRequestURI(head($branches))); } $projects = msort( mpull($projects, null, 'getID'), 'getName'); $branch_groups = mgroup($branches, 'getReleephProjectID'); require_celerity_resource('releeph-request-differential-create-dialog'); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Choose Releeph Branch')) ->setClass('releeph-request-differential-create-dialog') ->addCancelButton('/D'.$request->getStr('D')); $dialog->appendChild( pht( 'This differential revision changes code that is associated '. 'with multiple Releeph branches. Please select the branch '. 'where you would like this code to be picked.')); foreach ($branch_groups as $project_id => $branches) { $project = idx($projects, $project_id); $dialog->appendChild( phutil_tag( 'h1', array(), $project->getName())); $branches = msort($branches, 'getBasename'); foreach ($branches as $branch) { $uri = $this->buildReleephRequestURI($branch); $dialog->appendChild( phutil_tag( 'a', array( 'href' => $uri, ), $branch->getDisplayNameWithDetail())); } } return id(new AphrontDialogResponse()) ->setDialog($dialog); } private function buildReleephRequestURI(ReleephBranch $branch) { $uri = $branch->getURI('request/'); return id(new PhutilURI($uri)) - ->setQueryParam('D', $this->revision->getID()); + ->replaceQueryParam('D', $this->revision->getID()); } } diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index cf4f95c16d..6b43883c78 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -1,975 +1,975 @@ preface = $preface; return $this; } public function getPreface() { return $this->preface; } public function setQueryKey($query_key) { $this->queryKey = $query_key; return $this; } protected function getQueryKey() { return $this->queryKey; } public function setNavigation(AphrontSideNavFilterView $navigation) { $this->navigation = $navigation; return $this; } protected function getNavigation() { return $this->navigation; } public function setSearchEngine( PhabricatorApplicationSearchEngine $search_engine) { $this->searchEngine = $search_engine; return $this; } protected function getSearchEngine() { return $this->searchEngine; } protected function getActiveQuery() { if (!$this->activeQuery) { throw new Exception(pht('There is no active query yet.')); } return $this->activeQuery; } protected function validateDelegatingController() { $parent = $this->getDelegatingController(); if (!$parent) { throw new Exception( pht('You must delegate to this controller, not invoke it directly.')); } $engine = $this->getSearchEngine(); if (!$engine) { throw new PhutilInvalidStateException('setEngine'); } $engine->setViewer($this->getRequest()->getUser()); $parent = $this->getDelegatingController(); } public function processRequest() { $this->validateDelegatingController(); $query_action = $this->getRequest()->getURIData('queryAction'); if ($query_action == 'export') { return $this->processExportRequest(); } $key = $this->getQueryKey(); if ($key == 'edit') { return $this->processEditRequest(); } else { return $this->processSearchRequest(); } } private function processSearchRequest() { $parent = $this->getDelegatingController(); $request = $this->getRequest(); $user = $request->getUser(); $engine = $this->getSearchEngine(); $nav = $this->getNavigation(); if (!$nav) { $nav = $this->buildNavigation(); } if ($request->isFormPost()) { $saved_query = $engine->buildSavedQueryFromRequest($request); $engine->saveQuery($saved_query); return id(new AphrontRedirectResponse())->setURI( $engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R'); } $named_query = null; $run_query = true; $query_key = $this->queryKey; if ($this->queryKey == 'advanced') { $run_query = false; $query_key = $request->getStr('query'); } else if (!strlen($this->queryKey)) { $found_query_data = false; if ($request->isHTTPGet() || $request->isQuicksand()) { // If this is a GET request and it has some query data, don't // do anything unless it's only before= or after=. We'll build and // execute a query from it below. This allows external tools to build // URIs like "/query/?users=a,b". $pt_data = $request->getPassthroughRequestData(); $exempt = array( 'before' => true, 'after' => true, 'nux' => true, 'overheated' => true, ); foreach ($pt_data as $pt_key => $pt_value) { if (isset($exempt[$pt_key])) { continue; } $found_query_data = true; break; } } if (!$found_query_data) { // Otherwise, there's no query data so just run the user's default // query for this application. $query_key = $engine->getDefaultQueryKey(); } } if ($engine->isBuiltinQuery($query_key)) { $saved_query = $engine->buildSavedQueryFromBuiltin($query_key); $named_query = idx($engine->loadEnabledNamedQueries(), $query_key); } else if ($query_key) { $saved_query = id(new PhabricatorSavedQueryQuery()) ->setViewer($user) ->withQueryKeys(array($query_key)) ->executeOne(); if (!$saved_query) { return new Aphront404Response(); } $named_query = idx($engine->loadEnabledNamedQueries(), $query_key); } else { $saved_query = $engine->buildSavedQueryFromRequest($request); // Save the query to generate a query key, so "Save Custom Query..." and // other features like "Bulk Edit" and "Export Data" work correctly. $engine->saveQuery($saved_query); } $this->activeQuery = $saved_query; $nav->selectFilter( 'query/'.$saved_query->getQueryKey(), 'query/advanced'); $form = id(new AphrontFormView()) ->setUser($user) ->setAction($request->getPath()); $engine->buildSearchForm($form, $saved_query); $errors = $engine->getErrors(); if ($errors) { $run_query = false; } $submit = id(new AphrontFormSubmitControl()) ->setValue(pht('Search')); if ($run_query && !$named_query && $user->isLoggedIn()) { $save_button = id(new PHUIButtonView()) ->setTag('a') ->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/') ->setText(pht('Save Query')) ->setIcon('fa-floppy-o'); $submit->addButton($save_button); } // TODO: A "Create Dashboard Panel" action goes here somewhere once // we sort out T5307. $form->appendChild($submit); $body = array(); if ($this->getPreface()) { $body[] = $this->getPreface(); } if ($named_query) { $title = $named_query->getQueryName(); } else { $title = pht('Advanced Search'); } $header = id(new PHUIHeaderView()) ->setHeader($title) ->setProfileHeader(true); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addClass('application-search-results'); if ($run_query || $named_query) { $box->setShowHide( pht('Edit Query'), pht('Hide Query'), $form, $this->getApplicationURI('query/advanced/?query='.$query_key), (!$named_query ? true : false)); } else { $box->setForm($form); } $body[] = $box; $more_crumbs = null; if ($run_query) { $exec_errors = array(); $box->setAnchor( id(new PhabricatorAnchorView()) ->setAnchorName('R')); try { $engine->setRequest($request); $query = $engine->buildQueryFromSavedQuery($saved_query); $pager = $engine->newPagerForSavedQuery($saved_query); $pager->readFromRequest($request); $objects = $engine->executeQuery($query, $pager); $force_nux = $request->getBool('nux'); if (!$objects || $force_nux) { $nux_view = $this->renderNewUserView($engine, $force_nux); } else { $nux_view = null; } $is_overflowing = $pager->willShowPagingControls() && $engine->getResultBucket($saved_query); $force_overheated = $request->getBool('overheated'); $is_overheated = $query->getIsOverheated() || $force_overheated; if ($nux_view) { $box->appendChild($nux_view); } else { $list = $engine->renderResults($objects, $saved_query); if (!($list instanceof PhabricatorApplicationSearchResultView)) { throw new Exception( pht( 'SearchEngines must render a "%s" object, but this engine '. '(of class "%s") rendered something else.', 'PhabricatorApplicationSearchResultView', get_class($engine))); } if ($list->getObjectList()) { $box->setObjectList($list->getObjectList()); } if ($list->getTable()) { $box->setTable($list->getTable()); } if ($list->getInfoView()) { $box->setInfoView($list->getInfoView()); } if ($is_overflowing) { $box->appendChild($this->newOverflowingView()); } if ($list->getContent()) { $box->appendChild($list->getContent()); } if ($is_overheated) { $box->appendChild($this->newOverheatedView($objects)); } $result_header = $list->getHeader(); if ($result_header) { $box->setHeader($result_header); $header = $result_header; } $actions = $list->getActions(); if ($actions) { foreach ($actions as $action) { $header->addActionLink($action); } } $use_actions = $engine->newUseResultsActions($saved_query); // TODO: Eventually, modularize all this stuff. $builtin_use_actions = $this->newBuiltinUseActions(); if ($builtin_use_actions) { foreach ($builtin_use_actions as $builtin_use_action) { $use_actions[] = $builtin_use_action; } } if ($use_actions) { $use_dropdown = $this->newUseResultsDropdown( $saved_query, $use_actions); $header->addActionLink($use_dropdown); } $more_crumbs = $list->getCrumbs(); if ($pager->willShowPagingControls()) { $pager_box = id(new PHUIBoxView()) ->setColor(PHUIBoxView::GREY) ->addClass('application-search-pager') ->appendChild($pager); $body[] = $pager_box; } } } catch (PhabricatorTypeaheadInvalidTokenException $ex) { $exec_errors[] = pht( 'This query specifies an invalid parameter. Review the '. 'query parameters and correct errors.'); } catch (PhutilSearchQueryCompilerSyntaxException $ex) { $exec_errors[] = $ex->getMessage(); } catch (PhabricatorSearchConstraintException $ex) { $exec_errors[] = $ex->getMessage(); } // The engine may have encountered additional errors during rendering; // merge them in and show everything. foreach ($engine->getErrors() as $error) { $exec_errors[] = $error; } $errors = $exec_errors; } if ($errors) { $box->setFormErrors($errors, pht('Query Errors')); } $crumbs = $parent ->buildApplicationCrumbs() ->setBorder(true); if ($more_crumbs) { $query_uri = $engine->getQueryResultsPageURI($saved_query->getQueryKey()); $crumbs->addTextCrumb($title, $query_uri); foreach ($more_crumbs as $crumb) { $crumbs->addCrumb($crumb); } } else { $crumbs->addTextCrumb($title); } require_celerity_resource('application-search-view-css'); return $this->newPage() ->setApplicationMenu($this->buildApplicationMenu()) ->setTitle(pht('Query: %s', $title)) ->setCrumbs($crumbs) ->setNavigation($nav) ->addClass('application-search-view') ->appendChild($body); } private function processExportRequest() { $viewer = $this->getViewer(); $engine = $this->getSearchEngine(); $request = $this->getRequest(); if (!$this->canExport()) { return new Aphront404Response(); } $query_key = $this->getQueryKey(); if ($engine->isBuiltinQuery($query_key)) { $saved_query = $engine->buildSavedQueryFromBuiltin($query_key); } else if ($query_key) { $saved_query = id(new PhabricatorSavedQueryQuery()) ->setViewer($viewer) ->withQueryKeys(array($query_key)) ->executeOne(); } else { $saved_query = null; } if (!$saved_query) { return new Aphront404Response(); } $cancel_uri = $engine->getQueryResultsPageURI($query_key); $named_query = idx($engine->loadEnabledNamedQueries(), $query_key); if ($named_query) { $filename = $named_query->getQueryName(); $sheet_title = $named_query->getQueryName(); } else { $filename = $engine->getResultTypeDescription(); $sheet_title = $engine->getResultTypeDescription(); } $filename = phutil_utf8_strtolower($filename); $filename = PhabricatorFile::normalizeFileName($filename); $all_formats = PhabricatorExportFormat::getAllExportFormats(); $available_options = array(); $unavailable_options = array(); $formats = array(); $unavailable_formats = array(); foreach ($all_formats as $key => $format) { if ($format->isExportFormatEnabled()) { $available_options[$key] = $format->getExportFormatName(); $formats[$key] = $format; } else { $unavailable_options[$key] = pht( '%s (Not Available)', $format->getExportFormatName()); $unavailable_formats[$key] = $format; } } $format_options = $available_options + $unavailable_options; // Try to default to the format the user used last time. If you just // exported to Excel, you probably want to export to Excel again. $format_key = $this->readExportFormatPreference(); if (!isset($formats[$format_key])) { $format_key = head_key($format_options); } // Check if this is a large result set or not. If we're exporting a // large amount of data, we'll build the actual export file in the daemons. $threshold = 1000; $query = $engine->buildQueryFromSavedQuery($saved_query); $pager = $engine->newPagerForSavedQuery($saved_query); $pager->setPageSize($threshold + 1); $objects = $engine->executeQuery($query, $pager); $object_count = count($objects); $is_large_export = ($object_count > $threshold); $errors = array(); $e_format = null; if ($request->isFormPost()) { $format_key = $request->getStr('format'); if (isset($unavailable_formats[$format_key])) { $unavailable = $unavailable_formats[$format_key]; $instructions = $unavailable->getInstallInstructions(); $markup = id(new PHUIRemarkupView($viewer, $instructions)) ->setRemarkupOption( PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS, false); return $this->newDialog() ->setTitle(pht('Export Format Not Available')) ->appendChild($markup) ->addCancelButton($cancel_uri, pht('Done')); } $format = idx($formats, $format_key); if (!$format) { $e_format = pht('Invalid'); $errors[] = pht('Choose a valid export format.'); } if (!$errors) { $this->writeExportFormatPreference($format_key); $export_engine = id(new PhabricatorExportEngine()) ->setViewer($viewer) ->setSearchEngine($engine) ->setSavedQuery($saved_query) ->setTitle($sheet_title) ->setFilename($filename) ->setExportFormat($format); if ($is_large_export) { $job = $export_engine->newBulkJob($request); return id(new AphrontRedirectResponse()) ->setURI($job->getMonitorURI()); } else { $file = $export_engine->exportFile(); return $file->newDownloadResponse(); } } } $export_form = id(new AphrontFormView()) ->setViewer($viewer) ->appendControl( id(new AphrontFormSelectControl()) ->setName('format') ->setLabel(pht('Format')) ->setError($e_format) ->setValue($format_key) ->setOptions($format_options)); if ($is_large_export) { $submit_button = pht('Continue'); } else { $submit_button = pht('Download Data'); } return $this->newDialog() ->setTitle(pht('Export Results')) ->setErrors($errors) ->appendForm($export_form) ->addCancelButton($cancel_uri) ->addSubmitButton($submit_button); } private function processEditRequest() { $parent = $this->getDelegatingController(); $request = $this->getRequest(); $viewer = $request->getUser(); $engine = $this->getSearchEngine(); $nav = $this->getNavigation(); if (!$nav) { $nav = $this->buildNavigation(); } $named_queries = $engine->loadAllNamedQueries(); $can_global = $viewer->getIsAdmin(); $groups = array( 'personal' => array( 'name' => pht('Personal Saved Queries'), 'items' => array(), 'edit' => true, ), 'global' => array( 'name' => pht('Global Saved Queries'), 'items' => array(), 'edit' => $can_global, ), ); foreach ($named_queries as $named_query) { if ($named_query->isGlobal()) { $group = 'global'; } else { $group = 'personal'; } $groups[$group]['items'][] = $named_query; } $default_key = $engine->getDefaultQueryKey(); $lists = array(); foreach ($groups as $group) { $lists[] = $this->newQueryListView( $group['name'], $group['items'], $default_key, $group['edit']); } $crumbs = $parent ->buildApplicationCrumbs() ->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI()) ->setBorder(true); $nav->selectFilter('query/edit'); $header = id(new PHUIHeaderView()) ->setHeader(pht('Saved Queries')) ->setProfileHeader(true); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($lists); return $this->newPage() ->setApplicationMenu($this->buildApplicationMenu()) ->setTitle(pht('Saved Queries')) ->setCrumbs($crumbs) ->setNavigation($nav) ->appendChild($view); } private function newQueryListView( $list_name, array $named_queries, $default_key, $can_edit) { $engine = $this->getSearchEngine(); $viewer = $this->getViewer(); $list = id(new PHUIObjectItemListView()) ->setViewer($viewer); if ($can_edit) { $list_id = celerity_generate_unique_node_id(); $list->setID($list_id); Javelin::initBehavior( 'search-reorder-queries', array( 'listID' => $list_id, 'orderURI' => '/search/order/'.get_class($engine).'/', )); } foreach ($named_queries as $named_query) { $class = get_class($engine); $key = $named_query->getQueryKey(); $item = id(new PHUIObjectItemView()) ->setHeader($named_query->getQueryName()) ->setHref($engine->getQueryResultsPageURI($key)); if ($named_query->getIsDisabled()) { if ($can_edit) { $item->setDisabled(true); } else { // If an item is disabled and you don't have permission to edit it, // just skip it. continue; } } if ($can_edit) { if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) { $icon = 'fa-plus'; $disable_name = pht('Enable'); } else { $icon = 'fa-times'; if ($named_query->getIsBuiltin()) { $disable_name = pht('Disable'); } else { $disable_name = pht('Delete'); } } if ($named_query->getID()) { $disable_href = '/search/delete/id/'.$named_query->getID().'/'; } else { $disable_href = '/search/delete/key/'.$key.'/'.$class.'/'; } $item->addAction( id(new PHUIListItemView()) ->setIcon($icon) ->setHref($disable_href) ->setRenderNameAsTooltip(true) ->setName($disable_name) ->setWorkflow(true)); } $default_disabled = $named_query->getIsDisabled(); $default_icon = 'fa-thumb-tack'; if ($default_key === $key) { $default_color = 'green'; } else { $default_color = null; } $item->addAction( id(new PHUIListItemView()) ->setIcon("{$default_icon} {$default_color}") ->setHref('/search/default/'.$key.'/'.$class.'/') ->setRenderNameAsTooltip(true) ->setName(pht('Make Default')) ->setWorkflow(true) ->setDisabled($default_disabled)); if ($can_edit) { if ($named_query->getIsBuiltin()) { $edit_icon = 'fa-lock lightgreytext'; $edit_disabled = true; $edit_name = pht('Builtin'); $edit_href = null; } else { $edit_icon = 'fa-pencil'; $edit_disabled = false; $edit_name = pht('Edit'); $edit_href = '/search/edit/id/'.$named_query->getID().'/'; } $item->addAction( id(new PHUIListItemView()) ->setIcon($edit_icon) ->setHref($edit_href) ->setRenderNameAsTooltip(true) ->setName($edit_name) ->setDisabled($edit_disabled)); } $item->setGrippable($can_edit); $item->addSigil('named-query'); $item->setMetadata( array( 'queryKey' => $named_query->getQueryKey(), )); $list->addItem($item); } $list->setNoDataString(pht('No saved queries.')); return id(new PHUIObjectBoxView()) ->setHeaderText($list_name) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList($list); } public function buildApplicationMenu() { $menu = $this->getDelegatingController() ->buildApplicationMenu(); if ($menu instanceof PHUIApplicationMenuView) { $menu->setSearchEngine($this->getSearchEngine()); } return $menu; } private function buildNavigation() { $viewer = $this->getViewer(); $engine = $this->getSearchEngine(); $nav = id(new AphrontSideNavFilterView()) ->setUser($viewer) ->setBaseURI(new PhutilURI($this->getApplicationURI())); $engine->addNavigationItems($nav->getMenu()); return $nav; } private function renderNewUserView( PhabricatorApplicationSearchEngine $engine, $force_nux) { // Don't render NUX if the user has clicked away from the default page. if (strlen($this->getQueryKey())) { return null; } // Don't put NUX in panels because it would be weird. if ($engine->isPanelContext()) { return null; } // Try to render the view itself first, since this should be very cheap // (just returning some text). $nux_view = $engine->renderNewUserView(); if (!$nux_view) { return null; } $query = $engine->newQuery(); if (!$query) { return null; } // Try to load any object at all. If we can, the application has seen some // use so we just render the normal view. if (!$force_nux) { $object = $query ->setViewer(PhabricatorUser::getOmnipotentUser()) ->setLimit(1) ->execute(); if ($object) { return null; } } return $nux_view; } private function newUseResultsDropdown( PhabricatorSavedQuery $query, array $dropdown_items) { $viewer = $this->getViewer(); $action_list = id(new PhabricatorActionListView()) ->setViewer($viewer); foreach ($dropdown_items as $dropdown_item) { $action_list->addAction($dropdown_item); } return id(new PHUIButtonView()) ->setTag('a') ->setHref('#') ->setText(pht('Use Results')) ->setIcon('fa-bars') ->setDropdownMenu($action_list) ->addClass('dropdown'); } private function newOverflowingView() { $message = pht( 'The query matched more than one page of results. Results are '. 'paginated before bucketing, so later pages may contain additional '. 'results in any bucket.'); return id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setFlush(true) ->setTitle(pht('Buckets Overflowing')) ->setErrors( array( $message, )); } private function newOverheatedView(array $results) { if ($results) { $message = pht( 'Most objects matching your query are not visible to you, so '. 'filtering results is taking a long time. Only some results are '. 'shown. Refine your query to find results more quickly.'); } else { $message = pht( 'Most objects matching your query are not visible to you, so '. 'filtering results is taking a long time. Refine your query to '. 'find results more quickly.'); } return id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setFlush(true) ->setTitle(pht('Query Overheated')) ->setErrors( array( $message, )); } private function newBuiltinUseActions() { $actions = array(); $request = $this->getRequest(); $viewer = $request->getUser(); $is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); $engine = $this->getSearchEngine(); $engine_class = get_class($engine); $query_key = $this->getActiveQuery()->getQueryKey(); $can_use = $engine->canUseInPanelContext(); $is_installed = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorDashboardApplication', $viewer); if ($can_use && $is_installed) { $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-dashboard') ->setName(pht('Add to Dashboard')) ->setWorkflow(true) ->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/"); } if ($this->canExport()) { $export_uri = $engine->getExportURI($query_key); $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-download') ->setName(pht('Export Data')) ->setWorkflow(true) ->setHref($export_uri); } if ($is_dev) { $engine = $this->getSearchEngine(); $nux_uri = $engine->getQueryBaseURI(); $nux_uri = id(new PhutilURI($nux_uri)) - ->setQueryParam('nux', true); + ->replaceQueryParam('nux', true); $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-user-plus') ->setName(pht('DEV: New User State')) ->setHref($nux_uri); } if ($is_dev) { $overheated_uri = $this->getRequest()->getRequestURI() - ->setQueryParam('overheated', true); + ->replaceQueryParam('overheated', true); $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-fire') ->setName(pht('DEV: Overheated State')) ->setHref($overheated_uri); } return $actions; } private function canExport() { $engine = $this->getSearchEngine(); if (!$engine->canExport()) { return false; } // Don't allow logged-out users to perform exports. There's no technical // or policy reason they can't, but we don't normally give them access // to write files or jobs. For now, just err on the side of caution. $viewer = $this->getViewer(); if (!$viewer->getPHID()) { return false; } return true; } private function readExportFormatPreference() { $viewer = $this->getViewer(); $export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY; return $viewer->getUserSetting($export_key); } private function writeExportFormatPreference($value) { $viewer = $this->getViewer(); $request = $this->getRequest(); if (!$viewer->isLoggedIn()) { return; } $export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY; $preferences = PhabricatorUserPreferences::loadUserPreferences($viewer); $editor = id(new PhabricatorUserPreferencesEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $xactions = array(); $xactions[] = $preferences->newTransaction($export_key, $value); $editor->applyTransactions($preferences, $xactions); } } diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index 6809b51334..09193f3c96 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -1,468 +1,468 @@ isEnrollment = $is_enrollment; return $this; } public function getIsEnrollment() { return $this->isEnrollment; } public function processRequest(AphrontRequest $request) { if ($request->getExists('new') || $request->getExists('providerPHID')) { return $this->processNew($request); } if ($request->getExists('edit')) { return $this->processEdit($request); } if ($request->getExists('delete')) { return $this->processDelete($request); } $user = $this->getUser(); $viewer = $request->getUser(); $factors = id(new PhabricatorAuthFactorConfigQuery()) ->setViewer($viewer) ->withUserPHIDs(array($user->getPHID())) ->execute(); $factors = msort($factors, 'newSortVector'); $rows = array(); $rowc = array(); $highlight_id = $request->getInt('id'); foreach ($factors as $factor) { $provider = $factor->getFactorProvider(); if ($factor->getID() == $highlight_id) { $rowc[] = 'highlighted'; } else { $rowc[] = null; } $status = $provider->newStatus(); $status_icon = $status->getFactorIcon(); $status_color = $status->getFactorColor(); $icon = id(new PHUIIconView()) ->setIcon("{$status_icon} {$status_color}") ->setTooltip(pht('Provider: %s', $status->getName())); $details = $provider->getConfigurationListDetails($factor, $viewer); $rows[] = array( $icon, javelin_tag( 'a', array( 'href' => $this->getPanelURI('?edit='.$factor->getID()), 'sigil' => 'workflow', ), $factor->getFactorName()), $provider->getFactor()->getFactorShortName(), $provider->getDisplayName(), $details, phabricator_datetime($factor->getDateCreated(), $viewer), javelin_tag( 'a', array( 'href' => $this->getPanelURI('?delete='.$factor->getID()), 'sigil' => 'workflow', 'class' => 'small button button-grey', ), pht('Remove')), ); } $table = new AphrontTableView($rows); $table->setNoDataString( pht("You haven't added any authentication factors to your account yet.")); $table->setHeaders( array( null, pht('Name'), pht('Type'), pht('Provider'), pht('Details'), pht('Created'), null, )); $table->setColumnClasses( array( null, 'wide pri', null, null, null, 'right', 'action', )); $table->setRowClasses($rowc); $table->setDeviceVisibility( array( true, true, false, false, false, false, true, )); $help_uri = PhabricatorEnv::getDoclink( 'User Guide: Multi-Factor Authentication'); $buttons = array(); // If we're enrolling a new account in MFA, provide a small visual hint // that this is the button they want to click. if ($this->getIsEnrollment()) { $add_color = PHUIButtonView::BLUE; } else { $add_color = PHUIButtonView::GREY; } $can_add = (bool)$this->loadActiveMFAProviders(); $buttons[] = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-plus') ->setText(pht('Add Auth Factor')) ->setHref($this->getPanelURI('?new=true')) ->setWorkflow(true) ->setDisabled(!$can_add) ->setColor($add_color); $buttons[] = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-book') ->setText(pht('Help')) ->setHref($help_uri) ->setColor(PHUIButtonView::GREY); return $this->newBox(pht('Authentication Factors'), $table, $buttons); } private function processNew(AphrontRequest $request) { $viewer = $request->getUser(); $user = $this->getUser(); $cancel_uri = $this->getPanelURI(); // Check that we have providers before we send the user through the MFA // gate, so you don't authenticate and then immediately get roadblocked. $providers = $this->loadActiveMFAProviders(); if (!$providers) { return $this->newDialog() ->setTitle(pht('No MFA Providers')) ->appendParagraph( pht( 'This install does not have any active MFA providers configured. '. 'At least one provider must be configured and active before you '. 'can add new MFA factors.')) ->addCancelButton($cancel_uri); } $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $cancel_uri); $selected_phid = $request->getStr('providerPHID'); if (empty($providers[$selected_phid])) { $selected_provider = null; } else { $selected_provider = $providers[$selected_phid]; // Only let the user continue creating a factor for a given provider if // they actually pass the provider's checks. if (!$selected_provider->canCreateNewConfiguration($viewer)) { $selected_provider = null; } } if (!$selected_provider) { $menu = id(new PHUIObjectItemListView()) ->setViewer($viewer) ->setBig(true) ->setFlush(true); foreach ($providers as $provider_phid => $provider) { $provider_uri = id(new PhutilURI($this->getPanelURI())) - ->setQueryParam('providerPHID', $provider_phid); + ->replaceQueryParam('providerPHID', $provider_phid); $is_enabled = $provider->canCreateNewConfiguration($viewer); $item = id(new PHUIObjectItemView()) ->setHeader($provider->getDisplayName()) ->setImageIcon($provider->newIconView()) ->addAttribute($provider->getDisplayDescription()); if ($is_enabled) { $item ->setHref($provider_uri) ->setClickable(true); } else { $item->setDisabled(true); } $create_description = $provider->getConfigurationCreateDescription( $viewer); if ($create_description) { $item->appendChild($create_description); } $menu->addItem($item); } return $this->newDialog() ->setTitle(pht('Choose Factor Type')) ->appendChild($menu) ->addCancelButton($cancel_uri); } // NOTE: Beyond providing guidance, this step is also providing a CSRF gate // on this endpoint, since prompting the user to respond to a challenge // sometimes requires us to push a challenge to them as a side effect (for // example, with SMS). if (!$request->isFormPost() || !$request->getBool('mfa.start')) { $enroll = $selected_provider->getEnrollMessage(); if (!strlen($enroll)) { $enroll = $selected_provider->getEnrollDescription($viewer); } return $this->newDialog() ->addHiddenInput('providerPHID', $selected_provider->getPHID()) ->addHiddenInput('mfa.start', 1) ->setTitle(pht('Add Authentication Factor')) ->appendChild(new PHUIRemarkupView($viewer, $enroll)) ->addCancelButton($cancel_uri) ->addSubmitButton($selected_provider->getEnrollButtonText($viewer)); } $form = id(new AphrontFormView()) ->setViewer($viewer); if ($request->getBool('mfa.enroll')) { // Subject users to rate limiting so that it's difficult to add factors // by pure brute force. This is normally not much of an attack, but push // factor types may have side effects. PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), new PhabricatorAuthNewFactorAction(), 1); } else { // Test the limit before showing the user a form, so we don't give them // a form which can never possibly work because it will always hit rate // limiting. PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), new PhabricatorAuthNewFactorAction(), 0); } $config = $selected_provider->processAddFactorForm( $form, $request, $user); if ($config) { // If the user added a factor, give them a rate limiting point back. PhabricatorSystemActionEngine::willTakeAction( array($viewer->getPHID()), new PhabricatorAuthNewFactorAction(), -1); $config->save(); // If we used a temporary token to handle synchronizing the factor, // revoke it now. $sync_token = $config->getMFASyncToken(); if ($sync_token) { $sync_token->revokeToken(); } $log = PhabricatorUserLog::initializeNewLog( $viewer, $user->getPHID(), PhabricatorUserLog::ACTION_MULTI_ADD); $log->save(); $user->updateMultiFactorEnrollment(); // Terminate other sessions so they must log in and survive the // multi-factor auth check. id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( $user, new PhutilOpaqueEnvelope( $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?id='.$config->getID())); } return $this->newDialog() ->addHiddenInput('providerPHID', $selected_provider->getPHID()) ->addHiddenInput('mfa.start', 1) ->addHiddenInput('mfa.enroll', 1) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Add Authentication Factor')) ->appendChild($form->buildLayoutView()) ->addSubmitButton(pht('Continue')) ->addCancelButton($cancel_uri); } private function processEdit(AphrontRequest $request) { $viewer = $request->getUser(); $user = $this->getUser(); $factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere( 'id = %d AND userPHID = %s', $request->getInt('edit'), $user->getPHID()); if (!$factor) { return new Aphront404Response(); } $e_name = true; $errors = array(); if ($request->isFormPost()) { $name = $request->getStr('name'); if (!strlen($name)) { $e_name = pht('Required'); $errors[] = pht( 'Authentication factors must have a name to identify them.'); } if (!$errors) { $factor->setFactorName($name); $factor->save(); $user->updateMultiFactorEnrollment(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?id='.$factor->getID())); } } else { $name = $factor->getFactorName(); } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($name) ->setError($e_name)); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->addHiddenInput('edit', $factor->getID()) ->setTitle(pht('Edit Authentication Factor')) ->setErrors($errors) ->appendChild($form->buildLayoutView()) ->addSubmitButton(pht('Save')) ->addCancelButton($this->getPanelURI()); return id(new AphrontDialogResponse()) ->setDialog($dialog); } private function processDelete(AphrontRequest $request) { $viewer = $request->getUser(); $user = $this->getUser(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $this->getPanelURI()); $factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere( 'id = %d AND userPHID = %s', $request->getInt('delete'), $user->getPHID()); if (!$factor) { return new Aphront404Response(); } if ($request->isFormPost()) { $factor->delete(); $log = PhabricatorUserLog::initializeNewLog( $viewer, $user->getPHID(), PhabricatorUserLog::ACTION_MULTI_REMOVE); $log->save(); $user->updateMultiFactorEnrollment(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI()); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->addHiddenInput('delete', $factor->getID()) ->setTitle(pht('Delete Authentication Factor')) ->appendParagraph( pht( 'Really remove the authentication factor %s from your account?', phutil_tag('strong', array(), $factor->getFactorName()))) ->addSubmitButton(pht('Remove Factor')) ->addCancelButton($this->getPanelURI()); return id(new AphrontDialogResponse()) ->setDialog($dialog); } private function loadActiveMFAProviders() { $viewer = $this->getViewer(); $providers = id(new PhabricatorAuthFactorProviderQuery()) ->setViewer($viewer) ->withStatuses( array( PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE, )) ->execute(); $providers = mpull($providers, null, 'getPHID'); $providers = msortv($providers, 'newSortVector'); return $providers; } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index 1fb850887f..115c7b950e 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -1,618 +1,618 @@ objectPHID = $object_phid; return $this; } public function getObjectPHID() { return $this->objectPHID; } public function setShowPreview($show_preview) { $this->showPreview = $show_preview; return $this; } public function getShowPreview() { return $this->showPreview; } public function setRequestURI(PhutilURI $request_uri) { $this->requestURI = $request_uri; return $this; } public function getRequestURI() { return $this->requestURI; } public function setCurrentVersion($current_version) { $this->currentVersion = $current_version; return $this; } public function getCurrentVersion() { return $this->currentVersion; } public function setVersionedDraft( PhabricatorVersionedDraft $versioned_draft) { $this->versionedDraft = $versioned_draft; return $this; } public function getVersionedDraft() { return $this->versionedDraft; } public function setDraft(PhabricatorDraft $draft) { $this->draft = $draft; return $this; } public function getDraft() { return $this->draft; } public function setSubmitButtonName($submit_button_name) { $this->submitButtonName = $submit_button_name; return $this; } public function getSubmitButtonName() { return $this->submitButtonName; } public function setAction($action) { $this->action = $action; return $this; } public function getAction() { return $this->action; } public function setHeaderText($text) { $this->headerText = $text; return $this; } public function setFullWidth($fw) { $this->fullWidth = $fw; return $this; } public function setInfoView(PHUIInfoView $info_view) { $this->infoView = $info_view; return $this; } public function getInfoView() { return $this->infoView; } public function setCommentActions(array $comment_actions) { assert_instances_of($comment_actions, 'PhabricatorEditEngineCommentAction'); $this->commentActions = $comment_actions; return $this; } public function getCommentActions() { return $this->commentActions; } public function setCommentActionGroups(array $groups) { assert_instances_of($groups, 'PhabricatorEditEngineCommentActionGroup'); $this->commentActionGroups = $groups; return $this; } public function getCommentActionGroups() { return $this->commentActionGroups; } public function setNoPermission($no_permission) { $this->noPermission = $no_permission; return $this; } public function getNoPermission() { return $this->noPermission; } public function setEditEngineLock(PhabricatorEditEngineLock $lock) { $this->editEngineLock = $lock; return $this; } public function getEditEngineLock() { return $this->editEngineLock; } public function setRequiresMFA($requires_mfa) { $this->requiresMFA = $requires_mfa; return $this; } public function getRequiresMFA() { return $this->requiresMFA; } public function setTransactionTimeline( PhabricatorApplicationTransactionView $timeline) { $timeline->setQuoteTargetID($this->getCommentID()); if ($this->getNoPermission() || $this->getEditEngineLock()) { $timeline->setShouldTerminate(true); } $this->transactionTimeline = $timeline; return $this; } public function render() { if ($this->getNoPermission()) { return null; } $lock = $this->getEditEngineLock(); if ($lock) { return id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors( array( $lock->getLockedObjectDisplayText(), )); } $viewer = $this->getViewer(); if (!$viewer->isLoggedIn()) { $uri = id(new PhutilURI('/login/')) - ->setQueryParam('next', (string)$this->getRequestURI()); + ->replaceQueryParam('next', (string)$this->getRequestURI()); return id(new PHUIObjectBoxView()) ->setFlush(true) ->appendChild( javelin_tag( 'a', array( 'class' => 'login-to-comment button', 'href' => $uri, ), pht('Log In to Comment'))); } if ($this->getRequiresMFA()) { if (!$viewer->getIsEnrolledInMultiFactor()) { $viewer->updateMultiFactorEnrollment(); if (!$viewer->getIsEnrolledInMultiFactor()) { $messages = array(); $messages[] = pht( 'You must provide multi-factor credentials to comment or make '. 'changes, but you do not have multi-factor authentication '. 'configured on your account.'); $messages[] = pht( 'To continue, configure multi-factor authentication in Settings.'); return id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_MFA) ->setErrors($messages); } } } $data = array(); $comment = $this->renderCommentPanel(); if ($this->getShowPreview()) { $preview = $this->renderPreviewPanel(); } else { $preview = null; } if (!$this->getCommentActions()) { Javelin::initBehavior( 'phabricator-transaction-comment-form', array( 'formID' => $this->getFormID(), 'timelineID' => $this->getPreviewTimelineID(), 'panelID' => $this->getPreviewPanelID(), 'showPreview' => $this->getShowPreview(), 'actionURI' => $this->getAction(), )); } require_celerity_resource('phui-comment-form-css'); $image_uri = $viewer->getProfileImageURI(); $image = javelin_tag( 'div', array( 'style' => 'background-image: url('.$image_uri.')', 'class' => 'phui-comment-image', 'aural' => false, )); $wedge = phutil_tag( 'div', array( 'class' => 'phui-timeline-wedge', ), ''); $badge_view = $this->renderBadgeView(); $comment_box = id(new PHUIObjectBoxView()) ->setFlush(true) ->addClass('phui-comment-form-view') ->addSigil('phui-comment-form') ->appendChild( phutil_tag( 'h3', array( 'class' => 'aural-only', ), pht('Add Comment'))) ->appendChild($image) ->appendChild($badge_view) ->appendChild($wedge) ->appendChild($comment); return array($comment_box, $preview); } private function renderCommentPanel() { $draft_comment = ''; $draft_key = null; if ($this->getDraft()) { $draft_comment = $this->getDraft()->getDraft(); $draft_key = $this->getDraft()->getDraftKey(); } $versioned_draft = $this->getVersionedDraft(); if ($versioned_draft) { $draft_comment = $versioned_draft->getProperty('comment', ''); } if (!$this->getObjectPHID()) { throw new PhutilInvalidStateException('setObjectPHID', 'render'); } $version_key = PhabricatorVersionedDraft::KEY_VERSION; $version_value = $this->getCurrentVersion(); $form = id(new AphrontFormView()) ->setUser($this->getUser()) ->addSigil('transaction-append') ->setWorkflow(true) ->setFullWidth($this->fullWidth) ->setMetadata( array( 'objectPHID' => $this->getObjectPHID(), )) ->setAction($this->getAction()) ->setID($this->getFormID()) ->addHiddenInput('__draft__', $draft_key) ->addHiddenInput($version_key, $version_value); $comment_actions = $this->getCommentActions(); if ($comment_actions) { $action_map = array(); $type_map = array(); $comment_actions = mpull($comment_actions, null, 'getKey'); $draft_actions = array(); $draft_keys = array(); if ($versioned_draft) { $draft_actions = $versioned_draft->getProperty('actions', array()); if (!is_array($draft_actions)) { $draft_actions = array(); } foreach ($draft_actions as $action) { $type = idx($action, 'type'); $comment_action = idx($comment_actions, $type); if (!$comment_action) { continue; } $value = idx($action, 'value'); $comment_action->setValue($value); $draft_keys[] = $type; } } foreach ($comment_actions as $key => $comment_action) { $key = $comment_action->getKey(); $label = $comment_action->getLabel(); $action_map[$key] = array( 'key' => $key, 'label' => $label, 'type' => $comment_action->getPHUIXControlType(), 'spec' => $comment_action->getPHUIXControlSpecification(), 'initialValue' => $comment_action->getInitialValue(), 'groupKey' => $comment_action->getGroupKey(), 'conflictKey' => $comment_action->getConflictKey(), 'auralLabel' => pht('Remove Action: %s', $label), 'buttonText' => $comment_action->getSubmitButtonText(), ); $type_map[$key] = $comment_action; } $options = $this->newCommentActionOptions($action_map); $action_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); $place_id = celerity_generate_unique_node_id(); $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'editengine.actions', 'id' => $input_id, ))); $invisi_bar = phutil_tag( 'div', array( 'id' => $place_id, 'class' => 'phui-comment-control-stack', )); $action_select = id(new AphrontFormSelectControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-action-control') ->setID($action_id) ->setOptions($options); $action_bar = phutil_tag( 'div', array( 'class' => 'phui-comment-action-bar grouped', ), array( $action_select, )); $form->appendChild($action_bar); $info_view = $this->getInfoView(); if ($info_view) { $form->appendChild($info_view); } if ($this->getRequiresMFA()) { $message = pht( 'You will be required to provide multi-factor credentials to '. 'comment or make changes.'); $form->appendChild( id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_MFA) ->setErrors(array($message))); } $form->appendChild($invisi_bar); $form->addClass('phui-comment-has-actions'); $timeline = $this->transactionTimeline; $view_data = array(); if ($timeline) { $view_data = $timeline->getViewData(); } Javelin::initBehavior( 'comment-actions', array( 'actionID' => $action_id, 'inputID' => $input_id, 'formID' => $this->getFormID(), 'placeID' => $place_id, 'panelID' => $this->getPreviewPanelID(), 'timelineID' => $this->getPreviewTimelineID(), 'actions' => $action_map, 'showPreview' => $this->getShowPreview(), 'actionURI' => $this->getAction(), 'drafts' => $draft_keys, 'defaultButtonText' => $this->getSubmitButtonName(), 'viewData' => $view_data, )); } $submit_button = id(new AphrontFormSubmitControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-submit-control') ->setValue($this->getSubmitButtonName()); $form ->appendChild( id(new PhabricatorRemarkupControl()) ->setID($this->getCommentID()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-textarea-control') ->setCanPin(true) ->setName('comment') ->setUser($this->getUser()) ->setValue($draft_comment)) ->appendChild( id(new AphrontFormSubmitControl()) ->addClass('phui-comment-fullwidth-control') ->addClass('phui-comment-submit-control') ->addSigil('submit-transactions') ->setValue($this->getSubmitButtonName())); return $form; } private function renderPreviewPanel() { $preview = id(new PHUITimelineView()) ->setID($this->getPreviewTimelineID()); return phutil_tag( 'div', array( 'id' => $this->getPreviewPanelID(), 'style' => 'display: none', 'class' => 'phui-comment-preview-view', ), $preview); } private function getPreviewPanelID() { if (!$this->previewPanelID) { $this->previewPanelID = celerity_generate_unique_node_id(); } return $this->previewPanelID; } private function getPreviewTimelineID() { if (!$this->previewTimelineID) { $this->previewTimelineID = celerity_generate_unique_node_id(); } return $this->previewTimelineID; } public function setFormID($id) { $this->formID = $id; return $this; } private function getFormID() { if (!$this->formID) { $this->formID = celerity_generate_unique_node_id(); } return $this->formID; } private function getStatusID() { if (!$this->statusID) { $this->statusID = celerity_generate_unique_node_id(); } return $this->statusID; } private function getCommentID() { if (!$this->commentID) { $this->commentID = celerity_generate_unique_node_id(); } return $this->commentID; } private function newCommentActionOptions(array $action_map) { $options = array(); $options['+'] = pht('Add Action...'); // Merge options into groups. $groups = array(); foreach ($action_map as $key => $item) { $group_key = $item['groupKey']; if (!isset($groups[$group_key])) { $groups[$group_key] = array(); } $groups[$group_key][$key] = $item; } $group_specs = $this->getCommentActionGroups(); $group_labels = mpull($group_specs, 'getLabel', 'getKey'); // Reorder groups to put them in the same order as the recognized // group definitions. $groups = array_select_keys($groups, array_keys($group_labels)) + $groups; // Move options with no group to the end. $default_group = idx($groups, ''); if ($default_group) { unset($groups['']); $groups[''] = $default_group; } foreach ($groups as $group_key => $group_items) { if (strlen($group_key)) { $group_label = idx($group_labels, $group_key, $group_key); $options[$group_label] = ipull($group_items, 'label'); } else { foreach ($group_items as $key => $item) { $options[$key] = $item['label']; } } } return $options; } private function renderBadgeView() { $user = $this->getUser(); $can_use_badges = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorBadgesApplication', $user); if (!$can_use_badges) { return null; } // Pull Badges from UserCache $badges = $user->getRecentBadgeAwards(); $badge_view = null; if ($badges) { $badge_list = array(); foreach ($badges as $badge) { $badge_view = id(new PHUIBadgeMiniView()) ->setIcon($badge['icon']) ->setQuality($badge['quality']) ->setHeader($badge['name']) ->setTipDirection('E') ->setHref('/badges/view/'.$badge['id'].'/'); $badge_list[] = $badge_view; } $flex = new PHUIBadgeBoxView(); $flex->addItems($badge_list); $flex->setCollapsed(true); $badge_view = phutil_tag( 'div', array( 'class' => 'phui-timeline-badges', ), $flex); } return $badge_view; } } diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php index 7c2205df46..efc9ea5f65 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -1,453 +1,455 @@ getRequest(); $viewer = $request->getUser(); $query = $request->getStr('q'); $offset = $request->getInt('offset'); $select_phid = null; $is_browse = ($request->getURIData('action') == 'browse'); $select = $request->getStr('select'); if ($select) { $select = phutil_json_decode($select); $query = idx($select, 'q'); $offset = idx($select, 'offset'); $select_phid = idx($select, 'phid'); } // Default this to the query string to make debugging a little bit easier. $raw_query = nonempty($request->getStr('raw'), $query); // This makes form submission easier in the debug view. $class = nonempty($request->getURIData('class'), $request->getStr('class')); $sources = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorTypeaheadDatasource') ->execute(); if (isset($sources[$class])) { $source = $sources[$class]; $parameters = array(); $raw_parameters = $request->getStr('parameters'); if (strlen($raw_parameters)) { try { $parameters = phutil_json_decode($raw_parameters); } catch (PhutilJSONParserException $ex) { return $this->newDialog() ->setTitle(pht('Invalid Parameters')) ->appendParagraph( pht( 'The HTTP parameter named "parameters" for this request is '. 'not a valid JSON parameter. JSON is required. Exception: %s', $ex->getMessage())) ->addCancelButton('/'); } } $source->setParameters($parameters); $source->setViewer($viewer); // NOTE: Wrapping the source in a Composite datasource ensures we perform // application visibility checks for the viewer, so we do not need to do // those separately. $composite = new PhabricatorTypeaheadRuntimeCompositeDatasource(); $composite->addDatasource($source); $hard_limit = 1000; $limit = 100; $composite ->setViewer($viewer) ->setQuery($query) ->setRawQuery($raw_query) ->setLimit($limit + 1); if ($is_browse) { if (!$composite->isBrowsable()) { return new Aphront404Response(); } if (($offset + $limit) >= $hard_limit) { // Offset-based paging is intrinsically slow; hard-cap how far we're // willing to go with it. return new Aphront404Response(); } $composite ->setOffset($offset) ->setIsBrowse(true); } $results = $composite->loadResults(); if ($is_browse) { // If this is a request for a specific token after the user clicks // "Select", return the token in wire format so it can be added to // the tokenizer. if ($select_phid !== null) { $map = mpull($results, null, 'getPHID'); $token = idx($map, $select_phid); if (!$token) { return new Aphront404Response(); } $payload = array( 'key' => $token->getPHID(), 'token' => $token->getWireFormat(), ); return id(new AphrontAjaxResponse())->setContent($payload); } $format = $request->getStr('format'); switch ($format) { case 'html': case 'dialog': // These are the acceptable response formats. break; default: // Return a dialog if format information is missing or invalid. $format = 'dialog'; break; } $next_link = null; if (count($results) > $limit) { $results = array_slice($results, 0, $limit, $preserve_keys = true); if (($offset + (2 * $limit)) < $hard_limit) { $next_uri = id(new PhutilURI($request->getRequestURI())) - ->setQueryParam('offset', $offset + $limit) - ->setQueryParam('q', $query) - ->setQueryParam('raw', $raw_query) - ->setQueryParam('format', 'html'); + ->replaceQueryParam('offset', $offset + $limit) + ->replaceQueryParam('q', $query) + ->replaceQueryParam('raw', $raw_query) + ->replaceQueryParam('format', 'html'); $next_link = javelin_tag( 'a', array( 'href' => $next_uri, 'class' => 'typeahead-browse-more', 'sigil' => 'typeahead-browse-more', 'mustcapture' => true, ), pht('More Results')); } else { // If the user has paged through more than 1K results, don't // offer to page any further. $next_link = javelin_tag( 'div', array( 'class' => 'typeahead-browse-hard-limit', ), pht('You reach the edge of the abyss.')); } } $exclude = $request->getStrList('exclude'); $exclude = array_fuse($exclude); $select = array( 'offset' => $offset, 'q' => $query, ); $items = array(); foreach ($results as $result) { // Disable already-selected tokens. $disabled = isset($exclude[$result->getPHID()]); $value = $select + array('phid' => $result->getPHID()); $value = json_encode($value); $button = phutil_tag( 'button', array( 'class' => 'small grey', 'name' => 'select', 'value' => $value, 'disabled' => $disabled ? 'disabled' : null, ), pht('Select')); $information = $this->renderBrowseResult($result, $button); $items[] = phutil_tag( 'div', array( 'class' => 'typeahead-browse-item grouped', ), $information); } $markup = array( $items, $next_link, ); if ($format == 'html') { $content = array( 'markup' => hsprintf('%s', $markup), ); return id(new AphrontAjaxResponse())->setContent($content); } $this->requireResource('typeahead-browse-css'); $this->initBehavior('typeahead-browse'); $input_id = celerity_generate_unique_node_id(); $frame_id = celerity_generate_unique_node_id(); $config = array( 'inputID' => $input_id, 'frameID' => $frame_id, 'uri' => (string)$request->getRequestURI(), ); $this->initBehavior('typeahead-search', $config); $search = javelin_tag( 'input', array( 'type' => 'text', 'id' => $input_id, 'class' => 'typeahead-browse-input', 'autocomplete' => 'off', 'placeholder' => $source->getPlaceholderText(), )); $frame = phutil_tag( 'div', array( 'class' => 'typeahead-browse-frame', 'id' => $frame_id, ), $markup); $browser = array( phutil_tag( 'div', array( 'class' => 'typeahead-browse-header', ), $search), $frame, ); $function_help = null; if ($source->getAllDatasourceFunctions()) { $reference_uri = '/typeahead/help/'.get_class($source).'/'; $parameters = $source->getParameters(); if ($parameters) { $reference_uri = (string)id(new PhutilURI($reference_uri)) - ->setQueryParam('parameters', phutil_json_encode($parameters)); + ->replaceQueryParam( + 'parameters', + phutil_json_encode($parameters)); } $reference_link = phutil_tag( 'a', array( 'href' => $reference_uri, 'target' => '_blank', ), pht('Reference: Advanced Functions')); $function_help = array( id(new PHUIIconView()) ->setIcon('fa-book'), ' ', $reference_link, ); } return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FORM) ->setRenderDialogAsDiv(true) ->setTitle($source->getBrowseTitle()) ->appendChild($browser) ->setResizeX(true) ->setResizeY($frame_id) ->addFooter($function_help) ->addCancelButton('/', pht('Close')); } } else if ($is_browse) { return new Aphront404Response(); } else { $results = array(); } $content = mpull($results, 'getWireFormat'); $content = array_values($content); if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($content); } // If there's a non-Ajax request to this endpoint, show results in a tabular // format to make it easier to debug typeahead output. foreach ($sources as $key => $source) { // See T13119. Exclude proxy datasources from the dropdown since they // fatal if built like this without actually being configured with an // underlying datasource. This is a bit hacky but this is just a // debugging/development UI anyway. if ($source instanceof PhabricatorTypeaheadProxyDatasource) { unset($sources[$key]); continue; } // This can happen with composite or generic sources. if (!$source->getDatasourceApplicationClass()) { continue; } if (!PhabricatorApplication::isClassInstalledForViewer( $source->getDatasourceApplicationClass(), $viewer)) { unset($sources[$key]); } } $options = array_fuse(array_keys($sources)); asort($options); $form = id(new AphrontFormView()) ->setUser($viewer) ->setAction('/typeahead/class/') ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Source Class')) ->setName('class') ->setValue($class) ->setOptions($options)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Query')) ->setName('q') ->setValue($request->getStr('q'))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Raw Query')) ->setName('raw') ->setValue($request->getStr('raw'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Query'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Token Query')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); // Make "\n" delimiters more visible. foreach ($content as $key => $row) { $content[$key][0] = str_replace("\n", '<\n>', $row[0]); } $table = new AphrontTableView($content); $table->setHeaders( array( pht('Name'), pht('URI'), pht('PHID'), pht('Priority'), pht('Display Name'), pht('Display Type'), pht('Image URI'), pht('Priority Type'), pht('Icon'), pht('Closed'), pht('Sprite'), pht('Color'), pht('Type'), pht('Unique'), pht('Auto'), pht('Phase'), )); $result_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Token Results (%s)', $class)) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($table); $title = pht('Typeahead Results'); $header = id(new PHUIHeaderView()) ->setHeader($title); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $form_box, $result_box, )); return $this->newPage() ->setTitle($title) ->appendChild($view); } private function renderBrowseResult( PhabricatorTypeaheadResult $result, $button) { $class = array(); $style = array(); $separator = " \xC2\xB7 "; $class[] = 'phabricator-main-search-typeahead-result'; $name = phutil_tag( 'div', array( 'class' => 'result-name', ), $result->getDisplayName()); $icon = $result->getIcon(); $icon = id(new PHUIIconView())->setIcon($icon); $attributes = $result->getAttributes(); $attributes = phutil_implode_html($separator, $attributes); $attributes = array($icon, ' ', $attributes); $closed = $result->getClosed(); if ($closed) { $class[] = 'result-closed'; $attributes = array($closed, $separator, $attributes); } $attributes = phutil_tag( 'div', array( 'class' => 'result-type', ), $attributes); $image = $result->getImageURI(); if ($image) { $style[] = 'background-image: url('.$image.');'; $class[] = 'has-image'; } return phutil_tag( 'div', array( 'class' => implode(' ', $class), 'style' => implode(' ', $style), ), array( $button, $name, $attributes, )); } } diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 8b2af38d3d..24fb940c9a 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -1,971 +1,977 @@ overrideEnv('some.key', 'new-value-for-this-test'); * * // Some test which depends on the value of 'some.key'. * * } * * Your changes will persist until the `$env` object leaves scope or is * destroyed. * * You should //not// use this in normal code. * * * @task read Reading Configuration * @task uri URI Validation * @task test Unit Test Support * @task internal Internals */ final class PhabricatorEnv extends Phobject { private static $sourceStack; private static $repairSource; private static $overrideSource; private static $requestBaseURI; private static $cache; private static $localeCode; private static $readOnly; private static $readOnlyReason; const READONLY_CONFIG = 'config'; const READONLY_UNREACHABLE = 'unreachable'; const READONLY_SEVERED = 'severed'; const READONLY_MASTERLESS = 'masterless'; /** * @phutil-external-symbol class PhabricatorStartup */ public static function initializeWebEnvironment() { self::initializeCommonEnvironment(false); } public static function initializeScriptEnvironment($config_optional) { self::initializeCommonEnvironment($config_optional); // NOTE: This is dangerous in general, but we know we're in a script context // and are not vulnerable to CSRF. AphrontWriteGuard::allowDangerousUnguardedWrites(true); // There are several places where we log information (about errors, events, // service calls, etc.) for analysis via DarkConsole or similar. These are // useful for web requests, but grow unboundedly in long-running scripts and // daemons. Discard data as it arrives in these cases. PhutilServiceProfiler::getInstance()->enableDiscardMode(); DarkConsoleErrorLogPluginAPI::enableDiscardMode(); DarkConsoleEventPluginAPI::enableDiscardMode(); } private static function initializeCommonEnvironment($config_optional) { PhutilErrorHandler::initialize(); self::resetUmask(); self::buildConfigurationSourceStack($config_optional); // Force a valid timezone. If both PHP and Phabricator configuration are // invalid, use UTC. $tz = self::getEnvConfig('phabricator.timezone'); if ($tz) { @date_default_timezone_set($tz); } $ok = @date_default_timezone_set(date_default_timezone_get()); if (!$ok) { date_default_timezone_set('UTC'); } // Prepend '/support/bin' and append any paths to $PATH if we need to. $env_path = getenv('PATH'); $phabricator_path = dirname(phutil_get_library_root('phabricator')); $support_path = $phabricator_path.'/support/bin'; $env_path = $support_path.PATH_SEPARATOR.$env_path; $append_dirs = self::getEnvConfig('environment.append-paths'); if (!empty($append_dirs)) { $append_path = implode(PATH_SEPARATOR, $append_dirs); $env_path = $env_path.PATH_SEPARATOR.$append_path; } putenv('PATH='.$env_path); // Write this back into $_ENV, too, so ExecFuture picks it up when creating // subprocess environments. $_ENV['PATH'] = $env_path; // If an instance identifier is defined, write it into the environment so // it's available to subprocesses. $instance = self::getEnvConfig('cluster.instance'); if (strlen($instance)) { putenv('PHABRICATOR_INSTANCE='.$instance); $_ENV['PHABRICATOR_INSTANCE'] = $instance; } PhabricatorEventEngine::initialize(); // TODO: Add a "locale.default" config option once we have some reasonable // defaults which aren't silly nonsense. self::setLocaleCode('en_US'); } public static function beginScopedLocale($locale_code) { return new PhabricatorLocaleScopeGuard($locale_code); } public static function getLocaleCode() { return self::$localeCode; } public static function setLocaleCode($locale_code) { if (!$locale_code) { return; } if ($locale_code == self::$localeCode) { return; } try { $locale = PhutilLocale::loadLocale($locale_code); $translations = PhutilTranslation::getTranslationMapForLocale( $locale_code); $override = self::getEnvConfig('translation.override'); if (!is_array($override)) { $override = array(); } PhutilTranslator::getInstance() ->setLocale($locale) ->setTranslations($override + $translations); self::$localeCode = $locale_code; } catch (Exception $ex) { // Just ignore this; the user likely has an out-of-date locale code. } } private static function buildConfigurationSourceStack($config_optional) { self::dropConfigCache(); $stack = new PhabricatorConfigStackSource(); self::$sourceStack = $stack; $default_source = id(new PhabricatorConfigDefaultSource()) ->setName(pht('Global Default')); $stack->pushSource($default_source); $env = self::getSelectedEnvironmentName(); if ($env) { $stack->pushSource( id(new PhabricatorConfigFileSource($env)) ->setName(pht("File '%s'", $env))); } $stack->pushSource( id(new PhabricatorConfigLocalSource()) ->setName(pht('Local Config'))); // If the install overrides the database adapter, we might need to load // the database adapter class before we can push on the database config. // This config is locked and can't be edited from the web UI anyway. foreach (self::getEnvConfig('load-libraries') as $library) { phutil_load_library($library); } // Drop any class map caches, since they will have generated without // any classes from libraries. Without this, preflight setup checks can // cause generation of a setup check cache that omits checks defined in // libraries, for example. PhutilClassMapQuery::deleteCaches(); // If custom libraries specify config options, they won't get default // values as the Default source has already been loaded, so we get it to // pull in all options from non-phabricator libraries now they are loaded. $default_source->loadExternalOptions(); // If this install has site config sources, load them now. $site_sources = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorConfigSiteSource') ->setSortMethod('getPriority') ->execute(); foreach ($site_sources as $site_source) { $stack->pushSource($site_source); // If the site source did anything which reads config, throw it away // to make sure any additional site sources get clean reads. self::dropConfigCache(); } $masters = PhabricatorDatabaseRef::getMasterDatabaseRefs(); if (!$masters) { self::setReadOnly(true, self::READONLY_MASTERLESS); } else { // If any master is severed, we drop to readonly mode. In theory we // could try to continue if we're only missing some applications, but // this is very complex and we're unlikely to get it right. foreach ($masters as $master) { // Give severed masters one last chance to get healthy. if ($master->isSevered()) { $master->checkHealth(); } if ($master->isSevered()) { self::setReadOnly(true, self::READONLY_SEVERED); break; } } } try { $stack->pushSource( id(new PhabricatorConfigDatabaseSource('default')) ->setName(pht('Database'))); } catch (AphrontSchemaQueryException $exception) { // If the database is not available, just skip this configuration // source. This happens during `bin/storage upgrade`, `bin/conf` before // schema setup, etc. } catch (PhabricatorClusterStrandedException $ex) { // This means we can't connect to any database host. That's fine as // long as we're running a setup script like `bin/storage`. if (!$config_optional) { throw $ex; } } // Drop the config cache one final time to make sure we're getting clean // reads now that we've finished building the stack. self::dropConfigCache(); } public static function repairConfig($key, $value) { if (!self::$repairSource) { self::$repairSource = id(new PhabricatorConfigDictionarySource(array())) ->setName(pht('Repaired Config')); self::$sourceStack->pushSource(self::$repairSource); } self::$repairSource->setKeys(array($key => $value)); self::dropConfigCache(); } public static function overrideConfig($key, $value) { if (!self::$overrideSource) { self::$overrideSource = id(new PhabricatorConfigDictionarySource(array())) ->setName(pht('Overridden Config')); self::$sourceStack->pushSource(self::$overrideSource); } self::$overrideSource->setKeys(array($key => $value)); self::dropConfigCache(); } public static function getUnrepairedEnvConfig($key, $default = null) { foreach (self::$sourceStack->getStack() as $source) { if ($source === self::$repairSource) { continue; } $result = $source->getKeys(array($key)); if ($result) { return $result[$key]; } } return $default; } public static function getSelectedEnvironmentName() { $env_var = 'PHABRICATOR_ENV'; $env = idx($_SERVER, $env_var); if (!$env) { $env = getenv($env_var); } if (!$env) { $env = idx($_ENV, $env_var); } if (!$env) { $root = dirname(phutil_get_library_root('phabricator')); $path = $root.'/conf/local/ENVIRONMENT'; if (Filesystem::pathExists($path)) { $env = trim(Filesystem::readFile($path)); } } return $env; } /* -( Reading Configuration )---------------------------------------------- */ /** * Get the current configuration setting for a given key. * * If the key is not found, then throw an Exception. * * @task read */ public static function getEnvConfig($key) { if (!self::$sourceStack) { throw new Exception( pht( 'Trying to read configuration "%s" before configuration has been '. 'initialized.', $key)); } if (isset(self::$cache[$key])) { return self::$cache[$key]; } if (array_key_exists($key, self::$cache)) { return self::$cache[$key]; } $result = self::$sourceStack->getKeys(array($key)); if (array_key_exists($key, $result)) { self::$cache[$key] = $result[$key]; return $result[$key]; } else { throw new Exception( pht( "No config value specified for key '%s'.", $key)); } } /** * Get the current configuration setting for a given key. If the key * does not exist, return a default value instead of throwing. This is * primarily useful for migrations involving keys which are slated for * removal. * * @task read */ public static function getEnvConfigIfExists($key, $default = null) { try { return self::getEnvConfig($key); } catch (Exception $ex) { return $default; } } /** * Get the fully-qualified URI for a path. * * @task read */ public static function getURI($path) { return rtrim(self::getAnyBaseURI(), '/').$path; } /** * Get the fully-qualified production URI for a path. * * @task read */ public static function getProductionURI($path) { // If we're passed a URI which already has a domain, simply return it // unmodified. In particular, files may have URIs which point to a CDN // domain. $uri = new PhutilURI($path); if ($uri->getDomain()) { return $path; } $production_domain = self::getEnvConfig('phabricator.production-uri'); if (!$production_domain) { $production_domain = self::getAnyBaseURI(); } return rtrim($production_domain, '/').$path; } public static function isSelfURI($raw_uri) { $uri = new PhutilURI($raw_uri); $host = $uri->getDomain(); if (!strlen($host)) { return false; } $host = phutil_utf8_strtolower($host); $self_map = self::getSelfURIMap(); return isset($self_map[$host]); } private static function getSelfURIMap() { $self_uris = array(); $self_uris[] = self::getProductionURI('/'); $self_uris[] = self::getURI('/'); $allowed_uris = self::getEnvConfig('phabricator.allowed-uris'); foreach ($allowed_uris as $allowed_uri) { $self_uris[] = $allowed_uri; } $self_map = array(); foreach ($self_uris as $self_uri) { $host = id(new PhutilURI($self_uri))->getDomain(); if (!strlen($host)) { continue; } $host = phutil_utf8_strtolower($host); $self_map[$host] = $host; } return $self_map; } /** * Get the fully-qualified production URI for a static resource path. * * @task read */ public static function getCDNURI($path) { $alt = self::getEnvConfig('security.alternate-file-domain'); if (!$alt) { $alt = self::getAnyBaseURI(); } $uri = new PhutilURI($alt); $uri->setPath($path); return (string)$uri; } /** * Get the fully-qualified production URI for a documentation resource. * * @task read */ public static function getDoclink($resource, $type = 'article') { - $uri = new PhutilURI('https://secure.phabricator.com/diviner/find/'); - $uri->setQueryParam('name', $resource); - $uri->setQueryParam('type', $type); - $uri->setQueryParam('jump', true); - return (string)$uri; + $params = array( + 'name' => $resource, + 'type' => $type, + 'jump' => true, + ); + + $uri = new PhutilURI( + 'https://secure.phabricator.com/diviner/find/', + $params); + + return phutil_string_cast($uri); } /** * Build a concrete object from a configuration key. * * @task read */ public static function newObjectFromConfig($key, $args = array()) { $class = self::getEnvConfig($key); return newv($class, $args); } public static function getAnyBaseURI() { $base_uri = self::getEnvConfig('phabricator.base-uri'); if (!$base_uri) { $base_uri = self::getRequestBaseURI(); } if (!$base_uri) { throw new Exception( pht( "Define '%s' in your configuration to continue.", 'phabricator.base-uri')); } return $base_uri; } public static function getRequestBaseURI() { return self::$requestBaseURI; } public static function setRequestBaseURI($uri) { self::$requestBaseURI = $uri; } public static function isReadOnly() { if (self::$readOnly !== null) { return self::$readOnly; } return self::getEnvConfig('cluster.read-only'); } public static function setReadOnly($read_only, $reason) { self::$readOnly = $read_only; self::$readOnlyReason = $reason; } public static function getReadOnlyMessage() { $reason = self::getReadOnlyReason(); switch ($reason) { case self::READONLY_MASTERLESS: return pht( 'Phabricator is in read-only mode (no writable database '. 'is configured).'); case self::READONLY_UNREACHABLE: return pht( 'Phabricator is in read-only mode (unreachable master).'); case self::READONLY_SEVERED: return pht( 'Phabricator is in read-only mode (major interruption).'); } return pht('Phabricator is in read-only mode.'); } public static function getReadOnlyURI() { return urisprintf( '/readonly/%s/', self::getReadOnlyReason()); } public static function getReadOnlyReason() { if (!self::isReadOnly()) { return null; } if (self::$readOnlyReason !== null) { return self::$readOnlyReason; } return self::READONLY_CONFIG; } /* -( Unit Test Support )-------------------------------------------------- */ /** * @task test */ public static function beginScopedEnv() { return new PhabricatorScopedEnv(self::pushTestEnvironment()); } /** * @task test */ private static function pushTestEnvironment() { self::dropConfigCache(); $source = new PhabricatorConfigDictionarySource(array()); self::$sourceStack->pushSource($source); return spl_object_hash($source); } /** * @task test */ public static function popTestEnvironment($key) { self::dropConfigCache(); $source = self::$sourceStack->popSource(); $stack_key = spl_object_hash($source); if ($stack_key !== $key) { self::$sourceStack->pushSource($source); throw new Exception( pht( 'Scoped environments were destroyed in a different order than they '. 'were initialized.')); } } /* -( URI Validation )----------------------------------------------------- */ /** * Detect if a URI satisfies either @{method:isValidLocalURIForLink} or * @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the * URI of some other resource which has a valid protocol. This rejects * garbage URIs and URIs with protocols which do not appear in the * `uri.allowed-protocols` configuration, notably 'javascript:' URIs. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to put in an "href" link attribute. * * @param string URI to test. * @return bool True if the URI identifies a web resource. * @task uri */ public static function isValidURIForLink($uri) { return self::isValidLocalURIForLink($uri) || self::isValidRemoteURIForLink($uri); } /** * Detect if a URI identifies some page on this server. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to issue a "Location:" redirect to. * * @param string URI to test. * @return bool True if the URI identifies a local page. * @task uri */ public static function isValidLocalURIForLink($uri) { $uri = (string)$uri; if (!strlen($uri)) { return false; } if (preg_match('/\s/', $uri)) { // PHP hasn't been vulnerable to header injection attacks for a bunch of // years, but we can safely reject these anyway since they're never valid. return false; } // Chrome (at a minimum) interprets backslashes in Location headers and the // URL bar as forward slashes. This is probably intended to reduce user // error caused by confusion over which key is "forward slash" vs "back // slash". // // However, it means a URI like "/\evil.com" is interpreted like // "//evil.com", which is a protocol relative remote URI. // // Since we currently never generate URIs with backslashes in them, reject // these unconditionally rather than trying to figure out how browsers will // interpret them. if (preg_match('/\\\\/', $uri)) { return false; } // Valid URIs must begin with '/', followed by the end of the string or some // other non-'/' character. This rejects protocol-relative URIs like // "//evil.com/evil_stuff/". return (bool)preg_match('@^/([^/]|$)@', $uri); } /** * Detect if a URI identifies some valid linkable remote resource. * * @param string URI to test. * @return bool True if a URI identifies a remote resource with an allowed * protocol. * @task uri */ public static function isValidRemoteURIForLink($uri) { try { self::requireValidRemoteURIForLink($uri); return true; } catch (Exception $ex) { return false; } } /** * Detect if a URI identifies a valid linkable remote resource, throwing a * detailed message if it does not. * * A valid linkable remote resource can be safely linked or redirected to. * This is primarily a protocol whitelist check. * * @param string URI to test. * @return void * @task uri */ public static function requireValidRemoteURIForLink($raw_uri) { $uri = new PhutilURI($raw_uri); $proto = $uri->getProtocol(); if (!strlen($proto)) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must specify a protocol.', $raw_uri)); } $protocols = self::getEnvConfig('uri.allowed-protocols'); if (!isset($protocols[$proto])) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must use one of these protocols: %s.', $raw_uri, implode(', ', array_keys($protocols)))); } $domain = $uri->getDomain(); if (!strlen($domain)) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must specify a domain.', $raw_uri)); } } /** * Detect if a URI identifies a valid fetchable remote resource. * * @param string URI to test. * @param list Allowed protocols. * @return bool True if the URI is a valid fetchable remote resource. * @task uri */ public static function isValidRemoteURIForFetch($uri, array $protocols) { try { self::requireValidRemoteURIForFetch($uri, $protocols); return true; } catch (Exception $ex) { return false; } } /** * Detect if a URI identifies a valid fetchable remote resource, throwing * a detailed message if it does not. * * A valid fetchable remote resource can be safely fetched using a request * originating on this server. This is a primarily an address check against * the outbound address blacklist. * * @param string URI to test. * @param list Allowed protocols. * @return pair Pre-resolved URI and domain. * @task uri */ public static function requireValidRemoteURIForFetch( $raw_uri, array $protocols) { $uri = new PhutilURI($raw_uri); $proto = $uri->getProtocol(); if (!strlen($proto)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must specify a protocol.', $raw_uri)); } $protocols = array_fuse($protocols); if (!isset($protocols[$proto])) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must use one of these protocols: %s.', $raw_uri, implode(', ', array_keys($protocols)))); } $domain = $uri->getDomain(); if (!strlen($domain)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must specify a domain.', $raw_uri)); } $addresses = gethostbynamel($domain); if (!$addresses) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. The domain "%s" could '. 'not be resolved.', $raw_uri, $domain)); } foreach ($addresses as $address) { if (self::isBlacklistedOutboundAddress($address)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. The domain "%s" '. 'resolves to the address "%s", which is blacklisted for '. 'outbound requests.', $raw_uri, $domain, $address)); } } $resolved_uri = clone $uri; $resolved_uri->setDomain(head($addresses)); return array($resolved_uri, $domain); } /** * Determine if an IP address is in the outbound address blacklist. * * @param string IP address. * @return bool True if the address is blacklisted. */ public static function isBlacklistedOutboundAddress($address) { $blacklist = self::getEnvConfig('security.outbound-blacklist'); return PhutilCIDRList::newList($blacklist)->containsAddress($address); } public static function isClusterRemoteAddress() { $cluster_addresses = self::getEnvConfig('cluster.addresses'); if (!$cluster_addresses) { return false; } $address = self::getRemoteAddress(); if (!$address) { throw new Exception( pht( 'Unable to test remote address against cluster whitelist: '. 'REMOTE_ADDR is not defined or not valid.')); } return self::isClusterAddress($address); } public static function isClusterAddress($address) { $cluster_addresses = self::getEnvConfig('cluster.addresses'); if (!$cluster_addresses) { throw new Exception( pht( 'Phabricator is not configured to serve cluster requests. '. 'Set `cluster.addresses` in the configuration to whitelist '. 'cluster hosts before sending requests that use a cluster '. 'authentication mechanism.')); } return PhutilCIDRList::newList($cluster_addresses) ->containsAddress($address); } public static function getRemoteAddress() { $address = idx($_SERVER, 'REMOTE_ADDR'); if (!$address) { return null; } try { return PhutilIPAddress::newAddress($address); } catch (Exception $ex) { return null; } } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ public static function envConfigExists($key) { return array_key_exists($key, self::$sourceStack->getKeys(array($key))); } /** * @task internal */ public static function getAllConfigKeys() { return self::$sourceStack->getAllKeys(); } public static function getConfigSourceStack() { return self::$sourceStack; } /** * @task internal */ public static function overrideTestEnvConfig($stack_key, $key, $value) { $tmp = array(); // If we don't have the right key, we'll throw when popping the last // source off the stack. do { $source = self::$sourceStack->popSource(); array_unshift($tmp, $source); if (spl_object_hash($source) == $stack_key) { $source->setKeys(array($key => $value)); break; } } while (true); foreach ($tmp as $source) { self::$sourceStack->pushSource($source); } self::dropConfigCache(); } private static function dropConfigCache() { self::$cache = array(); } private static function resetUmask() { // Reset the umask to the common standard umask. The umask controls default // permissions when files are created and propagates to subprocesses. // "022" is the most common umask, but sometimes it is set to something // unusual by the calling environment. // Since various things rely on this umask to work properly and we are // not aware of any legitimate reasons to adjust it, unconditionally // normalize it until such reasons arise. See T7475 for discussion. umask(022); } /** * Get the path to an empty directory which is readable by all of the system * user accounts that Phabricator acts as. * * In some cases, a binary needs some valid HOME or CWD to continue, but not * all user accounts have valid home directories and even if they do they * may not be readable after a `sudo` operation. * * @return string Path to an empty directory suitable for use as a CWD. */ public static function getEmptyCWD() { $root = dirname(phutil_get_library_root('phabricator')); return $root.'/support/empty/'; } } diff --git a/src/view/phui/PHUITimelineView.php b/src/view/phui/PHUITimelineView.php index d0e942f461..d8c06d6a9f 100644 --- a/src/view/phui/PHUITimelineView.php +++ b/src/view/phui/PHUITimelineView.php @@ -1,283 +1,283 @@ id = $id; return $this; } public function setShouldTerminate($term) { $this->shouldTerminate = $term; return $this; } public function setShouldAddSpacers($bool) { $this->shouldAddSpacers = $bool; return $this; } public function setPager(AphrontCursorPagerView $pager) { $this->pager = $pager; return $this; } public function getPager() { return $this->pager; } public function addEvent(PHUITimelineEventView $event) { $this->events[] = $event; return $this; } public function setViewData(array $data) { $this->viewData = $data; return $this; } public function getViewData() { return $this->viewData; } public function setQuoteTargetID($quote_target_id) { $this->quoteTargetID = $quote_target_id; return $this; } public function getQuoteTargetID() { return $this->quoteTargetID; } public function setQuoteRef($quote_ref) { $this->quoteRef = $quote_ref; return $this; } public function getQuoteRef() { return $this->quoteRef; } public function render() { if ($this->getPager()) { if ($this->id === null) { $this->id = celerity_generate_unique_node_id(); } Javelin::initBehavior( 'phabricator-show-older-transactions', array( 'timelineID' => $this->id, 'viewData' => $this->getViewData(), )); } $events = $this->buildEvents(); return phutil_tag( 'div', array( 'class' => 'phui-timeline-view', 'id' => $this->id, ), array( phutil_tag( 'h3', array( 'class' => 'aural-only', ), pht('Event Timeline')), $events, )); } public function buildEvents() { require_celerity_resource('phui-timeline-view-css'); $spacer = self::renderSpacer(); // Track why we're hiding older results. $hide_reason = null; $hide = array(); $show = array(); // Bucket timeline events into events we'll hide by default (because they // predate your most recent interaction with the object) and events we'll // show by default. foreach ($this->events as $event) { if ($event->getHideByDefault()) { $hide[] = $event; } else { $show[] = $event; } } // If you've never interacted with the object, all the events will be shown // by default. We may still need to paginate if there are a large number // of events. $more = (bool)$hide; if ($more) { $hide_reason = 'comment'; } if ($this->getPager()) { if ($this->getPager()->getHasMoreResults()) { if (!$more) { $hide_reason = 'limit'; } $more = true; } } $events = array(); if ($more && $this->getPager()) { switch ($hide_reason) { case 'comment': $hide_help = pht( 'Changes from before your most recent comment are hidden.'); break; case 'limit': default: $hide_help = pht( 'There are a very large number of changes, so older changes are '. 'hidden.'); break; } $uri = $this->getPager()->getNextPageURI(); - $uri->setQueryParam('quoteTargetID', $this->getQuoteTargetID()); - $uri->setQueryParam('quoteRef', $this->getQuoteRef()); + $uri->replaceQueryParam('quoteTargetID', $this->getQuoteTargetID()); + $uri->replaceQueryParam('quoteRef', $this->getQuoteRef()); $events[] = javelin_tag( 'div', array( 'sigil' => 'show-older-block', 'class' => 'phui-timeline-older-transactions-are-hidden', ), array( $hide_help, ' ', javelin_tag( 'a', array( 'href' => (string)$uri, 'mustcapture' => true, 'sigil' => 'show-older-link', ), pht('Show Older Changes')), )); if ($show) { $events[] = $spacer; } } if ($show) { $this->prepareBadgeData($show); $events[] = phutil_implode_html($spacer, $show); } if ($events) { if ($this->shouldAddSpacers) { $events = array($spacer, $events, $spacer); } } else { $events = array($spacer); } if ($this->shouldTerminate) { $events[] = self::renderEnder(); } return $events; } public static function renderSpacer() { return phutil_tag( 'div', array( 'class' => 'phui-timeline-event-view '. 'phui-timeline-spacer', ), ''); } public static function renderEnder() { return phutil_tag( 'div', array( 'class' => 'phui-timeline-event-view '. 'the-worlds-end', ), ''); } private function prepareBadgeData(array $events) { assert_instances_of($events, 'PHUITimelineEventView'); $viewer = $this->getUser(); $can_use_badges = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorBadgesApplication', $viewer); if (!$can_use_badges) { return; } $user_phid_type = PhabricatorPeopleUserPHIDType::TYPECONST; $user_phids = array(); foreach ($events as $key => $event) { $author_phid = $event->getAuthorPHID(); if (!$author_phid) { unset($events[$key]); continue; } if (phid_get_type($author_phid) != $user_phid_type) { // This is likely an application actor, like "Herald" or "Harbormaster". // They can't have badges. unset($events[$key]); continue; } $user_phids[$author_phid] = $author_phid; } if (!$user_phids) { return; } $users = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs($user_phids) ->needBadgeAwards(true) ->execute(); $users = mpull($users, null, 'getPHID'); foreach ($events as $event) { $user_phid = $event->getAuthorPHID(); if (!array_key_exists($user_phid, $users)) { continue; } $badges = $users[$user_phid]->getRecentBadgeAwards(); foreach ($badges as $badge) { $badge_view = id(new PHUIBadgeMiniView()) ->setIcon($badge['icon']) ->setQuality($badge['quality']) ->setHeader($badge['name']) ->setTipDirection('E') ->setHref('/badges/view/'.$badge['id'].'/'); $event->addBadge($badge_view); } } } }