diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index 197e5a437c..ab6e0d9820 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -1,763 +1,763 @@ <?php /** * @task data Accessing Request Data * @task cookie Managing Cookies * @task cluster Working With a Phabricator Cluster */ final class AphrontRequest { // NOTE: These magic request-type parameters are automatically included in // certain requests (e.g., by phabricator_form(), JX.Request, // JX.Workflow, and ConduitClient) and help us figure out what sort of // response the client expects. const TYPE_AJAX = '__ajax__'; const TYPE_FORM = '__form__'; const TYPE_CONDUIT = '__conduit__'; const TYPE_WORKFLOW = '__wflow__'; const TYPE_CONTINUE = '__continue__'; const TYPE_PREVIEW = '__preview__'; const TYPE_HISEC = '__hisec__'; const TYPE_QUICKSAND = '__quicksand__'; private $host; private $path; private $requestData; private $user; private $applicationConfiguration; private $uriData; final public function __construct($host, $path) { $this->host = $host; $this->path = $path; } final public function setURIMap(array $uri_data) { $this->uriData = $uri_data; return $this; } final public function getURIMap() { return $this->uriData; } final public function getURIData($key, $default = null) { return idx($this->uriData, $key, $default); } final public function setApplicationConfiguration( $application_configuration) { $this->applicationConfiguration = $application_configuration; return $this; } final public function getApplicationConfiguration() { return $this->applicationConfiguration; } final public function setPath($path) { $this->path = $path; return $this; } final public function getPath() { return $this->path; } final public function getHost() { // The "Host" header may include a port number, or may be a malicious // header in the form "realdomain.com:ignored@evil.com". Invoke the full // parser to extract the real domain correctly. See here for coverage of // a similar issue in Django: // // https://www.djangoproject.com/weblog/2012/oct/17/security/ $uri = new PhutilURI('http://'.$this->host); return $uri->getDomain(); } /* -( Accessing Request Data )--------------------------------------------- */ /** * @task data */ final public function setRequestData(array $request_data) { $this->requestData = $request_data; return $this; } /** * @task data */ final public function getRequestData() { return $this->requestData; } /** * @task data */ final public function getInt($name, $default = null) { if (isset($this->requestData[$name])) { return (int)$this->requestData[$name]; } else { return $default; } } /** * @task data */ final 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 */ final 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 */ final 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 */ final 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 */ final public function getExists($name) { return array_key_exists($name, $this->requestData); } final public function getFileExists($name) { return isset($_FILES[$name]) && (idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE); } final public function isHTTPGet() { return ($_SERVER['REQUEST_METHOD'] == 'GET'); } final public function isHTTPPost() { return ($_SERVER['REQUEST_METHOD'] == 'POST'); } final public function isAjax() { return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand(); } final public function isWorkflow() { return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand(); } final public function isQuicksand() { return $this->getExists(self::TYPE_QUICKSAND); } final public function isConduit() { return $this->getExists(self::TYPE_CONDUIT); } public static function getCSRFTokenName() { return '__csrf__'; } public static function getCSRFHeaderName() { return 'X-Phabricator-Csrf'; } final public function validateCSRF() { $token_name = self::getCSRFTokenName(); $token = $this->getStr($token_name); // No token in the request, check the HTTP header which is added for Ajax // requests. if (empty($token)) { $token = self::getHTTPHeader(self::getCSRFHeaderName()); } $valid = $this->getUser()->validateCSRFToken($token); if (!$valid) { // Add some diagnostic details so we can figure out if some CSRF issues // are JS problems or people accessing Ajax URIs directly with their // browsers. $more_info = array(); if ($this->isAjax()) { $more_info[] = pht('This was an Ajax request.'); } else { $more_info[] = pht('This was a Web request.'); } if ($token) { $more_info[] = pht('This request had an invalid CSRF token.'); } else { $more_info[] = pht('This request had no CSRF token.'); } // Give a more detailed explanation of how to avoid the exception // in developer mode. if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { // TODO: Clean this up, see T1921. $more_info[] = "To avoid this error, use phabricator_form() to construct forms. ". "If you are already using phabricator_form(), make sure the form ". "'action' uses a relative URI (i.e., begins with a '/'). Forms ". "using absolute URIs do not include CSRF tokens, to prevent ". "leaking tokens to external sites.\n\n". "If this page performs writes which do not require CSRF ". "protection (usually, filling caches or logging), you can use ". "AphrontWriteGuard::beginScopedUnguardedWrites() to temporarily ". "bypass CSRF protection while writing. You should use this only ". "for writes which can not be protected with normal CSRF ". "mechanisms.\n\n". "Some UI elements (like PhabricatorActionListView) also have ". "methods which will allow you to render links as forms (like ". "setRenderAsForm(true))."; } // This should only be able to happen if you load a form, pull your // internet for 6 hours, and then reconnect and immediately submit, // but give the user some indication of what happened since the workflow // is incredibly confusing otherwise. throw new AphrontCSRFException( pht( "You are trying to save some data to Phabricator, but the request ". "your browser made included an incorrect token. Reload the page ". "and try again. You may need to clear your cookies.\n\n%s", implode("\n", $more_info))); } return true; } final public function isFormPost() { $post = $this->getExists(self::TYPE_FORM) && !$this->getExists(self::TYPE_HISEC) && $this->isHTTPPost(); if (!$post) { return false; } return $this->validateCSRF(); } final public function isFormOrHisecPost() { $post = $this->getExists(self::TYPE_FORM) && $this->isHTTPPost(); if (!$post) { return false; } return $this->validateCSRF(); } final public function setCookiePrefix($prefix) { $this->cookiePrefix = $prefix; return $this; } final private function getPrefixedCookieName($name) { if (strlen($this->cookiePrefix)) { return $this->cookiePrefix.'_'.$name; } else { return $name; } } final 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; } final 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 */ final 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 */ final 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 */ final 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 */ final private function setCookieWithExpiration( $name, $value, $expire) { $is_secure = false; $base_domain_uri = $this->getCookieDomainURI(); if (!$base_domain_uri) { $configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); $accessed_as = $this->getHost(); throw new Exception( pht( 'This Phabricator install is configured as "%s", but you are '. 'using the domain name "%s" to access a page which is trying to '. 'set a cookie. Acccess Phabricator on the configured primary '. 'domain or a configured alternate domain. Phabricator will not '. 'set cookies on other domains for security reasons.', $configured_as, $accessed_as)); } $base_domain = $base_domain_uri->getDomain(); $is_secure = ($base_domain_uri->getProtocol() == 'https'); $name = $this->getPrefixedCookieName($name); if (php_sapi_name() == 'cli') { // Do nothing, to avoid triggering "Cannot modify header information" // warnings. // TODO: This is effectively a test for whether we're running in a unit // test or not. Move this actual call to HTTPSink? } else { setcookie( $name, $value, $expire, $path = '/', $base_domain, $is_secure, $http_only = true); } $_COOKIE[$name] = $value; return $this; } final public function setUser($user) { $this->user = $user; return $this; } final public function getUser() { return $this->user; } final public function getViewer() { return $this->user; } final public function getRequestURI() { $get = $_GET; unset($get['__path__']); $path = phutil_escape_uri($this->getPath()); return id(new PhutilURI($path))->setQueryParams($get); } final public function isDialogFormPost() { return $this->isFormPost() && $this->getStr('__dialog__'); } final public function getRemoteAddr() { return $_SERVER['REMOTE_ADDR']; } public function isHTTPS() { if (empty($_SERVER['HTTPS'])) { return false; } if (!strcasecmp($_SERVER['HTTPS'], 'off')) { return false; } return true; } public function isContinueRequest() { return $this->isFormPost() && $this->getStr('__continue__'); } public function isPreviewRequest() { return $this->isFormPost() && $this->getStr('__preview__'); } /** * Get application request parameters in a flattened form suitable for * inclusion in an HTTP request, excluding parameters with special meanings. * This is primarily useful if you want to ask the user for more input and * then resubmit their request. * * @return dict<string, string> Original request parameters. */ public function getPassthroughRequestParameters($include_quicksand = false) { return self::flattenData( $this->getPassthroughRequestData($include_quicksand)); } /** * Get request data other than "magic" parameters. * * @return dict<string, wild> Request data, with magic filtered out. */ public function getPassthroughRequestData($include_quicksand = false) { $data = $this->getRequestData(); // Remove magic parameters like __dialog__ and __ajax__. foreach ($data as $key => $value) { if ($include_quicksand && $key == self::TYPE_QUICKSAND) { continue; } if (!strncmp($key, '__', 2)) { unset($data[$key]); } } return $data; } /** * Flatten an array of key-value pairs (possibly including arrays as values) * into a list of key-value pairs suitable for submitting via HTTP request * (with arrays flattened). * * @param dict<string, wild> Data to flatten. * @return dict<string, string> Flat data suitable for inclusion in an HTTP * request. */ public static function flattenData(array $data) { $result = array(); foreach ($data as $key => $value) { if (is_array($value)) { foreach (self::flattenData($value) as $fkey => $fvalue) { $fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1); $result[$key.$fkey] = $fvalue; } } else { $result[$key] = (string)$value; } } ksort($result); return $result; } /** * Read the value of an HTTP header from `$_SERVER`, or a similar datasource. * * This function accepts a canonical header name, like `"Accept-Encoding"`, * and looks up the appropriate value in `$_SERVER` (in this case, * `"HTTP_ACCEPT_ENCODING"`). * * @param string Canonical header name, like `"Accept-Encoding"`. * @param wild Default value to return if header is not present. * @param array? Read this instead of `$_SERVER`. * @return string|wild Header value if present, or `$default` if not. */ public static function getHTTPHeader($name, $default = null, $data = null) { // PHP mangles HTTP headers by uppercasing them and replacing hyphens with // underscores, then prepending 'HTTP_'. $php_index = strtoupper($name); $php_index = str_replace('-', '_', $php_index); $try_names = array(); $try_names[] = 'HTTP_'.$php_index; if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') { // These headers may be available under alternate names. See // http://www.php.net/manual/en/reserved.variables.server.php#110763 $try_names[] = $php_index; } if ($data === null) { $data = $_SERVER; } foreach ($try_names as $try_name) { if (array_key_exists($try_name, $data)) { return $data[$try_name]; } } return $default; } /* -( Working With a Phabricator Cluster )--------------------------------- */ /** * Is this a proxied request originating from within the Phabricator cluster? * * IMPORTANT: This means the request is dangerous! * * These requests are **more dangerous** than normal requests (they can not * be safely proxied, because proxying them may cause a loop). Cluster * requests are not guaranteed to come from a trusted source, and should * never be treated as safer than normal requests. They are strictly less * safe. */ public function isProxiedClusterRequest() { - return (bool)AphrontRequest::getHTTPHeader('X-Phabricator-Cluster'); + return (bool)self::getHTTPHeader('X-Phabricator-Cluster'); } /** * Build a new @{class:HTTPSFuture} which proxies this request to another * node in the cluster. * * IMPORTANT: This is very dangerous! * * The future forwards authentication information present in the request. * Proxied requests must only be sent to trusted hosts. (We attempt to * enforce this.) * * This is not a general-purpose proxying method; it is a specialized * method with niche applications and severe security implications. * * @param string URI identifying the host we are proxying the request to. * @return HTTPSFuture New proxy future. * * @phutil-external-symbol class PhabricatorStartup */ public function newClusterProxyFuture($uri) { $uri = new PhutilURI($uri); $domain = $uri->getDomain(); $ip = gethostbyname($domain); if (!$ip) { throw new Exception( pht( 'Unable to resolve domain "%s"!', $domain)); } if (!PhabricatorEnv::isClusterAddress($ip)) { throw new Exception( pht( 'Refusing to proxy a request to IP address ("%s") which is not '. 'in the cluster address block (this address was derived by '. 'resolving the domain "%s").', $ip, $domain)); } $uri->setPath($this->getPath()); $uri->setQueryParams(self::flattenData($_GET)); $input = PhabricatorStartup::getRawInput(); $future = id(new HTTPSFuture($uri)) ->addHeader('Host', self::getHost()) ->addHeader('X-Phabricator-Cluster', true) ->setMethod($_SERVER['REQUEST_METHOD']) ->write($input); if (isset($_SERVER['PHP_AUTH_USER'])) { $future->setHTTPBasicAuthCredentials( $_SERVER['PHP_AUTH_USER'], new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', ''))); } $headers = array(); $seen = array(); // NOTE: apache_request_headers() might provide a nicer way to do this, // but isn't available under FCGI until PHP 5.4.0. foreach ($_SERVER as $key => $value) { if (preg_match('/^HTTP_/', $key)) { // Unmangle the header as best we can. $key = str_replace('_', ' ', $key); $key = strtolower($key); $key = ucwords($key); $key = str_replace(' ', '-', $key); $headers[] = array($key, $value); $seen[$key] = true; } } // In some situations, this may not be mapped into the HTTP_X constants. // CONTENT_LENGTH is similarly affected, but we trust cURL to take care // of that if it matters, since we're handing off a request body. if (empty($seen['Content-Type'])) { if (isset($_SERVER['CONTENT_TYPE'])) { $headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']); } } foreach ($headers as $header) { list($key, $value) = $header; switch ($key) { case 'Host': case 'Authorization': // Don't forward these headers, we've already handled them elsewhere. unset($headers[$key]); break; default: break; } } foreach ($headers as $header) { list($key, $value) = $header; $future->addHeader($key, $value); } return $future; } } diff --git a/src/aphront/response/AphrontProxyResponse.php b/src/aphront/response/AphrontProxyResponse.php index 7e27ea2b1b..175f2d2c53 100644 --- a/src/aphront/response/AphrontProxyResponse.php +++ b/src/aphront/response/AphrontProxyResponse.php @@ -1,71 +1,74 @@ <?php /** * Base class for responses which augment other types of responses. For example, * a response might be substantially an Ajax response, but add structure to the * response content. It can do this by extending @{class:AphrontProxyResponse}, * instantiating an @{class:AphrontAjaxResponse} in @{method:buildProxy}, and * then constructing a real @{class:AphrontAjaxResponse} in * @{method:reduceProxyResponse}. */ abstract class AphrontProxyResponse extends AphrontResponse { private $proxy; protected function getProxy() { if (!$this->proxy) { $this->proxy = $this->buildProxy(); } return $this->proxy; } public function setRequest($request) { $this->getProxy()->setRequest($request); return $this; } public function getRequest() { return $this->getProxy()->getRequest(); } public function getHeaders() { return $this->getProxy()->getHeaders(); } public function setCacheDurationInSeconds($duration) { $this->getProxy()->setCacheDurationInSeconds($duration); return $this; } public function setLastModified($epoch_timestamp) { $this->getProxy()->setLastModified($epoch_timestamp); return $this; } public function setHTTPResponseCode($code) { $this->getProxy()->setHTTPResponseCode($code); return $this; } public function getHTTPResponseCode() { return $this->getProxy()->getHTTPResponseCode(); } public function setFrameable($frameable) { $this->getProxy()->setFrameable($frameable); return $this; } public function getCacheHeaders() { return $this->getProxy()->getCacheHeaders(); } abstract protected function buildProxy(); abstract public function reduceProxyResponse(); final public function buildResponseString() { throw new Exception( - 'AphrontProxyResponse must implement reduceProxyResponse().'); + pht( + '%s must implement %s.', + __CLASS__, + 'reduceProxyResponse()')); } } diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php index 0bbe22b5ec..b1e72f5529 100644 --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -1,233 +1,233 @@ <?php abstract class AphrontResponse { private $request; private $cacheable = false; private $responseCode = 200; private $lastModified = null; protected $frameable; public function setRequest($request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } /* -( Content )------------------------------------------------------------ */ public function getContentIterator() { return array($this->buildResponseString()); } public function buildResponseString() { throw new PhutilMethodNotImplementedException(); } /* -( Metadata )----------------------------------------------------------- */ public function getHeaders() { $headers = array(); if (!$this->frameable) { $headers[] = array('X-Frame-Options', 'Deny'); } if ($this->getRequest() && $this->getRequest()->isHTTPS()) { $hsts_key = 'security.strict-transport-security'; $use_hsts = PhabricatorEnv::getEnvConfig($hsts_key); if ($use_hsts) { $duration = phutil_units('365 days in seconds'); } else { // If HSTS has been disabled, tell browsers to turn it off. This may // not be effective because we can only disable it over a valid HTTPS // connection, but it best represents the configured intent. $duration = 0; } $headers[] = array( 'Strict-Transport-Security', "max-age={$duration}; includeSubdomains; preload", ); } return $headers; } public function setCacheDurationInSeconds($duration) { $this->cacheable = $duration; return $this; } public function setLastModified($epoch_timestamp) { $this->lastModified = $epoch_timestamp; return $this; } public function setHTTPResponseCode($code) { $this->responseCode = $code; return $this; } public function getHTTPResponseCode() { return $this->responseCode; } public function getHTTPResponseMessage() { switch ($this->getHTTPResponseCode()) { case 100: return 'Continue'; case 101: return 'Switching Protocols'; case 200: return 'OK'; case 201: return 'Created'; case 202: return 'Accepted'; case 203: return 'Non-Authoritative Information'; case 204: return 'No Content'; case 205: return 'Reset Content'; case 206: return 'Partial Content'; case 300: return 'Multiple Choices'; case 301: return 'Moved Permanently'; case 302: return 'Found'; case 303: return 'See Other'; case 304: return 'Not Modified'; case 305: return 'Use Proxy'; case 306: return 'Switch Proxy'; case 307: return 'Temporary Redirect'; case 400: return 'Bad Request'; case 401: return 'Unauthorized'; case 402: return 'Payment Required'; case 403: return 'Forbidden'; case 404: return 'Not Found'; case 405: return 'Method Not Allowed'; case 406: return 'Not Acceptable'; case 407: return 'Proxy Authentication Required'; case 408: return 'Request Timeout'; case 409: return 'Conflict'; case 410: return 'Gone'; case 411: return 'Length Required'; case 412: return 'Precondition Failed'; case 413: return 'Request Entity Too Large'; case 414: return 'Request-URI Too Long'; case 415: return 'Unsupported Media Type'; case 416: return 'Requested Range Not Satisfiable'; case 417: return 'Expectation Failed'; case 418: return "I'm a teapot"; case 426: return 'Upgrade Required'; case 500: return 'Internal Server Error'; case 501: return 'Not Implemented'; case 502: return 'Bad Gateway'; case 503: return 'Service Unavailable'; case 504: return 'Gateway Timeout'; case 505: return 'HTTP Version Not Supported'; default: return ''; } } public function setFrameable($frameable) { $this->frameable = $frameable; return $this; } public static function processValueForJSONEncoding(&$value, $key) { if ($value instanceof PhutilSafeHTMLProducerInterface) { // This renders the producer down to PhutilSafeHTML, which will then // be simplified into a string below. $value = hsprintf('%s', $value); } if ($value instanceof PhutilSafeHTML) { // TODO: Javelin supports implicity conversion of '__html' objects to // JX.HTML, but only for Ajax responses, not behaviors. Just leave things // as they are for now (where behaviors treat responses as HTML or plain // text at their discretion). $value = $value->getHTMLContent(); } } public static function encodeJSONForHTTPResponse(array $object) { array_walk_recursive( $object, - array('AphrontResponse', 'processValueForJSONEncoding')); + array(__CLASS__, 'processValueForJSONEncoding')); $response = json_encode($object); // Prevent content sniffing attacks by encoding "<" and ">", so browsers // won't try to execute the document as HTML even if they ignore // Content-Type and X-Content-Type-Options. See T865. $response = str_replace( array('<', '>'), array('\u003c', '\u003e'), $response); return $response; } protected function addJSONShield($json_response) { // Add a shield to prevent "JSON Hijacking" attacks where an attacker // requests a JSON response using a normal <script /> tag and then uses // Object.prototype.__defineSetter__() or similar to read response data. // This header causes the browser to loop infinitely instead of handing over // sensitive data. $shield = 'for (;;);'; $response = $shield.$json_response; return $response; } public function getCacheHeaders() { $headers = array(); if ($this->cacheable) { $headers[] = array( 'Expires', $this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable), ); } else { $headers[] = array( 'Cache-Control', 'private, no-cache, no-store, must-revalidate', ); $headers[] = array( 'Pragma', 'no-cache', ); $headers[] = array( 'Expires', 'Sat, 01 Jan 2000 00:00:00 GMT', ); } if ($this->lastModified) { $headers[] = array( 'Last-Modified', $this->formatEpochTimestampForHTTPHeader($this->lastModified), ); } // IE has a feature where it may override an explicit Content-Type // declaration by inferring a content type. This can be a security risk // and we always explicitly transmit the correct Content-Type header, so // prevent IE from using inferred content types. This only offers protection // on recent versions of IE; IE6/7 and Opera currently ignore this header. $headers[] = array('X-Content-Type-Options', 'nosniff'); return $headers; } private function formatEpochTimestampForHTTPHeader($epoch_timestamp) { return gmdate('D, d M Y H:i:s', $epoch_timestamp).' GMT'; } public function didCompleteWrite($aborted) { return; } } diff --git a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php index e1a4378ab3..78924905b9 100644 --- a/src/applications/audit/constants/PhabricatorAuditStatusConstants.php +++ b/src/applications/audit/constants/PhabricatorAuditStatusConstants.php @@ -1,97 +1,97 @@ <?php final class PhabricatorAuditStatusConstants { const NONE = ''; const AUDIT_NOT_REQUIRED = 'audit-not-required'; const AUDIT_REQUIRED = 'audit-required'; const CONCERNED = 'concerned'; const ACCEPTED = 'accepted'; const AUDIT_REQUESTED = 'requested'; const RESIGNED = 'resigned'; const CLOSED = 'closed'; const CC = 'cc'; public static function getStatusNameMap() { $map = array( self::NONE => pht('Not Applicable'), self::AUDIT_NOT_REQUIRED => pht('Audit Not Required'), self::AUDIT_REQUIRED => pht('Audit Required'), self::CONCERNED => pht('Concern Raised'), self::ACCEPTED => pht('Accepted'), self::AUDIT_REQUESTED => pht('Audit Requested'), self::RESIGNED => pht('Resigned'), self::CLOSED => pht('Closed'), self::CC => pht("Was CC'd"), ); return $map; } public static function getStatusName($code) { return idx(self::getStatusNameMap(), $code, pht('Unknown')); } public static function getStatusColor($code) { switch ($code) { case self::CONCERNED: $color = 'red'; break; case self::AUDIT_REQUIRED: case self::AUDIT_REQUESTED: $color = 'orange'; break; case self::ACCEPTED: $color = 'green'; break; case self::AUDIT_NOT_REQUIRED: $color = 'blue'; break; case self::RESIGNED: case self::CLOSED: $color = 'dark'; break; default: $color = 'bluegrey'; break; } return $color; } public static function getStatusIcon($code) { switch ($code) { - case PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED: - case PhabricatorAuditStatusConstants::RESIGNED: + case self::AUDIT_NOT_REQUIRED: + case self::RESIGNED: $icon = PHUIStatusItemView::ICON_OPEN; break; - case PhabricatorAuditStatusConstants::AUDIT_REQUIRED: - case PhabricatorAuditStatusConstants::AUDIT_REQUESTED: + case self::AUDIT_REQUIRED: + case self::AUDIT_REQUESTED: $icon = PHUIStatusItemView::ICON_WARNING; break; - case PhabricatorAuditStatusConstants::CONCERNED: + case self::CONCERNED: $icon = PHUIStatusItemView::ICON_REJECT; break; - case PhabricatorAuditStatusConstants::ACCEPTED: - case PhabricatorAuditStatusConstants::CLOSED: + case self::ACCEPTED: + case self::CLOSED: $icon = PHUIStatusItemView::ICON_ACCEPT; break; default: $icon = PHUIStatusItemView::ICON_QUESTION; break; } return $icon; } public static function getOpenStatusConstants() { return array( self::AUDIT_REQUIRED, self::AUDIT_REQUESTED, self::CONCERNED, ); } public static function isOpenStatus($status) { return in_array($status, self::getOpenStatusConstants()); } } diff --git a/src/applications/audit/storage/PhabricatorAuditInlineComment.php b/src/applications/audit/storage/PhabricatorAuditInlineComment.php index 8c0aa78d5d..292e0274a5 100644 --- a/src/applications/audit/storage/PhabricatorAuditInlineComment.php +++ b/src/applications/audit/storage/PhabricatorAuditInlineComment.php @@ -1,346 +1,346 @@ <?php final class PhabricatorAuditInlineComment implements PhabricatorInlineCommentInterface { private $proxy; private $syntheticAuthor; private $isGhost; public function __construct() { $this->proxy = new PhabricatorAuditTransactionComment(); } public function __clone() { $this->proxy = clone $this->proxy; } public function getTransactionPHID() { return $this->proxy->getTransactionPHID(); } public function getTransactionComment() { return $this->proxy; } public function getTransactionCommentForSave() { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_LEGACY, array()); $this->proxy ->setViewPolicy('public') ->setEditPolicy($this->getAuthorPHID()) ->setContentSource($content_source) ->setCommentVersion(1); return $this->proxy; } public static function loadID($id) { $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere( 'id = %d', $id); if (!$inlines) { return null; } return head(self::buildProxies($inlines)); } public static function loadPHID($phid) { $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere( 'phid = %s', $phid); if (!$inlines) { return null; } return head(self::buildProxies($inlines)); } public static function loadDraftComments( PhabricatorUser $viewer, $commit_phid) { $inlines = id(new DiffusionDiffInlineCommentQuery()) ->setViewer($viewer) ->withAuthorPHIDs(array($viewer->getPHID())) ->withCommitPHIDs(array($commit_phid)) ->withHasTransaction(false) ->withHasPath(true) ->withIsDeleted(false) ->needReplyToComments(true) ->execute(); return self::buildProxies($inlines); } public static function loadPublishedComments( PhabricatorUser $viewer, $commit_phid) { $inlines = id(new DiffusionDiffInlineCommentQuery()) ->setViewer($viewer) ->withCommitPHIDs(array($commit_phid)) ->withHasTransaction(true) ->withHasPath(true) ->execute(); return self::buildProxies($inlines); } public static function loadDraftAndPublishedComments( PhabricatorUser $viewer, $commit_phid, $path_id = null) { if ($path_id === null) { $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere( 'commitPHID = %s AND (transactionPHID IS NOT NULL OR authorPHID = %s) AND pathID IS NOT NULL', $commit_phid, $viewer->getPHID()); } else { $inlines = id(new PhabricatorAuditTransactionComment())->loadAllWhere( 'commitPHID = %s AND pathID = %d AND ((authorPHID = %s AND isDeleted = 0) OR transactionPHID IS NOT NULL)', $commit_phid, $path_id, $viewer->getPHID()); } return self::buildProxies($inlines); } private static function buildProxies(array $inlines) { $results = array(); foreach ($inlines as $key => $inline) { - $results[$key] = PhabricatorAuditInlineComment::newFromModernComment( + $results[$key] = self::newFromModernComment( $inline); } return $results; } public function setSyntheticAuthor($synthetic_author) { $this->syntheticAuthor = $synthetic_author; return $this; } public function getSyntheticAuthor() { return $this->syntheticAuthor; } public function openTransaction() { $this->proxy->openTransaction(); } public function saveTransaction() { $this->proxy->saveTransaction(); } public function save() { $this->getTransactionCommentForSave()->save(); return $this; } public function delete() { $this->proxy->delete(); return $this; } public function getID() { return $this->proxy->getID(); } public function getPHID() { return $this->proxy->getPHID(); } public static function newFromModernComment( PhabricatorAuditTransactionComment $comment) { $obj = new PhabricatorAuditInlineComment(); $obj->proxy = $comment; return $obj; } public function isCompatible(PhabricatorInlineCommentInterface $comment) { return ($this->getAuthorPHID() === $comment->getAuthorPHID()) && ($this->getSyntheticAuthor() === $comment->getSyntheticAuthor()) && ($this->getContent() === $comment->getContent()); } public function setContent($content) { $this->proxy->setContent($content); return $this; } public function getContent() { return $this->proxy->getContent(); } public function isDraft() { return !$this->proxy->getTransactionPHID(); } public function setPathID($id) { $this->proxy->setPathID($id); return $this; } public function getPathID() { return $this->proxy->getPathID(); } public function setIsNewFile($is_new) { $this->proxy->setIsNewFile($is_new); return $this; } public function getIsNewFile() { return $this->proxy->getIsNewFile(); } public function setLineNumber($number) { $this->proxy->setLineNumber($number); return $this; } public function getLineNumber() { return $this->proxy->getLineNumber(); } public function setLineLength($length) { $this->proxy->setLineLength($length); return $this; } public function getLineLength() { return $this->proxy->getLineLength(); } public function setCache($cache) { return $this; } public function getCache() { return null; } public function setAuthorPHID($phid) { $this->proxy->setAuthorPHID($phid); return $this; } public function getAuthorPHID() { return $this->proxy->getAuthorPHID(); } public function setCommitPHID($commit_phid) { $this->proxy->setCommitPHID($commit_phid); return $this; } public function getCommitPHID() { return $this->proxy->getCommitPHID(); } // When setting a comment ID, we also generate a phantom transaction PHID for // the future transaction. public function setAuditCommentID($id) { $this->proxy->setLegacyCommentID($id); $this->proxy->setTransactionPHID( PhabricatorPHID::generateNewPHID( PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST, PhabricatorRepositoryCommitPHIDType::TYPECONST)); return $this; } public function getAuditCommentID() { return $this->proxy->getLegacyCommentID(); } public function setChangesetID($id) { return $this->setPathID($id); } public function getChangesetID() { return $this->getPathID(); } public function setReplyToCommentPHID($phid) { $this->proxy->setReplyToCommentPHID($phid); return $this; } public function getReplyToCommentPHID() { return $this->proxy->getReplyToCommentPHID(); } public function setHasReplies($has_replies) { $this->proxy->setHasReplies($has_replies); return $this; } public function getHasReplies() { return $this->proxy->getHasReplies(); } public function setIsDeleted($is_deleted) { $this->proxy->setIsDeleted($is_deleted); return $this; } public function getIsDeleted() { return $this->proxy->getIsDeleted(); } public function setFixedState($state) { $this->proxy->setFixedState($state); return $this; } public function getFixedState() { return $this->proxy->getFixedState(); } public function setIsGhost($is_ghost) { $this->isGhost = $is_ghost; return $this; } public function getIsGhost() { return $this->isGhost; } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { return 'AI:'.$this->getID(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newDifferentialMarkupEngine(); } public function getMarkupText($field) { return $this->getContent(); } public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { // Only cache submitted comments. return ($this->getID() && $this->getAuditCommentID()); } } diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index d5237adc11..295d4db180 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -1,589 +1,589 @@ <?php /** * @task info Application Information * @task ui UI Integration * @task uri URI Routing * @task mail Email integration * @task fact Fact Integration * @task meta Application Management */ abstract class PhabricatorApplication implements PhabricatorPolicyInterface { const MAX_STATUS_ITEMS = 100; const GROUP_CORE = 'core'; const GROUP_UTILITIES = 'util'; const GROUP_ADMIN = 'admin'; const GROUP_DEVELOPER = 'developer'; public static function getApplicationGroups() { return array( self::GROUP_CORE => pht('Core Applications'), self::GROUP_UTILITIES => pht('Utilities'), self::GROUP_ADMIN => pht('Administration'), self::GROUP_DEVELOPER => pht('Developer Tools'), ); } /* -( Application Information )-------------------------------------------- */ public abstract function getName(); public function getShortDescription() { return $this->getName().' Application'; } public function isInstalled() { if (!$this->canUninstall()) { return true; } $prototypes = PhabricatorEnv::getEnvConfig('phabricator.show-prototypes'); if (!$prototypes && $this->isPrototype()) { return false; } $uninstalled = PhabricatorEnv::getEnvConfig( 'phabricator.uninstalled-applications'); return empty($uninstalled[get_class($this)]); } public function isPrototype() { return false; } /** * Return `true` if this application should never appear in application lists * in the UI. Primarily intended for unit test applications or other * pseudo-applications. * * Few applications should be unlisted. For most applications, use * @{method:isLaunchable} to hide them from main launch views instead. * * @return bool True to remove application from UI lists. */ public function isUnlisted() { return false; } /** * Return `true` if this application is a normal application with a base * URI and a web interface. * * Launchable applications can be pinned to the home page, and show up in the * "Launcher" view of the Applications application. Making an application * unlauncahble prevents pinning and hides it from this view. * * Usually, an application should be marked unlaunchable if: * * - it is available on every page anyway (like search); or * - it does not have a web interface (like subscriptions); or * - it is still pre-release and being intentionally buried. * * To hide applications more completely, use @{method:isUnlisted}. * * @return bool True if the application is launchable. */ public function isLaunchable() { return true; } /** * Return `true` if this application should be pinned by default. * * Users who have not yet set preferences see a default list of applications. * * @param PhabricatorUser User viewing the pinned application list. * @return bool True if this application should be pinned by default. */ public function isPinnedByDefault(PhabricatorUser $viewer) { return false; } /** * Returns true if an application is first-party (developed by Phacility) * and false otherwise. * * @return bool True if this application is developed by Phacility. */ final public function isFirstParty() { $where = id(new ReflectionClass($this))->getFileName(); $root = phutil_get_library_root('phabricator'); if (!Filesystem::isDescendant($where, $root)) { return false; } if (Filesystem::isDescendant($where, $root.'/extensions')) { return false; } return true; } public function canUninstall() { return true; } public function getPHID() { return 'PHID-APPS-'.get_class($this); } public function getTypeaheadURI() { return $this->isLaunchable() ? $this->getBaseURI() : null; } public function getBaseURI() { return null; } public function getApplicationURI($path = '') { return $this->getBaseURI().ltrim($path, '/'); } public function getIconURI() { return null; } public function getFontIcon() { return 'fa-puzzle-piece'; } public function getApplicationOrder() { return PHP_INT_MAX; } public function getApplicationGroup() { return self::GROUP_CORE; } public function getTitleGlyph() { return null; } public function getHelpMenuItems(PhabricatorUser $viewer) { $items = array(); $articles = $this->getHelpDocumentationArticles($viewer); if ($articles) { $items[] = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_LABEL) ->setName(pht('%s Documentation', $this->getName())); foreach ($articles as $article) { $item = id(new PHUIListItemView()) ->setName($article['name']) ->setIcon('fa-book') ->setHref($article['href']); $items[] = $item; } } $command_specs = $this->getMailCommandObjects(); if ($command_specs) { $items[] = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_LABEL) ->setName(pht('Email Help')); foreach ($command_specs as $key => $spec) { $object = $spec['object']; $class = get_class($this); $href = '/applications/mailcommands/'.$class.'/'.$key.'/'; $item = id(new PHUIListItemView()) ->setName($spec['name']) ->setIcon('fa-envelope-o') ->setHref($href); $items[] = $item; } } return $items; } public function getHelpDocumentationArticles(PhabricatorUser $viewer) { return array(); } public function getOverview() { return null; } public function getEventListeners() { return array(); } public function getRemarkupRules() { return array(); } public function getQuicksandURIPatternBlacklist() { return array(); } public function getMailCommandObjects() { return array(); } /* -( URI Routing )-------------------------------------------------------- */ public function getRoutes() { return array(); } /* -( Email Integration )-------------------------------------------------- */ public function supportsEmailIntegration() { return false; } protected function getInboundEmailSupportLink() { return PhabricatorEnv::getDocLink('Configuring Inbound Email'); } public function getAppEmailBlurb() { throw new Exception('Not Implemented.'); } /* -( Fact Integration )--------------------------------------------------- */ public function getFactObjectsForAnalysis() { return array(); } /* -( UI Integration )----------------------------------------------------- */ /** * Render status elements (like "3 Waiting Reviews") for application list * views. These provide a way to alert users to new or pending action items * in applications. * * @param PhabricatorUser Viewing user. * @return list<PhabricatorApplicationStatusView> Application status elements. * @task ui */ public function loadStatus(PhabricatorUser $user) { return array(); } /** * @return string * @task ui */ public static function formatStatusCount( $count, $limit_string = '%s', $base_string = '%d') { if ($count == self::MAX_STATUS_ITEMS) { $count_str = pht($limit_string, ($count - 1).'+'); } else { $count_str = pht($base_string, $count); } return $count_str; } /** * You can provide an optional piece of flavor text for the application. This * is currently rendered in application launch views if the application has no * status elements. * * @return string|null Flavor text. * @task ui */ public function getFlavorText() { return null; } /** * Build items for the main menu. * * @param PhabricatorUser The viewing user. * @param AphrontController The current controller. May be null for special * pages like 404, exception handlers, etc. * @return list<PHUIListItemView> List of menu items. * @task ui */ public function buildMainMenuItems( PhabricatorUser $user, PhabricatorController $controller = null) { return array(); } /** * Build extra items for the main menu. Generally, this is used to render * static dropdowns. * * @param PhabricatorUser The viewing user. * @param AphrontController The current controller. May be null for special * pages like 404, exception handlers, etc. * @return view List of menu items. * @task ui */ public function buildMainMenuExtraNodes( PhabricatorUser $viewer, PhabricatorController $controller = null) { return array(); } /** * Build items for the "quick create" menu. * * @param PhabricatorUser The viewing user. * @return list<PHUIListItemView> List of menu items. */ public function getQuickCreateItems(PhabricatorUser $viewer) { return array(); } /* -( Application Management )--------------------------------------------- */ public static function getByClass($class_name) { $selected = null; - $applications = PhabricatorApplication::getAllApplications(); + $applications = self::getAllApplications(); foreach ($applications as $application) { if (get_class($application) == $class_name) { $selected = $application; break; } } if (!$selected) { throw new Exception("No application '{$class_name}'!"); } return $selected; } public static function getAllApplications() { static $applications; if ($applications === null) { $apps = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); // Reorder the applications into "application order". Notably, this // ensures their event handlers register in application order. $apps = msort($apps, 'getApplicationOrder'); $apps = mgroup($apps, 'getApplicationGroup'); $group_order = array_keys(self::getApplicationGroups()); $apps = array_select_keys($apps, $group_order) + $apps; $apps = array_mergev($apps); $applications = $apps; } return $applications; } public static function getAllInstalledApplications() { $all_applications = self::getAllApplications(); $apps = array(); foreach ($all_applications as $app) { if (!$app->isInstalled()) { continue; } $apps[] = $app; } return $apps; } /** * Determine if an application is installed, by application class name. * * To check if an application is installed //and// available to a particular * viewer, user @{method:isClassInstalledForViewer}. * * @param string Application class name. * @return bool True if the class is installed. * @task meta */ public static function isClassInstalled($class) { return self::getByClass($class)->isInstalled(); } /** * Determine if an application is installed and available to a viewer, by * application class name. * * To check if an application is installed at all, use * @{method:isClassInstalled}. * * @param string Application class name. * @param PhabricatorUser Viewing user. * @return bool True if the class is installed for the viewer. * @task meta */ public static function isClassInstalledForViewer( $class, PhabricatorUser $viewer) { if (!self::isClassInstalled($class)) { return false; } return PhabricatorPolicyFilter::hasCapability( $viewer, self::getByClass($class), PhabricatorPolicyCapability::CAN_VIEW); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array_merge( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ), array_keys($this->getCustomCapabilities())); } public function getPolicy($capability) { $default = $this->getCustomPolicySetting($capability); if ($default) { return $default; } switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_ADMIN; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'default', PhabricatorPolicies::POLICY_USER); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( Policies )----------------------------------------------------------- */ protected function getCustomCapabilities() { return array(); } private function getCustomPolicySetting($capability) { if (!$this->isCapabilityEditable($capability)) { return null; } $policy_locked = PhabricatorEnv::getEnvConfig('policy.locked'); if (isset($policy_locked[$capability])) { return $policy_locked[$capability]; } $config = PhabricatorEnv::getEnvConfig('phabricator.application-settings'); $app = idx($config, $this->getPHID()); if (!$app) { return null; } $policy = idx($app, 'policy'); if (!$policy) { return null; } return idx($policy, $capability); } private function getCustomCapabilitySpecification($capability) { $custom = $this->getCustomCapabilities(); if (!isset($custom[$capability])) { throw new Exception("Unknown capability '{$capability}'!"); } return $custom[$capability]; } public function getCapabilityLabel($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht('Can Use Application'); case PhabricatorPolicyCapability::CAN_EDIT: return pht('Can Configure Application'); } $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if ($capobj) { return $capobj->getCapabilityName(); } return null; } public function isCapabilityEditable($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->canUninstall(); case PhabricatorPolicyCapability::CAN_EDIT: return false; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'edit', true); } } public function getCapabilityCaption($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if (!$this->canUninstall()) { return pht( 'This application is required for Phabricator to operate, so all '. 'users must have access to it.'); } else { return null; } case PhabricatorPolicyCapability::CAN_EDIT: return null; default: $spec = $this->getCustomCapabilitySpecification($capability); return idx($spec, 'caption'); } } public function getApplicationSearchDocumentTypes() { return array(); } } diff --git a/src/applications/cache/PhabricatorCaches.php b/src/applications/cache/PhabricatorCaches.php index 37708457b7..4e3af7a6a4 100644 --- a/src/applications/cache/PhabricatorCaches.php +++ b/src/applications/cache/PhabricatorCaches.php @@ -1,347 +1,347 @@ <?php /** * @task immutable Immutable Cache * @task setup Setup Cache * @task compress Compression */ final class PhabricatorCaches { public static function getNamespace() { return PhabricatorEnv::getEnvConfig('phabricator.cache-namespace'); } private static function newStackFromCaches(array $caches) { $caches = self::addNamespaceToCaches($caches); $caches = self::addProfilerToCaches($caches); return id(new PhutilKeyValueCacheStack()) ->setCaches($caches); } /* -( Local Cache )-------------------------------------------------------- */ /** * Gets an immutable cache stack. * * This stack trades mutability away for improved performance. Normally, it is * APC + DB. * * In the general case with multiple web frontends, this stack can not be * cleared, so it is only appropriate for use if the value of a given key is * permanent and immutable. * * @return PhutilKeyValueCacheStack Best immutable stack available. * @task immutable */ public static function getImmutableCache() { static $cache; if (!$cache) { $caches = self::buildImmutableCaches(); $cache = self::newStackFromCaches($caches); } return $cache; } /** * Build the immutable cache stack. * * @return list<PhutilKeyValueCache> List of caches. * @task immutable */ private static function buildImmutableCaches() { $caches = array(); $apc = new PhutilAPCKeyValueCache(); if ($apc->isAvailable()) { $caches[] = $apc; } $caches[] = new PhabricatorKeyValueDatabaseCache(); return $caches; } /* -( Repository Graph Cache )--------------------------------------------- */ public static function getRepositoryGraphL1Cache() { static $cache; if (!$cache) { $caches = self::buildRepositoryGraphL1Caches(); $cache = self::newStackFromCaches($caches); } return $cache; } private static function buildRepositoryGraphL1Caches() { $caches = array(); $request = new PhutilInRequestKeyValueCache(); $request->setLimit(32); $caches[] = $request; $apc = new PhutilAPCKeyValueCache(); if ($apc->isAvailable()) { $caches[] = $apc; } return $caches; } public static function getRepositoryGraphL2Cache() { static $cache; if (!$cache) { $caches = self::buildRepositoryGraphL2Caches(); $cache = self::newStackFromCaches($caches); } return $cache; } private static function buildRepositoryGraphL2Caches() { $caches = array(); $caches[] = new PhabricatorKeyValueDatabaseCache(); return $caches; } /* -( Setup Cache )-------------------------------------------------------- */ /** * Highly specialized cache for performing setup checks. We use this cache * to determine if we need to run expensive setup checks when the page * loads. Without it, we would need to run these checks every time. * * Normally, this cache is just APC. In the absence of APC, this cache * degrades into a slow, quirky on-disk cache. * * NOTE: Do not use this cache for anything else! It is not a general-purpose * cache! * * @return PhutilKeyValueCacheStack Most qualified available cache stack. * @task setup */ public static function getSetupCache() { static $cache; if (!$cache) { $caches = self::buildSetupCaches(); $cache = self::newStackFromCaches($caches); } return $cache; } /** * @task setup */ private static function buildSetupCaches() { // In most cases, we should have APC. This is an ideal cache for our // purposes -- it's fast and empties on server restart. $apc = new PhutilAPCKeyValueCache(); if ($apc->isAvailable()) { return array($apc); } // If we don't have APC, build a poor approximation on disk. This is still // much better than nothing; some setup steps are quite slow. $disk_path = self::getSetupCacheDiskCachePath(); if ($disk_path) { $disk = new PhutilOnDiskKeyValueCache(); $disk->setCacheFile($disk_path); $disk->setWait(0.1); if ($disk->isAvailable()) { return array($disk); } } return array(); } /** * @task setup */ private static function getSetupCacheDiskCachePath() { // The difficulty here is in choosing a path which will change on server // restart (we MUST have this property), but as rarely as possible // otherwise (we desire this property to give the cache the best hit rate // we can). // In some setups, the parent PID is more stable and longer-lived that the // PID (e.g., under apache, our PID will be a worker while the ppid will // be the main httpd process). If we're confident we're running under such // a setup, we can try to use the PPID as the basis for our cache instead // of our own PID. $use_ppid = false; switch (php_sapi_name()) { case 'cli-server': // This is the PHP5.4+ built-in webserver. We should use the pid // (the server), not the ppid (probably a shell or something). $use_ppid = false; break; case 'fpm-fcgi': // We should be safe to use PPID here. $use_ppid = true; break; case 'apache2handler': // We're definitely safe to use the PPID. $use_ppid = true; break; } $pid_basis = getmypid(); if ($use_ppid) { if (function_exists('posix_getppid')) { $parent_pid = posix_getppid(); // On most systems, normal processes can never have PIDs lower than 100, // so something likely went wrong if we we get one of these. if ($parent_pid > 100) { $pid_basis = $parent_pid; } } } // If possible, we also want to know when the process launched, so we can // drop the cache if a process restarts but gets the same PID an earlier // process had. "/proc" is not available everywhere (e.g., not on OSX), but // check if we have it. $epoch_basis = null; $stat = @stat("/proc/{$pid_basis}"); if ($stat !== false) { $epoch_basis = $stat['ctime']; } $tmp_dir = sys_get_temp_dir(); $tmp_path = $tmp_dir.DIRECTORY_SEPARATOR.'phabricator-setup'; if (!file_exists($tmp_path)) { @mkdir($tmp_path); } $is_ok = self::testTemporaryDirectory($tmp_path); if (!$is_ok) { $tmp_path = $tmp_dir; $is_ok = self::testTemporaryDirectory($tmp_path); if (!$is_ok) { // We can't find anywhere to write the cache, so just bail. return null; } } $tmp_name = 'setup-'.$pid_basis; if ($epoch_basis) { $tmp_name .= '.'.$epoch_basis; } $tmp_name .= '.cache'; return $tmp_path.DIRECTORY_SEPARATOR.$tmp_name; } /** * @task setup */ private static function testTemporaryDirectory($dir) { if (!@file_exists($dir)) { return false; } if (!@is_dir($dir)) { return false; } if (!@is_writable($dir)) { return false; } return true; } private static function addProfilerToCaches(array $caches) { foreach ($caches as $key => $cache) { $pcache = new PhutilKeyValueCacheProfiler($cache); $pcache->setProfiler(PhutilServiceProfiler::getInstance()); $caches[$key] = $pcache; } return $caches; } private static function addNamespaceToCaches(array $caches) { - $namespace = PhabricatorCaches::getNamespace(); + $namespace = self::getNamespace(); if (!$namespace) { return $caches; } foreach ($caches as $key => $cache) { $ncache = new PhutilKeyValueCacheNamespace($cache); $ncache->setNamespace($namespace); $caches[$key] = $ncache; } return $caches; } /** * Deflate a value, if deflation is available and has an impact. * * If the value is larger than 1KB, we have `gzdeflate()`, we successfully * can deflate it, and it benefits from deflation, we deflate it. Otherwise * we leave it as-is. * * Data can later be inflated with @{method:inflateData}. * * @param string String to attempt to deflate. * @return string|null Deflated string, or null if it was not deflated. * @task compress */ public static function maybeDeflateData($value) { $len = strlen($value); if ($len <= 1024) { return null; } if (!function_exists('gzdeflate')) { return null; } $deflated = gzdeflate($value); if ($deflated === false) { return null; } $deflated_len = strlen($deflated); if ($deflated_len >= ($len / 2)) { return null; } return $deflated; } /** * Inflate data previously deflated by @{method:maybeDeflateData}. * * @param string Deflated data, from @{method:maybeDeflateData}. * @return string Original, uncompressed data. * @task compress */ public static function inflateData($value) { if (!function_exists('gzinflate')) { throw new Exception( pht('gzinflate() is not available; unable to read deflated data!')); } $value = gzinflate($value); if ($value === false) { throw new Exception(pht('Failed to inflate data!')); } return $value; } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index af756b5273..3dd984ef7d 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1,410 +1,410 @@ <?php final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO implements PhabricatorPolicyInterface, PhabricatorMarkupInterface, PhabricatorApplicationTransactionInterface, PhabricatorSubscribableInterface, PhabricatorTokenReceiverInterface, PhabricatorDestructibleInterface, PhabricatorMentionableInterface, PhabricatorFlaggableInterface { protected $name; protected $userPHID; protected $dateFrom; protected $dateTo; protected $status; protected $description; protected $isCancelled; protected $isAllDay; protected $mailKey; protected $viewPolicy; protected $editPolicy; private $invitees = self::ATTACHABLE; private $appliedViewer; const STATUS_AWAY = 1; const STATUS_SPORADIC = 2; public static function initializeNewCalendarEvent(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorCalendarApplication')) ->executeOne(); return id(new PhabricatorCalendarEvent()) ->setUserPHID($actor->getPHID()) ->setIsCancelled(0) ->setIsAllDay(0) ->setViewPolicy($actor->getPHID()) ->setEditPolicy($actor->getPHID()) ->attachInvitees(array()) ->applyViewerTimezone($actor); } public function applyViewerTimezone(PhabricatorUser $viewer) { if ($this->appliedViewer) { throw new Exception(pht('Viewer timezone is already applied!')); } $this->appliedViewer = $viewer; if (!$this->getIsAllDay()) { return $this; } $zone = $viewer->getTimeZone(); $this->setDateFrom( $this->getDateEpochForTimeZone( $this->getDateFrom(), new DateTimeZone('Pacific/Kiritimati'), 'Y-m-d', null, $zone)); $this->setDateTo( $this->getDateEpochForTimeZone( $this->getDateTo(), new DateTimeZone('Pacific/Midway'), 'Y-m-d 23:59:00', '-1 day', $zone)); return $this; } public function removeViewerTimezone(PhabricatorUser $viewer) { if (!$this->appliedViewer) { throw new Exception(pht('Viewer timezone is not applied!')); } if ($viewer->getPHID() != $this->appliedViewer->getPHID()) { throw new Exception(pht('Removed viewer must match applied viewer!')); } $this->appliedViewer = null; if (!$this->getIsAllDay()) { return $this; } $zone = $viewer->getTimeZone(); $this->setDateFrom( $this->getDateEpochForTimeZone( $this->getDateFrom(), $zone, 'Y-m-d', null, new DateTimeZone('Pacific/Kiritimati'))); $this->setDateTo( $this->getDateEpochForTimeZone( $this->getDateTo(), $zone, 'Y-m-d', '+1 day', new DateTimeZone('Pacific/Midway'))); return $this; } private function getDateEpochForTimeZone( $epoch, $src_zone, $format, $adjust, $dst_zone) { $src = new DateTime('@'.$epoch); $src->setTimeZone($src_zone); if (strlen($adjust)) { $adjust = ' '.$adjust; } $dst = new DateTime($src->format($format).$adjust, $dst_zone); return $dst->format('U'); } public function save() { if ($this->appliedViewer) { throw new Exception( pht( 'Can not save event with viewer timezone still applied!')); } if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } return parent::save(); } private static $statusTexts = array( self::STATUS_AWAY => 'away', self::STATUS_SPORADIC => 'sporadic', ); public function setTextStatus($status) { $statuses = array_flip(self::$statusTexts); return $this->setStatus($statuses[$status]); } public function getTextStatus() { return self::$statusTexts[$this->status]; } public function getStatusOptions() { return array( self::STATUS_AWAY => pht('Away'), self::STATUS_SPORADIC => pht('Sporadic'), ); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'dateFrom' => 'epoch', 'dateTo' => 'epoch', 'status' => 'uint32', 'description' => 'text', 'isCancelled' => 'bool', 'isAllDay' => 'bool', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'userPHID_dateFrom' => array( 'columns' => array('userPHID', 'dateTo'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCalendarEventPHIDType::TYPECONST); } public function getMonogram() { return 'E'.$this->getID(); } public function getTerseSummary(PhabricatorUser $viewer) { $until = phabricator_date($this->dateTo, $viewer); - if ($this->status == PhabricatorCalendarEvent::STATUS_SPORADIC) { + if ($this->status == self::STATUS_SPORADIC) { return pht('Sporadic until %s', $until); } else { return pht('Away until %s', $until); } } public static function getNameForStatus($value) { switch ($value) { case self::STATUS_AWAY: return pht('Away'); case self::STATUS_SPORADIC: return pht('Sporadic'); default: return pht('Unknown'); } } public function loadCurrentStatuses($user_phids) { if (!$user_phids) { return array(); } $statuses = $this->loadAllWhere( 'userPHID IN (%Ls) AND UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo', $user_phids); return mpull($statuses, null, 'getUserPHID'); } public function getInvitees() { return $this->assertAttached($this->invitees); } public function attachInvitees(array $invitees) { $this->invitees = $invitees; return $this; } public function getUserInviteStatus($phid) { $invitees = $this->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); $invited = idx($invitees, $phid); if (!$invited) { return PhabricatorCalendarEventInvitee::STATUS_UNINVITED; } $invited = $invited->getStatus(); return $invited; } public function getIsUserAttending($phid) { $attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $old_status = $this->getUserInviteStatus($phid); $is_attending = ($old_status == $attending_status); return $is_attending; } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "calendar:T{$id}:{$field}:{$hash}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newCalendarMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { // The owner of a task can always view and edit it. $user_phid = $this->getUserPHID(); if ($user_phid) { $viewer_phid = $viewer->getPHID(); if ($viewer_phid == $user_phid) { return true; } } if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { $status = $this->getUserInviteStatus($viewer->getPHID()); if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING || $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht('The owner of an event can always view and edit it, and invitees can always view it.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorCalendarEventEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorCalendarEventTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getUserPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getUserPHID()); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/celerity/resources/CelerityPhysicalResources.php b/src/applications/celerity/resources/CelerityPhysicalResources.php index 995b9f1a55..41665d15f2 100644 --- a/src/applications/celerity/resources/CelerityPhysicalResources.php +++ b/src/applications/celerity/resources/CelerityPhysicalResources.php @@ -1,61 +1,61 @@ <?php /** * Defines the location of physical static resources which exist at build time * and are precomputed into a resource map. */ abstract class CelerityPhysicalResources extends CelerityResources { private $map; abstract public function getPathToMap(); abstract public function findBinaryResources(); abstract public function findTextResources(); public function loadMap() { if ($this->map === null) { $this->map = include $this->getPathToMap(); } return $this->map; } public static function getAll() { static $resources_map; if ($resources_map === null) { $resources_map = array(); $resources_list = id(new PhutilSymbolLoader()) - ->setAncestorClass('CelerityPhysicalResources') + ->setAncestorClass(__CLASS__) ->loadObjects(); foreach ($resources_list as $resources) { $name = $resources->getName(); if (!preg_match('/^[a-z0-9]+/', $name)) { throw new Exception( pht( 'Resources name "%s" is not valid; it must contain only '. 'lowercase latin letters and digits.', $name)); } if (empty($resources_map[$name])) { $resources_map[$name] = $resources; } else { $old = get_class($resources_map[$name]); $new = get_class($resources); throw new Exception( pht( 'Celerity resource maps must have unique names, but maps %s and '. '%s share the same name, "%s".', $old, $new, $name)); } } } return $resources_map; } } diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php index a048e3e0f0..f6d464cfc5 100644 --- a/src/applications/conduit/method/ConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitAPIMethod.php @@ -1,375 +1,375 @@ <?php /** * @task status Method Status * @task pager Paging Results */ abstract class ConduitAPIMethod extends Phobject implements PhabricatorPolicyInterface { const METHOD_STATUS_STABLE = 'stable'; const METHOD_STATUS_UNSTABLE = 'unstable'; const METHOD_STATUS_DEPRECATED = 'deprecated'; abstract public function getMethodDescription(); abstract protected function defineParamTypes(); abstract protected function defineReturnType(); protected function defineErrorTypes() { return array(); } abstract protected function execute(ConduitAPIRequest $request); public function __construct() {} public function getParamTypes() { $types = $this->defineParamTypes(); $query = $this->newQueryObject(); if ($query) { $types['order'] = 'order'; $types += $this->getPagerParamTypes(); } return $types; } public function getReturnType() { return $this->defineReturnType(); } public function getErrorTypes() { return $this->defineErrorTypes(); } /** * This is mostly for compatibility with * @{class:PhabricatorCursorPagedPolicyAwareQuery}. */ public function getID() { return $this->getAPIMethodName(); } /** * Get the status for this method (e.g., stable, unstable or deprecated). * Should return a METHOD_STATUS_* constant. By default, methods are * "stable". * * @return const METHOD_STATUS_* constant. * @task status */ public function getMethodStatus() { return self::METHOD_STATUS_STABLE; } /** * Optional description to supplement the method status. In particular, if * a method is deprecated, you can return a string here describing the reason * for deprecation and stable alternatives. * * @return string|null Description of the method status, if available. * @task status */ public function getMethodStatusDescription() { return null; } public function getErrorDescription($error_code) { return idx($this->getErrorTypes(), $error_code, 'Unknown Error'); } public function getRequiredScope() { // by default, conduit methods are not accessible via OAuth return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE; } public function executeMethod(ConduitAPIRequest $request) { return $this->execute($request); } public abstract function getAPIMethodName(); /** * Return a key which sorts methods by application name, then method status, * then method name. */ public function getSortOrder() { $name = $this->getAPIMethodName(); $map = array( - ConduitAPIMethod::METHOD_STATUS_STABLE => 0, - ConduitAPIMethod::METHOD_STATUS_UNSTABLE => 1, - ConduitAPIMethod::METHOD_STATUS_DEPRECATED => 2, + self::METHOD_STATUS_STABLE => 0, + self::METHOD_STATUS_UNSTABLE => 1, + self::METHOD_STATUS_DEPRECATED => 2, ); $ord = idx($map, $this->getMethodStatus(), 0); list($head, $tail) = explode('.', $name, 2); return "{$head}.{$ord}.{$tail}"; } public function getApplicationName() { return head(explode('.', $this->getAPIMethodName(), 2)); } public static function getConduitMethod($method_name) { static $method_map = null; if ($method_map === null) { $methods = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); foreach ($methods as $method) { $name = $method->getAPIMethodName(); if (empty($method_map[$name])) { $method_map[$name] = $method; continue; } $orig_class = get_class($method_map[$name]); $this_class = get_class($method); throw new Exception( "Two Conduit API method classes ({$orig_class}, {$this_class}) ". "both have the same method name ({$name}). API methods ". "must have unique method names."); } } return idx($method_map, $method_name); } public function shouldRequireAuthentication() { return true; } public function shouldAllowPublic() { return false; } public function shouldAllowUnguardedWrites() { return false; } /** * Optionally, return a @{class:PhabricatorApplication} which this call is * part of. The call will be disabled when the application is uninstalled. * * @return PhabricatorApplication|null Related application. */ public function getApplication() { return null; } protected function formatStringConstants($constants) { foreach ($constants as $key => $value) { $constants[$key] = '"'.$value.'"'; } $constants = implode(', ', $constants); return 'string-constant<'.$constants.'>'; } public static function getParameterMetadataKey($key) { if (strncmp($key, 'api.', 4) === 0) { // All keys passed beginning with "api." are always metadata keys. return substr($key, 4); } else { switch ($key) { // These are real keys which always belong to request metadata. case 'access_token': case 'scope': case 'output': // This is not a real metadata key; it is included here only to // prevent Conduit methods from defining it. case '__conduit__': // This is prevented globally as a blanket defense against OAuth // redirection attacks. It is included here to stop Conduit methods // from defining it. case 'code': // This is not a real metadata key, but the presence of this // parameter triggers an alternate request decoding pathway. case 'params': return $key; } } return null; } /* -( Paging Results )----------------------------------------------------- */ /** * @task pager */ protected function getPagerParamTypes() { return array( 'before' => 'optional string', 'after' => 'optional string', 'limit' => 'optional int (default = 100)', ); } /** * @task pager */ protected function newPager(ConduitAPIRequest $request) { $limit = $request->getValue('limit', 100); $limit = min(1000, $limit); $limit = max(1, $limit); $pager = id(new AphrontCursorPagerView()) ->setPageSize($limit); $before_id = $request->getValue('before'); if ($before_id !== null) { $pager->setBeforeID($before_id); } $after_id = $request->getValue('after'); if ($after_id !== null) { $pager->setAfterID($after_id); } return $pager; } /** * @task pager */ protected function addPagerResults( array $results, AphrontCursorPagerView $pager) { $results['cursor'] = array( 'limit' => $pager->getPageSize(), 'after' => $pager->getNextPageID(), 'before' => $pager->getPrevPageID(), ); return $results; } /* -( Implementing Query Methods )----------------------------------------- */ public function newQueryObject() { return null; } protected function newQueryForRequest(ConduitAPIRequest $request) { $query = $this->newQueryObject(); if (!$query) { throw new Exception( pht( 'You can not call newQueryFromRequest() in this method ("%s") '. 'because it does not implement newQueryObject().', get_class($this))); } if (!($query instanceof PhabricatorCursorPagedPolicyAwareQuery)) { throw new Exception( pht( 'Call to method newQueryObject() did not return an object of class '. '"%s".', 'PhabricatorCursorPagedPolicyAwareQuery')); } $query->setViewer($request->getUser()); $order = $request->getValue('order'); if ($order !== null) { if (is_scalar($order)) { $query->setOrder($order); } else { $query->setOrderVector($order); } } return $query; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getPHID() { return null; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // Application methods get application visibility; other methods get open // visibility. $application = $this->getApplication(); if ($application) { return $application->getPolicy($capability); } return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if (!$this->shouldRequireAuthentication()) { // Make unauthenticated methods universally visible. return true; } return false; } public function describeAutomaticCapability($capability) { return null; } protected function hasApplicationCapability( $capability, PhabricatorUser $viewer) { $application = $this->getApplication(); if (!$application) { return false; } return PhabricatorPolicyFilter::hasCapability( $viewer, $application, $capability); } protected function requireApplicationCapability( $capability, PhabricatorUser $viewer) { $application = $this->getApplication(); if (!$application) { return; } PhabricatorPolicyFilter::requireCapability( $viewer, $this->getApplication(), $capability); } } diff --git a/src/applications/conduit/query/PhabricatorConduitLogQuery.php b/src/applications/conduit/query/PhabricatorConduitLogQuery.php index 4d66cccae5..a08fd25a68 100644 --- a/src/applications/conduit/query/PhabricatorConduitLogQuery.php +++ b/src/applications/conduit/query/PhabricatorConduitLogQuery.php @@ -1,46 +1,46 @@ <?php final class PhabricatorConduitLogQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $methods; public function withMethods(array $methods) { $this->methods = $methods; return $this; } protected function loadPage() { $table = new PhabricatorConduitMethodCallLog(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); - return $table->loadAllFromArray($data);; + return $table->loadAllFromArray($data); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->methods) { $where[] = qsprintf( $conn_r, 'method IN (%Ls)', $this->methods); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorConduitApplication'; } } diff --git a/src/applications/conduit/query/PhabricatorConduitTokenQuery.php b/src/applications/conduit/query/PhabricatorConduitTokenQuery.php index 870043cac8..44586f1815 100644 --- a/src/applications/conduit/query/PhabricatorConduitTokenQuery.php +++ b/src/applications/conduit/query/PhabricatorConduitTokenQuery.php @@ -1,128 +1,128 @@ <?php final class PhabricatorConduitTokenQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $ids; private $objectPHIDs; private $expired; private $tokens; private $tokenTypes; public function withExpired($expired) { $this->expired = $expired; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withObjectPHIDs(array $phids) { $this->objectPHIDs = $phids; return $this; } public function withTokens(array $tokens) { $this->tokens = $tokens; return $this; } public function withTokenTypes(array $types) { $this->tokenTypes = $types; return $this; } protected function loadPage() { $table = new PhabricatorConduitToken(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); - return $table->loadAllFromArray($data);; + return $table->loadAllFromArray($data); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->objectPHIDs !== null) { $where[] = qsprintf( $conn_r, 'objectPHID IN (%Ls)', $this->objectPHIDs); } if ($this->tokens !== null) { $where[] = qsprintf( $conn_r, 'token IN (%Ls)', $this->tokens); } if ($this->tokenTypes !== null) { $where[] = qsprintf( $conn_r, 'tokenType IN (%Ls)', $this->tokenTypes); } if ($this->expired !== null) { if ($this->expired) { $where[] = qsprintf( $conn_r, 'expires <= %d', PhabricatorTime::getNow()); } else { $where[] = qsprintf( $conn_r, 'expires IS NULL OR expires > %d', PhabricatorTime::getNow()); } } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } protected function willFilterPage(array $tokens) { $object_phids = mpull($tokens, 'getObjectPHID'); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($object_phids) ->execute(); $objects = mpull($objects, null, 'getPHID'); foreach ($tokens as $key => $token) { $object = idx($objects, $token->getObjectPHID(), null); if (!$object) { $this->didRejectResult($token); unset($tokens[$key]); continue; } $token->attachObject($object); } return $tokens; } public function getQueryApplicationClass() { return 'PhabricatorConduitApplication'; } } diff --git a/src/applications/conduit/storage/PhabricatorConduitToken.php b/src/applications/conduit/storage/PhabricatorConduitToken.php index ab4d88335e..f5673fdab3 100644 --- a/src/applications/conduit/storage/PhabricatorConduitToken.php +++ b/src/applications/conduit/storage/PhabricatorConduitToken.php @@ -1,165 +1,165 @@ <?php final class PhabricatorConduitToken extends PhabricatorConduitDAO implements PhabricatorPolicyInterface { protected $objectPHID; protected $tokenType; protected $token; protected $expires; private $object = self::ATTACHABLE; const TYPE_STANDARD = 'api'; const TYPE_COMMANDLINE = 'cli'; const TYPE_CLUSTER = 'clr'; protected function getConfiguration() { return array( self::CONFIG_COLUMN_SCHEMA => array( 'tokenType' => 'text32', 'token' => 'text32', 'expires' => 'epoch?', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( 'columns' => array('objectPHID', 'tokenType'), ), 'key_token' => array( 'columns' => array('token'), 'unique' => true, ), 'key_expires' => array( 'columns' => array('expires'), ), ), ) + parent::getConfiguration(); } public static function loadClusterTokenForUser(PhabricatorUser $user) { if (!$user->isLoggedIn()) { return null; } $tokens = id(new PhabricatorConduitTokenQuery()) ->setViewer($user) ->withObjectPHIDs(array($user->getPHID())) ->withTokenTypes(array(self::TYPE_CLUSTER)) ->withExpired(false) ->execute(); // Only return a token if it has at least 5 minutes left before // expiration. Cluster tokens cycle regularly, so we don't want to use // one that's going to expire momentarily. $now = PhabricatorTime::getNow(); $must_expire_after = $now + phutil_units('5 minutes in seconds'); foreach ($tokens as $token) { if ($token->getExpires() > $must_expire_after) { return $token; } } // We didn't find any existing tokens (or the existing tokens are all about // to expire) so generate a new token. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $token = PhabricatorConduitToken::initializeNewToken( + $token = self::initializeNewToken( $user->getPHID(), self::TYPE_CLUSTER); $token->save(); unset($unguarded); return $token; } public static function initializeNewToken($object_phid, $token_type) { $token = new PhabricatorConduitToken(); $token->objectPHID = $object_phid; $token->tokenType = $token_type; $token->expires = $token->getTokenExpires($token_type); $secret = $token_type.'-'.Filesystem::readRandomCharacters(32); $secret = substr($secret, 0, 32); $token->token = $secret; return $token; } public static function getTokenTypeName($type) { $map = array( self::TYPE_STANDARD => pht('Standard API Token'), self::TYPE_COMMANDLINE => pht('Command Line API Token'), self::TYPE_CLUSTER => pht('Cluster API Token'), ); return idx($map, $type, $type); } public static function getAllTokenTypes() { return array( self::TYPE_STANDARD, self::TYPE_COMMANDLINE, self::TYPE_CLUSTER, ); } private function getTokenExpires($token_type) { $now = PhabricatorTime::getNow(); switch ($token_type) { case self::TYPE_STANDARD: return null; case self::TYPE_COMMANDLINE: return $now + phutil_units('1 hour in seconds'); case self::TYPE_CLUSTER: return $now + phutil_units('30 minutes in seconds'); default: throw new Exception( pht('Unknown Conduit token type "%s"!', $token_type)); } } public function getPublicTokenName() { switch ($this->getTokenType()) { case self::TYPE_CLUSTER: return pht('Cluster API Token'); default: return substr($this->getToken(), 0, 8).'...'; } } public function getObject() { return $this->assertAttached($this->object); } public function attachObject(PhabricatorUser $object) { $this->object = $object; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getObject()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Conduit tokens inherit the policies of the user they authenticate.'); } } diff --git a/src/applications/config/check/PhabricatorSetupCheck.php b/src/applications/config/check/PhabricatorSetupCheck.php index b40dd4c8cc..a689d14943 100644 --- a/src/applications/config/check/PhabricatorSetupCheck.php +++ b/src/applications/config/check/PhabricatorSetupCheck.php @@ -1,170 +1,170 @@ <?php abstract class PhabricatorSetupCheck { private $issues; abstract protected function executeChecks(); const GROUP_OTHER = 'other'; const GROUP_MYSQL = 'mysql'; const GROUP_PHP = 'php'; const GROUP_IMPORTANT = 'important'; public function getExecutionOrder() { return 1; } final protected function newIssue($key) { $issue = id(new PhabricatorSetupIssue()) ->setIssueKey($key); $this->issues[$key] = $issue; if ($this->getDefaultGroup()) { $issue->setGroup($this->getDefaultGroup()); } return $issue; } final public function getIssues() { return $this->issues; } protected function addIssue(PhabricatorSetupIssue $issue) { $this->issues[$issue->getIssueKey()] = $issue; return $this; } public function getDefaultGroup() { return null; } final public function runSetupChecks() { $this->issues = array(); $this->executeChecks(); } final public static function getOpenSetupIssueKeys() { $cache = PhabricatorCaches::getSetupCache(); return $cache->getKey('phabricator.setup.issue-keys'); } final public static function setOpenSetupIssueKeys(array $keys) { $cache = PhabricatorCaches::getSetupCache(); $cache->setKey('phabricator.setup.issue-keys', $keys); } final public static function getUnignoredIssueKeys(array $all_issues) { assert_instances_of($all_issues, 'PhabricatorSetupIssue'); $keys = array(); foreach ($all_issues as $issue) { if (!$issue->getIsIgnored()) { $keys[] = $issue->getIssueKey(); } } return $keys; } final public static function getConfigNeedsRepair() { $cache = PhabricatorCaches::getSetupCache(); return $cache->getKey('phabricator.setup.needs-repair'); } final public static function setConfigNeedsRepair($needs_repair) { $cache = PhabricatorCaches::getSetupCache(); $cache->setKey('phabricator.setup.needs-repair', $needs_repair); } final public static function deleteSetupCheckCache() { $cache = PhabricatorCaches::getSetupCache(); $cache->deleteKeys( array( 'phabricator.setup.needs-repair', 'phabricator.setup.issue-keys', )); } final public static function willProcessRequest() { $issue_keys = self::getOpenSetupIssueKeys(); if ($issue_keys === null) { $issues = self::runAllChecks(); foreach ($issues as $issue) { if ($issue->getIsFatal()) { $view = id(new PhabricatorSetupIssueView()) ->setIssue($issue); return id(new PhabricatorConfigResponse()) ->setView($view); } } self::setOpenSetupIssueKeys(self::getUnignoredIssueKeys($issues)); } // Try to repair configuration unless we have a clean bill of health on it. // We need to keep doing this on every page load until all the problems // are fixed, which is why it's separate from setup checks (which run // once per restart). $needs_repair = self::getConfigNeedsRepair(); if ($needs_repair !== false) { $needs_repair = self::repairConfig(); self::setConfigNeedsRepair($needs_repair); } } final public static function runAllChecks() { $symbols = id(new PhutilSymbolLoader()) - ->setAncestorClass('PhabricatorSetupCheck') + ->setAncestorClass(__CLASS__) ->setConcreteOnly(true) ->selectAndLoadSymbols(); $checks = array(); foreach ($symbols as $symbol) { $checks[] = newv($symbol['name'], array()); } $checks = msort($checks, 'getExecutionOrder'); $issues = array(); foreach ($checks as $check) { $check->runSetupChecks(); foreach ($check->getIssues() as $key => $issue) { if (isset($issues[$key])) { throw new Exception( "Two setup checks raised an issue with key '{$key}'!"); } $issues[$key] = $issue; if ($issue->getIsFatal()) { break 2; } } } foreach (PhabricatorEnv::getEnvConfig('config.ignore-issues') as $ignorable => $derp) { if (isset($issues[$ignorable])) { $issues[$ignorable]->setIsIgnored(true); } } return $issues; } final public static function repairConfig() { $needs_repair = false; $options = PhabricatorApplicationConfigOptions::loadAllOptions(); foreach ($options as $option) { try { $option->getGroup()->validateOption( $option, PhabricatorEnv::getEnvConfig($option->getKey())); } catch (PhabricatorConfigValidationException $ex) { PhabricatorEnv::repairConfig($option->getKey(), $option->getDefault()); $needs_repair = true; } } return $needs_repair; } } diff --git a/src/applications/config/option/PhabricatorApplicationConfigOptions.php b/src/applications/config/option/PhabricatorApplicationConfigOptions.php index 6982ef513f..7ac1df3e16 100644 --- a/src/applications/config/option/PhabricatorApplicationConfigOptions.php +++ b/src/applications/config/option/PhabricatorApplicationConfigOptions.php @@ -1,243 +1,249 @@ <?php abstract class PhabricatorApplicationConfigOptions extends Phobject { abstract public function getName(); abstract public function getDescription(); abstract public function getGroup(); abstract public function getOptions(); public function getFontIcon() { return 'fa-sliders'; } public function validateOption(PhabricatorConfigOption $option, $value) { if ($value === $option->getDefault()) { return; } if ($value === null) { return; } if ($option->isCustomType()) { return $option->getCustomObject()->validateOption($option, $value); } switch ($option->getType()) { case 'bool': if ($value !== true && $value !== false) { throw new PhabricatorConfigValidationException( pht( "Option '%s' is of type bool, but value is not true or false.", $option->getKey())); } break; case 'int': if (!is_int($value)) { throw new PhabricatorConfigValidationException( pht( "Option '%s' is of type int, but value is not an integer.", $option->getKey())); } break; case 'string': if (!is_string($value)) { throw new PhabricatorConfigValidationException( pht( "Option '%s' is of type string, but value is not a string.", $option->getKey())); } break; case 'class': $symbols = id(new PhutilSymbolLoader()) ->setType('class') ->setAncestorClass($option->getBaseClass()) ->setConcreteOnly(true) ->selectSymbolsWithoutLoading(); $names = ipull($symbols, 'name', 'name'); if (empty($names[$value])) { throw new PhabricatorConfigValidationException( pht( "Option '%s' value must name a class extending '%s'.", $option->getKey(), $option->getBaseClass())); } break; case 'set': $valid = true; if (!is_array($value)) { throw new PhabricatorConfigValidationException( pht( "Option '%s' must be a set, but value is not an array.", $option->getKey())); } foreach ($value as $v) { if ($v !== true) { throw new PhabricatorConfigValidationException( pht( "Option '%s' must be a set, but array contains values other ". "than 'true'.", $option->getKey())); } } break; case 'list<regex>': $valid = true; if (!is_array($value)) { throw new PhabricatorConfigValidationException( pht( "Option '%s' must be a list of regular expressions, but value ". "is not an array.", $option->getKey())); } if ($value && array_keys($value) != range(0, count($value) - 1)) { throw new PhabricatorConfigValidationException( pht( "Option '%s' must be a list of regular expressions, but the ". "value is a map with unnatural keys.", $option->getKey())); } foreach ($value as $v) { $ok = @preg_match($v, ''); if ($ok === false) { throw new PhabricatorConfigValidationException( pht( "Option '%s' must be a list of regular expressions, but the ". "value '%s' is not a valid regular expression.", $option->getKey(), $v)); } } break; case 'list<string>': $valid = true; if (!is_array($value)) { throw new PhabricatorConfigValidationException( pht( "Option '%s' must be a list of strings, but value is not ". "an array.", $option->getKey())); } if ($value && array_keys($value) != range(0, count($value) - 1)) { throw new PhabricatorConfigValidationException( pht( "Option '%s' must be a list of strings, but the value is a ". "map with unnatural keys.", $option->getKey())); } foreach ($value as $v) { if (!is_string($v)) { throw new PhabricatorConfigValidationException( pht( "Option '%s' must be a list of strings, but it contains one ". "or more non-strings.", $option->getKey())); } } break; case 'wild': default: break; } $this->didValidateOption($option, $value); } protected function didValidateOption( PhabricatorConfigOption $option, $value) { // Hook for subclasses to do complex validation. return; } /** * Hook to render additional hints based on, e.g., the viewing user, request, * or other context. For example, this is used to show workspace IDs when * configuring `asana.workspace-id`. * * @param PhabricatorConfigOption Option being rendered. * @param AphrontRequest Active request. * @return wild Additional contextual description * information. */ public function renderContextualDescription( PhabricatorConfigOption $option, AphrontRequest $request) { return null; } public function getKey() { $class = get_class($this); $matches = null; if (preg_match('/^Phabricator(.*)ConfigOptions$/', $class, $matches)) { return strtolower($matches[1]); } return strtolower(get_class($this)); } final protected function newOption($key, $type, $default) { return id(new PhabricatorConfigOption()) ->setKey($key) ->setType($type) ->setDefault($default) ->setGroup($this); } final public static function loadAll($external_only = false) { $symbols = id(new PhutilSymbolLoader()) - ->setAncestorClass('PhabricatorApplicationConfigOptions') + ->setAncestorClass(__CLASS__) ->setConcreteOnly(true) ->selectAndLoadSymbols(); $groups = array(); foreach ($symbols as $symbol) { if ($external_only && $symbol['library'] == 'phabricator') { continue; } $obj = newv($symbol['name'], array()); $key = $obj->getKey(); if (isset($groups[$key])) { $pclass = get_class($groups[$key]); $nclass = $symbol['name']; throw new Exception( - "Multiple PhabricatorApplicationConfigOptions subclasses have the ". - "same key ('{$key}'): {$pclass}, {$nclass}."); + pht( + "Multiple %s subclasses have the same key ('%s'): %s, %s.", + __CLASS__, + $key, + $pclass, + $nclass)); } $groups[$key] = $obj; } return $groups; } final public static function loadAllOptions($external_only = false) { $groups = self::loadAll($external_only); $options = array(); foreach ($groups as $group) { foreach ($group->getOptions() as $option) { $key = $option->getKey(); if (isset($options[$key])) { throw new Exception( - "Mulitple PhabricatorApplicationConfigOptions subclasses contain ". - "an option named '{$key}'!"); + pht( + "Mulitple % subclasses contain an option named '%s'!", + __CLASS__, + $key)); } $options[$key] = $option; } } return $options; } /** * Deformat a HEREDOC for use in remarkup by converting line breaks to * spaces. */ final protected function deformat($string) { return preg_replace('/(?<=\S)\n(?=\S)/', ' ', $string); } } diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index 03f8f6997e..8ed0e4fd18 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -1,484 +1,484 @@ <?php final class ConpherenceThread extends ConpherenceDAO implements PhabricatorPolicyInterface, PhabricatorApplicationTransactionInterface, PhabricatorMentionableInterface, PhabricatorDestructibleInterface { protected $title; protected $imagePHIDs = array(); protected $isRoom = 0; protected $messageCount; protected $recentParticipantPHIDs = array(); protected $mailKey; protected $viewPolicy; protected $editPolicy; protected $joinPolicy; private $participants = self::ATTACHABLE; private $transactions = self::ATTACHABLE; private $handles = self::ATTACHABLE; private $filePHIDs = self::ATTACHABLE; private $widgetData = self::ATTACHABLE; private $images = self::ATTACHABLE; public static function initializeNewThread(PhabricatorUser $sender) { return id(new ConpherenceThread()) ->setMessageCount(0) ->setTitle('') ->attachParticipants(array()) ->attachFilePHIDs(array()) ->attachImages(array()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_USER) ->setJoinPolicy(PhabricatorPolicies::POLICY_USER); } public static function initializeNewRoom(PhabricatorUser $creator) { return id(new ConpherenceThread()) ->setIsRoom(1) ->setMessageCount(0) ->setTitle('') ->attachParticipants(array()) ->attachFilePHIDs(array()) ->attachImages(array()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy($creator->getPHID()) ->setJoinPolicy(PhabricatorPolicies::POLICY_USER); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'recentParticipantPHIDs' => self::SERIALIZATION_JSON, 'imagePHIDs' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255?', 'isRoom' => 'bool', 'messageCount' => 'uint64', 'mailKey' => 'text20', 'joinPolicy' => 'policy', ), self::CONFIG_KEY_SCHEMA => array( 'key_room' => array( 'columns' => array('isRoom', 'dateModified'), ), 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorConpherenceThreadPHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getMonogram() { return 'Z'.$this->getID(); } public function getImagePHID($size) { $image_phids = $this->getImagePHIDs(); return idx($image_phids, $size); } public function setImagePHID($phid, $size) { $image_phids = $this->getImagePHIDs(); $image_phids[$size] = $phid; return $this->setImagePHIDs($image_phids); } public function getImage($size) { $images = $this->getImages(); return idx($images, $size); } public function setImage(PhabricatorFile $file, $size) { $files = $this->getImages(); $files[$size] = $file; return $this->attachImages($files); } public function attachImages(array $files) { assert_instances_of($files, 'PhabricatorFile'); $this->images = $files; return $this; } private function getImages() { return $this->assertAttached($this->images); } public function attachParticipants(array $participants) { assert_instances_of($participants, 'ConpherenceParticipant'); $this->participants = $participants; return $this; } public function getParticipants() { return $this->assertAttached($this->participants); } public function getParticipant($phid) { $participants = $this->getParticipants(); return $participants[$phid]; } public function getParticipantIfExists($phid, $default = null) { $participants = $this->getParticipants(); return idx($participants, $phid, $default); } public function getParticipantPHIDs() { $participants = $this->getParticipants(); return array_keys($participants); } public function attachHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function getHandles() { return $this->assertAttached($this->handles); } public function attachTransactions(array $transactions) { assert_instances_of($transactions, 'ConpherenceTransaction'); $this->transactions = $transactions; return $this; } public function getTransactions($assert_attached = true) { return $this->assertAttached($this->transactions); } public function hasAttachedTransactions() { return $this->transactions !== self::ATTACHABLE; } public function getTransactionsFrom($begin = 0, $amount = null) { $length = count($this->transactions); return array_slice( $this->getTransactions(), $length - $begin - $amount, $amount); } public function attachFilePHIDs(array $file_phids) { $this->filePHIDs = $file_phids; return $this; } public function getFilePHIDs() { return $this->assertAttached($this->filePHIDs); } public function attachWidgetData(array $widget_data) { $this->widgetData = $widget_data; return $this; } public function getWidgetData() { return $this->assertAttached($this->widgetData); } public function loadImageURI($size) { $file = $this->getImage($size); if ($file) { return $file->getBestURI(); } return PhabricatorUser::getDefaultProfileImageURI(); } /** * Get the thread's display title for a user. * * If a thread doesn't have a title set, this will return a string describing * recent participants. * * @param PhabricatorUser Viewer. * @return string Thread title. */ public function getDisplayTitle(PhabricatorUser $viewer) { $title = $this->getTitle(); if (strlen($title)) { return $title; } return $this->getRecentParticipantsString($viewer); } /** * Get recent participants (other than the viewer) as a string. * * For example, this method might return "alincoln, htaft, gwashington...". * * @param PhabricatorUser Viewer. * @return string Description of other participants. */ private function getRecentParticipantsString(PhabricatorUser $viewer) { $handles = $this->getHandles(); $phids = $this->getOtherRecentParticipantPHIDs($viewer); $limit = 3; $more = (count($phids) > $limit); $phids = array_slice($phids, 0, $limit); $names = array_select_keys($handles, $phids); $names = mpull($names, 'getName'); $names = implode(', ', $names); if ($more) { $names = $names.'...'; } return $names; } /** * Get PHIDs for recent participants who are not the viewer. * * @param PhabricatorUser Viewer. * @return list<phid> Participants who are not the viewer. */ private function getOtherRecentParticipantPHIDs(PhabricatorUser $viewer) { $phids = $this->getRecentParticipantPHIDs(); $phids = array_fuse($phids); unset($phids[$viewer->getPHID()]); return array_values($phids); } public function getDisplayData(PhabricatorUser $viewer) { $handles = $this->getHandles(); if ($this->hasAttachedTransactions()) { $transactions = $this->getTransactions(); } else { $transactions = array(); } if ($transactions) { $subtitle_mode = 'message'; } else { $subtitle_mode = 'recent'; } $lucky_phid = head($this->getOtherRecentParticipantPHIDs($viewer)); if ($lucky_phid) { $lucky_handle = $handles[$lucky_phid]; } else { // This will be just the user talking to themselves. Weirdo. $lucky_handle = reset($handles); } $img_src = null; $size = ConpherenceImageData::SIZE_CROP; if ($this->getImagePHID($size)) { $img_src = $this->getImage($size)->getBestURI(); } else if ($lucky_handle) { $img_src = $lucky_handle->getImageURI(); } $message_title = null; if ($subtitle_mode == 'message') { $message_transaction = null; foreach ($transactions as $transaction) { switch ($transaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $message_transaction = $transaction; break 2; default: break; } } if ($message_transaction) { $message_handle = $handles[$message_transaction->getAuthorPHID()]; $message_title = sprintf( '%s: %s', $message_handle->getName(), id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(60) ->truncateString( $message_transaction->getComment()->getContent())); } } switch ($subtitle_mode) { case 'recent': $subtitle = $this->getRecentParticipantsString($viewer); break; case 'message': if ($message_title) { $subtitle = $message_title; } else { $subtitle = $this->getRecentParticipantsString($viewer); } break; } $user_participation = $this->getParticipantIfExists($viewer->getPHID()); if ($user_participation) { $user_seen_count = $user_participation->getSeenMessageCount(); } else { $user_seen_count = 0; } $unread_count = $this->getMessageCount() - $user_seen_count; $title = $this->getDisplayTitle($viewer); return array( 'title' => $title, 'subtitle' => $subtitle, 'unread_count' => $unread_count, 'epoch' => $this->getDateModified(), 'image' => $img_src, ); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, PhabricatorPolicyCapability::CAN_JOIN, ); } public function getPolicy($capability) { if ($this->getIsRoom()) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case PhabricatorPolicyCapability::CAN_JOIN: return $this->getJoinPolicy(); } } return PhabricatorPolicies::POLICY_NOONE; } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // this bad boy isn't even created yet so go nuts $user if (!$this->getID()) { return true; } if ($this->getIsRoom()) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: case PhabricatorPolicyCapability::CAN_JOIN: return false; } } $participants = $this->getParticipants(); return isset($participants[$user->getPHID()]); } public function describeAutomaticCapability($capability) { if ($this->getIsRoom()) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht('Participants in a room can always view it.'); break; } } else { return pht('Participants in a thread can always view and edit it.'); } } public static function loadPolicyObjects( PhabricatorUser $viewer, array $conpherences) { - assert_instances_of($conpherences, 'ConpherenceThread'); + assert_instances_of($conpherences, __CLASS__); $grouped = mgroup($conpherences, 'getIsRoom'); $rooms = idx($grouped, 1, array()); $policies = array(); foreach ($rooms as $room) { $policies[] = $room->getViewPolicy(); } $policy_objects = array(); if ($policies) { $policy_objects = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->withPHIDs($policies) ->execute(); } return $policy_objects; } public function getPolicyIconName(array $policy_objects) { assert_instances_of($policy_objects, 'PhabricatorPolicy'); if ($this->getIsRoom()) { $icon = $policy_objects[$this->getViewPolicy()]->getIcon(); } else if (count($this->getRecentParticipantPHIDs()) > 2) { $icon = 'fa-users'; } else { $icon = 'fa-user'; } return $icon; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new ConpherenceEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new ConpherenceTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $participants = id(new ConpherenceParticipant()) ->loadAllWhere('conpherencePHID = %s', $this->getPHID()); foreach ($participants as $participant) { $participant->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php b/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php index 0708c98eec..5d9723cc4b 100644 --- a/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php +++ b/src/applications/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php @@ -1,75 +1,75 @@ <?php final class DarkConsoleErrorLogPluginAPI { private static $errors = array(); private static $discardMode = false; public static function registerErrorHandler() { // NOTE: This forces PhutilReadableSerializer to load, so that we are // able to handle errors which fire from inside autoloaders (PHP will not // reenter autoloaders). PhutilReadableSerializer::printableValue(null); PhutilErrorHandler::setErrorListener( - array('DarkConsoleErrorLogPluginAPI', 'handleErrors')); + array(__CLASS__, 'handleErrors')); } public static function enableDiscardMode() { self::$discardMode = true; } public static function disableDiscardMode() { self::$discardMode = false; } public static function getErrors() { return self::$errors; } public static function handleErrors($event, $value, $metadata) { if (self::$discardMode) { return; } switch ($event) { case PhutilErrorHandler::EXCEPTION: // $value is of type Exception self::$errors[] = array( 'details' => $value->getMessage(), 'event' => $event, 'file' => $value->getFile(), 'line' => $value->getLine(), 'str' => $value->getMessage(), 'trace' => $metadata['trace'], ); break; case PhutilErrorHandler::ERROR: // $value is a simple string self::$errors[] = array( 'details' => $value, 'event' => $event, 'file' => $metadata['file'], 'line' => $metadata['line'], 'str' => $value, 'trace' => $metadata['trace'], ); break; case PhutilErrorHandler::PHLOG: // $value can be anything self::$errors[] = array( 'details' => PhutilReadableSerializer::printShallow($value, 3), 'event' => $event, 'file' => $metadata['file'], 'line' => $metadata['line'], 'str' => PhutilReadableSerializer::printShort($value), 'trace' => $metadata['trace'], ); break; default: error_log('Unknown event : '.$event); break; } } } diff --git a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php index 5822194d06..961c1cfc61 100644 --- a/src/applications/daemon/query/PhabricatorDaemonLogQuery.php +++ b/src/applications/daemon/query/PhabricatorDaemonLogQuery.php @@ -1,194 +1,194 @@ <?php final class PhabricatorDaemonLogQuery extends PhabricatorCursorPagedPolicyAwareQuery { const STATUS_ALL = 'status-all'; const STATUS_ALIVE = 'status-alive'; const STATUS_RUNNING = 'status-running'; private $ids; private $notIDs; private $status = self::STATUS_ALL; private $daemonClasses; private $allowStatusWrites; private $daemonIDs; public static function getTimeUntilUnknown() { return 3 * PhutilDaemonHandle::getHeartbeatEventFrequency(); } public static function getTimeUntilDead() { return 30 * PhutilDaemonHandle::getHeartbeatEventFrequency(); } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withoutIDs(array $ids) { $this->notIDs = $ids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withDaemonClasses(array $classes) { $this->daemonClasses = $classes; return $this; } public function setAllowStatusWrites($allow) { $this->allowStatusWrites = $allow; return $this; } public function withDaemonIDs(array $daemon_ids) { $this->daemonIDs = $daemon_ids; return $this; } protected function loadPage() { $table = new PhabricatorDaemonLog(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $daemons) { - $unknown_delay = PhabricatorDaemonLogQuery::getTimeUntilUnknown(); - $dead_delay = PhabricatorDaemonLogQuery::getTimeUntilDead(); + $unknown_delay = self::getTimeUntilUnknown(); + $dead_delay = self::getTimeUntilDead(); $status_running = PhabricatorDaemonLog::STATUS_RUNNING; $status_unknown = PhabricatorDaemonLog::STATUS_UNKNOWN; $status_wait = PhabricatorDaemonLog::STATUS_WAIT; $status_exiting = PhabricatorDaemonLog::STATUS_EXITING; $status_exited = PhabricatorDaemonLog::STATUS_EXITED; $status_dead = PhabricatorDaemonLog::STATUS_DEAD; $filter = array_fuse($this->getStatusConstants()); foreach ($daemons as $key => $daemon) { $status = $daemon->getStatus(); $seen = $daemon->getDateModified(); $is_running = ($status == $status_running) || ($status == $status_wait) || ($status == $status_exiting); // If we haven't seen the daemon recently, downgrade its status to // unknown. $unknown_time = ($seen + $unknown_delay); if ($is_running && ($unknown_time < time())) { $status = $status_unknown; } // If the daemon hasn't been seen in quite a while, assume it is dead. $dead_time = ($seen + $dead_delay); if (($status == $status_unknown) && ($dead_time < time())) { $status = $status_dead; } // If we changed the daemon's status, adjust it. if ($status != $daemon->getStatus()) { $daemon->setStatus($status); // ...and write it, if we're in a context where that's reasonable. if ($this->allowStatusWrites) { $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); $daemon->save(); unset($guard); } } // If the daemon no longer matches the filter, get rid of it. if ($filter) { if (empty($filter[$daemon->getStatus()])) { unset($daemons[$key]); } } } return $daemons; } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->notIDs !== null) { $where[] = qsprintf( $conn_r, 'id NOT IN (%Ld)', $this->notIDs); } if ($this->getStatusConstants()) { $where[] = qsprintf( $conn_r, 'status IN (%Ls)', $this->getStatusConstants()); } if ($this->daemonClasses !== null) { $where[] = qsprintf( $conn_r, 'daemon IN (%Ls)', $this->daemonClasses); } if ($this->daemonIDs !== null) { $where[] = qsprintf( $conn_r, 'daemonID IN (%Ls)', $this->daemonIDs); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } private function getStatusConstants() { $status = $this->status; switch ($status) { case self::STATUS_ALL: return array(); case self::STATUS_RUNNING: return array( PhabricatorDaemonLog::STATUS_RUNNING, ); case self::STATUS_ALIVE: return array( PhabricatorDaemonLog::STATUS_UNKNOWN, PhabricatorDaemonLog::STATUS_RUNNING, PhabricatorDaemonLog::STATUS_WAIT, PhabricatorDaemonLog::STATUS_EXITING, ); default: throw new Exception(pht('Unknown status "%s"!', $status)); } } public function getQueryApplicationClass() { return 'PhabricatorDaemonsApplication'; } } diff --git a/src/applications/differential/constants/DifferentialAction.php b/src/applications/differential/constants/DifferentialAction.php index 9238990161..48a263c45a 100644 --- a/src/applications/differential/constants/DifferentialAction.php +++ b/src/applications/differential/constants/DifferentialAction.php @@ -1,138 +1,138 @@ <?php final class DifferentialAction { const ACTION_CLOSE = 'commit'; const ACTION_COMMENT = 'none'; const ACTION_ACCEPT = 'accept'; const ACTION_REJECT = 'reject'; const ACTION_RETHINK = 'rethink'; const ACTION_ABANDON = 'abandon'; const ACTION_REQUEST = 'request_review'; const ACTION_RECLAIM = 'reclaim'; const ACTION_UPDATE = 'update'; const ACTION_RESIGN = 'resign'; const ACTION_SUMMARIZE = 'summarize'; const ACTION_TESTPLAN = 'testplan'; const ACTION_CREATE = 'create'; const ACTION_ADDREVIEWERS = 'add_reviewers'; const ACTION_ADDCCS = 'add_ccs'; const ACTION_CLAIM = 'claim'; const ACTION_REOPEN = 'reopen'; public static function getBasicStoryText($action, $author_name) { switch ($action) { - case DifferentialAction::ACTION_COMMENT: + case self::ACTION_COMMENT: $title = pht('%s commented on this revision.', $author_name); break; - case DifferentialAction::ACTION_ACCEPT: + case self::ACTION_ACCEPT: $title = pht('%s accepted this revision.', $author_name); break; - case DifferentialAction::ACTION_REJECT: + case self::ACTION_REJECT: $title = pht('%s requested changes to this revision.', $author_name); break; - case DifferentialAction::ACTION_RETHINK: + case self::ACTION_RETHINK: $title = pht('%s planned changes to this revision.', $author_name); break; - case DifferentialAction::ACTION_ABANDON: + case self::ACTION_ABANDON: $title = pht('%s abandoned this revision.', $author_name); break; - case DifferentialAction::ACTION_CLOSE: + case self::ACTION_CLOSE: $title = pht('%s closed this revision.', $author_name); break; - case DifferentialAction::ACTION_REQUEST: + case self::ACTION_REQUEST: $title = pht('%s requested a review of this revision.', $author_name); break; - case DifferentialAction::ACTION_RECLAIM: + case self::ACTION_RECLAIM: $title = pht('%s reclaimed this revision.', $author_name); break; - case DifferentialAction::ACTION_UPDATE: + case self::ACTION_UPDATE: $title = pht('%s updated this revision.', $author_name); break; - case DifferentialAction::ACTION_RESIGN: + case self::ACTION_RESIGN: $title = pht('%s resigned from this revision.', $author_name); break; - case DifferentialAction::ACTION_SUMMARIZE: + case self::ACTION_SUMMARIZE: $title = pht('%s summarized this revision.', $author_name); break; - case DifferentialAction::ACTION_TESTPLAN: + case self::ACTION_TESTPLAN: $title = pht('%s explained the test plan for this revision.', $author_name); break; - case DifferentialAction::ACTION_CREATE: + case self::ACTION_CREATE: $title = pht('%s created this revision.', $author_name); break; - case DifferentialAction::ACTION_ADDREVIEWERS: + case self::ACTION_ADDREVIEWERS: $title = pht('%s added reviewers to this revision.', $author_name); break; - case DifferentialAction::ACTION_ADDCCS: + case self::ACTION_ADDCCS: $title = pht('%s added CCs to this revision.', $author_name); break; - case DifferentialAction::ACTION_CLAIM: + case self::ACTION_CLAIM: $title = pht('%s commandeered this revision.', $author_name); break; - case DifferentialAction::ACTION_REOPEN: + case self::ACTION_REOPEN: $title = pht('%s reopened this revision.', $author_name); break; case DifferentialTransaction::TYPE_INLINE: $title = pht( '%s added an inline comment.', $author_name); break; default: $title = pht('Ghosts happened to this revision.'); break; } return $title; } public static function getActionVerb($action) { $verbs = array( self::ACTION_COMMENT => pht('Comment'), self::ACTION_ACCEPT => pht("Accept Revision \xE2\x9C\x94"), self::ACTION_REJECT => pht("Request Changes \xE2\x9C\x98"), self::ACTION_RETHINK => pht("Plan Changes \xE2\x9C\x98"), self::ACTION_ABANDON => pht('Abandon Revision'), self::ACTION_REQUEST => pht('Request Review'), self::ACTION_RECLAIM => pht('Reclaim Revision'), self::ACTION_RESIGN => pht('Resign as Reviewer'), self::ACTION_ADDREVIEWERS => pht('Add Reviewers'), self::ACTION_ADDCCS => pht('Add Subscribers'), self::ACTION_CLOSE => pht('Close Revision'), self::ACTION_CLAIM => pht('Commandeer Revision'), self::ACTION_REOPEN => pht('Reopen'), ); if (!empty($verbs[$action])) { return $verbs[$action]; } else { return 'brazenly '.$action; } } public static function allowReviewers($action) { - if ($action == DifferentialAction::ACTION_ADDREVIEWERS || - $action == DifferentialAction::ACTION_REQUEST || - $action == DifferentialAction::ACTION_RESIGN) { + if ($action == self::ACTION_ADDREVIEWERS || + $action == self::ACTION_REQUEST || + $action == self::ACTION_RESIGN) { return true; } return false; } } diff --git a/src/applications/differential/constants/DifferentialChangeType.php b/src/applications/differential/constants/DifferentialChangeType.php index 0a4f7861cc..2ce5392165 100644 --- a/src/applications/differential/constants/DifferentialChangeType.php +++ b/src/applications/differential/constants/DifferentialChangeType.php @@ -1,111 +1,111 @@ <?php final class DifferentialChangeType { const TYPE_ADD = 1; const TYPE_CHANGE = 2; const TYPE_DELETE = 3; const TYPE_MOVE_AWAY = 4; const TYPE_COPY_AWAY = 5; const TYPE_MOVE_HERE = 6; const TYPE_COPY_HERE = 7; const TYPE_MULTICOPY = 8; const TYPE_MESSAGE = 9; const TYPE_CHILD = 10; const FILE_TEXT = 1; const FILE_IMAGE = 2; const FILE_BINARY = 3; const FILE_DIRECTORY = 4; const FILE_SYMLINK = 5; const FILE_DELETED = 6; const FILE_NORMAL = 7; const FILE_SUBMODULE = 8; public static function getSummaryCharacterForChangeType($type) { static $types = array( self::TYPE_ADD => 'A', self::TYPE_CHANGE => 'M', self::TYPE_DELETE => 'D', self::TYPE_MOVE_AWAY => 'V', self::TYPE_COPY_AWAY => 'P', self::TYPE_MOVE_HERE => 'V', self::TYPE_COPY_HERE => 'P', self::TYPE_MULTICOPY => 'P', self::TYPE_MESSAGE => 'Q', self::TYPE_CHILD => '@', ); return idx($types, coalesce($type, '?'), '~'); } public static function getShortNameForFileType($type) { static $names = array( self::FILE_TEXT => null, self::FILE_DIRECTORY => 'dir', self::FILE_IMAGE => 'img', self::FILE_BINARY => 'bin', self::FILE_SYMLINK => 'sym', self::FILE_SUBMODULE => 'sub', ); return idx($names, coalesce($type, '?'), '???'); } public static function isOldLocationChangeType($type) { static $types = array( - DifferentialChangeType::TYPE_MOVE_AWAY => true, - DifferentialChangeType::TYPE_COPY_AWAY => true, - DifferentialChangeType::TYPE_MULTICOPY => true, + self::TYPE_MOVE_AWAY => true, + self::TYPE_COPY_AWAY => true, + self::TYPE_MULTICOPY => true, ); return isset($types[$type]); } public static function isNewLocationChangeType($type) { static $types = array( - DifferentialChangeType::TYPE_MOVE_HERE => true, - DifferentialChangeType::TYPE_COPY_HERE => true, + self::TYPE_MOVE_HERE => true, + self::TYPE_COPY_HERE => true, ); return isset($types[$type]); } public static function isDeleteChangeType($type) { static $types = array( - DifferentialChangeType::TYPE_DELETE => true, - DifferentialChangeType::TYPE_MOVE_AWAY => true, - DifferentialChangeType::TYPE_MULTICOPY => true, + self::TYPE_DELETE => true, + self::TYPE_MOVE_AWAY => true, + self::TYPE_MULTICOPY => true, ); return isset($types[$type]); } public static function isCreateChangeType($type) { static $types = array( - DifferentialChangeType::TYPE_ADD => true, - DifferentialChangeType::TYPE_COPY_HERE => true, - DifferentialChangeType::TYPE_MOVE_HERE => true, + self::TYPE_ADD => true, + self::TYPE_COPY_HERE => true, + self::TYPE_MOVE_HERE => true, ); return isset($types[$type]); } public static function isModifyChangeType($type) { static $types = array( - DifferentialChangeType::TYPE_CHANGE => true, + self::TYPE_CHANGE => true, ); return isset($types[$type]); } public static function getFullNameForChangeType($type) { $types = array( self::TYPE_ADD => pht('Added'), self::TYPE_CHANGE => pht('Modified'), self::TYPE_DELETE => pht('Deleted'), self::TYPE_MOVE_AWAY => pht('Moved Away'), self::TYPE_COPY_AWAY => pht('Copied Away'), self::TYPE_MOVE_HERE => pht('Moved Here'), self::TYPE_COPY_HERE => pht('Copied Here'), self::TYPE_MULTICOPY => pht('Deleted After Multiple Copy'), self::TYPE_MESSAGE => pht('Commit Message'), self::TYPE_CHILD => pht('Contents Modified'), ); return idx($types, coalesce($type, '?'), 'Unknown'); } } diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php index 5cb6fd7e87..2857d24272 100644 --- a/src/applications/differential/storage/DifferentialTransaction.php +++ b/src/applications/differential/storage/DifferentialTransaction.php @@ -1,665 +1,665 @@ <?php final class DifferentialTransaction extends PhabricatorApplicationTransaction { private $isCommandeerSideEffect; public function setIsCommandeerSideEffect($is_side_effect) { $this->isCommandeerSideEffect = $is_side_effect; return $this; } public function getIsCommandeerSideEffect() { return $this->isCommandeerSideEffect; } const TYPE_INLINE = 'differential:inline'; const TYPE_UPDATE = 'differential:update'; const TYPE_ACTION = 'differential:action'; const TYPE_STATUS = 'differential:status'; public function getApplicationName() { return 'differential'; } public function getApplicationTransactionType() { return DifferentialRevisionPHIDType::TYPECONST; } public function getApplicationTransactionCommentObject() { return new DifferentialTransactionComment(); } public function getApplicationTransactionViewObject() { return new DifferentialTransactionView(); } public function shouldHide() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_UPDATE: // Older versions of this transaction have an ID for the new value, // and/or do not record the old value. Only hide the transaction if // the new value is a PHID, indicating that this is a newer style // transaction. if ($old === null) { if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) { return true; } } break; case PhabricatorTransactions::TYPE_EDGE: $add = array_diff_key($new, $old); $rem = array_diff_key($old, $new); // Hide metadata-only edge transactions. These correspond to users // accepting or rejecting revisions, but the change is always explicit // because of the TYPE_ACTION transaction. Rendering these transactions // just creates clutter. if (!$add && !$rem) { return true; } break; } return parent::shouldHide(); } public function isInlineCommentTransaction() { switch ($this->getTransactionType()) { case self::TYPE_INLINE: return true; } return parent::isInlineCommentTransaction(); } public function getRequiredHandlePHIDs() { $phids = parent::getRequiredHandlePHIDs(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_ACTION: if ($new == DifferentialAction::ACTION_CLOSE && $this->getMetadataValue('isCommitClose')) { $phids[] = $this->getMetadataValue('commitPHID'); if ($this->getMetadataValue('committerPHID')) { $phids[] = $this->getMetadataValue('committerPHID'); } if ($this->getMetadataValue('authorPHID')) { $phids[] = $this->getMetadataValue('authorPHID'); } } break; case self::TYPE_UPDATE: if ($new) { $phids[] = $new; } break; } return $phids; } public function getActionStrength() { switch ($this->getTransactionType()) { case self::TYPE_ACTION: return 3; case self::TYPE_UPDATE: return 2; } return parent::getActionStrength(); } public function getActionName() { switch ($this->getTransactionType()) { case self::TYPE_INLINE: return pht('Commented On'); case self::TYPE_UPDATE: $old = $this->getOldValue(); if ($old === null) { return pht('Request'); } else { return pht('Updated'); } case self::TYPE_ACTION: $map = array( DifferentialAction::ACTION_ACCEPT => pht('Accepted'), DifferentialAction::ACTION_REJECT => pht('Requested Changes To'), DifferentialAction::ACTION_RETHINK => pht('Planned Changes To'), DifferentialAction::ACTION_ABANDON => pht('Abandoned'), DifferentialAction::ACTION_CLOSE => pht('Closed'), DifferentialAction::ACTION_REQUEST => pht('Requested A Review Of'), DifferentialAction::ACTION_RESIGN => pht('Resigned From'), DifferentialAction::ACTION_ADDREVIEWERS => pht('Added Reviewers'), DifferentialAction::ACTION_CLAIM => pht('Commandeered'), DifferentialAction::ACTION_REOPEN => pht('Reopened'), ); $name = idx($map, $this->getNewValue()); if ($name !== null) { return $name; } break; } return parent::getActionName(); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_SUBSCRIBERS; $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CC; break; case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED; break; } break; case self::TYPE_UPDATE: $old = $this->getOldValue(); if ($old === null) { $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEW_REQUEST; } else { $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_UPDATED; } break; case PhabricatorTransactions::TYPE_EDGE: switch ($this->getMetadataValue('edge:type')) { case DifferentialRevisionHasReviewerEdgeType::EDGECONST: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_REVIEWERS; break; } break; case PhabricatorTransactions::TYPE_COMMENT: case self::TYPE_INLINE: $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT; break; } if (!$tags) { $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_OTHER; } return $tags; } public function getTitle() { $author_phid = $this->getAuthorPHID(); $author_handle = $this->renderHandleLink($author_phid); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_INLINE: return pht( '%s added inline comments.', $author_handle); case self::TYPE_UPDATE: if ($this->getMetadataValue('isCommitUpdate')) { return pht( 'This revision was automatically updated to reflect the '. 'committed changes.'); } else if ($new) { // TODO: Migrate to PHIDs and use handles here? if (phid_get_type($new) == DifferentialDiffPHIDType::TYPECONST) { return pht( '%s updated this revision to %s.', $author_handle, $this->renderHandleLink($new)); } else { return pht( '%s updated this revision.', $author_handle); } } else { return pht( '%s updated this revision.', $author_handle); } case self::TYPE_ACTION: switch ($new) { case DifferentialAction::ACTION_CLOSE: if (!$this->getMetadataValue('isCommitClose')) { return DifferentialAction::getBasicStoryText( $new, $author_handle); } $commit_name = $this->renderHandleLink( $this->getMetadataValue('commitPHID')); $committer_phid = $this->getMetadataValue('committerPHID'); $author_phid = $this->getMetadataValue('authorPHID'); if ($this->getHandleIfExists($committer_phid)) { $committer_name = $this->renderHandleLink($committer_phid); } else { $committer_name = $this->getMetadataValue('committerName'); } if ($this->getHandleIfExists($author_phid)) { $author_name = $this->renderHandleLink($author_phid); } else { $author_name = $this->getMetadataValue('authorName'); } if ($committer_name && ($committer_name != $author_name)) { return pht( 'Closed by commit %s (authored by %s, committed by %s).', $commit_name, $author_name, $committer_name); } else { return pht( 'Closed by commit %s (authored by %s).', $commit_name, $author_name); } break; default: return DifferentialAction::getBasicStoryText($new, $author_handle); } break; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return pht( 'This revision is now accepted and ready to land.'); case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return pht( 'This revision now requires changes to proceed.'); case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return pht( 'This revision now requires review to proceed.'); } } return parent::getTitle(); } public function renderExtraInformationLink() { if ($this->getMetadataValue('revisionMatchData')) { $details_href = '/differential/revision/closedetails/'.$this->getPHID().'/'; $details_link = javelin_tag( 'a', array( 'href' => $details_href, 'sigil' => 'workflow', ), pht('Explain Why')); return $details_link; } return parent::renderExtraInformationLink(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $author_link = $this->renderHandleLink($author_phid); $object_link = $this->renderHandleLink($object_phid); switch ($this->getTransactionType()) { case self::TYPE_INLINE: return pht( '%s added inline comments to %s.', $author_link, $object_link); case self::TYPE_UPDATE: return pht( '%s updated the diff for %s.', $author_link, $object_link); case self::TYPE_ACTION: switch ($new) { case DifferentialAction::ACTION_ACCEPT: return pht( '%s accepted %s.', $author_link, $object_link); case DifferentialAction::ACTION_REJECT: return pht( '%s requested changes to %s.', $author_link, $object_link); case DifferentialAction::ACTION_RETHINK: return pht( '%s planned changes to %s.', $author_link, $object_link); case DifferentialAction::ACTION_ABANDON: return pht( '%s abandoned %s.', $author_link, $object_link); case DifferentialAction::ACTION_CLOSE: if (!$this->getMetadataValue('isCommitClose')) { return pht( '%s closed %s.', $author_link, $object_link); } else { $commit_name = $this->renderHandleLink( $this->getMetadataValue('commitPHID')); $committer_phid = $this->getMetadataValue('committerPHID'); $author_phid = $this->getMetadataValue('authorPHID'); if ($this->getHandleIfExists($committer_phid)) { $committer_name = $this->renderHandleLink($committer_phid); } else { $committer_name = $this->getMetadataValue('committerName'); } if ($this->getHandleIfExists($author_phid)) { $author_name = $this->renderHandleLink($author_phid); } else { $author_name = $this->getMetadataValue('authorName'); } // Check if the committer and author are the same. They're the // same if both resolved and are the same user, or if neither // resolved and the text is identical. if ($committer_phid && $author_phid) { $same_author = ($committer_phid == $author_phid); } else if (!$committer_phid && !$author_phid) { $same_author = ($committer_name == $author_name); } else { $same_author = false; } if ($committer_name && !$same_author) { return pht( '%s closed %s by committing %s (authored by %s).', $author_link, $object_link, $commit_name, $author_name); } else { return pht( '%s closed %s by committing %s.', $author_link, $object_link, $commit_name); } } break; case DifferentialAction::ACTION_REQUEST: return pht( '%s requested review of %s.', $author_link, $object_link); case DifferentialAction::ACTION_RECLAIM: return pht( '%s reclaimed %s.', $author_link, $object_link); case DifferentialAction::ACTION_RESIGN: return pht( '%s resigned from %s.', $author_link, $object_link); case DifferentialAction::ACTION_CLAIM: return pht( '%s commandeered %s.', $author_link, $object_link); case DifferentialAction::ACTION_REOPEN: return pht( '%s reopened %s.', $author_link, $object_link); } break; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return pht( '%s is now accepted and ready to land.', $object_link); case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return pht( '%s now requires changes to proceed.', $object_link); case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return pht( '%s now requires review to proceed.', $object_link); } } return parent::getTitleForFeed(); } public function getIcon() { switch ($this->getTransactionType()) { case self::TYPE_INLINE: return 'fa-comment'; case self::TYPE_UPDATE: return 'fa-refresh'; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return 'fa-check'; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return 'fa-times'; case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return 'fa-undo'; } break; case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: return 'fa-check'; case DifferentialAction::ACTION_ACCEPT: return 'fa-check-circle-o'; case DifferentialAction::ACTION_REJECT: return 'fa-times-circle-o'; case DifferentialAction::ACTION_ABANDON: return 'fa-plane'; case DifferentialAction::ACTION_RETHINK: return 'fa-headphones'; case DifferentialAction::ACTION_REQUEST: return 'fa-refresh'; case DifferentialAction::ACTION_RECLAIM: case DifferentialAction::ACTION_REOPEN: return 'fa-bullhorn'; case DifferentialAction::ACTION_RESIGN: return 'fa-flag'; case DifferentialAction::ACTION_CLAIM: return 'fa-flag'; } case PhabricatorTransactions::TYPE_EDGE: switch ($this->getMetadataValue('edge:type')) { case DifferentialRevisionHasReviewerEdgeType::EDGECONST: return 'fa-user'; } } return parent::getIcon(); } public function shouldDisplayGroupWith(array $group) { // Never group status changes with other types of actions, they're indirect // and don't make sense when combined with direct actions. $type_status = self::TYPE_STATUS; if ($this->getTransactionType() == $type_status) { return false; } foreach ($group as $xaction) { if ($xaction->getTransactionType() == $type_status) { return false; } } return parent::shouldDisplayGroupWith($group); } public function getColor() { switch ($this->getTransactionType()) { case self::TYPE_UPDATE: return PhabricatorTransactions::COLOR_SKY; case self::TYPE_STATUS: switch ($this->getNewValue()) { case ArcanistDifferentialRevisionStatus::ACCEPTED: return PhabricatorTransactions::COLOR_GREEN; case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: return PhabricatorTransactions::COLOR_RED; case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: return PhabricatorTransactions::COLOR_ORANGE; } break; case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: return PhabricatorTransactions::COLOR_INDIGO; case DifferentialAction::ACTION_ACCEPT: return PhabricatorTransactions::COLOR_GREEN; case DifferentialAction::ACTION_REJECT: return PhabricatorTransactions::COLOR_RED; case DifferentialAction::ACTION_ABANDON: return PhabricatorTransactions::COLOR_INDIGO; case DifferentialAction::ACTION_RETHINK: return PhabricatorTransactions::COLOR_RED; case DifferentialAction::ACTION_REQUEST: return PhabricatorTransactions::COLOR_SKY; case DifferentialAction::ACTION_RECLAIM: return PhabricatorTransactions::COLOR_SKY; case DifferentialAction::ACTION_REOPEN: return PhabricatorTransactions::COLOR_SKY; case DifferentialAction::ACTION_RESIGN: return PhabricatorTransactions::COLOR_ORANGE; case DifferentialAction::ACTION_CLAIM: return PhabricatorTransactions::COLOR_YELLOW; } } return parent::getColor(); } public function getNoEffectDescription() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_EDGE: switch ($this->getMetadataValue('edge:type')) { case DifferentialRevisionHasReviewerEdgeType::EDGECONST: return pht( 'The reviewers you are trying to add are already reviewing '. 'this revision.'); } break; - case DifferentialTransaction::TYPE_ACTION: + case self::TYPE_ACTION: switch ($this->getNewValue()) { case DifferentialAction::ACTION_CLOSE: return pht('This revision is already closed.'); case DifferentialAction::ACTION_ABANDON: return pht('This revision has already been abandoned.'); case DifferentialAction::ACTION_RECLAIM: return pht( 'You can not reclaim this revision because his revision is '. 'not abandoned.'); case DifferentialAction::ACTION_REOPEN: return pht( 'You can not reopen this revision because this revision is '. 'not closed.'); case DifferentialAction::ACTION_RETHINK: return pht('This revision already requires changes.'); case DifferentialAction::ACTION_REQUEST: return pht('Review is already requested for this revision.'); case DifferentialAction::ACTION_RESIGN: return pht( 'You can not resign from this revision because you are not '. 'a reviewer.'); case DifferentialAction::ACTION_CLAIM: return pht( 'You can not commandeer this revision because you already own '. 'it.'); case DifferentialAction::ACTION_ACCEPT: return pht( 'You have already accepted this revision.'); case DifferentialAction::ACTION_REJECT: return pht( 'You have already requested changes to this revision.'); } break; } return parent::getNoEffectDescription(); } public function renderAsTextForDoorkeeper( DoorkeeperFeedStoryPublisher $publisher, PhabricatorFeedStory $story, array $xactions) { $body = parent::renderAsTextForDoorkeeper($publisher, $story, $xactions); $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == self::TYPE_INLINE) { $inlines[] = $xaction; } } // TODO: This is a bit gross, but far less bad than it used to be. It // could be further cleaned up at some point. if ($inlines) { $engine = PhabricatorMarkupEngine::newMarkupEngine(array()) ->setConfig('viewer', new PhabricatorUser()) ->setMode(PhutilRemarkupEngine::MODE_TEXT); $body .= "\n\n"; $body .= pht('Inline Comments'); $body .= "\n"; $changeset_ids = array(); foreach ($inlines as $inline) { $changeset_ids[] = $inline->getComment()->getChangesetID(); } $changesets = id(new DifferentialChangeset())->loadAllWhere( 'id IN (%Ld)', $changeset_ids); foreach ($inlines as $inline) { $comment = $inline->getComment(); $changeset = idx($changesets, $comment->getChangesetID()); if (!$changeset) { continue; } $filename = $changeset->getDisplayFilename(); $linenumber = $comment->getLineNumber(); $inline_text = $engine->markupText($comment->getContent()); $inline_text = rtrim($inline_text); $body .= "{$filename}:{$linenumber} {$inline_text}\n"; } } return $body; } } diff --git a/src/applications/diffusion/data/DiffusionPathChange.php b/src/applications/diffusion/data/DiffusionPathChange.php index b1d7286fdd..6f96057014 100644 --- a/src/applications/diffusion/data/DiffusionPathChange.php +++ b/src/applications/diffusion/data/DiffusionPathChange.php @@ -1,202 +1,202 @@ <?php final class DiffusionPathChange { private $path; private $commitIdentifier; private $commit; private $commitData; private $changeType; private $fileType; private $targetPath; private $targetCommitIdentifier; private $awayPaths = array(); final public function setPath($path) { $this->path = $path; return $this; } final public function getPath() { return $this->path; } public function setChangeType($change_type) { $this->changeType = $change_type; return $this; } public function getChangeType() { return $this->changeType; } public function setFileType($file_type) { $this->fileType = $file_type; return $this; } public function getFileType() { return $this->fileType; } public function setTargetPath($target_path) { $this->targetPath = $target_path; return $this; } public function getTargetPath() { return $this->targetPath; } public function setAwayPaths(array $away_paths) { $this->awayPaths = $away_paths; return $this; } public function getAwayPaths() { return $this->awayPaths; } final public function setCommitIdentifier($commit) { $this->commitIdentifier = $commit; return $this; } final public function getCommitIdentifier() { return $this->commitIdentifier; } final public function setTargetCommitIdentifier($target_commit_identifier) { $this->targetCommitIdentifier = $target_commit_identifier; return $this; } final public function getTargetCommitIdentifier() { return $this->targetCommitIdentifier; } final public function setCommit($commit) { $this->commit = $commit; return $this; } final public function getCommit() { return $this->commit; } final public function setCommitData($commit_data) { $this->commitData = $commit_data; return $this; } final public function getCommitData() { return $this->commitData; } final public function getEpoch() { if ($this->getCommit()) { return $this->getCommit()->getEpoch(); } return null; } final public function getAuthorName() { if ($this->getCommitData()) { return $this->getCommitData()->getAuthorName(); } return null; } final public function getSummary() { if (!$this->getCommitData()) { return null; } $message = $this->getCommitData()->getCommitMessage(); $first = idx(explode("\n", $message), 0); return substr($first, 0, 80); } final public static function convertToArcanistChanges(array $changes) { - assert_instances_of($changes, 'DiffusionPathChange'); + assert_instances_of($changes, __CLASS__); $direct = array(); $result = array(); foreach ($changes as $path) { $change = new ArcanistDiffChange(); $change->setCurrentPath($path->getPath()); $direct[] = $path->getPath(); $change->setType($path->getChangeType()); $file_type = $path->getFileType(); if ($file_type == DifferentialChangeType::FILE_NORMAL) { $file_type = DifferentialChangeType::FILE_TEXT; } $change->setFileType($file_type); $change->setOldPath($path->getTargetPath()); foreach ($path->getAwayPaths() as $away_path) { $change->addAwayPath($away_path); } $result[$path->getPath()] = $change; } return array_select_keys($result, $direct); } final public static function convertToDifferentialChangesets( PhabricatorUser $user, array $changes) { - assert_instances_of($changes, 'DiffusionPathChange'); + assert_instances_of($changes, __CLASS__); $arcanist_changes = self::convertToArcanistChanges($changes); $diff = DifferentialDiff::newEphemeralFromRawChanges( $arcanist_changes); return $diff->getChangesets(); } public function toDictionary() { $commit = $this->getCommit(); if ($commit) { $commit_dict = $commit->toDictionary(); } else { $commit_dict = array(); } $commit_data = $this->getCommitData(); if ($commit_data) { $commit_data_dict = $commit_data->toDictionary(); } else { $commit_data_dict = array(); } return array( 'path' => $this->getPath(), 'commitIdentifier' => $this->getCommitIdentifier(), 'commit' => $commit_dict, 'commitData' => $commit_data_dict, 'fileType' => $this->getFileType(), 'changeType' => $this->getChangeType(), 'targetPath' => $this->getTargetPath(), 'targetCommitIdentifier' => $this->getTargetCommitIdentifier(), 'awayPaths' => $this->getAwayPaths(), ); } public static function newFromConduit(array $dicts) { $results = array(); foreach ($dicts as $dict) { $commit = PhabricatorRepositoryCommit::newFromDictionary($dict['commit']); $commit_data = PhabricatorRepositoryCommitData::newFromDictionary( $dict['commitData']); $results[] = id(new DiffusionPathChange()) ->setPath($dict['path']) ->setCommitIdentifier($dict['commitIdentifier']) ->setCommit($commit) ->setCommitData($commit_data) ->setFileType($dict['fileType']) ->setChangeType($dict['changeType']) ->setTargetPath($dict['targetPath']) ->setTargetCommitIdentifier($dict['targetCommitIdentifier']) ->setAwayPaths($dict['awayPaths']); } return $results; } } diff --git a/src/applications/diviner/storage/DivinerLiveSymbol.php b/src/applications/diviner/storage/DivinerLiveSymbol.php index b6a7cde226..ba35eed255 100644 --- a/src/applications/diviner/storage/DivinerLiveSymbol.php +++ b/src/applications/diviner/storage/DivinerLiveSymbol.php @@ -1,266 +1,266 @@ <?php final class DivinerLiveSymbol extends DivinerDAO implements PhabricatorPolicyInterface, PhabricatorMarkupInterface, PhabricatorDestructibleInterface { protected $bookPHID; protected $context; protected $type; protected $name; protected $atomIndex; protected $graphHash; protected $identityHash; protected $nodeHash; protected $title; protected $titleSlugHash; protected $groupName; protected $summary; protected $isDocumentable = 0; private $book = self::ATTACHABLE; private $atom = self::ATTACHABLE; private $extends = self::ATTACHABLE; private $children = self::ATTACHABLE; protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'context' => 'text255?', 'type' => 'text32', 'name' => 'text255', 'atomIndex' => 'uint32', 'identityHash' => 'bytes12', 'graphHash' => 'text64?', 'title' => 'text?', 'titleSlugHash' => 'bytes12?', 'groupName' => 'text255?', 'summary' => 'text?', 'isDocumentable' => 'bool', 'nodeHash' => 'text64?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'identityHash' => array( 'columns' => array('identityHash'), 'unique' => true, ), 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'graphHash' => array( 'columns' => array('graphHash'), 'unique' => true, ), 'nodeHash' => array( 'columns' => array('nodeHash'), 'unique' => true, ), 'bookPHID' => array( 'columns' => array( 'bookPHID', 'type', 'name(64)', 'context(64)', 'atomIndex', ), ), 'name' => array( 'columns' => array('name(64)'), ), 'key_slug' => array( 'columns' => array('titleSlugHash'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(DivinerAtomPHIDType::TYPECONST); } public function getBook() { return $this->assertAttached($this->book); } public function attachBook(DivinerLiveBook $book) { $this->book = $book; return $this; } public function getAtom() { return $this->assertAttached($this->atom); } public function attachAtom(DivinerLiveAtom $atom) { $this->atom = DivinerAtom::newFromDictionary($atom->getAtomData()); return $this; } public function getURI() { $parts = array( 'book', $this->getBook()->getName(), $this->getType(), ); if ($this->getContext()) { $parts[] = $this->getContext(); } $parts[] = $this->getName(); if ($this->getAtomIndex()) { $parts[] = $this->getAtomIndex(); } return '/'.implode('/', $parts).'/'; } public function getSortKey() { // Sort articles before other types of content. Then, sort atoms in a // case-insensitive way. return sprintf( '%c:%s', ($this->getType() == DivinerAtom::TYPE_ARTICLE ? '0' : '1'), phutil_utf8_strtolower($this->getTitle())); } public function save() { // NOTE: The identity hash is just a sanity check because the unique tuple // on this table is way way too long to fit into a normal UNIQUE KEY. We // don't use it directly, but its existence prevents duplicate records. if (!$this->identityHash) { $this->identityHash = PhabricatorHash::digestForIndex( serialize( array( 'bookPHID' => $this->getBookPHID(), 'context' => $this->getContext(), 'type' => $this->getType(), 'name' => $this->getName(), 'index' => $this->getAtomIndex(), ))); } return parent::save(); } public function getTitle() { $title = parent::getTitle(); if (!strlen($title)) { $title = $this->getName(); } return $title; } public function setTitle($value) { $this->writeField('title', $value); if (strlen($value)) { $slug = DivinerAtomRef::normalizeTitleString($value); $hash = PhabricatorHash::digestForIndex($slug); $this->titleSlugHash = $hash; } else { $this->titleSlugHash = null; } return $this; } public function attachExtends(array $extends) { - assert_instances_of($extends, 'DivinerLiveSymbol'); + assert_instances_of($extends, __CLASS__); $this->extends = $extends; return $this; } public function getExtends() { return $this->assertAttached($this->extends); } public function attachChildren(array $children) { - assert_instances_of($children, 'DivinerLiveSymbol'); + assert_instances_of($children, __CLASS__); $this->children = $children; return $this; } public function getChildren() { return $this->assertAttached($this->children); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return $this->getBook()->getCapabilities(); } public function getPolicy($capability) { return $this->getBook()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBook()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Atoms inherit the policies of the books they are part of.'); } /* -( Markup Interface )--------------------------------------------------- */ public function getMarkupFieldKey($field) { return $this->getPHID().':'.$field.':'.$this->getGraphHash(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine('diviner'); } public function getMarkupText($field) { return $this->getAtom()->getDocblockText(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return true; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE symbolPHID = %s', id(new DivinerLiveAtom())->getTableName(), $this->getPHID()); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php index 18ca597a90..43891d0d07 100644 --- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php @@ -1,469 +1,469 @@ <?php /** * @task lease Lease Acquisition * @task resource Resource Allocation * @task log Logging */ abstract class DrydockBlueprintImplementation { private $activeResource; private $activeLease; private $instance; abstract public function getType(); abstract public function getInterface( DrydockResource $resource, DrydockLease $lease, $type); abstract public function isEnabled(); abstract public function getBlueprintName(); abstract public function getDescription(); public function getBlueprintClass() { return get_class($this); } protected function loadLease($lease_id) { // TODO: Get rid of this? $query = id(new DrydockLeaseQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($lease_id)) ->execute(); $lease = idx($query, $lease_id); if (!$lease) { throw new Exception("No such lease '{$lease_id}'!"); } return $lease; } protected function getInstance() { if (!$this->instance) { throw new Exception( 'Attach the blueprint instance to the implementation.'); } return $this->instance; } public function attachInstance(DrydockBlueprint $instance) { $this->instance = $instance; return $this; } public function getFieldSpecifications() { return array(); } public function getDetail($key, $default = null) { return $this->getInstance()->getDetail($key, $default); } /* -( Lease Acquisition )-------------------------------------------------- */ /** * @task lease */ final public function filterResource( DrydockResource $resource, DrydockLease $lease) { $scope = $this->pushActiveScope($resource, $lease); return $this->canAllocateLease($resource, $lease); } /** * Enforce basic checks on lease/resource compatibility. Allows resources to * reject leases if they are incompatible, even if the resource types match. * * For example, if a resource represents a 32-bit host, this method might * reject leases that need a 64-bit host. If a resource represents a working * copy of repository "X", this method might reject leases which need a * working copy of repository "Y". Generally, although the main types of * a lease and resource may match (e.g., both "host"), it may not actually be * possible to satisfy the lease with a specific resource. * * This method generally should not enforce limits or perform capacity * checks. Perform those in @{method:shouldAllocateLease} instead. It also * should not perform actual acquisition of the lease; perform that in * @{method:executeAcquireLease} instead. * * @param DrydockResource Candidiate resource to allocate the lease on. * @param DrydockLease Pending lease that wants to allocate here. * @return bool True if the resource and lease are compatible. * @task lease */ abstract protected function canAllocateLease( DrydockResource $resource, DrydockLease $lease); /** * @task lease */ final public function allocateLease( DrydockResource $resource, DrydockLease $lease) { $scope = $this->pushActiveScope($resource, $lease); $this->log('Trying to Allocate Lease'); $lease->setStatus(DrydockLeaseStatus::STATUS_ACQUIRING); $lease->setResourceID($resource->getID()); $lease->attachResource($resource); $ephemeral_lease = id(clone $lease)->makeEphemeral(); $allocated = false; $allocation_exception = null; $resource->openTransaction(); $resource->beginReadLocking(); $resource->reload(); // TODO: Policy stuff. $other_leases = id(new DrydockLease())->loadAllWhere( 'status IN (%Ld) AND resourceID = %d', array( DrydockLeaseStatus::STATUS_ACQUIRING, DrydockLeaseStatus::STATUS_ACTIVE, ), $resource->getID()); try { $allocated = $this->shouldAllocateLease( $resource, $ephemeral_lease, $other_leases); } catch (Exception $ex) { $allocation_exception = $ex; } if ($allocated) { $lease->save(); } $resource->endReadLocking(); if ($allocated) { $resource->saveTransaction(); $this->log('Allocated Lease'); } else { $resource->killTransaction(); $this->log('Failed to Allocate Lease'); } if ($allocation_exception) { $this->logException($allocation_exception); } return $allocated; } /** * Enforce lease limits on resources. Allows resources to reject leases if * they would become over-allocated by accepting them. * * For example, if a resource represents disk space, this method might check * how much space the lease is asking for (say, 200MB) and how much space is * left unallocated on the resource. It could grant the lease (return true) * if it has enough remaining space (more than 200MB), and reject the lease * (return false) if it does not (less than 200MB). * * A resource might also allow only exclusive leases. In this case it could * accept a new lease (return true) if there are no active leases, or reject * the new lease (return false) if there any other leases. * * A lock is held on the resource while this method executes to prevent * multiple processes from allocating leases on the resource simultaneously. * However, this means you should implement the method as cheaply as possible. * In particular, do not perform any actual acquisition or setup in this * method. * * If allocation is permitted, the lease will be moved to `ACQUIRING` status * and @{method:executeAcquireLease} will be called to actually perform * acquisition. * * General compatibility checks unrelated to resource limits and capacity are * better implemented in @{method:canAllocateLease}, which serves as a * cheap filter before lock acquisition. * * @param DrydockResource Candidate resource to allocate the lease on. * @param DrydockLease Pending lease that wants to allocate here. * @param list<DrydockLease> Other allocated and acquired leases on the * resource. The implementation can inspect them * to verify it can safely add the new lease. * @return bool True to allocate the lease on the resource; * false to reject it. * @task lease */ abstract protected function shouldAllocateLease( DrydockResource $resource, DrydockLease $lease, array $other_leases); /** * @task lease */ final public function acquireLease( DrydockResource $resource, DrydockLease $lease) { $scope = $this->pushActiveScope($resource, $lease); $this->log('Acquiring Lease'); $lease->setStatus(DrydockLeaseStatus::STATUS_ACTIVE); $lease->setResourceID($resource->getID()); $lease->attachResource($resource); $ephemeral_lease = id(clone $lease)->makeEphemeral(); try { $this->executeAcquireLease($resource, $ephemeral_lease); } catch (Exception $ex) { $this->logException($ex); throw $ex; } $lease->setAttributes($ephemeral_lease->getAttributes()); $lease->save(); $this->log('Acquired Lease'); } /** * Acquire and activate an allocated lease. Allows resources to peform setup * as leases are brought online. * * Following a successful call to @{method:canAllocateLease}, a lease is moved * to `ACQUIRING` status and this method is called after resource locks are * released. Nothing is locked while this method executes; the implementation * is free to perform expensive operations like writing files and directories, * executing commands, etc. * * After this method executes, the lease status is moved to `ACTIVE` and the * original leasee may access it. * * If acquisition fails, throw an exception. * * @param DrydockResource Resource to acquire a lease on. * @param DrydockLease Lease to acquire. * @return void */ abstract protected function executeAcquireLease( DrydockResource $resource, DrydockLease $lease); final public function releaseLease( DrydockResource $resource, DrydockLease $lease) { $scope = $this->pushActiveScope(null, $lease); $released = false; $lease->openTransaction(); $lease->beginReadLocking(); $lease->reload(); if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) { $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED); $lease->save(); $released = true; } $lease->endReadLocking(); $lease->saveTransaction(); if (!$released) { throw new Exception('Unable to release lease: lease not active!'); } } /* -( Resource Allocation )------------------------------------------------ */ public function canAllocateMoreResources(array $pool) { return true; } abstract protected function executeAllocateResource(DrydockLease $lease); final public function allocateResource(DrydockLease $lease) { $scope = $this->pushActiveScope(null, $lease); $this->log( pht( "Blueprint '%s': Allocating Resource for '%s'", $this->getBlueprintClass(), $lease->getLeaseName())); try { $resource = $this->executeAllocateResource($lease); $this->validateAllocatedResource($resource); } catch (Exception $ex) { $this->logException($ex); throw $ex; } return $resource; } /* -( Logging )------------------------------------------------------------ */ /** * @task log */ protected function logException(Exception $ex) { $this->log($ex->getMessage()); } /** * @task log */ protected function log($message) { self::writeLog( $this->activeResource, $this->activeLease, $message); } /** * @task log */ public static function writeLog( DrydockResource $resource = null, DrydockLease $lease = null, $message) { $log = id(new DrydockLog()) ->setEpoch(time()) ->setMessage($message); if ($resource) { $log->setResourceID($resource->getID()); } if ($lease) { $log->setLeaseID($lease->getID()); } $log->save(); } public static function getAllBlueprintImplementations() { static $list = null; if ($list === null) { $blueprints = id(new PhutilSymbolLoader()) ->setType('class') - ->setAncestorClass('DrydockBlueprintImplementation') + ->setAncestorClass(__CLASS__) ->setConcreteOnly(true) ->selectAndLoadSymbols(); $list = ipull($blueprints, 'name', 'name'); foreach ($list as $class_name => $ignored) { $list[$class_name] = newv($class_name, array()); } } return $list; } public static function getAllBlueprintImplementationsForResource($type) { static $groups = null; if ($groups === null) { $groups = mgroup(self::getAllBlueprintImplementations(), 'getType'); } return idx($groups, $type, array()); } public static function getNamedImplementation($class) { return idx(self::getAllBlueprintImplementations(), $class); } protected function newResourceTemplate($name) { $resource = id(new DrydockResource()) ->setBlueprintPHID($this->getInstance()->getPHID()) ->setBlueprintClass($this->getBlueprintClass()) ->setType($this->getType()) ->setStatus(DrydockResourceStatus::STATUS_PENDING) ->setName($name) ->save(); $this->activeResource = $resource; $this->log( pht( "Blueprint '%s': Created New Template", $this->getBlueprintClass())); return $resource; } /** * Sanity checks that the blueprint is implemented properly. */ private function validateAllocatedResource($resource) { $blueprint = $this->getBlueprintClass(); if (!($resource instanceof DrydockResource)) { throw new Exception( "Blueprint '{$blueprint}' is not properly implemented: ". "executeAllocateResource() must return an object of type ". "DrydockResource or throw, but returned something else."); } $current_status = $resource->getStatus(); $req_status = DrydockResourceStatus::STATUS_OPEN; if ($current_status != $req_status) { $current_name = DrydockResourceStatus::getNameForStatus($current_status); $req_name = DrydockResourceStatus::getNameForStatus($req_status); throw new Exception( "Blueprint '{$blueprint}' is not properly implemented: ". "executeAllocateResource() must return a DrydockResource with ". "status '{$req_name}', but returned one with status ". "'{$current_name}'."); } } private function pushActiveScope( DrydockResource $resource = null, DrydockLease $lease = null) { if (($this->activeResource !== null) || ($this->activeLease !== null)) { throw new Exception('There is already an active resource or lease!'); } $this->activeResource = $resource; $this->activeLease = $lease; return new DrydockBlueprintScopeGuard($this); } public function popActiveScope() { $this->activeResource = null; $this->activeLease = null; } } diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php index cb61e54490..252c922065 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -1,229 +1,229 @@ <?php final class DrydockLease extends DrydockDAO implements PhabricatorPolicyInterface { protected $resourceID; protected $resourceType; protected $until; protected $ownerPHID; protected $attributes = array(); protected $status = DrydockLeaseStatus::STATUS_PENDING; protected $taskID; private $resource = self::ATTACHABLE; private $releaseOnDestruction; /** * Flag this lease to be released when its destructor is called. This is * mostly useful if you have a script which acquires, uses, and then releases * a lease, as you don't need to explicitly handle exceptions to properly * release the lease. */ public function releaseOnDestruction() { $this->releaseOnDestruction = true; return $this; } public function __destruct() { if ($this->releaseOnDestruction) { if ($this->isActive()) { $this->release(); } } } public function getLeaseName() { return pht('Lease %d', $this->getID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attributes' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'uint32', 'until' => 'epoch?', 'resourceType' => 'text128', 'taskID' => 'id?', 'ownerPHID' => 'phid?', 'resourceID' => 'id?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function setAttribute($key, $value) { $this->attributes[$key] = $value; return $this; } public function getAttribute($key, $default = null) { return idx($this->attributes, $key, $default); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(DrydockLeasePHIDType::TYPECONST); } public function getInterface($type) { return $this->getResource()->getInterface($this, $type); } public function getResource() { return $this->assertAttached($this->resource); } public function attachResource(DrydockResource $resource = null) { $this->resource = $resource; return $this; } public function hasAttachedResource() { return ($this->resource !== null); } public function loadResource() { return id(new DrydockResource())->loadOneWhere( 'id = %d', $this->getResourceID()); } public function queueForActivation() { if ($this->getID()) { throw new Exception( 'Only new leases may be queued for activation!'); } $this->setStatus(DrydockLeaseStatus::STATUS_PENDING); $this->save(); $task = PhabricatorWorker::scheduleTask( 'DrydockAllocatorWorker', $this->getID()); // NOTE: Scheduling the task might execute it in-process, if we're running // from a CLI script. Reload the lease to make sure we have the most // up-to-date information. Normally, this has no effect. $this->reload(); $this->setTaskID($task->getID()); $this->save(); return $this; } public function release() { $this->assertActive(); $this->setStatus(DrydockLeaseStatus::STATUS_RELEASED); $this->save(); $this->resource = null; return $this; } public function isActive() { switch ($this->status) { case DrydockLeaseStatus::STATUS_ACTIVE: case DrydockLeaseStatus::STATUS_ACQUIRING: return true; } return false; } private function assertActive() { if (!$this->isActive()) { throw new Exception( 'Lease is not active! You can not interact with resources through '. 'an inactive lease.'); } } public static function waitForLeases(array $leases) { - assert_instances_of($leases, 'DrydockLease'); + assert_instances_of($leases, __CLASS__); $task_ids = array_filter(mpull($leases, 'getTaskID')); PhabricatorWorker::waitForTasks($task_ids); $unresolved = $leases; while (true) { foreach ($unresolved as $key => $lease) { $lease->reload(); switch ($lease->getStatus()) { case DrydockLeaseStatus::STATUS_ACTIVE: unset($unresolved[$key]); break; case DrydockLeaseStatus::STATUS_RELEASED: throw new Exception('Lease has already been released!'); case DrydockLeaseStatus::STATUS_EXPIRED: throw new Exception('Lease has already expired!'); case DrydockLeaseStatus::STATUS_BROKEN: throw new Exception('Lease has been broken!'); case DrydockLeaseStatus::STATUS_PENDING: case DrydockLeaseStatus::STATUS_ACQUIRING: break; default: throw new Exception('Unknown status??'); } } if ($unresolved) { sleep(1); } else { break; } } foreach ($leases as $lease) { $lease->attachResource($lease->loadResource()); } } public function waitUntilActive() { if (!$this->getID()) { $this->queueForActivation(); } self::waitForLeases(array($this)); return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { if ($this->getResource()) { return $this->getResource()->getPolicy($capability); } return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->getResource()) { return $this->getResource()->hasAutomaticCapability($capability, $viewer); } return false; } public function describeAutomaticCapability($capability) { return pht('Leases inherit policies from the resources they lease.'); } } diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php index 0c989d4948..f94c2601e8 100644 --- a/src/applications/feed/story/PhabricatorFeedStory.php +++ b/src/applications/feed/story/PhabricatorFeedStory.php @@ -1,532 +1,532 @@ <?php /** * Manages rendering and aggregation of a story. A story is an event (like a * user adding a comment) which may be represented in different forms on * different channels (like feed, notifications and realtime alerts). * * @task load Loading Stories * @task policy Policy Implementation */ abstract class PhabricatorFeedStory implements PhabricatorPolicyInterface, PhabricatorMarkupInterface { private $data; private $hasViewed; private $framed; private $hovercard = false; private $renderingTarget = PhabricatorApplicationTransaction::TARGET_HTML; private $handles = array(); private $objects = array(); private $projectPHIDs = array(); private $markupFieldOutput = array(); /* -( Loading Stories )---------------------------------------------------- */ /** * Given @{class:PhabricatorFeedStoryData} rows, load them into objects and * construct appropriate @{class:PhabricatorFeedStory} wrappers for each * data row. * * @param list<dict> List of @{class:PhabricatorFeedStoryData} rows from the * database. * @return list<PhabricatorFeedStory> List of @{class:PhabricatorFeedStory} * objects. * @task load */ public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) { $stories = array(); $data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows); foreach ($data as $story_data) { $class = $story_data->getStoryType(); try { $ok = class_exists($class) && - is_subclass_of($class, 'PhabricatorFeedStory'); + is_subclass_of($class, __CLASS__); } catch (PhutilMissingSymbolException $ex) { $ok = false; } // If the story type isn't a valid class or isn't a subclass of // PhabricatorFeedStory, decline to load it. if (!$ok) { continue; } $key = $story_data->getChronologicalKey(); $stories[$key] = newv($class, array($story_data)); } $object_phids = array(); $key_phids = array(); foreach ($stories as $key => $story) { $phids = array(); foreach ($story->getRequiredObjectPHIDs() as $phid) { $phids[$phid] = true; } if ($story->getPrimaryObjectPHID()) { $phids[$story->getPrimaryObjectPHID()] = true; } $key_phids[$key] = $phids; $object_phids += $phids; } $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($object_phids)) ->execute(); foreach ($key_phids as $key => $phids) { if (!$phids) { continue; } $story_objects = array_select_keys($objects, array_keys($phids)); if (count($story_objects) != count($phids)) { // An object this story requires either does not exist or is not visible // to the user. Decline to render the story. unset($stories[$key]); unset($key_phids[$key]); continue; } $stories[$key]->setObjects($story_objects); } // If stories are about PhabricatorProjectInterface objects, load the // projects the objects are a part of so we can render project tags // on the stories. $project_phids = array(); foreach ($objects as $object) { if ($object instanceof PhabricatorProjectInterface) { $project_phids[$object->getPHID()] = array(); } } if ($project_phids) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array_keys($project_phids)) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); foreach ($project_phids as $phid => $ignored) { $project_phids[$phid] = $edge_query->getDestinationPHIDs(array($phid)); } } $handle_phids = array(); foreach ($stories as $key => $story) { foreach ($story->getRequiredHandlePHIDs() as $phid) { $key_phids[$key][$phid] = true; } if ($story->getAuthorPHID()) { $key_phids[$key][$story->getAuthorPHID()] = true; } $object_phid = $story->getPrimaryObjectPHID(); $object_project_phids = idx($project_phids, $object_phid, array()); $story->setProjectPHIDs($object_project_phids); foreach ($object_project_phids as $dst) { $key_phids[$key][$dst] = true; } $handle_phids += $key_phids[$key]; } $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($handle_phids)) ->execute(); foreach ($key_phids as $key => $phids) { if (!$phids) { continue; } $story_handles = array_select_keys($handles, array_keys($phids)); $stories[$key]->setHandles($story_handles); } // Load and process story markup blocks. $engine = new PhabricatorMarkupEngine(); $engine->setViewer($viewer); foreach ($stories as $story) { foreach ($story->getFieldStoryMarkupFields() as $field) { $engine->addObject($story, $field); } } $engine->process(); foreach ($stories as $story) { foreach ($story->getFieldStoryMarkupFields() as $field) { $story->setMarkupFieldOutput( $field, $engine->getOutput($story, $field)); } } return $stories; } public function setMarkupFieldOutput($field, $output) { $this->markupFieldOutput[$field] = $output; return $this; } public function getMarkupFieldOutput($field) { if (!array_key_exists($field, $this->markupFieldOutput)) { throw new Exception( pht( 'Trying to retrieve markup field key "%s", but this feed story '. 'did not request it be rendered.', $field)); } return $this->markupFieldOutput[$field]; } public function setHovercard($hover) { $this->hovercard = $hover; return $this; } public function setRenderingTarget($target) { $this->validateRenderingTarget($target); $this->renderingTarget = $target; return $this; } public function getRenderingTarget() { return $this->renderingTarget; } private function validateRenderingTarget($target) { switch ($target) { case PhabricatorApplicationTransaction::TARGET_HTML: case PhabricatorApplicationTransaction::TARGET_TEXT: break; default: throw new Exception('Unknown rendering target: '.$target); break; } } public function setObjects(array $objects) { $this->objects = $objects; return $this; } public function getObject($phid) { $object = idx($this->objects, $phid); if (!$object) { throw new Exception( "Story is asking for an object it did not request ('{$phid}')!"); } return $object; } public function getPrimaryObject() { $phid = $this->getPrimaryObjectPHID(); if (!$phid) { throw new Exception('Story has no primary object!'); } return $this->getObject($phid); } public function getPrimaryObjectPHID() { return null; } final public function __construct(PhabricatorFeedStoryData $data) { $this->data = $data; } abstract public function renderView(); public function renderAsTextForDoorkeeper( DoorkeeperFeedStoryPublisher $publisher) { // TODO: This (and text rendering) should be properly abstract and // universal. However, this is far less bad than it used to be, and we // need to clean up more old feed code to really make this reasonable. return pht( '(Unable to render story of class %s for Doorkeeper.)', get_class($this)); } public function getRequiredHandlePHIDs() { return array(); } public function getRequiredObjectPHIDs() { return array(); } public function setHasViewed($has_viewed) { $this->hasViewed = $has_viewed; return $this; } public function getHasViewed() { return $this->hasViewed; } final public function setFramed($framed) { $this->framed = $framed; return $this; } final public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } final protected function getObjects() { return $this->objects; } final protected function getHandles() { return $this->handles; } final protected function getHandle($phid) { if (isset($this->handles[$phid])) { if ($this->handles[$phid] instanceof PhabricatorObjectHandle) { return $this->handles[$phid]; } } $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setName("Unloaded Object '{$phid}'"); return $handle; } final public function getStoryData() { return $this->data; } final public function getEpoch() { return $this->getStoryData()->getEpoch(); } final public function getChronologicalKey() { return $this->getStoryData()->getChronologicalKey(); } final public function getValue($key, $default = null) { return $this->getStoryData()->getValue($key, $default); } final public function getAuthorPHID() { return $this->getStoryData()->getAuthorPHID(); } final protected function renderHandleList(array $phids) { $items = array(); foreach ($phids as $phid) { $items[] = $this->linkTo($phid); } $list = null; switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_TEXT: $list = implode(', ', $items); break; case PhabricatorApplicationTransaction::TARGET_HTML: $list = phutil_implode_html(', ', $items); break; } return $list; } final protected function linkTo($phid) { $handle = $this->getHandle($phid); switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_TEXT: return $handle->getLinkName(); } // NOTE: We render our own link here to customize the styling and add // the '_top' target for framed feeds. $class = null; if ($handle->getType() == PhabricatorPeopleUserPHIDType::TYPECONST) { $class = 'phui-link-person'; } return javelin_tag( 'a', array( 'href' => $handle->getURI(), 'target' => $this->framed ? '_top' : null, 'sigil' => $this->hovercard ? 'hovercard' : null, 'meta' => $this->hovercard ? array('hoverPHID' => $phid) : null, 'class' => $class, ), $handle->getLinkName()); } final protected function renderString($str) { switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_TEXT: return $str; case PhabricatorApplicationTransaction::TARGET_HTML: return phutil_tag('strong', array(), $str); } } final public function renderSummary($text, $len = 128) { if ($len) { $text = id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs($len) ->truncateString($text); } switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_HTML: $text = phutil_escape_html_newlines($text); break; } return $text; } public function getNotificationAggregations() { return array(); } protected function newStoryView() { $view = id(new PHUIFeedStoryView()) ->setChronologicalKey($this->getChronologicalKey()) ->setEpoch($this->getEpoch()) ->setViewed($this->getHasViewed()); $project_phids = $this->getProjectPHIDs(); if ($project_phids) { $view->setTags($this->renderHandleList($project_phids)); } return $view; } public function setProjectPHIDs(array $phids) { $this->projectPHIDs = $phids; return $this; } public function getProjectPHIDs() { return $this->projectPHIDs; } public function getFieldStoryMarkupFields() { return array(); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getPHID() { return null; } /** * @task policy */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } /** * @task policy */ public function getPolicy($capability) { $policy_object = $this->getPrimaryPolicyObject(); if ($policy_object) { return $policy_object->getPolicy($capability); } // TODO: Remove this once all objects are policy-aware. For now, keep // respecting the `feed.public` setting. return PhabricatorEnv::getEnvConfig('feed.public') ? PhabricatorPolicies::POLICY_PUBLIC : PhabricatorPolicies::POLICY_USER; } /** * @task policy */ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { $policy_object = $this->getPrimaryPolicyObject(); if ($policy_object) { return $policy_object->hasAutomaticCapability($capability, $viewer); } return false; } public function describeAutomaticCapability($capability) { return null; } /** * Get the policy object this story is about, if such a policy object * exists. * * @return PhabricatorPolicyInterface|null Policy object, if available. * @task policy */ private function getPrimaryPolicyObject() { $primary_phid = $this->getPrimaryObjectPHID(); if (empty($this->objects[$primary_phid])) { $object = $this->objects[$primary_phid]; if ($object instanceof PhabricatorPolicyInterface) { return $object; } } return null; } /* -( PhabricatorMarkupInterface Implementation )--------------------------- */ public function getMarkupFieldKey($field) { return 'feed:'.$this->getChronologicalKey().':'.$field; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newMarkupEngine(array()); } public function getMarkupText($field) { throw new PhutilMethodNotImplementedException(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return true; } } diff --git a/src/applications/files/controller/PhabricatorFileComposeController.php b/src/applications/files/controller/PhabricatorFileComposeController.php index bb95cb67eb..d1e6041131 100644 --- a/src/applications/files/controller/PhabricatorFileComposeController.php +++ b/src/applications/files/controller/PhabricatorFileComposeController.php @@ -1,339 +1,339 @@ <?php final class PhabricatorFileComposeController extends PhabricatorFileController { public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $colors = array( 'red' => pht('Verbillion'), 'orange' => pht('Navel Orange'), 'yellow' => pht('Prim Goldenrod'), 'green' => pht('Lustrous Verdant'), 'blue' => pht('Tropical Deep'), 'sky' => pht('Wide Open Sky'), 'indigo' => pht('Pleated Khaki'), 'violet' => pht('Aged Merlot'), 'pink' => pht('Easter Bunny'), 'charcoal' => pht('Gemstone'), 'backdrop' => pht('Driven Snow'), ); $manifest = PHUIIconView::getSheetManifest(PHUIIconView::SPRITE_PROJECTS); if ($request->isFormPost()) { $project_phid = $request->getStr('projectPHID'); if ($project_phid) { $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withPHIDs(array($project_phid)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $icon = $project->getIcon(); $color = $project->getColor(); switch ($color) { case 'grey': $color = 'charcoal'; break; case 'checkered': $color = 'backdrop'; break; } } else { $icon = $request->getStr('icon'); $color = $request->getStr('color'); } if (!isset($colors[$color]) || !isset($manifest['projects-'.$icon])) { return new Aphront404Response(); } $root = dirname(phutil_get_library_root('phabricator')); $icon_file = $root.'/resources/sprite/projects_2x/'.$icon.'.png'; $icon_data = Filesystem::readFile($icon_file); $data = $this->composeImage($color, $icon_data); $file = PhabricatorFile::buildFromFileDataOrHash( $data, array( 'name' => 'project.png', 'profile' => true, 'canCDN' => true, )); if ($project_phid) { $edit_uri = '/project/profile/'.$project->getID().'/'; $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorProjectTransaction::TYPE_IMAGE) ->setNewValue($file->getPHID()); $editor = id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true); $editor->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } else { $content = array( 'phid' => $file->getPHID(), ); return id(new AphrontAjaxResponse())->setContent($content); } } $value_color = head_key($colors); $value_icon = head_key($manifest); $value_icon = substr($value_icon, strlen('projects-')); require_celerity_resource('people-profile-css'); $buttons = array(); foreach ($colors as $color => $name) { $buttons[] = javelin_tag( 'button', array( 'class' => 'grey profile-image-button', 'sigil' => 'has-tooltip compose-select-color', 'style' => 'margin: 0 8px 8px 0', 'meta' => array( 'color' => $color, 'tip' => $name, ), ), id(new PHUIIconView()) ->addClass('compose-background-'.$color)); } $sort_these_first = array( 'projects-fa-briefcase', 'projects-fa-tags', 'projects-fa-folder', 'projects-fa-group', 'projects-fa-bug', 'projects-fa-trash-o', 'projects-fa-calendar', 'projects-fa-flag-checkered', 'projects-fa-envelope', 'projects-fa-truck', 'projects-fa-lock', 'projects-fa-umbrella', 'projects-fa-cloud', 'projects-fa-building', 'projects-fa-credit-card', 'projects-fa-flask', ); $manifest = array_select_keys( $manifest, $sort_these_first) + $manifest; $icons = array(); $icon_quips = array( '8ball' => pht('Take a Risk'), 'alien' => pht('Foreign Interface'), 'announce' => pht('Louder is Better'), 'art' => pht('Unique Snowflake'), 'award' => pht('Shooting Star'), 'bacon' => pht('Healthy Vegetables'), 'bandaid' => pht('Durable Infrastructure'), 'beer' => pht('Healthy Vegetable Juice'), 'bomb' => pht('Imminent Success'), 'briefcase' => pht('Adventure Pack'), 'bug' => pht('Costumed Egg'), 'calendar' => pht('Everyone Loves Meetings'), 'cloud' => pht('Water Cycle'), 'coffee' => pht('Half-Whip Nonfat Soy Latte'), 'creditcard' => pht('Expense It'), 'death' => pht('Calcium Promotes Bone Health'), 'desktop' => pht('Magical Portal'), 'dropbox' => pht('Cardboard Box'), 'education' => pht('Debt'), 'experimental' => pht('CAUTION: Dangerous Chemicals'), 'facebook' => pht('Popular Social Network'), 'facility' => pht('Pollution Solves Problems'), 'film' => pht('Actual Physical Film'), 'forked' => pht('You Can\'t Eat Soup'), 'games' => pht('Serious Business'), 'ghost' => pht('Haunted'), 'gift' => pht('Surprise!'), 'globe' => pht('Scanner Sweep'), 'golf' => pht('Business Meeting'), 'heart' => pht('Undergoing a Major Surgery'), 'intergalactic' => pht('Jupiter'), 'lock' => pht('Extremely Secret'), 'mail' => pht('Oragami'), 'martini' => pht('Healthy Olive Drink'), 'medical' => pht('Medic!'), 'mobile' => pht('Cellular Telephone'), 'music' => pht("\xE2\x99\xAB"), 'news' => pht('Actual Physical Newspaper'), 'orgchart' => pht('It\'s Good to be King'), 'peoples' => pht('Angel and Devil'), 'piechart' => pht('Actual Physical Pie'), 'poison' => pht('Healthy Bone Juice'), 'putabirdonit' => pht('Put a Bird On It'), 'radiate' => pht('Radiant Beauty'), 'savings' => pht('Oink Oink'), 'search' => pht('Sleuthing'), 'shield' => pht('Royal Crest'), 'speed' => pht('Slow and Steady'), 'sprint' => pht('Fire Exit'), 'star' => pht('The More You Know'), 'storage' => pht('Stack of Pancakes'), 'tablet' => pht('Cellular Telephone For Giants'), 'travel' => pht('Pretty Clearly an Airplane'), 'twitter' => pht('Bird Stencil'), 'warning' => pht('No Caution Required, Everything Looks Safe'), 'whale' => pht('Friendly Walrus'), 'fa-flask' => pht('Experimental'), 'fa-briefcase' => pht('Briefcase'), 'fa-bug' => pht('Bug'), 'fa-building' => pht('Company'), 'fa-calendar' => pht('Deadline'), 'fa-cloud' => pht('The Cloud'), 'fa-credit-card' => pht('Accounting'), 'fa-envelope' => pht('Communication'), 'fa-flag-checkered' => pht('Goal'), 'fa-folder' => pht('Folder'), 'fa-group' => pht('Team'), 'fa-lock' => pht('Policy'), 'fa-tags' => pht('Tag'), 'fa-trash-o' => pht('Garbage'), 'fa-truck' => pht('Release'), 'fa-umbrella' => pht('An Umbrella'), ); foreach ($manifest as $icon => $spec) { $icon = substr($icon, strlen('projects-')); $icons[] = javelin_tag( 'button', array( 'class' => 'grey profile-image-button', 'sigil' => 'has-tooltip compose-select-icon', 'style' => 'margin: 0 8px 8px 0', 'meta' => array( 'icon' => $icon, 'tip' => idx($icon_quips, $icon, $icon), ), ), id(new PHUIIconView()) ->setSpriteIcon($icon) ->setSpriteSheet(PHUIIconView::SPRITE_PROJECTS)); } $dialog_id = celerity_generate_unique_node_id(); - $color_input_id = celerity_generate_unique_node_id();; + $color_input_id = celerity_generate_unique_node_id(); $icon_input_id = celerity_generate_unique_node_id(); $preview_id = celerity_generate_unique_node_id(); $preview = id(new PHUIIconView()) ->setID($preview_id) ->addClass('compose-background-'.$value_color) ->setSpriteIcon($value_icon) ->setSpriteSheet(PHUIIconView::SPRITE_PROJECTS); $color_input = javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'color', 'value' => $value_color, 'id' => $color_input_id, )); $icon_input = javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'icon', 'value' => $value_icon, 'id' => $icon_input_id, )); Javelin::initBehavior('phabricator-tooltips'); Javelin::initBehavior( 'icon-composer', array( 'dialogID' => $dialog_id, 'colorInputID' => $color_input_id, 'iconInputID' => $icon_input_id, 'previewID' => $preview_id, 'defaultColor' => $value_color, 'defaultIcon' => $value_icon, )); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setFormID($dialog_id) ->setClass('compose-dialog') ->setTitle(pht('Compose Image')) ->appendChild( phutil_tag( 'div', array( 'class' => 'compose-header', ), pht('Choose Background Color'))) ->appendChild($buttons) ->appendChild( phutil_tag( 'div', array( 'class' => 'compose-header', ), pht('Choose Icon'))) ->appendChild($icons) ->appendChild( phutil_tag( 'div', array( 'class' => 'compose-header', ), pht('Preview'))) ->appendChild($preview) ->appendChild($color_input) ->appendChild($icon_input) ->addCancelButton('/') ->addSubmitButton(pht('Save Image')); return id(new AphrontDialogResponse())->setDialog($dialog); } private function composeImage($color, $icon_data) { $icon_img = imagecreatefromstring($icon_data); $map = CelerityResourceTransformer::getCSSVariableMap(); $color_string = idx($map, $color, '#ff00ff'); $color_const = hexdec(trim($color_string, '#')); $canvas = imagecreatetruecolor(100, 100); imagefill($canvas, 0, 0, $color_const); imagecopy($canvas, $icon_img, 0, 0, 0, 0, 100, 100); return PhabricatorImageTransformer::saveImageDataInAnyFormat( $canvas, 'image/png'); } } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 34a2bb5df5..f8e99bc965 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1,1386 +1,1386 @@ <?php /** * Parameters * ========== * * When creating a new file using a method like @{method:newFromFileData}, these * parameters are supported: * * | name | Human readable filename. * | authorPHID | User PHID of uploader. * | ttl | Temporary file lifetime, in seconds. * | viewPolicy | File visibility policy. * | isExplicitUpload | Used to show users files they explicitly uploaded. * | canCDN | Allows the file to be cached and delivered over a CDN. * | mime-type | Optional, explicit file MIME type. * | builtin | Optional filename, identifies this as a builtin. * */ final class PhabricatorFile extends PhabricatorFileDAO implements PhabricatorApplicationTransactionInterface, PhabricatorTokenReceiverInterface, PhabricatorSubscribableInterface, PhabricatorFlaggableInterface, PhabricatorPolicyInterface, PhabricatorDestructibleInterface { const ONETIME_TEMPORARY_TOKEN_TYPE = 'file:onetime'; const STORAGE_FORMAT_RAW = 'raw'; const METADATA_IMAGE_WIDTH = 'width'; const METADATA_IMAGE_HEIGHT = 'height'; const METADATA_CAN_CDN = 'canCDN'; const METADATA_BUILTIN = 'builtin'; const METADATA_PARTIAL = 'partial'; const METADATA_PROFILE = 'profile'; protected $name; protected $mimeType; protected $byteSize; protected $authorPHID; protected $secretKey; protected $contentHash; protected $metadata = array(); protected $mailKey; protected $storageEngine; protected $storageFormat; protected $storageHandle; protected $ttl; protected $isExplicitUpload = 1; protected $viewPolicy = PhabricatorPolicies::POLICY_USER; protected $isPartial = 0; private $objects = self::ATTACHABLE; private $objectPHIDs = self::ATTACHABLE; private $originalFile = self::ATTACHABLE; public static function initializeNewFile() { $app = id(new PhabricatorApplicationQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withClasses(array('PhabricatorFilesApplication')) ->executeOne(); $view_policy = $app->getPolicy( FilesDefaultViewCapability::CAPABILITY); return id(new PhabricatorFile()) ->setViewPolicy($view_policy) ->setIsPartial(0) ->attachOriginalFile(null) ->attachObjects(array()) ->attachObjectPHIDs(array()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255?', 'mimeType' => 'text255?', 'byteSize' => 'uint64', 'storageEngine' => 'text32', 'storageFormat' => 'text32', 'storageHandle' => 'text255', 'authorPHID' => 'phid?', 'secretKey' => 'bytes20?', 'contentHash' => 'bytes40?', 'ttl' => 'epoch?', 'isExplicitUpload' => 'bool?', 'mailKey' => 'bytes20', 'isPartial' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'contentHash' => array( 'columns' => array('contentHash'), ), 'key_ttl' => array( 'columns' => array('ttl'), ), 'key_dateCreated' => array( 'columns' => array('dateCreated'), ), 'key_partial' => array( 'columns' => array('authorPHID', 'isPartial'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorFileFilePHIDType::TYPECONST); } public function save() { if (!$this->getSecretKey()) { $this->setSecretKey($this->generateSecretKey()); } if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getMonogram() { return 'F'.$this->getID(); } public static function readUploadedFileData($spec) { if (!$spec) { throw new Exception('No file was uploaded!'); } $err = idx($spec, 'error'); if ($err) { throw new PhabricatorFileUploadException($err); } $tmp_name = idx($spec, 'tmp_name'); $is_valid = @is_uploaded_file($tmp_name); if (!$is_valid) { throw new Exception('File is not an uploaded file.'); } $file_data = Filesystem::readFile($tmp_name); $file_size = idx($spec, 'size'); if (strlen($file_data) != $file_size) { throw new Exception('File size disagrees with uploaded size.'); } return $file_data; } public static function newFromPHPUpload($spec, array $params = array()) { $file_data = self::readUploadedFileData($spec); $file_name = nonempty( idx($params, 'name'), idx($spec, 'name')); $params = array( 'name' => $file_name, ) + $params; return self::newFromFileData($file_data, $params); } public static function newFromXHRUpload($data, array $params = array()) { return self::newFromFileData($data, $params); } /** * Given a block of data, try to load an existing file with the same content * if one exists. If it does not, build a new file. * * This method is generally used when we have some piece of semi-trusted data * like a diff or a file from a repository that we want to show to the user. * We can't just dump it out because it may be dangerous for any number of * reasons; instead, we need to serve it through the File abstraction so it * ends up on the CDN domain if one is configured and so on. However, if we * simply wrote a new file every time we'd potentially end up with a lot * of redundant data in file storage. * * To solve these problems, we use file storage as a cache and reuse the * same file again if we've previously written it. * * NOTE: This method unguards writes. * * @param string Raw file data. * @param dict Dictionary of file information. */ public static function buildFromFileDataOrHash( $data, array $params = array()) { $file = id(new PhabricatorFile())->loadOneWhere( 'name = %s AND contentHash = %s LIMIT 1', idx($params, 'name'), self::hashFileContent($data)); if (!$file) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $file = PhabricatorFile::newFromFileData($data, $params); + $file = self::newFromFileData($data, $params); unset($unguarded); } return $file; } public static function newFileFromContentHash($hash, array $params) { // Check to see if a file with same contentHash exist $file = id(new PhabricatorFile())->loadOneWhere( 'contentHash = %s LIMIT 1', $hash); if ($file) { // copy storageEngine, storageHandle, storageFormat $copy_of_storage_engine = $file->getStorageEngine(); $copy_of_storage_handle = $file->getStorageHandle(); $copy_of_storage_format = $file->getStorageFormat(); $copy_of_byte_size = $file->getByteSize(); $copy_of_mime_type = $file->getMimeType(); - $new_file = PhabricatorFile::initializeNewFile(); + $new_file = self::initializeNewFile(); $new_file->setByteSize($copy_of_byte_size); $new_file->setContentHash($hash); $new_file->setStorageEngine($copy_of_storage_engine); $new_file->setStorageHandle($copy_of_storage_handle); $new_file->setStorageFormat($copy_of_storage_format); $new_file->setMimeType($copy_of_mime_type); $new_file->copyDimensions($file); $new_file->readPropertiesFromParameters($params); $new_file->save(); return $new_file; } return $file; } public static function newChunkedFile( PhabricatorFileStorageEngine $engine, $length, array $params) { - $file = PhabricatorFile::initializeNewFile(); + $file = self::initializeNewFile(); $file->setByteSize($length); // TODO: We might be able to test the first chunk in order to figure // this out more reliably, since MIME detection usually examines headers. // However, enormous files are probably always either actually raw data // or reasonable to treat like raw data. $file->setMimeType('application/octet-stream'); $chunked_hash = idx($params, 'chunkedHash'); if ($chunked_hash) { $file->setContentHash($chunked_hash); } else { // See PhabricatorChunkedFileStorageEngine::getChunkedHash() for some // discussion of this. $seed = Filesystem::readRandomBytes(64); $hash = PhabricatorChunkedFileStorageEngine::getChunkedHashForInput( $seed); $file->setContentHash($hash); } $file->setStorageEngine($engine->getEngineIdentifier()); $file->setStorageHandle(PhabricatorFileChunk::newChunkHandle()); $file->setStorageFormat(self::STORAGE_FORMAT_RAW); $file->setIsPartial(1); $file->readPropertiesFromParameters($params); return $file; } private static function buildFromFileData($data, array $params = array()) { if (isset($params['storageEngines'])) { $engines = $params['storageEngines']; } else { $size = strlen($data); $engines = PhabricatorFileStorageEngine::loadStorageEngines($size); if (!$engines) { throw new Exception( pht( 'No configured storage engine can store this file. See '. '"Configuring File Storage" in the documentation for '. 'information on configuring storage engines.')); } } assert_instances_of($engines, 'PhabricatorFileStorageEngine'); if (!$engines) { throw new Exception(pht('No valid storage engines are available!')); } - $file = PhabricatorFile::initializeNewFile(); + $file = self::initializeNewFile(); $data_handle = null; $engine_identifier = null; $exceptions = array(); foreach ($engines as $engine) { $engine_class = get_class($engine); try { list($engine_identifier, $data_handle) = $file->writeToEngine( $engine, $data, $params); // We stored the file somewhere so stop trying to write it to other // places. break; } catch (PhabricatorFileStorageConfigurationException $ex) { // If an engine is outright misconfigured (or misimplemented), raise // that immediately since it probably needs attention. throw $ex; } catch (Exception $ex) { phlog($ex); // If an engine doesn't work, keep trying all the other valid engines // in case something else works. $exceptions[$engine_class] = $ex; } } if (!$data_handle) { throw new PhutilAggregateException( 'All storage engines failed to write file:', $exceptions); } $file->setByteSize(strlen($data)); $file->setContentHash(self::hashFileContent($data)); $file->setStorageEngine($engine_identifier); $file->setStorageHandle($data_handle); // TODO: This is probably YAGNI, but allows for us to do encryption or // compression later if we want. $file->setStorageFormat(self::STORAGE_FORMAT_RAW); $file->readPropertiesFromParameters($params); if (!$file->getMimeType()) { $tmp = new TempFile(); Filesystem::writeFile($tmp, $data); $file->setMimeType(Filesystem::getMimeType($tmp)); } try { $file->updateDimensions(false); } catch (Exception $ex) { // Do nothing } $file->save(); return $file; } public static function newFromFileData($data, array $params = array()) { $hash = self::hashFileContent($data); $file = self::newFileFromContentHash($hash, $params); if ($file) { return $file; } return self::buildFromFileData($data, $params); } public function migrateToEngine(PhabricatorFileStorageEngine $engine) { if (!$this->getID() || !$this->getStorageHandle()) { throw new Exception( "You can not migrate a file which hasn't yet been saved."); } $data = $this->loadFileData(); $params = array( 'name' => $this->getName(), ); list($new_identifier, $new_handle) = $this->writeToEngine( $engine, $data, $params); $old_engine = $this->instantiateStorageEngine(); $old_identifier = $this->getStorageEngine(); $old_handle = $this->getStorageHandle(); $this->setStorageEngine($new_identifier); $this->setStorageHandle($new_handle); $this->save(); $this->deleteFileDataIfUnused( $old_engine, $old_identifier, $old_handle); return $this; } private function writeToEngine( PhabricatorFileStorageEngine $engine, $data, array $params) { $engine_class = get_class($engine); $data_handle = $engine->writeFile($data, $params); if (!$data_handle || strlen($data_handle) > 255) { // This indicates an improperly implemented storage engine. throw new PhabricatorFileStorageConfigurationException( "Storage engine '{$engine_class}' executed writeFile() but did ". "not return a valid handle ('{$data_handle}') to the data: it ". "must be nonempty and no longer than 255 characters."); } $engine_identifier = $engine->getEngineIdentifier(); if (!$engine_identifier || strlen($engine_identifier) > 32) { throw new PhabricatorFileStorageConfigurationException( "Storage engine '{$engine_class}' returned an improper engine ". "identifier '{$engine_identifier}': it must be nonempty ". "and no longer than 32 characters."); } return array($engine_identifier, $data_handle); } /** * Download a remote resource over HTTP and save the response body as a file. * * This method respects `security.outbound-blacklist`, and protects against * HTTP redirection (by manually following "Location" headers and verifying * each destination). It does not protect against DNS rebinding. See * discussion in T6755. */ public static function newFromFileDownload($uri, array $params = array()) { $timeout = 5; $redirects = array(); $current = $uri; while (true) { try { if (count($redirects) > 10) { throw new Exception( pht('Too many redirects trying to fetch remote URI.')); } $resolved = PhabricatorEnv::requireValidRemoteURIForFetch( $current, array( 'http', 'https', )); list($resolved_uri, $resolved_domain) = $resolved; $current = new PhutilURI($current); if ($current->getProtocol() == 'http') { // For HTTP, we can use a pre-resolved URI to defuse DNS rebinding. $fetch_uri = $resolved_uri; $fetch_host = $resolved_domain; } else { // For HTTPS, we can't: cURL won't verify the SSL certificate if // the domain has been replaced with an IP. But internal services // presumably will not have valid certificates for rebindable // domain names on attacker-controlled domains, so the DNS rebinding // attack should generally not be possible anyway. $fetch_uri = $current; $fetch_host = null; } $future = id(new HTTPSFuture($fetch_uri)) ->setFollowLocation(false) ->setTimeout($timeout); if ($fetch_host !== null) { $future->addHeader('Host', $fetch_host); } list($status, $body, $headers) = $future->resolve(); if ($status->isRedirect()) { // This is an HTTP 3XX status, so look for a "Location" header. $location = null; foreach ($headers as $header) { list($name, $value) = $header; if (phutil_utf8_strtolower($name) == 'location') { $location = $value; break; } } // HTTP 3XX status with no "Location" header, just treat this like // a normal HTTP error. if ($location === null) { throw $status; } if (isset($redirects[$location])) { throw new Exception( pht( 'Encountered loop while following redirects.')); } $redirects[$location] = $location; $current = $location; // We'll fall off the bottom and go try this URI now. } else if ($status->isError()) { // This is something other than an HTTP 2XX or HTTP 3XX status, so // just bail out. throw $status; } else { // This is HTTP 2XX, so use the the response body to save the // file data. $params = $params + array( 'name' => basename($uri), ); return self::newFromFileData($body, $params); } } catch (Exception $ex) { if ($redirects) { throw new PhutilProxyException( pht( 'Failed to fetch remote URI "%s" after following %s redirect(s) '. '(%s): %s', $uri, new PhutilNumber(count($redirects)), implode(' > ', array_keys($redirects)), $ex->getMessage()), $ex); } else { throw $ex; } } } } public static function normalizeFileName($file_name) { $pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@"; $file_name = preg_replace($pattern, '_', $file_name); $file_name = preg_replace('@_+@', '_', $file_name); $file_name = trim($file_name, '_'); $disallowed_filenames = array( '.' => 'dot', '..' => 'dotdot', '' => 'file', ); $file_name = idx($disallowed_filenames, $file_name, $file_name); return $file_name; } public function delete() { // We want to delete all the rows which mark this file as the transformation // of some other file (since we're getting rid of it). We also delete all // the transformations of this file, so that a user who deletes an image // doesn't need to separately hunt down and delete a bunch of thumbnails and // resizes of it. $outbound_xforms = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withTransforms( array( array( 'originalPHID' => $this->getPHID(), 'transform' => true, ), )) ->execute(); foreach ($outbound_xforms as $outbound_xform) { $outbound_xform->delete(); } $inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 'transformedPHID = %s', $this->getPHID()); $this->openTransaction(); foreach ($inbound_xforms as $inbound_xform) { $inbound_xform->delete(); } $ret = parent::delete(); $this->saveTransaction(); $this->deleteFileDataIfUnused( $this->instantiateStorageEngine(), $this->getStorageEngine(), $this->getStorageHandle()); return $ret; } /** * Destroy stored file data if there are no remaining files which reference * it. */ public function deleteFileDataIfUnused( PhabricatorFileStorageEngine $engine, $engine_identifier, $handle) { // Check to see if any files are using storage. $usage = id(new PhabricatorFile())->loadAllWhere( 'storageEngine = %s AND storageHandle = %s LIMIT 1', $engine_identifier, $handle); // If there are no files using the storage, destroy the actual storage. if (!$usage) { try { $engine->deleteFile($handle); } catch (Exception $ex) { // In the worst case, we're leaving some data stranded in a storage // engine, which is not a big deal. phlog($ex); } } } public static function hashFileContent($data) { return sha1($data); } public function loadFileData() { $engine = $this->instantiateStorageEngine(); $data = $engine->readFile($this->getStorageHandle()); switch ($this->getStorageFormat()) { case self::STORAGE_FORMAT_RAW: $data = $data; break; default: throw new Exception('Unknown storage format.'); } return $data; } /** * Return an iterable which emits file content bytes. * * @param int Offset for the start of data. * @param int Offset for the end of data. * @return Iterable Iterable object which emits requested data. */ public function getFileDataIterator($begin = null, $end = null) { $engine = $this->instantiateStorageEngine(); return $engine->getFileDataIterator($this, $begin, $end); } public function getViewURI() { if (!$this->getPHID()) { throw new Exception( 'You must save a file before you can generate a view URI.'); } return $this->getCDNURI(null); } private function getCDNURI($token) { $name = self::normalizeFileName($this->getName()); $name = phutil_escape_uri($name); $parts = array(); $parts[] = 'file'; $parts[] = 'data'; // If this is an instanced install, add the instance identifier to the URI. // Instanced configurations behind a CDN may not be able to control the // request domain used by the CDN (as with AWS CloudFront). Embedding the // instance identity in the path allows us to distinguish between requests // originating from different instances but served through the same CDN. $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if (strlen($instance)) { $parts[] = '@'.$instance; } $parts[] = $this->getSecretKey(); $parts[] = $this->getPHID(); if ($token) { $parts[] = $token; } $parts[] = $name; $path = '/'.implode('/', $parts); // If this file is only partially uploaded, we're just going to return a // local URI to make sure that Ajax works, since the page is inevitably // going to give us an error back. if ($this->getIsPartial()) { return PhabricatorEnv::getURI($path); } else { return PhabricatorEnv::getCDNURI($path); } } /** * Get the CDN URI for this file, including a one-time-use security token. * */ public function getCDNURIWithToken() { if (!$this->getPHID()) { throw new Exception( 'You must save a file before you can generate a CDN URI.'); } return $this->getCDNURI($this->generateOneTimeToken()); } public function getInfoURI() { return '/'.$this->getMonogram(); } public function getBestURI() { if ($this->isViewableInBrowser()) { return $this->getViewURI(); } else { return $this->getInfoURI(); } } public function getDownloadURI() { $uri = id(new PhutilURI($this->getViewURI())) ->setQueryParam('download', true); return (string) $uri; } public function getURIForTransform(PhabricatorFileTransform $transform) { return $this->getTransformedURI($transform->getTransformKey()); } private function getTransformedURI($transform) { $parts = array(); $parts[] = 'file'; $parts[] = 'xform'; $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if (strlen($instance)) { $parts[] = '@'.$instance; } $parts[] = $transform; $parts[] = $this->getPHID(); $parts[] = $this->getSecretKey(); $path = implode('/', $parts); $path = $path.'/'; return PhabricatorEnv::getCDNURI($path); } public function isViewableInBrowser() { return ($this->getViewableMimeType() !== null); } public function isViewableImage() { if (!$this->isViewableInBrowser()) { return false; } $mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type); } public function isAudio() { if (!$this->isViewableInBrowser()) { return false; } $mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type); } public function isTransformableImage() { // NOTE: The way the 'gd' extension works in PHP is that you can install it // with support for only some file types, so it might be able to handle // PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup // warns you if you don't have complete support. $matches = null; $ok = preg_match( '@^image/(gif|png|jpe?g)@', $this->getViewableMimeType(), $matches); if (!$ok) { return false; } switch ($matches[1]) { case 'jpg'; case 'jpeg': return function_exists('imagejpeg'); break; case 'png': return function_exists('imagepng'); break; case 'gif': return function_exists('imagegif'); break; default: throw new Exception('Unknown type matched as image MIME type.'); } } public static function getTransformableImageFormats() { $supported = array(); if (function_exists('imagejpeg')) { $supported[] = 'jpg'; } if (function_exists('imagepng')) { $supported[] = 'png'; } if (function_exists('imagegif')) { $supported[] = 'gif'; } return $supported; } public function instantiateStorageEngine() { return self::buildEngine($this->getStorageEngine()); } public static function buildEngine($engine_identifier) { $engines = self::buildAllEngines(); foreach ($engines as $engine) { if ($engine->getEngineIdentifier() == $engine_identifier) { return $engine; } } throw new Exception( "Storage engine '{$engine_identifier}' could not be located!"); } public static function buildAllEngines() { $engines = id(new PhutilSymbolLoader()) ->setType('class') ->setConcreteOnly(true) ->setAncestorClass('PhabricatorFileStorageEngine') ->selectAndLoadSymbols(); $results = array(); foreach ($engines as $engine_class) { $results[] = newv($engine_class['name'], array()); } return $results; } public function getViewableMimeType() { $mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); $mime_type = $this->getMimeType(); $mime_parts = explode(';', $mime_type); $mime_type = trim(reset($mime_parts)); return idx($mime_map, $mime_type); } public function getDisplayIconForMimeType() { $mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type, 'fa-file-o'); } public function validateSecretKey($key) { return ($key == $this->getSecretKey()); } public function generateSecretKey() { return Filesystem::readRandomCharacters(20); } public function updateDimensions($save = true) { if (!$this->isViewableImage()) { throw new Exception( 'This file is not a viewable image.'); } if (!function_exists('imagecreatefromstring')) { throw new Exception( 'Cannot retrieve image information.'); } $data = $this->loadFileData(); $img = imagecreatefromstring($data); if ($img === false) { throw new Exception( 'Error when decoding image.'); } $this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img); $this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img); if ($save) { $this->save(); } return $this; } public function copyDimensions(PhabricatorFile $file) { $metadata = $file->getMetadata(); $width = idx($metadata, self::METADATA_IMAGE_WIDTH); if ($width) { $this->metadata[self::METADATA_IMAGE_WIDTH] = $width; } $height = idx($metadata, self::METADATA_IMAGE_HEIGHT); if ($height) { $this->metadata[self::METADATA_IMAGE_HEIGHT] = $height; } return $this; } /** * Load (or build) the {@class:PhabricatorFile} objects for builtin file * resources. The builtin mechanism allows files shipped with Phabricator * to be treated like normal files so that APIs do not need to special case * things like default images or deleted files. * * Builtins are located in `resources/builtin/` and identified by their * name. * * @param PhabricatorUser Viewing user. * @param list<string> List of builtin file names. * @return dict<string, PhabricatorFile> Dictionary of named builtins. */ public static function loadBuiltins(PhabricatorUser $user, array $names) { $specs = array(); foreach ($names as $name) { $specs[] = array( 'originalPHID' => PhabricatorPHIDConstants::PHID_VOID, 'transform' => 'builtin:'.$name, ); } // NOTE: Anyone is allowed to access builtin files. $files = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withTransforms($specs) ->execute(); $files = mpull($files, null, 'getName'); $root = dirname(phutil_get_library_root('phabricator')); $root = $root.'/resources/builtin/'; $build = array(); foreach ($names as $name) { if (isset($files[$name])) { continue; } // This is just a sanity check to prevent loading arbitrary files. if (basename($name) != $name) { throw new Exception("Invalid builtin name '{$name}'!"); } $path = $root.$name; if (!Filesystem::pathExists($path)) { throw new Exception("Builtin '{$path}' does not exist!"); } $data = Filesystem::readFile($path); $params = array( 'name' => $name, 'ttl' => time() + (60 * 60 * 24 * 7), 'canCDN' => true, 'builtin' => $name, ); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $file = PhabricatorFile::newFromFileData($data, $params); + $file = self::newFromFileData($data, $params); $xform = id(new PhabricatorTransformedFile()) ->setOriginalPHID(PhabricatorPHIDConstants::PHID_VOID) ->setTransform('builtin:'.$name) ->setTransformedPHID($file->getPHID()) ->save(); unset($unguarded); $file->attachObjectPHIDs(array()); $file->attachObjects(array()); $files[$name] = $file; } return $files; } /** * Convenience wrapper for @{method:loadBuiltins}. * * @param PhabricatorUser Viewing user. * @param string Single builtin name to load. * @return PhabricatorFile Corresponding builtin file. */ public static function loadBuiltin(PhabricatorUser $user, $name) { return idx(self::loadBuiltins($user, array($name)), $name); } public function getObjects() { return $this->assertAttached($this->objects); } public function attachObjects(array $objects) { $this->objects = $objects; return $this; } public function getObjectPHIDs() { return $this->assertAttached($this->objectPHIDs); } public function attachObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function getOriginalFile() { return $this->assertAttached($this->originalFile); } public function attachOriginalFile(PhabricatorFile $file = null) { $this->originalFile = $file; return $this; } public function getImageHeight() { if (!$this->isViewableImage()) { return null; } return idx($this->metadata, self::METADATA_IMAGE_HEIGHT); } public function getImageWidth() { if (!$this->isViewableImage()) { return null; } return idx($this->metadata, self::METADATA_IMAGE_WIDTH); } public function getCanCDN() { if (!$this->isViewableImage()) { return false; } return idx($this->metadata, self::METADATA_CAN_CDN); } public function setCanCDN($can_cdn) { $this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0; return $this; } public function isBuiltin() { return ($this->getBuiltinName() !== null); } public function getBuiltinName() { return idx($this->metadata, self::METADATA_BUILTIN); } public function setBuiltinName($name) { $this->metadata[self::METADATA_BUILTIN] = $name; return $this; } public function getIsProfileImage() { return idx($this->metadata, self::METADATA_PROFILE); } public function setIsProfileImage($value) { $this->metadata[self::METADATA_PROFILE] = $value; return $this; } protected function generateOneTimeToken() { $key = Filesystem::readRandomCharacters(16); // Save the new secret. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $token = id(new PhabricatorAuthTemporaryToken()) ->setObjectPHID($this->getPHID()) ->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE) ->setTokenExpires(time() + phutil_units('1 hour in seconds')) ->setTokenCode(PhabricatorHash::digest($key)) ->save(); unset($unguarded); return $key; } public function validateOneTimeToken($token_code) { $token = id(new PhabricatorAuthTemporaryTokenQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withObjectPHIDs(array($this->getPHID())) ->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE)) ->withExpired(false) ->withTokenCodes(array(PhabricatorHash::digest($token_code))) ->executeOne(); return $token; } /** * Write the policy edge between this file and some object. * * @param phid Object PHID to attach to. * @return this */ public function attachToObject($phid) { $edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST; id(new PhabricatorEdgeEditor()) ->addEdge($phid, $edge_type, $this->getPHID()) ->save(); return $this; } /** * Remove the policy edge between this file and some object. * * @param phid Object PHID to detach from. * @return this */ public function detachFromObject($phid) { $edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST; id(new PhabricatorEdgeEditor()) ->removeEdge($phid, $edge_type, $this->getPHID()) ->save(); return $this; } /** * Configure a newly created file object according to specified parameters. * * This method is called both when creating a file from fresh data, and * when creating a new file which reuses existing storage. * * @param map<string, wild> Bag of parameters, see @{class:PhabricatorFile} * for documentation. * @return this */ private function readPropertiesFromParameters(array $params) { $file_name = idx($params, 'name'); $this->setName($file_name); $author_phid = idx($params, 'authorPHID'); $this->setAuthorPHID($author_phid); $file_ttl = idx($params, 'ttl'); $this->setTtl($file_ttl); $view_policy = idx($params, 'viewPolicy'); if ($view_policy) { $this->setViewPolicy($params['viewPolicy']); } $is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0); $this->setIsExplicitUpload($is_explicit); $can_cdn = idx($params, 'canCDN'); if ($can_cdn) { $this->setCanCDN(true); } $builtin = idx($params, 'builtin'); if ($builtin) { $this->setBuiltinName($builtin); } $profile = idx($params, 'profile'); if ($profile) { $this->setIsProfileImage(true); } $mime_type = idx($params, 'mime-type'); if ($mime_type) { $this->setMimeType($mime_type); } return $this; } public function getRedirectResponse() { $uri = $this->getBestURI(); // TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI // (if the file is a viewable image) and sometimes a local URI (if not). // For now, just detect which one we got and configure the response // appropriately. In the long run, if this endpoint is served from a CDN // domain, we can't issue a local redirect to an info URI (which is not // present on the CDN domain). We probably never actually issue local // redirects here anyway, since we only ever transform viewable images // right now. $is_external = strlen(id(new PhutilURI($uri))->getDomain()); return id(new AphrontRedirectResponse()) ->setIsExternal($is_external) ->setURI($uri); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorFileEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorFileTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isBuiltin()) { return PhabricatorPolicies::getMostOpenPolicy(); } if ($this->getIsProfileImage()) { return PhabricatorPolicies::getMostOpenPolicy(); } return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { $viewer_phid = $viewer->getPHID(); if ($viewer_phid) { if ($this->getAuthorPHID() == $viewer_phid) { return true; } } switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: // If you can see the file this file is a transform of, you can see // this file. if ($this->getOriginalFile()) { return true; } // If you can see any object this file is attached to, you can see // the file. return (count($this->getObjects()) > 0); } return false; } public function describeAutomaticCapability($capability) { $out = array(); $out[] = pht('The user who uploaded a file can always view and edit it.'); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $out[] = pht( 'Files attached to objects are visible to users who can view '. 'those objects.'); $out[] = pht( 'Thumbnails are visible only to users who can view the original '. 'file.'); break; } return $out; } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->authorPHID == $phid); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/fund/storage/FundInitiativeTransaction.php b/src/applications/fund/storage/FundInitiativeTransaction.php index 3531ff3e5e..c06d62e229 100644 --- a/src/applications/fund/storage/FundInitiativeTransaction.php +++ b/src/applications/fund/storage/FundInitiativeTransaction.php @@ -1,249 +1,249 @@ <?php final class FundInitiativeTransaction extends PhabricatorApplicationTransaction { const TYPE_NAME = 'fund:name'; const TYPE_DESCRIPTION = 'fund:description'; const TYPE_RISKS = 'fund:risks'; const TYPE_STATUS = 'fund:status'; const TYPE_BACKER = 'fund:backer'; const TYPE_REFUND = 'fund:refund'; const TYPE_MERCHANT = 'fund:merchant'; const MAILTAG_BACKER = 'fund.backer'; const MAILTAG_STATUS = 'fund.status'; const MAILTAG_OTHER = 'fund.other'; const PROPERTY_AMOUNT = 'fund.amount'; const PROPERTY_BACKER = 'fund.backer'; public function getApplicationName() { return 'fund'; } public function getApplicationTransactionType() { return FundInitiativePHIDType::TYPECONST; } public function getApplicationTransactionCommentObject() { return null; } public function getRequiredHandlePHIDs() { $phids = parent::getRequiredHandlePHIDs(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { - case FundInitiativeTransaction::TYPE_MERCHANT: + case self::TYPE_MERCHANT: if ($old) { $phids[] = $old; } if ($new) { $phids[] = $new; } break; - case FundInitiativeTransaction::TYPE_REFUND: + case self::TYPE_REFUND: $phids[] = $this->getMetadataValue(self::PROPERTY_BACKER); break; } return $phids; } public function getTitle() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { - case FundInitiativeTransaction::TYPE_NAME: + case self::TYPE_NAME: if ($old === null) { return pht( '%s created this initiative.', $this->renderHandleLink($author_phid)); } else { return pht( '%s renamed this initiative from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; - case FundInitiativeTransaction::TYPE_RISKS: + case self::TYPE_RISKS: return pht( '%s edited the risks for this initiative.', $this->renderHandleLink($author_phid)); - case FundInitiativeTransaction::TYPE_DESCRIPTION: + case self::TYPE_DESCRIPTION: return pht( '%s edited the description of this initiative.', $this->renderHandleLink($author_phid)); - case FundInitiativeTransaction::TYPE_STATUS: + case self::TYPE_STATUS: switch ($new) { case FundInitiative::STATUS_OPEN: return pht( '%s reopened this initiative.', $this->renderHandleLink($author_phid)); case FundInitiative::STATUS_CLOSED: return pht( '%s closed this initiative.', $this->renderHandleLink($author_phid)); } break; - case FundInitiativeTransaction::TYPE_BACKER: + case self::TYPE_BACKER: $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT); $amount = PhortuneCurrency::newFromString($amount); return pht( '%s backed this initiative with %s.', $this->renderHandleLink($author_phid), $amount->formatForDisplay()); - case FundInitiativeTransaction::TYPE_REFUND: + case self::TYPE_REFUND: $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT); $amount = PhortuneCurrency::newFromString($amount); $backer_phid = $this->getMetadataValue(self::PROPERTY_BACKER); return pht( '%s refunded %s to %s.', $this->renderHandleLink($author_phid), $amount->formatForDisplay(), $this->renderHandleLink($backer_phid)); - case FundInitiativeTransaction::TYPE_MERCHANT: + case self::TYPE_MERCHANT: if ($old === null) { return pht( '%s set this initiative to pay to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); } else { return pht( '%s changed the merchant receiving funds from this '. 'initiative from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); } } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { - case FundInitiativeTransaction::TYPE_NAME: + case self::TYPE_NAME: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s renamed %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } break; - case FundInitiativeTransaction::TYPE_DESCRIPTION: + case self::TYPE_DESCRIPTION: return pht( '%s updated the description for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); - case FundInitiativeTransaction::TYPE_STATUS: + case self::TYPE_STATUS: switch ($new) { case FundInitiative::STATUS_OPEN: return pht( '%s reopened %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case FundInitiative::STATUS_CLOSED: return pht( '%s closed %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } break; - case FundInitiativeTransaction::TYPE_BACKER: + case self::TYPE_BACKER: $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT); $amount = PhortuneCurrency::newFromString($amount); return pht( '%s backed %s with %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $amount->formatForDisplay()); - case FundInitiativeTransaction::TYPE_REFUND: + case self::TYPE_REFUND: $amount = $this->getMetadataValue(self::PROPERTY_AMOUNT); $amount = PhortuneCurrency::newFromString($amount); $backer_phid = $this->getMetadataValue(self::PROPERTY_BACKER); return pht( '%s refunded %s to %s for %s.', $this->renderHandleLink($author_phid), $amount->formatForDisplay(), $this->renderHandleLink($backer_phid), $this->renderHandleLink($object_phid)); } return parent::getTitleForFeed(); } public function getMailTags() { $tags = parent::getMailTags(); switch ($this->getTransactionType()) { case self::TYPE_STATUS: $tags[] = self::MAILTAG_STATUS; break; case self::TYPE_BACKER: case self::TYPE_REFUND: $tags[] = self::MAILTAG_BACKER; break; default: $tags[] = self::MAILTAG_OTHER; break; } return $tags; } public function shouldHide() { $old = $this->getOldValue(); switch ($this->getTransactionType()) { - case FundInitiativeTransaction::TYPE_DESCRIPTION: - case FundInitiativeTransaction::TYPE_RISKS: + case self::TYPE_DESCRIPTION: + case self::TYPE_RISKS: return ($old === null); } return parent::shouldHide(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { - case FundInitiativeTransaction::TYPE_DESCRIPTION: - case FundInitiativeTransaction::TYPE_RISKS: + case self::TYPE_DESCRIPTION: + case self::TYPE_RISKS: return ($this->getOldValue() !== null); } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } } diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php index 5057097fac..4fe1128ac0 100644 --- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php @@ -1,250 +1,250 @@ <?php abstract class HarbormasterBuildStepImplementation { public static function getImplementations() { return id(new PhutilSymbolLoader()) - ->setAncestorClass('HarbormasterBuildStepImplementation') + ->setAncestorClass(__CLASS__) ->loadObjects(); } public static function getImplementation($class) { $base = idx(self::getImplementations(), $class); if ($base) { return (clone $base); } return null; } public static function requireImplementation($class) { if (!$class) { throw new Exception(pht('No implementation is specified!')); } $implementation = self::getImplementation($class); if (!$implementation) { throw new Exception(pht('No such implementation "%s" exists!', $class)); } return $implementation; } /** * The name of the implementation. */ abstract public function getName(); /** * The generic description of the implementation. */ public function getGenericDescription() { return ''; } /** * The description of the implementation, based on the current settings. */ public function getDescription() { return $this->getGenericDescription(); } /** * Run the build target against the specified build. */ abstract public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target); /** * Gets the settings for this build step. */ public function getSettings() { return $this->settings; } public function getSetting($key, $default = null) { return idx($this->settings, $key, $default); } /** * Loads the settings for this build step implementation from a build * step or target. */ public final function loadSettings($build_object) { $this->settings = $build_object->getDetails(); return $this; } /** * Return the name of artifacts produced by this command. * * Something like: * * return array( * 'some_name_input_by_user' => 'host'); * * Future steps will calculate all available artifact mappings * before them and filter on the type. * * @return array The mappings of artifact names to their types. */ public function getArtifactInputs() { return array(); } public function getArtifactOutputs() { return array(); } public function getDependencies(HarbormasterBuildStep $build_step) { $dependencies = $build_step->getDetail('dependsOn', array()); $inputs = $build_step->getStepImplementation()->getArtifactInputs(); $inputs = ipull($inputs, null, 'key'); $artifacts = $this->getAvailableArtifacts( $build_step->getBuildPlan(), $build_step, null); foreach ($artifacts as $key => $type) { if (!array_key_exists($key, $inputs)) { unset($artifacts[$key]); } } $artifact_steps = ipull($artifacts, 'step'); $artifact_steps = mpull($artifact_steps, 'getPHID'); $dependencies = array_merge($dependencies, $artifact_steps); return $dependencies; } /** * Returns a list of all artifacts made available in the build plan. */ public static function getAvailableArtifacts( HarbormasterBuildPlan $build_plan, $current_build_step, $artifact_type) { $steps = id(new HarbormasterBuildStepQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildPlanPHIDs(array($build_plan->getPHID())) ->execute(); $artifacts = array(); $artifact_arrays = array(); foreach ($steps as $step) { if ($current_build_step !== null && $step->getPHID() === $current_build_step->getPHID()) { continue; } $implementation = $step->getStepImplementation(); $array = $implementation->getArtifactOutputs(); $array = ipull($array, 'type', 'key'); foreach ($array as $name => $type) { if ($type !== $artifact_type && $artifact_type !== null) { continue; } $artifacts[$name] = array('type' => $type, 'step' => $step); } } return $artifacts; } /** * Convert a user-provided string with variables in it, like: * * ls ${dirname} * * ...into a string with variables merged into it safely: * * ls 'dir with spaces' * * @param string Name of a `vxsprintf` function, like @{function:vcsprintf}. * @param string User-provided pattern string containing `${variables}`. * @param dict List of available replacement variables. * @return string String with variables replaced safely into it. */ protected function mergeVariables($function, $pattern, array $variables) { $regexp = '/\\$\\{(?P<name>[a-z\\.]+)\\}/'; $matches = null; preg_match_all($regexp, $pattern, $matches); $argv = array(); foreach ($matches['name'] as $name) { if (!array_key_exists($name, $variables)) { throw new Exception(pht("No such variable '%s'!", $name)); } $argv[] = $variables[$name]; } $pattern = str_replace('%', '%%', $pattern); $pattern = preg_replace($regexp, '%s', $pattern); return call_user_func($function, $pattern, $argv); } public function getFieldSpecifications() { return array(); } protected function formatSettingForDescription($key, $default = null) { return $this->formatValueForDescription($this->getSetting($key, $default)); } protected function formatValueForDescription($value) { if (strlen($value)) { return phutil_tag('strong', array(), $value); } else { return phutil_tag('em', array(), pht('(null)')); } } public function supportsWaitForMessage() { return false; } public function shouldWaitForMessage(HarbormasterBuildTarget $target) { if (!$this->supportsWaitForMessage()) { return false; } return (bool)$target->getDetail('builtin.wait-for-message'); } protected function shouldAbort( HarbormasterBuild $build, HarbormasterBuildTarget $target) { return $build->getBuildGeneration() !== $target->getBuildGeneration(); } protected function resolveFuture( HarbormasterBuild $build, HarbormasterBuildTarget $target, Future $future) { $futures = new FutureIterator(array($future)); foreach ($futures->setUpdateInterval(5) as $key => $future) { if ($future === null) { $build->reload(); if ($this->shouldAbort($build, $target)) { throw new HarbormasterBuildAbortedException(); } } else { return $future->resolve(); } } } } diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php index b53e2bc2d3..81f8e66d07 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildable.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php @@ -1,302 +1,302 @@ <?php final class HarbormasterBuildable extends HarbormasterDAO implements PhabricatorApplicationTransactionInterface, PhabricatorPolicyInterface, HarbormasterBuildableInterface { protected $buildablePHID; protected $containerPHID; protected $buildableStatus; protected $isManualBuildable; private $buildableObject = self::ATTACHABLE; private $containerObject = self::ATTACHABLE; private $buildableHandle = self::ATTACHABLE; private $containerHandle = self::ATTACHABLE; private $builds = self::ATTACHABLE; const STATUS_BUILDING = 'building'; const STATUS_PASSED = 'passed'; const STATUS_FAILED = 'failed'; public static function getBuildableStatusName($status) { switch ($status) { case self::STATUS_BUILDING: return pht('Building'); case self::STATUS_PASSED: return pht('Passed'); case self::STATUS_FAILED: return pht('Failed'); default: return pht('Unknown'); } } public static function getBuildableStatusIcon($status) { switch ($status) { case self::STATUS_BUILDING: return PHUIStatusItemView::ICON_RIGHT; case self::STATUS_PASSED: return PHUIStatusItemView::ICON_ACCEPT; case self::STATUS_FAILED: return PHUIStatusItemView::ICON_REJECT; default: return PHUIStatusItemView::ICON_QUESTION; } } public static function getBuildableStatusColor($status) { switch ($status) { case self::STATUS_BUILDING: return 'blue'; case self::STATUS_PASSED: return 'green'; case self::STATUS_FAILED: return 'red'; default: return 'bluegrey'; } } public static function initializeNewBuildable(PhabricatorUser $actor) { return id(new HarbormasterBuildable()) ->setIsManualBuildable(0) ->setBuildableStatus(self::STATUS_BUILDING); } public function getMonogram() { return 'B'.$this->getID(); } /** * Returns an existing buildable for the object's PHID or creates a * new buildable implicitly if needed. */ public static function createOrLoadExisting( PhabricatorUser $actor, $buildable_object_phid, $container_object_phid) { $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($actor) ->withBuildablePHIDs(array($buildable_object_phid)) ->withManualBuildables(false) ->setLimit(1) ->executeOne(); if ($buildable) { return $buildable; } - $buildable = HarbormasterBuildable::initializeNewBuildable($actor) + $buildable = self::initializeNewBuildable($actor) ->setBuildablePHID($buildable_object_phid) ->setContainerPHID($container_object_phid); $buildable->save(); return $buildable; } /** * Looks up the plan PHIDs and applies the plans to the specified * object identified by it's PHID. */ public static function applyBuildPlans( $phid, $container_phid, array $plan_phids) { if (count($plan_phids) === 0) { return; } // Skip all of this logic if the Harbormaster application // isn't currently installed. $harbormaster_app = 'PhabricatorHarbormasterApplication'; if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) { return; } - $buildable = HarbormasterBuildable::createOrLoadExisting( + $buildable = self::createOrLoadExisting( PhabricatorUser::getOmnipotentUser(), $phid, $container_phid); $plans = id(new HarbormasterBuildPlanQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($plan_phids) ->execute(); foreach ($plans as $plan) { if ($plan->isDisabled()) { // TODO: This should be communicated more clearly -- maybe we should // create the build but set the status to "disabled" or "derelict". continue; } $buildable->applyPlan($plan); } } public function applyPlan(HarbormasterBuildPlan $plan) { $viewer = PhabricatorUser::getOmnipotentUser(); $build = HarbormasterBuild::initializeNewBuild($viewer) ->setBuildablePHID($this->getPHID()) ->setBuildPlanPHID($plan->getPHID()) ->setBuildStatus(HarbormasterBuild::STATUS_PENDING) ->save(); PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( 'buildID' => $build->getID(), )); return $build; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'containerPHID' => 'phid?', 'buildableStatus' => 'text32', 'isManualBuildable' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_container' => array( 'columns' => array('containerPHID'), ), 'key_manual' => array( 'columns' => array('isManualBuildable'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildablePHIDType::TYPECONST); } public function attachBuildableObject($buildable_object) { $this->buildableObject = $buildable_object; return $this; } public function getBuildableObject() { return $this->assertAttached($this->buildableObject); } public function attachContainerObject($container_object) { $this->containerObject = $container_object; return $this; } public function getContainerObject() { return $this->assertAttached($this->containerObject); } public function attachContainerHandle($container_handle) { $this->containerHandle = $container_handle; return $this; } public function getContainerHandle() { return $this->assertAttached($this->containerHandle); } public function attachBuildableHandle($buildable_handle) { $this->buildableHandle = $buildable_handle; return $this; } public function getBuildableHandle() { return $this->assertAttached($this->buildableHandle); } public function attachBuilds(array $builds) { assert_instances_of($builds, 'HarbormasterBuild'); $this->builds = $builds; return $this; } public function getBuilds() { return $this->assertAttached($this->builds); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildableTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildableTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBuildableObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildableObject()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A buildable inherits policies from the underlying object.'); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildablePHID() { // NOTE: This is essentially just for convenience, as it allows you create // a copy of a buildable by specifying `B123` without bothering to go // look up the underlying object. return $this->getBuildablePHID(); } public function getHarbormasterContainerPHID() { return $this->getContainerPHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } } diff --git a/src/applications/help/application/PhabricatorHelpApplication.php b/src/applications/help/application/PhabricatorHelpApplication.php index c86d5cd0d0..b1f66b02cd 100644 --- a/src/applications/help/application/PhabricatorHelpApplication.php +++ b/src/applications/help/application/PhabricatorHelpApplication.php @@ -1,108 +1,108 @@ <?php final class PhabricatorHelpApplication extends PhabricatorApplication { public function getName() { return pht('Help'); } public function canUninstall() { return false; } public function isUnlisted() { return true; } public function getRoutes() { return array( '/help/' => array( 'keyboardshortcut/' => 'PhabricatorHelpKeyboardShortcutController', 'editorprotocol/' => 'PhabricatorHelpEditorProtocolController', 'documentation/(?P<application>\w+)/' => 'PhabricatorHelpDocumentationController', ), ); } public function buildMainMenuItems( PhabricatorUser $user, PhabricatorController $controller = null) { $application = null; if ($controller) { $application = $controller->getCurrentApplication(); } $items = array(); $help_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'aphlict-dropdown', array( 'bubbleID' => $help_id, 'dropdownID' => 'phabricator-help-menu', - 'applicationClass' => 'PhabricatorHelpApplication', + 'applicationClass' => __CLASS__, 'local' => true, 'desktop' => true, 'right' => true, )); $item = id(new PHUIListItemView()) ->setIcon('fa-life-ring') ->addClass('core-menu-item') ->setID($help_id) ->setOrder(200); $hide = true; if ($application) { $help_name = pht('%s Help', $application->getName()); $item ->setName($help_name) ->setHref('/help/documentation/'.get_class($application).'/') ->setAural($help_name); $help_items = $application->getHelpMenuItems($user); if ($help_items) { $hide = false; } } if ($hide) { $item->setStyle('display: none'); } $items[] = $item; return $items; } public function buildMainMenuExtraNodes( PhabricatorUser $viewer, PhabricatorController $controller = null) { $application = null; if ($controller) { $application = $controller->getCurrentApplication(); } $view = null; if ($application) { $help_items = $application->getHelpMenuItems($viewer); if ($help_items) { $view = new PHUIListView(); foreach ($help_items as $item) { $view->addMenuItem($item); } } } return phutil_tag( 'div', array( 'id' => 'phabricator-help-menu', 'class' => 'phabricator-main-menu-dropdown phui-list-sidenav', 'style' => 'display: none', ), $view); } } diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index 6a51ade55f..f56572a398 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -1,1651 +1,1651 @@ <?php /** * @task customfield Custom Field Integration */ abstract class HeraldAdapter { const FIELD_TITLE = 'title'; const FIELD_BODY = 'body'; const FIELD_AUTHOR = 'author'; const FIELD_ASSIGNEE = 'assignee'; const FIELD_REVIEWER = 'reviewer'; const FIELD_REVIEWERS = 'reviewers'; const FIELD_COMMITTER = 'committer'; const FIELD_CC = 'cc'; const FIELD_TAGS = 'tags'; const FIELD_DIFF_FILE = 'diff-file'; const FIELD_DIFF_CONTENT = 'diff-content'; const FIELD_DIFF_ADDED_CONTENT = 'diff-added-content'; const FIELD_DIFF_REMOVED_CONTENT = 'diff-removed-content'; const FIELD_DIFF_ENORMOUS = 'diff-enormous'; const FIELD_REPOSITORY = 'repository'; const FIELD_REPOSITORY_PROJECTS = 'repository-projects'; const FIELD_RULE = 'rule'; const FIELD_AFFECTED_PACKAGE = 'affected-package'; const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner'; const FIELD_CONTENT_SOURCE = 'contentsource'; const FIELD_ALWAYS = 'always'; const FIELD_AUTHOR_PROJECTS = 'authorprojects'; const FIELD_PROJECTS = 'projects'; const FIELD_PUSHER = 'pusher'; const FIELD_PUSHER_PROJECTS = 'pusher-projects'; const FIELD_DIFFERENTIAL_REVISION = 'differential-revision'; const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers'; const FIELD_DIFFERENTIAL_CCS = 'differential-ccs'; const FIELD_DIFFERENTIAL_ACCEPTED = 'differential-accepted'; const FIELD_IS_MERGE_COMMIT = 'is-merge-commit'; const FIELD_BRANCHES = 'branches'; const FIELD_AUTHOR_RAW = 'author-raw'; const FIELD_COMMITTER_RAW = 'committer-raw'; const FIELD_IS_NEW_OBJECT = 'new-object'; const FIELD_APPLICATION_EMAIL = 'applicaton-email'; const FIELD_TASK_PRIORITY = 'taskpriority'; const FIELD_TASK_STATUS = 'taskstatus'; const FIELD_ARCANIST_PROJECT = 'arcanist-project'; const FIELD_PUSHER_IS_COMMITTER = 'pusher-is-committer'; const FIELD_PATH = 'path'; const CONDITION_CONTAINS = 'contains'; const CONDITION_NOT_CONTAINS = '!contains'; const CONDITION_IS = 'is'; const CONDITION_IS_NOT = '!is'; const CONDITION_IS_ANY = 'isany'; const CONDITION_IS_NOT_ANY = '!isany'; const CONDITION_INCLUDE_ALL = 'all'; const CONDITION_INCLUDE_ANY = 'any'; const CONDITION_INCLUDE_NONE = 'none'; const CONDITION_IS_ME = 'me'; const CONDITION_IS_NOT_ME = '!me'; const CONDITION_REGEXP = 'regexp'; const CONDITION_RULE = 'conditions'; const CONDITION_NOT_RULE = '!conditions'; const CONDITION_EXISTS = 'exists'; const CONDITION_NOT_EXISTS = '!exists'; const CONDITION_UNCONDITIONALLY = 'unconditionally'; const CONDITION_NEVER = 'never'; const CONDITION_REGEXP_PAIR = 'regexp-pair'; const CONDITION_HAS_BIT = 'bit'; const CONDITION_NOT_BIT = '!bit'; const CONDITION_IS_TRUE = 'true'; const CONDITION_IS_FALSE = 'false'; const ACTION_ADD_CC = 'addcc'; const ACTION_REMOVE_CC = 'remcc'; const ACTION_EMAIL = 'email'; const ACTION_NOTHING = 'nothing'; const ACTION_AUDIT = 'audit'; const ACTION_FLAG = 'flag'; const ACTION_ASSIGN_TASK = 'assigntask'; const ACTION_ADD_PROJECTS = 'addprojects'; const ACTION_REMOVE_PROJECTS = 'removeprojects'; const ACTION_ADD_REVIEWERS = 'addreviewers'; const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers'; const ACTION_APPLY_BUILD_PLANS = 'applybuildplans'; const ACTION_BLOCK = 'block'; const ACTION_REQUIRE_SIGNATURE = 'signature'; const VALUE_TEXT = 'text'; const VALUE_NONE = 'none'; const VALUE_EMAIL = 'email'; const VALUE_USER = 'user'; const VALUE_TAG = 'tag'; const VALUE_RULE = 'rule'; const VALUE_REPOSITORY = 'repository'; const VALUE_OWNERS_PACKAGE = 'package'; const VALUE_PROJECT = 'project'; const VALUE_FLAG_COLOR = 'flagcolor'; const VALUE_CONTENT_SOURCE = 'contentsource'; const VALUE_USER_OR_PROJECT = 'userorproject'; const VALUE_BUILD_PLAN = 'buildplan'; const VALUE_TASK_PRIORITY = 'taskpriority'; const VALUE_TASK_STATUS = 'taskstatus'; const VALUE_ARCANIST_PROJECT = 'arcanistprojects'; const VALUE_LEGAL_DOCUMENTS = 'legaldocuments'; const VALUE_APPLICATION_EMAIL = 'applicationemail'; private $contentSource; private $isNewObject; private $applicationEmail; private $customFields = false; private $customActions = null; private $queuedTransactions = array(); private $emailPHIDs = array(); private $forcedEmailPHIDs = array(); public function getEmailPHIDs() { return array_values($this->emailPHIDs); } public function getForcedEmailPHIDs() { return array_values($this->forcedEmailPHIDs); } public function getCustomActions() { if ($this->customActions === null) { $custom_actions = id(new PhutilSymbolLoader()) ->setAncestorClass('HeraldCustomAction') ->loadObjects(); foreach ($custom_actions as $key => $object) { if (!$object->appliesToAdapter($this)) { unset($custom_actions[$key]); } } $this->customActions = array(); foreach ($custom_actions as $action) { $key = $action->getActionKey(); if (array_key_exists($key, $this->customActions)) { throw new Exception( 'More than one Herald custom action implementation '. 'handles the action key: \''.$key.'\'.'); } $this->customActions[$key] = $action; } } return $this->customActions; } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function getContentSource() { return $this->contentSource; } public function getIsNewObject() { if (is_bool($this->isNewObject)) { return $this->isNewObject; } throw new Exception(pht('You must setIsNewObject to a boolean first!')); } public function setIsNewObject($new) { $this->isNewObject = (bool) $new; return $this; } public function setApplicationEmail( PhabricatorMetaMTAApplicationEmail $email) { $this->applicationEmail = $email; return $this; } public function getApplicationEmail() { return $this->applicationEmail; } abstract public function getPHID(); abstract public function getHeraldName(); public function getHeraldField($field_name) { switch ($field_name) { case self::FIELD_RULE: return null; case self::FIELD_CONTENT_SOURCE: return $this->getContentSource()->getSource(); case self::FIELD_ALWAYS: return true; case self::FIELD_IS_NEW_OBJECT: return $this->getIsNewObject(); case self::FIELD_APPLICATION_EMAIL: $value = array(); // while there is only one match by implementation, we do set // comparisons on phids, so return an array with just the phid if ($this->getApplicationEmail()) { $value[] = $this->getApplicationEmail()->getPHID(); } return $value; default: if ($this->isHeraldCustomKey($field_name)) { return $this->getCustomFieldValue($field_name); } throw new Exception( "Unknown field '{$field_name}'!"); } } abstract public function applyHeraldEffects(array $effects); protected function handleCustomHeraldEffect(HeraldEffect $effect) { $custom_action = idx($this->getCustomActions(), $effect->getAction()); if ($custom_action !== null) { return $custom_action->applyEffect( $this, $this->getObject(), $effect); } return null; } public function isAvailableToUser(PhabricatorUser $viewer) { $applications = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) ->withInstalled(true) ->withClasses(array($this->getAdapterApplicationClass())) ->execute(); return !empty($applications); } public function queueTransaction($transaction) { $this->queuedTransactions[] = $transaction; } public function getQueuedTransactions() { return $this->queuedTransactions; } protected function newTransaction() { $object = $this->newObject(); if (!($object instanceof PhabricatorApplicationTransactionInterface)) { throw new Exception( pht( 'Unable to build a new transaction for adapter object; it does '. 'not implement "%s".', 'PhabricatorApplicationTransactionInterface')); } return $object->getApplicationTransactionTemplate(); } /** * NOTE: You generally should not override this; it exists to support legacy * adapters which had hard-coded content types. */ public function getAdapterContentType() { return get_class($this); } abstract public function getAdapterContentName(); abstract public function getAdapterContentDescription(); abstract public function getAdapterApplicationClass(); abstract public function getObject(); /** * Return a new characteristic object for this adapter. * * The adapter will use this object to test for interfaces, generate * transactions, and interact with custom fields. * * Adapters must return an object from this method to enable custom * field rules and various implicit actions. * * Normally, you'll return an empty version of the adapted object: * * return new ApplicationObject(); * * @return null|object Template object. */ protected function newObject() { return null; } public function supportsRuleType($rule_type) { return false; } public function canTriggerOnObject($object) { return false; } public function explainValidTriggerObjects() { return pht('This adapter can not trigger on objects.'); } public function getTriggerObjectPHIDs() { return array($this->getPHID()); } public function getAdapterSortKey() { return sprintf( '%08d%s', $this->getAdapterSortOrder(), $this->getAdapterContentName()); } public function getAdapterSortOrder() { return 1000; } /* -( Fields )------------------------------------------------------------- */ public function getFields() { $fields = array(); $fields[] = self::FIELD_ALWAYS; $fields[] = self::FIELD_RULE; $custom_fields = $this->getCustomFields(); if ($custom_fields) { foreach ($custom_fields->getFields() as $custom_field) { $key = $custom_field->getFieldKey(); $fields[] = $this->getHeraldKeyFromCustomKey($key); } } return $fields; } public function getFieldNameMap() { return array( self::FIELD_TITLE => pht('Title'), self::FIELD_BODY => pht('Body'), self::FIELD_AUTHOR => pht('Author'), self::FIELD_ASSIGNEE => pht('Assignee'), self::FIELD_COMMITTER => pht('Committer'), self::FIELD_REVIEWER => pht('Reviewer'), self::FIELD_REVIEWERS => pht('Reviewers'), self::FIELD_CC => pht('CCs'), self::FIELD_TAGS => pht('Tags'), self::FIELD_DIFF_FILE => pht('Any changed filename'), self::FIELD_DIFF_CONTENT => pht('Any changed file content'), self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'), self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'), self::FIELD_DIFF_ENORMOUS => pht('Change is enormous'), self::FIELD_REPOSITORY => pht('Repository'), self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'), self::FIELD_RULE => pht('Another Herald rule'), self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'), self::FIELD_AFFECTED_PACKAGE_OWNER => pht("Any affected package's owner"), self::FIELD_CONTENT_SOURCE => pht('Content Source'), self::FIELD_ALWAYS => pht('Always'), self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"), self::FIELD_PROJECTS => pht('Projects'), self::FIELD_PUSHER => pht('Pusher'), self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"), self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'), self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'), self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'), self::FIELD_DIFFERENTIAL_ACCEPTED => pht('Accepted Differential revision'), self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'), self::FIELD_BRANCHES => pht('Commit\'s branches'), self::FIELD_AUTHOR_RAW => pht('Raw author name'), self::FIELD_COMMITTER_RAW => pht('Raw committer name'), self::FIELD_IS_NEW_OBJECT => pht('Is newly created?'), self::FIELD_APPLICATION_EMAIL => pht('Receiving email address'), self::FIELD_TASK_PRIORITY => pht('Task priority'), self::FIELD_TASK_STATUS => pht('Task status'), self::FIELD_ARCANIST_PROJECT => pht('Arcanist Project'), self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'), self::FIELD_PATH => pht('Path'), ) + $this->getCustomFieldNameMap(); } /* -( Conditions )--------------------------------------------------------- */ public function getConditionNameMap() { return array( self::CONDITION_CONTAINS => pht('contains'), self::CONDITION_NOT_CONTAINS => pht('does not contain'), self::CONDITION_IS => pht('is'), self::CONDITION_IS_NOT => pht('is not'), self::CONDITION_IS_ANY => pht('is any of'), self::CONDITION_IS_TRUE => pht('is true'), self::CONDITION_IS_FALSE => pht('is false'), self::CONDITION_IS_NOT_ANY => pht('is not any of'), self::CONDITION_INCLUDE_ALL => pht('include all of'), self::CONDITION_INCLUDE_ANY => pht('include any of'), self::CONDITION_INCLUDE_NONE => pht('do not include'), self::CONDITION_IS_ME => pht('is myself'), self::CONDITION_IS_NOT_ME => pht('is not myself'), self::CONDITION_REGEXP => pht('matches regexp'), self::CONDITION_RULE => pht('matches:'), self::CONDITION_NOT_RULE => pht('does not match:'), self::CONDITION_EXISTS => pht('exists'), self::CONDITION_NOT_EXISTS => pht('does not exist'), self::CONDITION_UNCONDITIONALLY => '', // don't show anything! self::CONDITION_NEVER => '', // don't show anything! self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'), self::CONDITION_HAS_BIT => pht('has bit'), self::CONDITION_NOT_BIT => pht('lacks bit'), ); } public function getConditionsForField($field) { switch ($field) { case self::FIELD_TITLE: case self::FIELD_BODY: case self::FIELD_COMMITTER_RAW: case self::FIELD_AUTHOR_RAW: case self::FIELD_PATH: return array( self::CONDITION_CONTAINS, self::CONDITION_NOT_CONTAINS, self::CONDITION_IS, self::CONDITION_IS_NOT, self::CONDITION_REGEXP, ); case self::FIELD_REVIEWER: case self::FIELD_PUSHER: case self::FIELD_TASK_PRIORITY: case self::FIELD_TASK_STATUS: case self::FIELD_ARCANIST_PROJECT: return array( self::CONDITION_IS_ANY, self::CONDITION_IS_NOT_ANY, ); case self::FIELD_REPOSITORY: case self::FIELD_ASSIGNEE: case self::FIELD_AUTHOR: case self::FIELD_COMMITTER: return array( self::CONDITION_IS_ANY, self::CONDITION_IS_NOT_ANY, self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_TAGS: case self::FIELD_REVIEWERS: case self::FIELD_CC: case self::FIELD_AUTHOR_PROJECTS: case self::FIELD_PROJECTS: case self::FIELD_AFFECTED_PACKAGE: case self::FIELD_AFFECTED_PACKAGE_OWNER: case self::FIELD_PUSHER_PROJECTS: case self::FIELD_REPOSITORY_PROJECTS: return array( self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_APPLICATION_EMAIL: return array( self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_DIFF_FILE: case self::FIELD_BRANCHES: return array( self::CONDITION_CONTAINS, self::CONDITION_REGEXP, ); case self::FIELD_DIFF_CONTENT: case self::FIELD_DIFF_ADDED_CONTENT: case self::FIELD_DIFF_REMOVED_CONTENT: return array( self::CONDITION_CONTAINS, self::CONDITION_REGEXP, self::CONDITION_REGEXP_PAIR, ); case self::FIELD_RULE: return array( self::CONDITION_RULE, self::CONDITION_NOT_RULE, ); case self::FIELD_CONTENT_SOURCE: return array( self::CONDITION_IS, self::CONDITION_IS_NOT, ); case self::FIELD_ALWAYS: return array( self::CONDITION_UNCONDITIONALLY, ); case self::FIELD_DIFFERENTIAL_REVIEWERS: return array( self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_DIFFERENTIAL_CCS: return array( self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_DIFFERENTIAL_REVISION: case self::FIELD_DIFFERENTIAL_ACCEPTED: return array( self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_IS_MERGE_COMMIT: case self::FIELD_DIFF_ENORMOUS: case self::FIELD_IS_NEW_OBJECT: case self::FIELD_PUSHER_IS_COMMITTER: return array( self::CONDITION_IS_TRUE, self::CONDITION_IS_FALSE, ); default: if ($this->isHeraldCustomKey($field)) { return $this->getCustomFieldConditions($field); } throw new Exception( "This adapter does not define conditions for field '{$field}'!"); } } public function doesConditionMatch( HeraldEngine $engine, HeraldRule $rule, HeraldCondition $condition, $field_value) { $condition_type = $condition->getFieldCondition(); $condition_value = $condition->getValue(); switch ($condition_type) { case self::CONDITION_CONTAINS: // "Contains" can take an array of strings, as in "Any changed // filename" for diffs. foreach ((array)$field_value as $value) { if (stripos($value, $condition_value) !== false) { return true; } } return false; case self::CONDITION_NOT_CONTAINS: return (stripos($field_value, $condition_value) === false); case self::CONDITION_IS: return ($field_value == $condition_value); case self::CONDITION_IS_NOT: return ($field_value != $condition_value); case self::CONDITION_IS_ME: return ($field_value == $rule->getAuthorPHID()); case self::CONDITION_IS_NOT_ME: return ($field_value != $rule->getAuthorPHID()); case self::CONDITION_IS_ANY: if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( 'Expected condition value to be an array.'); } $condition_value = array_fuse($condition_value); return isset($condition_value[$field_value]); case self::CONDITION_IS_NOT_ANY: if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( 'Expected condition value to be an array.'); } $condition_value = array_fuse($condition_value); return !isset($condition_value[$field_value]); case self::CONDITION_INCLUDE_ALL: if (!is_array($field_value)) { throw new HeraldInvalidConditionException( 'Object produced non-array value!'); } if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( 'Expected condition value to be an array.'); } $have = array_select_keys(array_fuse($field_value), $condition_value); return (count($have) == count($condition_value)); case self::CONDITION_INCLUDE_ANY: return (bool)array_select_keys( array_fuse($field_value), $condition_value); case self::CONDITION_INCLUDE_NONE: return !array_select_keys( array_fuse($field_value), $condition_value); case self::CONDITION_EXISTS: case self::CONDITION_IS_TRUE: return (bool)$field_value; case self::CONDITION_NOT_EXISTS: case self::CONDITION_IS_FALSE: return !$field_value; case self::CONDITION_UNCONDITIONALLY: return (bool)$field_value; case self::CONDITION_NEVER: return false; case self::CONDITION_REGEXP: foreach ((array)$field_value as $value) { // We add the 'S' flag because we use the regexp multiple times. // It shouldn't cause any troubles if the flag is already there // - /.*/S is evaluated same as /.*/SS. $result = @preg_match($condition_value.'S', $value); if ($result === false) { throw new HeraldInvalidConditionException( 'Regular expression is not valid!'); } if ($result) { return true; } } return false; case self::CONDITION_REGEXP_PAIR: // Match a JSON-encoded pair of regular expressions against a // dictionary. The first regexp must match the dictionary key, and the // second regexp must match the dictionary value. If any key/value pair // in the dictionary matches both regexps, the condition is satisfied. $regexp_pair = null; try { $regexp_pair = phutil_json_decode($condition_value); } catch (PhutilJSONParserException $ex) { throw new HeraldInvalidConditionException( pht('Regular expression pair is not valid JSON!')); } if (count($regexp_pair) != 2) { throw new HeraldInvalidConditionException( pht('Regular expression pair is not a pair!')); } $key_regexp = array_shift($regexp_pair); $value_regexp = array_shift($regexp_pair); foreach ((array)$field_value as $key => $value) { $key_matches = @preg_match($key_regexp, $key); if ($key_matches === false) { throw new HeraldInvalidConditionException( 'First regular expression is invalid!'); } if ($key_matches) { $value_matches = @preg_match($value_regexp, $value); if ($value_matches === false) { throw new HeraldInvalidConditionException( 'Second regular expression is invalid!'); } if ($value_matches) { return true; } } } return false; case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: $rule = $engine->getRule($condition_value); if (!$rule) { throw new HeraldInvalidConditionException( 'Condition references a rule which does not exist!'); } $is_not = ($condition_type == self::CONDITION_NOT_RULE); $result = $engine->doesRuleMatch($rule, $this); if ($is_not) { $result = !$result; } return $result; case self::CONDITION_HAS_BIT: return (($condition_value & $field_value) === (int) $condition_value); case self::CONDITION_NOT_BIT: return (($condition_value & $field_value) !== (int) $condition_value); default: throw new HeraldInvalidConditionException( "Unknown condition '{$condition_type}'."); } } public function willSaveCondition(HeraldCondition $condition) { $condition_type = $condition->getFieldCondition(); $condition_value = $condition->getValue(); switch ($condition_type) { case self::CONDITION_REGEXP: $ok = @preg_match($condition_value, ''); if ($ok === false) { throw new HeraldInvalidConditionException( pht( 'The regular expression "%s" is not valid. Regular expressions '. 'must have enclosing characters (e.g. "@/path/to/file@", not '. '"/path/to/file") and be syntactically correct.', $condition_value)); } break; case self::CONDITION_REGEXP_PAIR: $json = null; try { $json = phutil_json_decode($condition_value); } catch (PhutilJSONParserException $ex) { throw new HeraldInvalidConditionException( pht( 'The regular expression pair "%s" is not valid JSON. Enter a '. 'valid JSON array with two elements.', $condition_value)); } if (count($json) != 2) { throw new HeraldInvalidConditionException( pht( 'The regular expression pair "%s" must have exactly two '. 'elements.', $condition_value)); } $key_regexp = array_shift($json); $val_regexp = array_shift($json); $key_ok = @preg_match($key_regexp, ''); if ($key_ok === false) { throw new HeraldInvalidConditionException( pht( 'The first regexp in the regexp pair, "%s", is not a valid '. 'regexp.', $key_regexp)); } $val_ok = @preg_match($val_regexp, ''); if ($val_ok === false) { throw new HeraldInvalidConditionException( pht( 'The second regexp in the regexp pair, "%s", is not a valid '. 'regexp.', $val_regexp)); } break; case self::CONDITION_CONTAINS: case self::CONDITION_NOT_CONTAINS: case self::CONDITION_IS: case self::CONDITION_IS_NOT: case self::CONDITION_IS_ANY: case self::CONDITION_IS_NOT_ANY: case self::CONDITION_INCLUDE_ALL: case self::CONDITION_INCLUDE_ANY: case self::CONDITION_INCLUDE_NONE: case self::CONDITION_IS_ME: case self::CONDITION_IS_NOT_ME: case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: case self::CONDITION_NEVER: case self::CONDITION_HAS_BIT: case self::CONDITION_NOT_BIT: case self::CONDITION_IS_TRUE: case self::CONDITION_IS_FALSE: // No explicit validation for these types, although there probably // should be in some cases. break; default: throw new HeraldInvalidConditionException( pht( 'Unknown condition "%s"!', $condition_type)); } } /* -( Actions )------------------------------------------------------------ */ public function getCustomActionsForRuleType($rule_type) { $results = array(); foreach ($this->getCustomActions() as $custom_action) { if ($custom_action->appliesToRuleType($rule_type)) { $results[] = $custom_action; } } return $results; } public function getActions($rule_type) { $custom_actions = $this->getCustomActionsForRuleType($rule_type); $custom_actions = mpull($custom_actions, 'getActionKey'); $actions = $custom_actions; $object = $this->newObject(); if (($object instanceof PhabricatorProjectInterface)) { if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_GLOBAL) { $actions[] = self::ACTION_ADD_PROJECTS; $actions[] = self::ACTION_REMOVE_PROJECTS; } } return $actions; } public function getActionNameMap($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: $standard = array( self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_ADD_CC => pht('Add emails to CC'), self::ACTION_REMOVE_CC => pht('Remove emails from CC'), self::ACTION_EMAIL => pht('Send an email to'), self::ACTION_AUDIT => pht('Trigger an Audit by'), self::ACTION_FLAG => pht('Mark with flag'), self::ACTION_ASSIGN_TASK => pht('Assign task to'), self::ACTION_ADD_PROJECTS => pht('Add projects'), self::ACTION_REMOVE_PROJECTS => pht('Remove projects'), self::ACTION_ADD_REVIEWERS => pht('Add reviewers'), self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'), self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'), self::ACTION_REQUIRE_SIGNATURE => pht('Require legal signatures'), self::ACTION_BLOCK => pht('Block change with message'), ); break; case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: $standard = array( self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_ADD_CC => pht('Add me to CC'), self::ACTION_REMOVE_CC => pht('Remove me from CC'), self::ACTION_EMAIL => pht('Send me an email'), self::ACTION_AUDIT => pht('Trigger an Audit by me'), self::ACTION_FLAG => pht('Mark with flag'), self::ACTION_ASSIGN_TASK => pht('Assign task to me'), self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'), self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add me as a blocking reviewer'), ); break; default: throw new Exception("Unknown rule type '{$rule_type}'!"); } $custom_actions = $this->getCustomActionsForRuleType($rule_type); $standard += mpull($custom_actions, 'getActionName', 'getActionKey'); return $standard; } public function willSaveAction( HeraldRule $rule, HeraldAction $action) { $target = $action->getTarget(); if (is_array($target)) { $target = array_keys($target); } $author_phid = $rule->getAuthorPHID(); $rule_type = $rule->getRuleType(); if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) { switch ($action->getAction()) { case self::ACTION_EMAIL: case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_AUDIT: case self::ACTION_ASSIGN_TASK: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: // For personal rules, force these actions to target the rule owner. $target = array($author_phid); break; case self::ACTION_FLAG: // Make sure flag color is valid; set to blue if not. $color_map = PhabricatorFlagColor::getColorNameMap(); if (empty($color_map[$target])) { $target = PhabricatorFlagColor::COLOR_BLUE; } break; case self::ACTION_BLOCK: case self::ACTION_NOTHING: break; default: throw new HeraldInvalidActionException( pht( 'Unrecognized action type "%s"!', $action->getAction())); } } $action->setTarget($target); } /* -( Values )------------------------------------------------------------- */ public function getValueTypeForFieldAndCondition($field, $condition) { if ($this->isHeraldCustomKey($field)) { $value_type = $this->getCustomFieldValueTypeForFieldAndCondition( $field, $condition); if ($value_type !== null) { return $value_type; } } switch ($condition) { case self::CONDITION_CONTAINS: case self::CONDITION_NOT_CONTAINS: case self::CONDITION_REGEXP: case self::CONDITION_REGEXP_PAIR: return self::VALUE_TEXT; case self::CONDITION_IS: case self::CONDITION_IS_NOT: switch ($field) { case self::FIELD_CONTENT_SOURCE: return self::VALUE_CONTENT_SOURCE; default: return self::VALUE_TEXT; } break; case self::CONDITION_IS_ANY: case self::CONDITION_IS_NOT_ANY: switch ($field) { case self::FIELD_REPOSITORY: return self::VALUE_REPOSITORY; case self::FIELD_TASK_PRIORITY: return self::VALUE_TASK_PRIORITY; case self::FIELD_TASK_STATUS: return self::VALUE_TASK_STATUS; case self::FIELD_ARCANIST_PROJECT: return self::VALUE_ARCANIST_PROJECT; default: return self::VALUE_USER; } break; case self::CONDITION_INCLUDE_ALL: case self::CONDITION_INCLUDE_ANY: case self::CONDITION_INCLUDE_NONE: switch ($field) { case self::FIELD_REPOSITORY: return self::VALUE_REPOSITORY; case self::FIELD_CC: return self::VALUE_EMAIL; case self::FIELD_TAGS: return self::VALUE_TAG; case self::FIELD_AFFECTED_PACKAGE: return self::VALUE_OWNERS_PACKAGE; case self::FIELD_AUTHOR_PROJECTS: case self::FIELD_PUSHER_PROJECTS: case self::FIELD_PROJECTS: case self::FIELD_REPOSITORY_PROJECTS: return self::VALUE_PROJECT; case self::FIELD_REVIEWERS: return self::VALUE_USER_OR_PROJECT; case self::FIELD_APPLICATION_EMAIL: return self::VALUE_APPLICATION_EMAIL; default: return self::VALUE_USER; } break; case self::CONDITION_IS_ME: case self::CONDITION_IS_NOT_ME: case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: case self::CONDITION_NEVER: case self::CONDITION_IS_TRUE: case self::CONDITION_IS_FALSE: return self::VALUE_NONE; case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: return self::VALUE_RULE; default: throw new Exception("Unknown condition '{$condition}'."); } } public function getValueTypeForAction($action, $rule_type) { $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); if ($is_personal) { switch ($action) { case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_EMAIL: case self::ACTION_NOTHING: case self::ACTION_AUDIT: case self::ACTION_ASSIGN_TASK: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: return self::VALUE_NONE; case self::ACTION_FLAG: return self::VALUE_FLAG_COLOR; case self::ACTION_ADD_PROJECTS: case self::ACTION_REMOVE_PROJECTS: return self::VALUE_PROJECT; } } else { switch ($action) { case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_EMAIL: return self::VALUE_EMAIL; case self::ACTION_NOTHING: return self::VALUE_NONE; case self::ACTION_ADD_PROJECTS: case self::ACTION_REMOVE_PROJECTS: return self::VALUE_PROJECT; case self::ACTION_FLAG: return self::VALUE_FLAG_COLOR; case self::ACTION_ASSIGN_TASK: return self::VALUE_USER; case self::ACTION_AUDIT: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: return self::VALUE_USER_OR_PROJECT; case self::ACTION_APPLY_BUILD_PLANS: return self::VALUE_BUILD_PLAN; case self::ACTION_REQUIRE_SIGNATURE: return self::VALUE_LEGAL_DOCUMENTS; case self::ACTION_BLOCK: return self::VALUE_TEXT; } } $custom_action = idx($this->getCustomActions(), $action); if ($custom_action !== null) { return $custom_action->getActionType(); } throw new Exception("Unknown or invalid action '".$action."'."); } /* -( Repetition )--------------------------------------------------------- */ public function getRepetitionOptions() { return array( HeraldRepetitionPolicyConfig::EVERY, ); } public static function getAllAdapters() { static $adapters; if (!$adapters) { $adapters = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); $adapters = msort($adapters, 'getAdapterSortKey'); } return $adapters; } public static function getAdapterForContentType($content_type) { $adapters = self::getAllAdapters(); foreach ($adapters as $adapter) { if ($adapter->getAdapterContentType() == $content_type) { return $adapter; } } throw new Exception( pht( 'No adapter exists for Herald content type "%s".', $content_type)); } public static function getEnabledAdapterMap(PhabricatorUser $viewer) { $map = array(); - $adapters = HeraldAdapter::getAllAdapters(); + $adapters = self::getAllAdapters(); foreach ($adapters as $adapter) { if (!$adapter->isAvailableToUser($viewer)) { continue; } $type = $adapter->getAdapterContentType(); $name = $adapter->getAdapterContentName(); $map[$type] = $name; } return $map; } public function renderRuleAsText( HeraldRule $rule, PhabricatorHandleList $handles) { require_celerity_resource('herald-css'); $icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-circle-right lightgreytext') ->addClass('herald-list-icon'); if ($rule->getMustMatchAll()) { $match_text = pht('When all of these conditions are met:'); } else { $match_text = pht('When any of these conditions are met:'); } $match_title = phutil_tag( 'p', array( 'class' => 'herald-list-description', ), $match_text); $match_list = array(); foreach ($rule->getConditions() as $condition) { $match_list[] = phutil_tag( 'div', array( 'class' => 'herald-list-item', ), array( $icon, $this->renderConditionAsText($condition, $handles), )); } $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt( HeraldRepetitionPolicyConfig::EVERY); if ($rule->getRepetitionPolicy() == $integer_code_for_every) { $action_text = pht('Take these actions every time this rule matches:'); } else { $action_text = pht('Take these actions the first time this rule matches:'); } $action_title = phutil_tag( 'p', array( 'class' => 'herald-list-description', ), $action_text); $action_list = array(); foreach ($rule->getActions() as $action) { $action_list[] = phutil_tag( 'div', array( 'class' => 'herald-list-item', ), array( $icon, $this->renderActionAsText($action, $handles), )); } return array( $match_title, $match_list, $action_title, $action_list, ); } private function renderConditionAsText( HeraldCondition $condition, PhabricatorHandleList $handles) { $field_type = $condition->getFieldName(); $default = $this->isHeraldCustomKey($field_type) ? pht('(Unknown Custom Field "%s")', $field_type) : pht('(Unknown Field "%s")', $field_type); $field_name = idx($this->getFieldNameMap(), $field_type, $default); $condition_type = $condition->getFieldCondition(); $condition_name = idx($this->getConditionNameMap(), $condition_type); $value = $this->renderConditionValueAsText($condition, $handles); return hsprintf(' %s %s %s', $field_name, $condition_name, $value); } private function renderActionAsText( HeraldAction $action, PhabricatorHandleList $handles) { $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL; $action_type = $action->getAction(); $action_name = idx($this->getActionNameMap($rule_global), $action_type); $target = $this->renderActionTargetAsText($action, $handles); return hsprintf(' %s %s', $action_name, $target); } private function renderConditionValueAsText( HeraldCondition $condition, PhabricatorHandleList $handles) { $value = $condition->getValue(); if (!is_array($value)) { $value = array($value); } switch ($condition->getFieldName()) { case self::FIELD_TASK_PRIORITY: $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); foreach ($value as $index => $val) { $name = idx($priority_map, $val); if ($name) { $value[$index] = $name; } } break; case self::FIELD_TASK_STATUS: $status_map = ManiphestTaskStatus::getTaskStatusMap(); foreach ($value as $index => $val) { $name = idx($status_map, $val); if ($name) { $value[$index] = $name; } } break; case HeraldPreCommitRefAdapter::FIELD_REF_CHANGE: $change_map = PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions(); foreach ($value as $index => $val) { $name = idx($change_map, $val); if ($name) { $value[$index] = $name; } } break; default: foreach ($value as $index => $val) { $handle = $handles->getHandleIfExists($val); if ($handle) { $value[$index] = $handle->renderLink(); } } break; } $value = phutil_implode_html(', ', $value); return $value; } private function renderActionTargetAsText( HeraldAction $action, PhabricatorHandleList $handles) { $target = $action->getTarget(); if (!is_array($target)) { $target = array($target); } foreach ($target as $index => $val) { switch ($action->getAction()) { case self::ACTION_FLAG: $target[$index] = PhabricatorFlagColor::getColorName($val); break; default: $handle = $handles->getHandleIfExists($val); if ($handle) { $target[$index] = $handle->renderLink(); } break; } } $target = phutil_implode_html(', ', $target); return $target; } /** * Given a @{class:HeraldRule}, this function extracts all the phids that * we'll want to load as handles later. * * This function performs a somewhat hacky approach to figuring out what * is and is not a phid - try to get the phid type and if the type is * *not* unknown assume its a valid phid. * * Don't try this at home. Use more strongly typed data at home. * * Think of the children. */ public static function getHandlePHIDs(HeraldRule $rule) { $phids = array($rule->getAuthorPHID()); foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); if (!is_array($value)) { $value = array($value); } foreach ($value as $val) { if (phid_get_type($val) != PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { $phids[] = $val; } } } foreach ($rule->getActions() as $action) { $target = $action->getTarget(); if (!is_array($target)) { $target = array($target); } foreach ($target as $val) { if (phid_get_type($val) != PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { $phids[] = $val; } } } if ($rule->isObjectRule()) { $phids[] = $rule->getTriggerObjectPHID(); } return $phids; } /* -( Custom Field Integration )------------------------------------------- */ /** * Returns the prefix used to namespace Herald fields which are based on * custom fields. * * @return string Key prefix. * @task customfield */ private function getCustomKeyPrefix() { return 'herald.custom/'; } /** * Determine if a field key is based on a custom field or a regular internal * field. * * @param string Field key. * @return bool True if the field key is based on a custom field. * @task customfield */ private function isHeraldCustomKey($key) { $prefix = $this->getCustomKeyPrefix(); return (strncmp($key, $prefix, strlen($prefix)) == 0); } /** * Convert a custom field key into a Herald field key. * * @param string Custom field key. * @return string Herald field key. * @task customfield */ private function getHeraldKeyFromCustomKey($key) { return $this->getCustomKeyPrefix().$key; } /** * Get custom fields for this adapter, if appliable. This will either return * a field list or `null` if the adapted object does not implement custom * fields or the adapter does not support them. * * @return PhabricatorCustomFieldList|null List of fields, or `null`. * @task customfield */ private function getCustomFields() { if ($this->customFields === false) { $this->customFields = null; $template_object = $this->newObject(); if ($template_object instanceof PhabricatorCustomFieldInterface) { $object = $this->getObject(); if (!$object) { $object = $template_object; } $fields = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_HERALD); $fields->setViewer(PhabricatorUser::getOmnipotentUser()); $fields->readFieldsFromStorage($object); $this->customFields = $fields; } } return $this->customFields; } /** * Get a custom field by Herald field key, or `null` if it does not exist * or custom fields are not supported. * * @param string Herald field key. * @return PhabricatorCustomField|null Matching field, if it exists. * @task customfield */ private function getCustomField($herald_field_key) { $fields = $this->getCustomFields(); if (!$fields) { return null; } foreach ($fields->getFields() as $custom_field) { $key = $custom_field->getFieldKey(); if ($this->getHeraldKeyFromCustomKey($key) == $herald_field_key) { return $custom_field; } } return null; } /** * Get the field map for custom fields. * * @return map<string, string> Map of Herald field keys to field names. * @task customfield */ private function getCustomFieldNameMap() { $fields = $this->getCustomFields(); if (!$fields) { return array(); } $map = array(); foreach ($fields->getFields() as $field) { $key = $field->getFieldKey(); $name = $field->getHeraldFieldName(); $map[$this->getHeraldKeyFromCustomKey($key)] = $name; } return $map; } /** * Get the value for a custom field. * * @param string Herald field key. * @return wild Custom field value. * @task customfield */ private function getCustomFieldValue($field_key) { $field = $this->getCustomField($field_key); if (!$field) { return null; } return $field->getHeraldFieldValue(); } /** * Get the Herald conditions for a custom field. * * @param string Herald field key. * @return list<const> List of Herald conditions. * @task customfield */ private function getCustomFieldConditions($field_key) { $field = $this->getCustomField($field_key); if (!$field) { return array( self::CONDITION_NEVER, ); } return $field->getHeraldFieldConditions(); } /** * Get the Herald value type for a custom field and condition. * * @param string Herald field key. * @param const Herald condition constant. * @return const|null Herald value type constant, or null to use the default. * @task customfield */ private function getCustomFieldValueTypeForFieldAndCondition( $field_key, $condition) { $field = $this->getCustomField($field_key); if (!$field) { return self::VALUE_NONE; } return $field->getHeraldFieldValueType($condition); } /* -( Applying Effects )--------------------------------------------------- */ /** * @task apply */ protected function applyStandardEffect(HeraldEffect $effect) { $action = $effect->getAction(); $rule_type = $effect->getRule()->getRuleType(); $supported = $this->getActions($rule_type); $supported = array_fuse($supported); if (empty($supported[$action])) { throw new Exception( pht( 'Adapter "%s" does not support action "%s" for rule type "%s".', get_class($this), $action, $rule_type)); } switch ($action) { case self::ACTION_ADD_PROJECTS: case self::ACTION_REMOVE_PROJECTS: return $this->applyProjectsEffect($effect); case self::ACTION_FLAG: return $this->applyFlagEffect($effect); case self::ACTION_EMAIL: return $this->applyEmailEffect($effect); default: break; } $result = $this->handleCustomHeraldEffect($effect); if (!$result) { throw new Exception( pht( 'No custom action exists to handle rule action "%s".', $action)); } return $result; } /** * @task apply */ private function applyProjectsEffect(HeraldEffect $effect) { if ($effect->getAction() == self::ACTION_ADD_PROJECTS) { $kind = '+'; } else { $kind = '-'; } $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $project_phids = $effect->getTarget(); $xaction = $this->newTransaction() ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $project_type) ->setNewValue( array( $kind => array_fuse($project_phids), )); $this->queueTransaction($xaction); return new HeraldApplyTranscript( $effect, true, pht('Added projects.')); } /** * @task apply */ private function applyFlagEffect(HeraldEffect $effect) { $phid = $this->getPHID(); $color = $effect->getTarget(); $rule = $effect->getRule(); $user = $rule->getAuthor(); $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid); if ($flag) { return new HeraldApplyTranscript( $effect, false, pht('Object already flagged.')); } $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($phid)) ->executeOne(); $flag = new PhabricatorFlag(); $flag->setOwnerPHID($user->getPHID()); $flag->setType($handle->getType()); $flag->setObjectPHID($handle->getPHID()); // TOOD: Should really be transcript PHID, but it doesn't exist yet. $flag->setReasonPHID($user->getPHID()); $flag->setColor($color); $flag->setNote( pht('Flagged by Herald Rule "%s".', $rule->getName())); $flag->save(); return new HeraldApplyTranscript( $effect, true, pht('Added flag.')); } /** * @task apply */ private function applyEmailEffect(HeraldEffect $effect) { foreach ($effect->getTarget() as $phid) { $this->emailPHIDs[$phid] = $phid; // If this is a personal rule, we'll force delivery of a real email. This // effect is stronger than notification preferences, so you get an actual // email even if your preferences are set to "Notify" or "Ignore". $rule = $effect->getRule(); if ($rule->isPersonalRule()) { $this->forcedEmailPHIDs[$phid] = $phid; } } return new HeraldApplyTranscript( $effect, true, pht('Added mailable to mail targets.')); } } diff --git a/src/applications/macro/controller/PhabricatorMacroMemeController.php b/src/applications/macro/controller/PhabricatorMacroMemeController.php index badb1759ad..ff25a8a8a4 100644 --- a/src/applications/macro/controller/PhabricatorMacroMemeController.php +++ b/src/applications/macro/controller/PhabricatorMacroMemeController.php @@ -1,69 +1,69 @@ <?php final class PhabricatorMacroMemeController extends PhabricatorMacroController { public function shouldAllowPublic() { return true; } public function processRequest() { $request = $this->getRequest(); $macro_name = $request->getStr('macro'); $upper_text = $request->getStr('uppertext'); $lower_text = $request->getStr('lowertext'); $user = $request->getUser(); - $uri = PhabricatorMacroMemeController::generateMacro($user, $macro_name, + $uri = self::generateMacro($user, $macro_name, $upper_text, $lower_text); if ($uri === false) { return new Aphront404Response(); } return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI($uri); } public static function generateMacro($user, $macro_name, $upper_text, $lower_text) { $macro = id(new PhabricatorMacroQuery()) ->setViewer($user) ->withNames(array($macro_name)) ->needFiles(true) ->executeOne(); if (!$macro) { return false; } $file = $macro->getFile(); $upper_text = strtoupper($upper_text); $lower_text = strtoupper($lower_text); $mixed_text = md5($upper_text).':'.md5($lower_text); $hash = 'meme'.hash('sha256', $mixed_text); $xform = id(new PhabricatorTransformedFile()) ->loadOneWhere('originalphid=%s and transform=%s', $file->getPHID(), $hash); if ($xform) { $memefile = id(new PhabricatorFileQuery()) ->setViewer($user) ->withPHIDs(array($xform->getTransformedPHID())) ->executeOne(); if ($memefile) { return $memefile->getBestURI(); } } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $transformers = (new PhabricatorImageTransformer()); $newfile = $transformers ->executeMemeTransform($file, $upper_text, $lower_text); $xfile = new PhabricatorTransformedFile(); $xfile->setOriginalPHID($file->getPHID()); $xfile->setTransformedPHID($newfile->getPHID()); $xfile->setTransform($hash); $xfile->save(); return $newfile->getBestURI(); } } diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index 60e3425af6..4a6381581b 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -1,862 +1,862 @@ <?php /** * Query tasks by specific criteria. This class uses the higher-performance * but less-general Maniphest indexes to satisfy queries. */ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $taskIDs = array(); private $taskPHIDs = array(); private $authorPHIDs = array(); private $ownerPHIDs = array(); private $noOwner; private $anyOwner; private $subscriberPHIDs = array(); private $dateCreatedAfter; private $dateCreatedBefore; private $dateModifiedAfter; private $dateModifiedBefore; private $subpriorityMin; private $subpriorityMax; private $fullTextSearch = ''; private $status = 'status-any'; const STATUS_ANY = 'status-any'; const STATUS_OPEN = 'status-open'; const STATUS_CLOSED = 'status-closed'; const STATUS_RESOLVED = 'status-resolved'; const STATUS_WONTFIX = 'status-wontfix'; const STATUS_INVALID = 'status-invalid'; const STATUS_SPITE = 'status-spite'; const STATUS_DUPLICATE = 'status-duplicate'; private $statuses; private $priorities; private $subpriorities; private $groupBy = 'group-none'; const GROUP_NONE = 'group-none'; const GROUP_PRIORITY = 'group-priority'; const GROUP_OWNER = 'group-owner'; const GROUP_STATUS = 'group-status'; const GROUP_PROJECT = 'group-project'; private $orderBy = 'order-modified'; const ORDER_PRIORITY = 'order-priority'; const ORDER_CREATED = 'order-created'; const ORDER_MODIFIED = 'order-modified'; const ORDER_TITLE = 'order-title'; private $needSubscriberPHIDs; private $needProjectPHIDs; private $blockingTasks; private $blockedTasks; public function withAuthors(array $authors) { $this->authorPHIDs = $authors; return $this; } public function withIDs(array $ids) { $this->taskIDs = $ids; return $this; } public function withPHIDs(array $phids) { $this->taskPHIDs = $phids; return $this; } public function withOwners(array $owners) { $no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; $any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN; foreach ($owners as $k => $phid) { if ($phid === $no_owner || $phid === null) { $this->noOwner = true; unset($owners[$k]); break; } if ($phid === $any_owner) { $this->anyOwner = true; unset($owners[$k]); break; } } $this->ownerPHIDs = $owners; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withPriorities(array $priorities) { $this->priorities = $priorities; return $this; } public function withSubpriorities(array $subpriorities) { $this->subpriorities = $subpriorities; return $this; } public function withSubpriorityBetween($min, $max) { $this->subpriorityMin = $min; $this->subpriorityMax = $max; return $this; } public function withSubscribers(array $subscribers) { $this->subscriberPHIDs = $subscribers; return $this; } public function withFullTextSearch($fulltext_search) { $this->fullTextSearch = $fulltext_search; return $this; } public function setGroupBy($group) { $this->groupBy = $group; return $this; } public function setOrderBy($order) { $this->orderBy = $order; return $this; } /** * True returns tasks that are blocking other tasks only. * False returns tasks that are not blocking other tasks only. * Null returns tasks regardless of blocking status. */ public function withBlockingTasks($mode) { $this->blockingTasks = $mode; return $this; } public function shouldJoinBlockingTasks() { return $this->blockingTasks !== null; } /** * True returns tasks that are blocked by other tasks only. * False returns tasks that are not blocked by other tasks only. * Null returns tasks regardless of blocked by status. */ public function withBlockedTasks($mode) { $this->blockedTasks = $mode; return $this; } public function shouldJoinBlockedTasks() { return $this->blockedTasks !== null; } public function withDateCreatedBefore($date_created_before) { $this->dateCreatedBefore = $date_created_before; return $this; } public function withDateCreatedAfter($date_created_after) { $this->dateCreatedAfter = $date_created_after; return $this; } public function withDateModifiedBefore($date_modified_before) { $this->dateModifiedBefore = $date_modified_before; return $this; } public function withDateModifiedAfter($date_modified_after) { $this->dateModifiedAfter = $date_modified_after; return $this; } public function needSubscriberPHIDs($bool) { $this->needSubscriberPHIDs = $bool; return $this; } public function needProjectPHIDs($bool) { $this->needProjectPHIDs = $bool; return $this; } protected function newResultObject() { return new ManiphestTask(); } protected function willExecute() { // If we already have an order vector, use it as provided. // TODO: This is a messy hack to make setOrderVector() stronger than // setPriority(). $vector = $this->getOrderVector(); $keys = mpull(iterator_to_array($vector), 'getOrderKey'); if (array_values($keys) !== array('id')) { return; } $parts = array(); switch ($this->groupBy) { case self::GROUP_NONE: break; case self::GROUP_PRIORITY: $parts[] = array('priority'); break; case self::GROUP_OWNER: $parts[] = array('owner'); break; case self::GROUP_STATUS: $parts[] = array('status'); break; case self::GROUP_PROJECT: $parts[] = array('project'); break; } if ($this->applicationSearchOrders) { $columns = array(); foreach ($this->applicationSearchOrders as $order) { $part = 'custom:'.$order['key']; if ($order['ascending']) { $part = '-'.$part; } $columns[] = $part; } $columns[] = 'id'; $parts[] = $columns; } else { switch ($this->orderBy) { case self::ORDER_PRIORITY: $parts[] = array('priority', 'subpriority', 'id'); break; case self::ORDER_CREATED: $parts[] = array('id'); break; case self::ORDER_MODIFIED: $parts[] = array('updated', 'id'); break; case self::ORDER_TITLE: $parts[] = array('title', 'id'); break; } } $parts = array_mergev($parts); // We may have a duplicate column if we are both ordering and grouping // by priority. $parts = array_unique($parts); $this->setOrderVector($parts); } protected function loadPage() { $task_dao = new ManiphestTask(); $conn = $task_dao->establishConnection('r'); $where = array(); $where[] = $this->buildTaskIDsWhereClause($conn); $where[] = $this->buildTaskPHIDsWhereClause($conn); $where[] = $this->buildStatusWhereClause($conn); $where[] = $this->buildStatusesWhereClause($conn); $where[] = $this->buildDependenciesWhereClause($conn); $where[] = $this->buildAuthorWhereClause($conn); $where[] = $this->buildOwnerWhereClause($conn); $where[] = $this->buildFullTextWhereClause($conn); if ($this->dateCreatedAfter) { $where[] = qsprintf( $conn, 'task.dateCreated >= %d', $this->dateCreatedAfter); } if ($this->dateCreatedBefore) { $where[] = qsprintf( $conn, 'task.dateCreated <= %d', $this->dateCreatedBefore); } if ($this->dateModifiedAfter) { $where[] = qsprintf( $conn, 'task.dateModified >= %d', $this->dateModifiedAfter); } if ($this->dateModifiedBefore) { $where[] = qsprintf( $conn, 'task.dateModified <= %d', $this->dateModifiedBefore); } if ($this->priorities) { $where[] = qsprintf( $conn, 'task.priority IN (%Ld)', $this->priorities); } if ($this->subpriorities) { $where[] = qsprintf( $conn, 'task.subpriority IN (%Lf)', $this->subpriorities); } if ($this->subpriorityMin) { $where[] = qsprintf( $conn, 'task.subpriority >= %f', $this->subpriorityMin); } if ($this->subpriorityMax) { $where[] = qsprintf( $conn, 'task.subpriority <= %f', $this->subpriorityMax); } $where[] = $this->buildWhereClauseParts($conn); $where = $this->formatWhereClause($where); $group_column = ''; switch ($this->groupBy) { case self::GROUP_PROJECT: $group_column = qsprintf( $conn, ', projectGroupName.indexedObjectPHID projectGroupPHID'); break; } $rows = queryfx_all( $conn, '%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q', $this->buildSelectClause($conn), $group_column, $task_dao->getTableName(), $this->buildJoinClause($conn), $where, $this->buildGroupClause($conn), $this->buildHavingClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); switch ($this->groupBy) { case self::GROUP_PROJECT: $data = ipull($rows, null, 'id'); break; default: $data = $rows; break; } $tasks = $task_dao->loadAllFromArray($data); switch ($this->groupBy) { case self::GROUP_PROJECT: $results = array(); foreach ($rows as $row) { $task = clone $tasks[$row['id']]; $task->attachGroupByProjectPHID($row['projectGroupPHID']); $results[] = $task; } $tasks = $results; break; } return $tasks; } protected function willFilterPage(array $tasks) { if ($this->groupBy == self::GROUP_PROJECT) { // We should only return project groups which the user can actually see. $project_phids = mpull($tasks, 'getGroupByProjectPHID'); $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($project_phids) ->execute(); $projects = mpull($projects, null, 'getPHID'); foreach ($tasks as $key => $task) { if (!$task->getGroupByProjectPHID()) { // This task is either not in any projects, or only in projects // which we're ignoring because they're being queried for explicitly. continue; } if (empty($projects[$task->getGroupByProjectPHID()])) { unset($tasks[$key]); } } } return $tasks; } protected function didFilterPage(array $tasks) { $phids = mpull($tasks, 'getPHID'); if ($this->needProjectPHIDs) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); foreach ($tasks as $task) { $project_phids = $edge_query->getDestinationPHIDs( array($task->getPHID())); $task->attachProjectPHIDs($project_phids); } } if ($this->needSubscriberPHIDs) { $subscriber_sets = id(new PhabricatorSubscribersQuery()) ->withObjectPHIDs($phids) ->execute(); foreach ($tasks as $task) { $subscribers = idx($subscriber_sets, $task->getPHID(), array()); $task->attachSubscriberPHIDs($subscribers); } } return $tasks; } private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) { if (!$this->taskIDs) { return null; } return qsprintf( $conn, 'task.id in (%Ld)', $this->taskIDs); } private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) { if (!$this->taskPHIDs) { return null; } return qsprintf( $conn, 'task.phid in (%Ls)', $this->taskPHIDs); } private function buildStatusWhereClause(AphrontDatabaseConnection $conn) { static $map = array( self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID, self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE, self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE, ); switch ($this->status) { case self::STATUS_ANY: return null; case self::STATUS_OPEN: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); case self::STATUS_CLOSED: return qsprintf( $conn, 'task.status IN (%Ls)', ManiphestTaskStatus::getClosedStatusConstants()); default: $constant = idx($map, $this->status); if (!$constant) { throw new Exception("Unknown status query '{$this->status}'!"); } return qsprintf( $conn, 'task.status = %s', $constant); } } private function buildStatusesWhereClause(AphrontDatabaseConnection $conn) { if ($this->statuses) { return qsprintf( $conn, 'task.status IN (%Ls)', $this->statuses); } return null; } private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) { if (!$this->authorPHIDs) { return null; } return qsprintf( $conn, 'task.authorPHID in (%Ls)', $this->authorPHIDs); } private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) { $subclause = array(); if ($this->noOwner) { $subclause[] = qsprintf( $conn, 'task.ownerPHID IS NULL'); } if ($this->anyOwner) { $subclause[] = qsprintf( $conn, 'task.ownerPHID IS NOT NULL'); } if ($this->ownerPHIDs) { $subclause[] = qsprintf( $conn, 'task.ownerPHID IN (%Ls)', $this->ownerPHIDs); } if (!$subclause) { return ''; } return '('.implode(') OR (', $subclause).')'; } private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) { if (!strlen($this->fullTextSearch)) { return null; } // In doing a fulltext search, we first find all the PHIDs that match the // fulltext search, and then use that to limit the rest of the search $fulltext_query = id(new PhabricatorSavedQuery()) ->setEngineClassName('PhabricatorSearchApplicationSearchEngine') ->setParameter('query', $this->fullTextSearch); // NOTE: Setting this to something larger than 2^53 will raise errors in // ElasticSearch, and billions of results won't fit in memory anyway. $fulltext_query->setParameter('limit', 100000); $fulltext_query->setParameter('types', array(ManiphestTaskPHIDType::TYPECONST)); $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine(); $fulltext_results = $engine->executeSearch($fulltext_query); if (empty($fulltext_results)) { $fulltext_results = array(null); } return qsprintf( $conn, 'task.phid IN (%Ls)', $fulltext_results); } private function buildDependenciesWhereClause( AphrontDatabaseConnection $conn) { if (!$this->shouldJoinBlockedTasks() && !$this->shouldJoinBlockingTasks()) { return null; } $parts = array(); if ($this->blockingTasks === true) { $parts[] = qsprintf( $conn, 'blocking.dst IS NOT NULL AND blockingtask.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } else if ($this->blockingTasks === false) { $parts[] = qsprintf( $conn, 'blocking.dst IS NULL OR blockingtask.status NOT IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } if ($this->blockedTasks === true) { $parts[] = qsprintf( $conn, 'blocked.dst IS NOT NULL AND blockedtask.status IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } else if ($this->blockedTasks === false) { $parts[] = qsprintf( $conn, 'blocked.dst IS NULL OR blockedtask.status NOT IN (%Ls)', ManiphestTaskStatus::getOpenStatusConstants()); } return '('.implode(') OR (', $parts).')'; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) { $edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE; $joins = array(); if ($this->shouldJoinBlockingTasks()) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T blocking ON blocking.src = task.phid '. 'AND blocking.type = %d '. 'LEFT JOIN %T blockingtask ON blocking.dst = blockingtask.phid', $edge_table, ManiphestTaskDependedOnByTaskEdgeType::EDGECONST, id(new ManiphestTask())->getTableName()); } if ($this->shouldJoinBlockedTasks()) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T blocked ON blocked.src = task.phid '. 'AND blocked.type = %d '. 'LEFT JOIN %T blockedtask ON blocked.dst = blockedtask.phid', $edge_table, ManiphestTaskDependsOnTaskEdgeType::EDGECONST, id(new ManiphestTask())->getTableName()); } if ($this->subscriberPHIDs) { $joins[] = qsprintf( $conn_r, 'JOIN %T e_ccs ON e_ccs.src = task.phid '. 'AND e_ccs.type = %s '. 'AND e_ccs.dst in (%Ls)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasSubscriberEdgeType::EDGECONST, $this->subscriberPHIDs); } switch ($this->groupBy) { case self::GROUP_PROJECT: $ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs(); if ($ignore_group_phids) { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d AND projectGroup.dst NOT IN (%Ls)', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $ignore_group_phids); } else { $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src AND projectGroup.type = %d', $edge_table, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); } $joins[] = qsprintf( $conn_r, 'LEFT JOIN %T projectGroupName ON projectGroup.dst = projectGroupName.indexedObjectPHID', id(new ManiphestNameIndex())->getTableName()); break; } $joins[] = parent::buildJoinClauseParts($conn_r); return $joins; } protected function buildGroupClause(AphrontDatabaseConnection $conn_r) { $joined_multiple_rows = $this->shouldJoinBlockingTasks() || $this->shouldJoinBlockedTasks() || ($this->shouldGroupQueryResultRows()); $joined_project_name = ($this->groupBy == self::GROUP_PROJECT); // If we're joining multiple rows, we need to group the results by the // task IDs. if ($joined_multiple_rows) { if ($joined_project_name) { return 'GROUP BY task.phid, projectGroup.dst'; } else { return 'GROUP BY task.phid'; } } else { return ''; } } /** * Return project PHIDs which we should ignore when grouping tasks by * project. For example, if a user issues a query like: * * Tasks in all projects: Frontend, Bugs * * ...then we don't show "Frontend" or "Bugs" groups in the result set, since * they're meaningless as all results are in both groups. * * Similarly, for queries like: * * Tasks in any projects: Public Relations * * ...we ignore the single project, as every result is in that project. (In * the case that there are several "any" projects, we do not ignore them.) * * @return list<phid> Project PHIDs which should be ignored in query * construction. */ private function getIgnoreGroupedProjectPHIDs() { // Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't // impact the results, but we might end up with a better query plan. // Investigate this on real data? This is likely very rare. $edge_types = array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, ); $phids = array(); $phids[] = $this->getEdgeLogicValues( $edge_types, array( PhabricatorQueryConstraint::OPERATOR_AND, )); $any = $this->getEdgeLogicValues( $edge_types, array( PhabricatorQueryConstraint::OPERATOR_OR, )); if (count($any) == 1) { $phids[] = $any; } return array_mergev($phids); } protected function getResultCursor($result) { $id = $result->getID(); if ($this->groupBy == self::GROUP_PROJECT) { - return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');; + return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.'); } return $id; } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'priority' => array( 'table' => 'task', 'column' => 'priority', 'type' => 'int', ), 'owner' => array( 'table' => 'task', 'column' => 'ownerOrdering', 'null' => 'head', 'reverse' => true, 'type' => 'string', ), 'status' => array( 'table' => 'task', 'column' => 'status', 'type' => 'string', 'reverse' => true, ), 'project' => array( 'table' => 'projectGroupName', 'column' => 'indexedObjectName', 'type' => 'string', 'null' => 'head', 'reverse' => true, ), 'title' => array( 'table' => 'task', 'column' => 'title', 'type' => 'string', 'reverse' => true, ), 'subpriority' => array( 'table' => 'task', 'column' => 'subpriority', 'type' => 'float', ), 'updated' => array( 'table' => 'task', 'column' => 'dateModified', 'type' => 'int', ), ); } protected function getPagingValueMap($cursor, array $keys) { $cursor_parts = explode('.', $cursor, 2); $task_id = $cursor_parts[0]; $group_id = idx($cursor_parts, 1); $task = $this->loadCursorObject($task_id); $map = array( 'id' => $task->getID(), 'priority' => $task->getPriority(), 'subpriority' => $task->getSubpriority(), 'owner' => $task->getOwnerOrdering(), 'status' => $task->getStatus(), 'title' => $task->getTitle(), 'updated' => $task->getDateModified(), ); foreach ($keys as $key) { switch ($key) { case 'project': $value = null; if ($group_id) { $paging_projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($group_id)) ->execute(); if ($paging_projects) { $value = head($paging_projects)->getName(); } } $map[$key] = $value; break; } } foreach ($keys as $key) { if ($this->isCustomFieldOrderKey($key)) { $map += $this->getPagingValueMapForCustomFields($task); break; } } return $map; } protected function getPrimaryTableAlias() { return 'task'; } public function getQueryApplicationClass() { return 'PhabricatorManiphestApplication'; } } diff --git a/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php b/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php index e6011768df..cc78df3492 100644 --- a/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php +++ b/src/applications/metamta/command/MetaMTAEmailTransactionCommand.php @@ -1,120 +1,120 @@ <?php /** * @task docs Command Documentation */ abstract class MetaMTAEmailTransactionCommand extends Phobject { abstract public function getCommand(); /** * Return a brief human-readable description of the command effect. * * This should normally be one or two sentences briefly describing the * command behavior. * * @return string Brief human-readable remarkup. * @task docs */ abstract public function getCommandSummary(); /** * Return a one-line Remarkup description of command syntax for documentation. * * @return string Brief human-readable remarkup. * @task docs */ public function getCommandSyntax() { return '**!'.$this->getCommand().'**'; } /** * Return a longer human-readable description of the command effect. * * This can be as long as necessary to explain the command. * * @return string Human-readable remarkup of whatever length is desired. * @task docs */ public function getCommandDescription() { return null; } abstract public function isCommandSupportedForObject( PhabricatorApplicationTransactionInterface $object); abstract public function buildTransactions( PhabricatorUser $viewer, PhabricatorApplicationTransactionInterface $object, PhabricatorMetaMTAReceivedMail $mail, $command, array $argv); public function getCommandAliases() { return array(); } public function getCommandObjects() { return array($this); } public static function getAllCommands() { static $commands; if ($commands === null) { $kinds = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); $commands = array(); foreach ($kinds as $kind) { foreach ($kind->getCommandObjects() as $command) { $commands[] = $command; } } } return $commands; } public static function getAllCommandsForObject( PhabricatorApplicationTransactionInterface $object) { $commands = self::getAllCommands(); foreach ($commands as $key => $command) { if (!$command->isCommandSupportedForObject($object)) { unset($commands[$key]); } } return $commands; } public static function getCommandMap(array $commands) { - assert_instances_of($commands, 'MetaMTAEmailTransactionCommand'); + assert_instances_of($commands, __CLASS__); $map = array(); foreach ($commands as $command) { $keywords = $command->getCommandAliases(); $keywords[] = $command->getCommand(); foreach ($keywords as $keyword) { $keyword = phutil_utf8_strtolower($keyword); if (empty($map[$keyword])) { $map[$keyword] = $command; } else { throw new Exception( pht( 'Mail commands "%s" and "%s" both respond to keyword "%s". '. 'Keywords must be uniquely associated with commands.', get_class($command), get_class($map[$keyword]), $keyword)); } } } return $map; } } diff --git a/src/applications/metamta/contentsource/PhabricatorContentSource.php b/src/applications/metamta/contentsource/PhabricatorContentSource.php index a1cbd061f2..e5a29572c1 100644 --- a/src/applications/metamta/contentsource/PhabricatorContentSource.php +++ b/src/applications/metamta/contentsource/PhabricatorContentSource.php @@ -1,104 +1,104 @@ <?php final class PhabricatorContentSource { const SOURCE_UNKNOWN = 'unknown'; const SOURCE_WEB = 'web'; const SOURCE_EMAIL = 'email'; const SOURCE_CONDUIT = 'conduit'; const SOURCE_MOBILE = 'mobile'; const SOURCE_TABLET = 'tablet'; const SOURCE_FAX = 'fax'; const SOURCE_CONSOLE = 'console'; const SOURCE_HERALD = 'herald'; const SOURCE_LEGACY = 'legacy'; const SOURCE_DAEMON = 'daemon'; const SOURCE_LIPSUM = 'lipsum'; const SOURCE_PHORTUNE = 'phortune'; private $source; private $params = array(); private function __construct() { // <empty> } public static function newForSource($source, array $params) { $obj = new PhabricatorContentSource(); $obj->source = $source; $obj->params = $params; return $obj; } public static function newFromSerialized($serialized) { $dict = json_decode($serialized, true); if (!is_array($dict)) { $dict = array(); } $obj = new PhabricatorContentSource(); $obj->source = idx($dict, 'source', self::SOURCE_UNKNOWN); $obj->params = idx($dict, 'params', array()); return $obj; } public static function newConsoleSource() { return self::newForSource( - PhabricatorContentSource::SOURCE_CONSOLE, + self::SOURCE_CONSOLE, array()); } public static function newFromRequest(AphrontRequest $request) { return self::newForSource( - PhabricatorContentSource::SOURCE_WEB, + self::SOURCE_WEB, array( 'ip' => $request->getRemoteAddr(), )); } public static function newFromConduitRequest(ConduitAPIRequest $request) { return self::newForSource( - PhabricatorContentSource::SOURCE_CONDUIT, + self::SOURCE_CONDUIT, array()); } public static function getSourceNameMap() { return array( self::SOURCE_WEB => pht('Web'), self::SOURCE_EMAIL => pht('Email'), self::SOURCE_CONDUIT => pht('Conduit'), self::SOURCE_MOBILE => pht('Mobile'), self::SOURCE_TABLET => pht('Tablet'), self::SOURCE_FAX => pht('Fax'), self::SOURCE_CONSOLE => pht('Console'), self::SOURCE_LEGACY => pht('Legacy'), self::SOURCE_HERALD => pht('Herald'), self::SOURCE_DAEMON => pht('Daemons'), self::SOURCE_LIPSUM => pht('Lipsum'), self::SOURCE_UNKNOWN => pht('Old World'), self::SOURCE_PHORTUNE => pht('Phortune'), ); } public function serialize() { return json_encode(array( 'source' => $this->getSource(), 'params' => $this->getParams(), )); } public function getSource() { return $this->source; } public function getParams() { return $this->params; } public function getParam($key, $default = null) { return idx($this->params, $key, $default); } } diff --git a/src/applications/nuance/storage/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php index 2db403cd2e..50d7ee72ef 100644 --- a/src/applications/nuance/storage/NuanceItem.php +++ b/src/applications/nuance/storage/NuanceItem.php @@ -1,137 +1,137 @@ <?php final class NuanceItem extends NuanceDAO implements PhabricatorPolicyInterface { const STATUS_OPEN = 0; const STATUS_ASSIGNED = 10; const STATUS_CLOSED = 20; protected $status; protected $ownerPHID; protected $requestorPHID; protected $sourcePHID; protected $sourceLabel; protected $data; protected $mailKey; protected $dateNuanced; public static function initializeNewItem(PhabricatorUser $user) { return id(new NuanceItem()) ->setDateNuanced(time()) - ->setStatus(NuanceItem::STATUS_OPEN); + ->setStatus(self::STATUS_OPEN); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'data' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'ownerPHID' => 'phid?', 'sourceLabel' => 'text255?', 'status' => 'uint32', 'mailKey' => 'bytes20', 'dateNuanced' => 'epoch', ), self::CONFIG_KEY_SCHEMA => array( 'key_source' => array( 'columns' => array('sourcePHID', 'status', 'dateNuanced', 'id'), ), 'key_owner' => array( 'columns' => array('ownerPHID', 'status', 'dateNuanced', 'id'), ), 'key_contacter' => array( 'columns' => array('requestorPHID', 'status', 'dateNuanced', 'id'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( NuanceItemPHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getURI() { return '/nuance/item/view/'.$this->getID().'/'; } public function getLabel(PhabricatorUser $viewer) { // this is generated at the time the item is created based on // the configuration from the item source. It is typically // something like 'Twitter'. $source_label = $this->getSourceLabel(); return pht( 'Item via %s @ %s.', $source_label, phabricator_datetime($this->getDateCreated(), $viewer)); } public function getRequestor() { return $this->assertAttached($this->requestor); } public function attachRequestor(NuanceRequestor $requestor) { return $this->requestor = $requestor; } public function getSource() { return $this->assertAttached($this->source); } public function attachSource(NuanceSource $source) { $this->source = $source; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // TODO - this should be based on the queues the item currently resides in return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { // TODO - requestors should get auto access too! return $viewer->getPHID() == $this->ownerPHID; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht('Owners of an item can always view it.'); case PhabricatorPolicyCapability::CAN_EDIT: return pht('Owners of an item can always edit it.'); } return null; } public function toDictionary() { return array( 'id' => $this->getID(), 'phid' => $this->getPHID(), 'ownerPHID' => $this->getOwnerPHID(), 'requestorPHID' => $this->getRequestorPHID(), 'sourcePHID' => $this->getSourcePHID(), 'sourceLabel' => $this->getSourceLabel(), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), 'dateNuanced' => $this->getDateNuanced(), ); } } diff --git a/src/applications/paste/storage/PhabricatorPasteTransaction.php b/src/applications/paste/storage/PhabricatorPasteTransaction.php index cc4e36d2ff..34cc6bd92d 100644 --- a/src/applications/paste/storage/PhabricatorPasteTransaction.php +++ b/src/applications/paste/storage/PhabricatorPasteTransaction.php @@ -1,207 +1,207 @@ <?php final class PhabricatorPasteTransaction extends PhabricatorApplicationTransaction { const TYPE_CONTENT = 'paste.create'; const TYPE_TITLE = 'paste.title'; const TYPE_LANGUAGE = 'paste.language'; const MAILTAG_CONTENT = 'paste-content'; const MAILTAG_OTHER = 'paste-other'; const MAILTAG_COMMENT = 'paste-comment'; public function getApplicationName() { return 'pastebin'; } public function getApplicationTransactionType() { return PhabricatorPastePastePHIDType::TYPECONST; } public function getApplicationTransactionCommentObject() { return new PhabricatorPasteTransactionComment(); } public function getRequiredHandlePHIDs() { $phids = parent::getRequiredHandlePHIDs(); switch ($this->getTransactionType()) { case self::TYPE_CONTENT: $phids[] = $this->getObjectPHID(); break; } return $phids; } public function shouldHide() { $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: case self::TYPE_LANGUAGE: return ($old === null); } return parent::shouldHide(); } public function getIcon() { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return 'fa-plus'; break; case self::TYPE_TITLE: case self::TYPE_LANGUAGE: return 'fa-pencil'; break; } return parent::getIcon(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { - case PhabricatorPasteTransaction::TYPE_CONTENT: + case self::TYPE_CONTENT: if ($old === null) { return pht( '%s created this paste.', $this->renderHandleLink($author_phid)); } else { return pht( '%s edited the content of this paste.', $this->renderHandleLink($author_phid)); } break; - case PhabricatorPasteTransaction::TYPE_TITLE: + case self::TYPE_TITLE: return pht( '%s updated the paste\'s title to "%s".', $this->renderHandleLink($author_phid), $new); break; - case PhabricatorPasteTransaction::TYPE_LANGUAGE: + case self::TYPE_LANGUAGE: return pht( "%s updated the paste's language.", $this->renderHandleLink($author_phid)); break; } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { - case PhabricatorPasteTransaction::TYPE_CONTENT: + case self::TYPE_CONTENT: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s edited %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } break; - case PhabricatorPasteTransaction::TYPE_TITLE: + case self::TYPE_TITLE: return pht( '%s updated the title for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; - case PhabricatorPasteTransaction::TYPE_LANGUAGE: + case self::TYPE_LANGUAGE: return pht( '%s updated the language for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; } return parent::getTitleForFeed(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return PhabricatorTransactions::COLOR_GREEN; } return parent::getColor(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return ($this->getOldValue() !== null); } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: $old = $this->getOldValue(); $new = $this->getNewValue(); $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array_filter(array($old, $new))) ->execute(); $files = mpull($files, null, 'getPHID'); $old_text = ''; if (idx($files, $old)) { $old_text = $files[$old]->loadFileData(); } $new_text = ''; if (idx($files, $new)) { $new_text = $files[$new]->loadFileData(); } return $this->renderTextCorpusChangeDetails( $viewer, $old_text, $new_text); } return parent::renderChangeDetails($viewer); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: case self::TYPE_CONTENT: case self::TYPE_LANGUAGE: $tags[] = self::MAILTAG_CONTENT; break; case PhabricatorTransactions::TYPE_COMMENT: $tags[] = self::MAILTAG_COMMENT; break; default: $tags[] = self::MAILTAG_OTHER; break; } return $tags; } } diff --git a/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php b/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php index f5c3c73a9a..6cc6abf37b 100644 --- a/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php +++ b/src/applications/people/editor/__tests__/PhabricatorUserEditorTestCase.php @@ -1,90 +1,90 @@ <?php final class PhabricatorUserEditorTestCase extends PhabricatorTestCase { protected function getPhabricatorTestCaseConfiguration() { return array( self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true, ); } public function testRegistrationEmailOK() { $env = PhabricatorEnv::beginScopedEnv(); $env->overrideEnvConfig('auth.email-domains', array('example.com')); $this->registerUser( 'PhabricatorUserEditorTestCaseOK', - 'PhabricatorUserEditorTestCase@example.com'); + 'PhabricatorUserEditorTest@example.com'); $this->assertTrue(true); } public function testRegistrationEmailInvalid() { $env = PhabricatorEnv::beginScopedEnv(); $env->overrideEnvConfig('auth.email-domains', array('example.com')); $prefix = str_repeat('a', PhabricatorUserEmail::MAX_ADDRESS_LENGTH); $email = $prefix.'@evil.com@example.com'; try { $this->registerUser( 'PhabricatorUserEditorTestCaseInvalid', $email); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof Exception); } public function testRegistrationEmailDomain() { $env = PhabricatorEnv::beginScopedEnv(); $env->overrideEnvConfig('auth.email-domains', array('example.com')); $caught = null; try { $this->registerUser( 'PhabricatorUserEditorTestCaseDomain', - 'PhabricatorUserEditorTestCase@whitehouse.gov'); + 'PhabricatorUserEditorTest@whitehouse.gov'); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof Exception); } public function testRegistrationEmailApplicationEmailCollide() { $app_email = 'bugs@whitehouse.gov'; $app_email_object = PhabricatorMetaMTAApplicationEmail::initializeNewAppEmail( $this->generateNewTestUser()); $app_email_object->setAddress($app_email); $app_email_object->setApplicationPHID('test'); $app_email_object->save(); $caught = null; try { $this->registerUser( 'PhabricatorUserEditorTestCaseDomain', $app_email); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof Exception); } private function registerUser($username, $email) { $user = id(new PhabricatorUser()) ->setUsername($username) ->setRealname($username); $email = id(new PhabricatorUserEmail()) ->setAddress($email) ->setIsVerified(0); id(new PhabricatorUserEditor()) ->setActor($user) ->createNewUser($user, $email); } } diff --git a/src/applications/phid/handle/pool/PhabricatorHandleList.php b/src/applications/phid/handle/pool/PhabricatorHandleList.php index 21a695b258..7a8fed529f 100644 --- a/src/applications/phid/handle/pool/PhabricatorHandleList.php +++ b/src/applications/phid/handle/pool/PhabricatorHandleList.php @@ -1,173 +1,174 @@ <?php /** * A list of object handles. * * This is a convenience class which behaves like an array but makes working * with handles more convenient, improves their caching and batching semantics, * and provides some utility behavior. * * Load a handle list by calling `loadHandles()` on a `$viewer`: * * $handles = $viewer->loadHandles($phids); * * This creates a handle list object, which behaves like an array of handles. * However, it benefits from the viewer's internal handle cache and performs * just-in-time bulk loading. */ final class PhabricatorHandleList extends Phobject implements Iterator, ArrayAccess, Countable { private $handlePool; private $phids; private $count; private $handles; private $cursor; private $map; public function setHandlePool(PhabricatorHandlePool $pool) { $this->handlePool = $pool; return $this; } public function setPHIDs(array $phids) { $this->phids = $phids; $this->count = count($phids); return $this; } private function loadHandles() { $this->handles = $this->handlePool->loadPHIDs($this->phids); } private function getHandle($phid) { if ($this->handles === null) { $this->loadHandles(); } if (empty($this->handles[$phid])) { throw new Exception( pht( 'Requested handle "%s" was not loaded.', $phid)); } return $this->handles[$phid]; } /** * Get a handle from this list if it exists. * * This has similar semantics to @{function:idx}. */ public function getHandleIfExists($phid, $default = null) { if ($this->handles === null) { $this->loadHandles(); } return idx($this->handles, $phid, $default); } /* -( Rendering )---------------------------------------------------------- */ /** * Return a @{class:PHUIHandleListView} which can render the handles in * this list. */ public function renderList() { return id(new PHUIHandleListView()) ->setHandleList($this); } /** * Return a @{class:PHUIHandleView} which can render a specific handle. */ public function renderHandle($phid) { if (!isset($this[$phid])) { throw new Exception( pht('Trying to render a handle which does not exist!')); } return id(new PHUIHandleView()) ->setHandleList($this) ->setHandlePHID($phid); } /* -( Iterator )----------------------------------------------------------- */ public function rewind() { $this->cursor = 0; } public function current() { return $this->getHandle($this->phids[$this->cursor]); } public function key() { return $this->phids[$this->cursor]; } public function next() { ++$this->cursor; } public function valid() { return ($this->cursor < $this->count); } /* -( ArrayAccess )-------------------------------------------------------- */ public function offsetExists($offset) { // NOTE: We're intentionally not loading handles here so that isset() // checks do not trigger fetches. This gives us better bulk loading // behavior, particularly when invoked through methods like renderHandle(). if ($this->map === null) { $this->map = array_fill_keys($this->phids, true); } return isset($this->map[$offset]); } public function offsetGet($offset) { if ($this->handles === null) { $this->loadHandles(); } return $this->handles[$offset]; } public function offsetSet($offset, $value) { $this->raiseImmutableException(); } public function offsetUnset($offset) { $this->raiseImmutableException(); } private function raiseImmutableException() { throw new Exception( pht( - 'Trying to mutate a PhabricatorHandleList, but this is not permitted; '. - 'handle lists are immutable.')); + 'Trying to mutate a %s, but this is not permitted; '. + 'handle lists are immutable.', + __CLASS__)); } /* -( Countable )---------------------------------------------------------- */ public function count() { return $this->count; } } diff --git a/src/applications/phid/type/PhabricatorPHIDType.php b/src/applications/phid/type/PhabricatorPHIDType.php index 4445ab863d..fd64ab7bd1 100644 --- a/src/applications/phid/type/PhabricatorPHIDType.php +++ b/src/applications/phid/type/PhabricatorPHIDType.php @@ -1,231 +1,235 @@ <?php abstract class PhabricatorPHIDType { final public function getTypeConstant() { $class = new ReflectionClass($this); $const = $class->getConstant('TYPECONST'); if ($const === false) { throw new Exception( pht( 'PHIDType class "%s" must define an TYPECONST property.', get_class($this))); } if (!is_string($const) || !preg_match('/^[A-Z]{4}$/', $const)) { throw new Exception( pht( 'PHIDType class "%s" has an invalid TYPECONST property. PHID '. 'constants must be a four character uppercase string.', get_class($this))); } return $const; } abstract public function getTypeName(); public function newObject() { return null; } public function getTypeIcon() { // Default to the application icon if the type doesn't specify one. $application_class = $this->getPHIDTypeApplicationClass(); if ($application_class) { $application = newv($application_class, array()); return $application->getFontIcon(); } return null; } /** * Get the class name for the application this type belongs to. * * @return string|null Class name of the corresponding application, or null * if the type is not bound to an application. */ public function getPHIDTypeApplicationClass() { // TODO: Some day this should probably be abstract, but for now it only // affects global search and there's no real burning need to go classify // every PHID type. return null; } /** * Build a @{class:PhabricatorPolicyAwareQuery} to load objects of this type * by PHID. * * If you can not build a single query which satisfies this requirement, you * can provide a dummy implementation for this method and overload * @{method:loadObjects} instead. * * @param PhabricatorObjectQuery Query being executed. * @param list<phid> PHIDs to load. * @return PhabricatorPolicyAwareQuery Query object which loads the * specified PHIDs when executed. */ abstract protected function buildQueryForObjects( PhabricatorObjectQuery $query, array $phids); /** * Load objects of this type, by PHID. For most PHID types, it is only * necessary to implement @{method:buildQueryForObjects} to get object * loading to work. * * @param PhabricatorObjectQuery Query being executed. * @param list<phid> PHIDs to load. * @return list<wild> Corresponding objects. */ public function loadObjects( PhabricatorObjectQuery $query, array $phids) { $object_query = $this->buildQueryForObjects($query, $phids) ->setViewer($query->getViewer()) ->setParentQuery($query); // If the user doesn't have permission to use the application at all, // just mark all the PHIDs as filtered. This primarily makes these // objects show up as "Restricted" instead of "Unknown" when loaded as // handles, which is technically true. if (!$object_query->canViewerUseQueryApplication()) { $object_query->addPolicyFilteredPHIDs(array_fuse($phids)); return array(); } return $object_query->execute(); } /** * Populate provided handles with application-specific data, like titles and * URIs. * * NOTE: The `$handles` and `$objects` lists are guaranteed to be nonempty * and have the same keys: subclasses are expected to load information only * for handles with visible objects. * * Because of this guarantee, a safe implementation will typically look like* * * foreach ($handles as $phid => $handle) { * $object = $objects[$phid]; * * $handle->setStuff($object->getStuff()); * // ... * } * * In general, an implementation should call `setName()` and `setURI()` on * each handle at a minimum. See @{class:PhabricatorObjectHandle} for other * handle properties. * * @param PhabricatorHandleQuery Issuing query object. * @param list<PhabricatorObjectHandle> Handles to populate with data. * @param list<Object> Objects for these PHIDs loaded by * @{method:buildQueryForObjects()}. * @return void */ abstract public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects); public function canLoadNamedObject($name) { return false; } public function loadNamedObjects( PhabricatorObjectQuery $query, array $names) { throw new PhutilMethodNotImplementedException(); } /** * Get all known PHID types. * * To get PHID types a given user has access to, see * @{method:getAllInstalledTypes}. * * @return dict<string, PhabricatorPHIDType> Map of type constants to types. */ public static function getAllTypes() { static $types; if ($types === null) { $objects = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); $map = array(); $original = array(); foreach ($objects as $object) { $type = $object->getTypeConstant(); if (isset($map[$type])) { $that_class = $original[$type]; $this_class = get_class($object); throw new Exception( - "Two PhabricatorPHIDType classes ({$that_class}, {$this_class}) ". - "both handle PHID type '{$type}'. A type may be handled by only ". - "one class."); + pht( + "Two %s classes (%s, %s) both handle PHID type '%s'. ". + "A type may be handled by only one class.", + __CLASS__, + $that_class, + $this_class, + $type)); } $original[$type] = get_class($object); $map[$type] = $object; } $types = $map; } return $types; } /** * Get all PHID types of applications installed for a given viewer. * * @param PhabricatorUser Viewing user. * @return dict<string, PhabricatorPHIDType> Map of constants to installed * types. */ public static function getAllInstalledTypes(PhabricatorUser $viewer) { $all_types = self::getAllTypes(); $installed_types = array(); $app_classes = array(); foreach ($all_types as $key => $type) { $app_class = $type->getPHIDTypeApplicationClass(); if ($app_class === null) { // If the PHID type isn't bound to an application, include it as // installed. $installed_types[$key] = $type; continue; } // Otherwise, we need to check if this application is installed before // including the PHID type. $app_classes[$app_class][$key] = $type; } if ($app_classes) { $apps = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) ->withInstalled(true) ->withClasses(array_keys($app_classes)) ->execute(); foreach ($apps as $app_class => $app) { $installed_types += $app_classes[$app_class]; } } return $installed_types; } } diff --git a/src/applications/phortune/currency/PhortuneCurrency.php b/src/applications/phortune/currency/PhortuneCurrency.php index a473738ed6..b59d9b144a 100644 --- a/src/applications/phortune/currency/PhortuneCurrency.php +++ b/src/applications/phortune/currency/PhortuneCurrency.php @@ -1,239 +1,239 @@ <?php final class PhortuneCurrency extends Phobject { private $value; private $currency; private function __construct() { // Intentionally private. } public static function getDefaultCurrency() { return 'USD'; } public static function newEmptyCurrency() { return self::newFromString('0.00 USD'); } public static function newFromUserInput(PhabricatorUser $user, $string) { // Eventually, this might select a default currency based on user settings. return self::newFromString($string, self::getDefaultCurrency()); } public static function newFromString($string, $default = null) { $matches = null; $ok = preg_match( '/^([-$]*(?:\d+)?(?:[.]\d{0,2})?)(?:\s+([A-Z]+))?$/', trim($string), $matches); if (!$ok) { self::throwFormatException($string); } $value = $matches[1]; if (substr_count($value, '-') > 1) { self::throwFormatException($string); } if (substr_count($value, '$') > 1) { self::throwFormatException($string); } $value = str_replace('$', '', $value); $value = (float)$value; $value = (int)round(100 * $value); $currency = idx($matches, 2, $default); switch ($currency) { case 'USD': break; default: throw new Exception("Unsupported currency '{$currency}'!"); } return self::newFromValueAndCurrency($value, $currency); } public static function newFromValueAndCurrency($value, $currency) { $obj = new PhortuneCurrency(); $obj->value = $value; $obj->currency = $currency; return $obj; } public static function newFromList(array $list) { - assert_instances_of($list, 'PhortuneCurrency'); + assert_instances_of($list, __CLASS__); if (!$list) { - return PhortuneCurrency::newEmptyCurrency(); + return self::newEmptyCurrency(); } $total = null; foreach ($list as $item) { if ($total === null) { $total = $item; } else { $total = $total->add($item); } } return $total; } public function formatForDisplay() { $bare = $this->formatBareValue(); return '$'.$bare.' '.$this->currency; } public function serializeForStorage() { return $this->formatBareValue().' '.$this->currency; } public function formatBareValue() { switch ($this->currency) { case 'USD': return sprintf('%.02f', $this->value / 100); default: throw new Exception( pht('Unsupported currency ("%s")!', $this->currency)); } } public function getValue() { return $this->value; } public function getCurrency() { return $this->currency; } public function getValueInUSDCents() { if ($this->currency !== 'USD') { throw new Exception(pht('Unexpected currency!')); } return $this->value; } private static function throwFormatException($string) { throw new Exception("Invalid currency format ('{$string}')."); } private function throwUnlikeCurrenciesException(PhortuneCurrency $other) { throw new Exception( pht( 'Trying to operate on unlike currencies ("%s" and "%s")!', $this->currency, $other->currency)); } public function add(PhortuneCurrency $other) { if ($this->currency !== $other->currency) { $this->throwUnlikeCurrenciesException($other); } $currency = new PhortuneCurrency(); // TODO: This should check for integer overflows, etc. $currency->value = $this->value + $other->value; $currency->currency = $this->currency; return $currency; } public function subtract(PhortuneCurrency $other) { if ($this->currency !== $other->currency) { $this->throwUnlikeCurrenciesException($other); } $currency = new PhortuneCurrency(); // TODO: This should check for integer overflows, etc. $currency->value = $this->value - $other->value; $currency->currency = $this->currency; return $currency; } public function isEqualTo(PhortuneCurrency $other) { if ($this->currency !== $other->currency) { $this->throwUnlikeCurrenciesException($other); } return ($this->value === $other->value); } public function negate() { $currency = new PhortuneCurrency(); $currency->value = -$this->value; $currency->currency = $this->currency; return $currency; } public function isPositive() { return ($this->value > 0); } public function isGreaterThan(PhortuneCurrency $other) { if ($this->currency !== $other->currency) { $this->throwUnlikeCurrenciesException($other); } return $this->value > $other->value; } /** * Assert that a currency value lies within a range. * * Throws if the value is not between the minimum and maximum, inclusive. * * In particular, currency values can be negative (to represent a debt or * credit), so checking against zero may be useful to make sure a value * has the expected sign. * * @param string|null Currency string, or null to skip check. * @param string|null Currency string, or null to skip check. * @return this */ public function assertInRange($minimum, $maximum) { if ($minimum !== null && $maximum !== null) { - $min = PhortuneCurrency::newFromString($minimum); - $max = PhortuneCurrency::newFromString($maximum); + $min = self::newFromString($minimum); + $max = self::newFromString($maximum); if ($min->value > $max->value) { throw new Exception( pht( 'Range (%s - %s) is not valid!', $min->formatForDisplay(), $max->formatForDisplay())); } } if ($minimum !== null) { - $min = PhortuneCurrency::newFromString($minimum); + $min = self::newFromString($minimum); if ($min->value > $this->value) { throw new Exception( pht( 'Minimum allowed amount is %s.', $min->formatForDisplay())); } } if ($maximum !== null) { - $max = PhortuneCurrency::newFromString($maximum); + $max = self::newFromString($maximum); if ($max->value < $this->value) { throw new Exception( pht( 'Maximum allowed amount is %s.', $max->formatForDisplay())); } } return $this; } } diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php index 49c80dcd72..da36779d06 100644 --- a/src/applications/phortune/provider/PhortunePaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePaymentProvider.php @@ -1,296 +1,296 @@ <?php /** * @task addmethod Adding Payment Methods */ abstract class PhortunePaymentProvider { private $providerConfig; public function setProviderConfig( PhortunePaymentProviderConfig $provider_config) { $this->providerConfig = $provider_config; return $this; } public function getProviderConfig() { return $this->providerConfig; } /** * Return a short name which identifies this provider. */ abstract public function getName(); /* -( Configuring Providers )---------------------------------------------- */ /** * Return a human-readable provider name for use on the merchant workflow * where a merchant owner adds providers. */ abstract public function getConfigureName(); /** * Return a human-readable provider description for use on the merchant * workflow where a merchant owner adds providers. */ abstract public function getConfigureDescription(); abstract public function getConfigureInstructions(); abstract public function getConfigureProvidesDescription(); abstract public function getAllConfigurableProperties(); abstract public function getAllConfigurableSecretProperties(); /** * Read a dictionary of properties from the provider's configuration for * use when editing the provider. */ public function readEditFormValuesFromProviderConfig() { $properties = $this->getAllConfigurableProperties(); $config = $this->getProviderConfig(); $secrets = $this->getAllConfigurableSecretProperties(); $secrets = array_fuse($secrets); $map = array(); foreach ($properties as $property) { $map[$property] = $config->getMetadataValue($property); if (isset($secrets[$property])) { $map[$property] = $this->renderConfigurationSecret($map[$property]); } } return $map; } /** * Read a dictionary of properties from a request for use when editing the * provider. */ public function readEditFormValuesFromRequest(AphrontRequest $request) { $properties = $this->getAllConfigurableProperties(); $map = array(); foreach ($properties as $property) { $map[$property] = $request->getStr($property); } return $map; } abstract public function processEditForm( AphrontRequest $request, array $values); abstract public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues); protected function renderConfigurationSecret($value) { if (strlen($value)) { return str_repeat('*', strlen($value)); } return ''; } public function isConfigurationSecret($value) { return preg_match('/^\*+\z/', trim($value)); } abstract public function canRunConfigurationTest(); public function runConfigurationTest() { throw new PhortuneNotImplementedException($this); } /* -( Selecting Providers )------------------------------------------------ */ public static function getAllProviders() { return id(new PhutilSymbolLoader()) - ->setAncestorClass('PhortunePaymentProvider') + ->setAncestorClass(__CLASS__) ->loadObjects(); } public function isEnabled() { return $this->getProviderConfig()->getIsEnabled(); } abstract public function isAcceptingLivePayments(); abstract public function getPaymentMethodDescription(); abstract public function getPaymentMethodIcon(); abstract public function getPaymentMethodProviderDescription(); final public function applyCharge( PhortunePaymentMethod $payment_method, PhortuneCharge $charge) { $this->executeCharge($payment_method, $charge); } final public function refundCharge( PhortuneCharge $charge, PhortuneCharge $refund) { $this->executeRefund($charge, $refund); } abstract protected function executeCharge( PhortunePaymentMethod $payment_method, PhortuneCharge $charge); abstract protected function executeRefund( PhortuneCharge $charge, PhortuneCharge $refund); abstract public function updateCharge(PhortuneCharge $charge); /* -( Adding Payment Methods )--------------------------------------------- */ /** * @task addmethod */ public function canCreatePaymentMethods() { return false; } /** * @task addmethod */ public function translateCreatePaymentMethodErrorCode($error_code) { throw new PhortuneNotImplementedException($this); } /** * @task addmethod */ public function getCreatePaymentMethodErrorMessage($error_code) { throw new PhortuneNotImplementedException($this); } /** * @task addmethod */ public function validateCreatePaymentMethodToken(array $token) { throw new PhortuneNotImplementedException($this); } /** * @task addmethod */ public function createPaymentMethodFromRequest( AphrontRequest $request, PhortunePaymentMethod $method, array $token) { throw new PhortuneNotImplementedException($this); } /** * @task addmethod */ public function renderCreatePaymentMethodForm( AphrontRequest $request, array $errors) { throw new PhortuneNotImplementedException($this); } public function getDefaultPaymentMethodDisplayName( PhortunePaymentMethod $method) { throw new PhortuneNotImplementedException($this); } /* -( One-Time Payments )-------------------------------------------------- */ public function canProcessOneTimePayments() { return false; } public function renderOneTimePaymentButton( PhortuneAccount $account, PhortuneCart $cart, PhabricatorUser $user) { require_celerity_resource('phortune-css'); $description = $this->getPaymentMethodProviderDescription(); $details = $this->getPaymentMethodDescription(); $icon = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) ->setSpriteIcon($this->getPaymentMethodIcon()); $button = id(new PHUIButtonView()) ->setSize(PHUIButtonView::BIG) ->setColor(PHUIButtonView::GREY) ->setIcon($icon) ->setText($description) ->setSubtext($details); // NOTE: We generate a local URI to make sure the form picks up CSRF tokens. $uri = $this->getControllerURI( 'checkout', array( 'cartID' => $cart->getID(), ), $local = true); return phabricator_form( $user, array( 'action' => $uri, 'method' => 'POST', ), $button); } /* -( Controllers )-------------------------------------------------------- */ final public function getControllerURI( $action, array $params = array(), $local = false) { $id = $this->getProviderConfig()->getID(); $app = PhabricatorApplication::getByClass('PhabricatorPhortuneApplication'); $path = $app->getBaseURI().'provider/'.$id.'/'.$action.'/'; $uri = new PhutilURI($path); $uri->setQueryParams($params); if ($local) { return $uri; } else { return PhabricatorEnv::getURI((string)$uri); } } public function canRespondToControllerAction($action) { return false; } public function processControllerRequest( PhortuneProviderActionController $controller, AphrontRequest $request) { throw new PhortuneNotImplementedException($this); } } diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index facb9d5089..e86fd53df2 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -1,170 +1,170 @@ <?php /** * An account represents a purchasing entity. An account may have multiple users * on it (e.g., several employees of a company have access to the company * account), and a user may have several accounts (e.g., a company account and * a personal account). */ final class PhortuneAccount extends PhortuneDAO implements PhabricatorApplicationTransactionInterface, PhabricatorPolicyInterface { protected $name; private $memberPHIDs = self::ATTACHABLE; public static function initializeNewAccount(PhabricatorUser $actor) { $account = id(new PhortuneAccount()); $account->memberPHIDs = array(); return $account; } public static function createNewAccount( PhabricatorUser $actor, PhabricatorContentSource $content_source) { - $account = PhortuneAccount::initializeNewAccount($actor); + $account = self::initializeNewAccount($actor); $xactions = array(); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType(PhortuneAccountTransaction::TYPE_NAME) ->setNewValue(pht('Default Account')); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue( 'edge:type', PhortuneAccountHasMemberEdgeType::EDGECONST) ->setNewValue( array( '=' => array($actor->getPHID() => $actor->getPHID()), )); $editor = id(new PhortuneAccountEditor()) ->setActor($actor) ->setContentSource($content_source); // We create an account for you the first time you visit Phortune. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $editor->applyTransactions($account, $xactions); unset($unguarded); return $account; } public function newCart( PhabricatorUser $actor, PhortuneCartImplementation $implementation, PhortuneMerchant $merchant) { $cart = PhortuneCart::initializeNewCart($actor, $this, $merchant); $cart->setCartClass(get_class($implementation)); $cart->attachImplementation($implementation); $implementation->willCreateCart($actor, $cart); return $cart->save(); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortuneAccountPHIDType::TYPECONST); } public function getMemberPHIDs() { return $this->assertAttached($this->memberPHIDs); } public function attachMemberPHIDs(array $phids) { $this->memberPHIDs = $phids; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhortuneAccountEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhortuneAccountTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getPHID() === null) { // Allow a user to create an account for themselves. return PhabricatorPolicies::POLICY_USER; } else { return PhabricatorPolicies::POLICY_NOONE; } } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { $members = array_fuse($this->getMemberPHIDs()); if (isset($members[$viewer->getPHID()])) { return true; } // If the viewer is acting on behalf of a merchant, they can see // payment accounts. if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { foreach ($viewer->getAuthorities() as $authority) { if ($authority instanceof PhortuneMerchant) { return true; } } } return false; } public function describeAutomaticCapability($capability) { return pht('Members of an account can always view and edit it.'); } } diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 46d474cc93..70ecbb21c9 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -1,689 +1,689 @@ <?php final class PhortuneCart extends PhortuneDAO implements PhabricatorApplicationTransactionInterface, PhabricatorPolicyInterface { const STATUS_BUILDING = 'cart:building'; const STATUS_READY = 'cart:ready'; const STATUS_PURCHASING = 'cart:purchasing'; const STATUS_CHARGED = 'cart:charged'; const STATUS_HOLD = 'cart:hold'; const STATUS_REVIEW = 'cart:review'; const STATUS_PURCHASED = 'cart:purchased'; protected $accountPHID; protected $authorPHID; protected $merchantPHID; protected $subscriptionPHID; protected $cartClass; protected $status; protected $metadata = array(); protected $mailKey; protected $isInvoice; private $account = self::ATTACHABLE; private $purchases = self::ATTACHABLE; private $implementation = self::ATTACHABLE; private $merchant = self::ATTACHABLE; public static function initializeNewCart( PhabricatorUser $actor, PhortuneAccount $account, PhortuneMerchant $merchant) { $cart = id(new PhortuneCart()) ->setAuthorPHID($actor->getPHID()) ->setStatus(self::STATUS_BUILDING) ->setAccountPHID($account->getPHID()) ->setIsInvoice(0) ->attachAccount($account) ->setMerchantPHID($merchant->getPHID()) ->attachMerchant($merchant); $cart->account = $account; $cart->purchases = array(); return $cart; } public function newPurchase( PhabricatorUser $actor, PhortuneProduct $product) { $purchase = PhortunePurchase::initializeNewPurchase($actor, $product) ->setAccountPHID($this->getAccount()->getPHID()) ->setCartPHID($this->getPHID()) ->save(); $this->purchases[] = $purchase; return $purchase; } public static function getStatusNameMap() { return array( self::STATUS_BUILDING => pht('Building'), self::STATUS_READY => pht('Ready'), self::STATUS_PURCHASING => pht('Purchasing'), self::STATUS_CHARGED => pht('Charged'), self::STATUS_HOLD => pht('Hold'), self::STATUS_REVIEW => pht('Review'), self::STATUS_PURCHASED => pht('Purchased'), ); } public static function getNameForStatus($status) { return idx(self::getStatusNameMap(), $status, $status); } public function activateCart() { $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if ($copy->getStatus() !== self::STATUS_BUILDING) { throw new Exception( pht( 'Cart has wrong status ("%s") to call willApplyCharge().', $copy->getStatus())); } $this->setStatus(self::STATUS_READY)->save(); $this->endReadLocking(); $this->saveTransaction(); $this->recordCartTransaction(PhortuneCartTransaction::TYPE_CREATED); return $this; } public function willApplyCharge( PhabricatorUser $actor, PhortunePaymentProvider $provider, PhortunePaymentMethod $method = null) { $account = $this->getAccount(); $charge = PhortuneCharge::initializeNewCharge() ->setAccountPHID($account->getPHID()) ->setCartPHID($this->getPHID()) ->setAuthorPHID($actor->getPHID()) ->setMerchantPHID($this->getMerchant()->getPHID()) ->setProviderPHID($provider->getProviderConfig()->getPHID()) ->setAmountAsCurrency($this->getTotalPriceAsCurrency()); if ($method) { $charge->setPaymentMethodPHID($method->getPHID()); } $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if ($copy->getStatus() !== self::STATUS_READY) { throw new Exception( pht( 'Cart has wrong status ("%s") to call willApplyCharge(), '. 'expected "%s".', $copy->getStatus(), self::STATUS_READY)); } $charge->save(); - $this->setStatus(PhortuneCart::STATUS_PURCHASING)->save(); + $this->setStatus(self::STATUS_PURCHASING)->save(); $this->endReadLocking(); $this->saveTransaction(); return $charge; } public function didHoldCharge(PhortuneCharge $charge) { $charge->setStatus(PhortuneCharge::STATUS_HOLD); $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if ($copy->getStatus() !== self::STATUS_PURCHASING) { throw new Exception( pht( 'Cart has wrong status ("%s") to call didHoldCharge(), '. 'expected "%s".', $copy->getStatus(), self::STATUS_PURCHASING)); } $charge->save(); $this->setStatus(self::STATUS_HOLD)->save(); $this->endReadLocking(); $this->saveTransaction(); $this->recordCartTransaction(PhortuneCartTransaction::TYPE_HOLD); } public function didApplyCharge(PhortuneCharge $charge) { $charge->setStatus(PhortuneCharge::STATUS_CHARGED); $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if (($copy->getStatus() !== self::STATUS_PURCHASING) && ($copy->getStatus() !== self::STATUS_HOLD)) { throw new Exception( pht( 'Cart has wrong status ("%s") to call didApplyCharge().', $copy->getStatus())); } $charge->save(); $this->setStatus(self::STATUS_CHARGED)->save(); $this->endReadLocking(); $this->saveTransaction(); // TODO: Perform purchase review. Here, we would apply rules to determine // whether the charge needs manual review (maybe making the decision via // Herald, configuration, or by examining provider fraud data). For now, // don't require review. $needs_review = false; if ($needs_review) { $this->willReviewCart(); } else { $this->didReviewCart(); } return $this; } public function willReviewCart() { $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if (($copy->getStatus() !== self::STATUS_CHARGED)) { throw new Exception( pht( 'Cart has wrong status ("%s") to call willReviewCart()!', $copy->getStatus())); } $this->setStatus(self::STATUS_REVIEW)->save(); $this->endReadLocking(); $this->saveTransaction(); $this->recordCartTransaction(PhortuneCartTransaction::TYPE_REVIEW); return $this; } public function didReviewCart() { $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if (($copy->getStatus() !== self::STATUS_CHARGED) && ($copy->getStatus() !== self::STATUS_REVIEW)) { throw new Exception( pht( 'Cart has wrong status ("%s") to call didReviewCart()!', $copy->getStatus())); } foreach ($this->purchases as $purchase) { $purchase->getProduct()->didPurchaseProduct($purchase); } $this->setStatus(self::STATUS_PURCHASED)->save(); $this->endReadLocking(); $this->saveTransaction(); $this->recordCartTransaction(PhortuneCartTransaction::TYPE_PURCHASED); return $this; } public function didFailCharge(PhortuneCharge $charge) { $charge->setStatus(PhortuneCharge::STATUS_FAILED); $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if (($copy->getStatus() !== self::STATUS_PURCHASING) && ($copy->getStatus() !== self::STATUS_HOLD)) { throw new Exception( pht( 'Cart has wrong status ("%s") to call didFailCharge().', $copy->getStatus())); } $charge->save(); // Move the cart back into STATUS_READY so the user can try // making the purchase again. $this->setStatus(self::STATUS_READY)->save(); $this->endReadLocking(); $this->saveTransaction(); return $this; } public function willRefundCharge( PhabricatorUser $actor, PhortunePaymentProvider $provider, PhortuneCharge $charge, PhortuneCurrency $amount) { if (!$amount->isPositive()) { throw new Exception( pht('Trying to refund nonpositive amount of money!')); } if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) { throw new Exception( pht('Trying to refund more money than remaining on charge!')); } if ($charge->getRefundedChargePHID()) { throw new Exception( pht('Trying to refund a refund!')); } if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) && ($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) { throw new Exception( pht('Trying to refund an uncharged charge!')); } $refund_charge = PhortuneCharge::initializeNewCharge() ->setAccountPHID($this->getAccount()->getPHID()) ->setCartPHID($this->getPHID()) ->setAuthorPHID($actor->getPHID()) ->setMerchantPHID($this->getMerchant()->getPHID()) ->setProviderPHID($provider->getProviderConfig()->getPHID()) ->setPaymentMethodPHID($charge->getPaymentMethodPHID()) ->setRefundedChargePHID($charge->getPHID()) ->setAmountAsCurrency($amount->negate()); $charge->openTransaction(); $charge->beginReadLocking(); $copy = clone $charge; $copy->reload(); if ($copy->getRefundingPHID() !== null) { throw new Exception( pht('Trying to refund a charge which is already refunding!')); } $refund_charge->save(); $charge->setRefundingPHID($refund_charge->getPHID()); $charge->save(); $charge->endReadLocking(); $charge->saveTransaction(); return $refund_charge; } public function didRefundCharge( PhortuneCharge $charge, PhortuneCharge $refund) { $refund->setStatus(PhortuneCharge::STATUS_CHARGED); $this->openTransaction(); $this->beginReadLocking(); $copy = clone $charge; $copy->reload(); if ($charge->getRefundingPHID() !== $refund->getPHID()) { throw new Exception( pht('Charge is in the wrong refunding state!')); } $charge->setRefundingPHID(null); // NOTE: There's some trickiness here to get the signs right. Both // these values are positive but the refund has a negative value. $total_refunded = $charge ->getAmountRefundedAsCurrency() ->add($refund->getAmountAsCurrency()->negate()); $charge->setAmountRefundedAsCurrency($total_refunded); $charge->save(); $refund->save(); $this->endReadLocking(); $this->saveTransaction(); $amount = $refund->getAmountAsCurrency()->negate(); foreach ($this->purchases as $purchase) { $purchase->getProduct()->didRefundProduct($purchase, $amount); } return $this; } public function didFailRefund( PhortuneCharge $charge, PhortuneCharge $refund) { $refund->setStatus(PhortuneCharge::STATUS_FAILED); $this->openTransaction(); $this->beginReadLocking(); $copy = clone $charge; $copy->reload(); if ($charge->getRefundingPHID() !== $refund->getPHID()) { throw new Exception( pht('Charge is in the wrong refunding state!')); } $charge->setRefundingPHID(null); $charge->save(); $refund->save(); $this->endReadLocking(); $this->saveTransaction(); } private function recordCartTransaction($type) { $omnipotent_user = PhabricatorUser::getOmnipotentUser(); $phortune_phid = id(new PhabricatorPhortuneApplication())->getPHID(); $xactions = array(); $xactions[] = id(new PhortuneCartTransaction()) ->setTransactionType($type) ->setNewValue(true); $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_PHORTUNE, array()); $editor = id(new PhortuneCartEditor()) ->setActor($omnipotent_user) ->setActingAsPHID($phortune_phid) ->setContentSource($content_source) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true); $editor->applyTransactions($this, $xactions); } public function getName() { return $this->getImplementation()->getName($this); } public function getDoneURI() { return $this->getImplementation()->getDoneURI($this); } public function getDoneActionName() { return $this->getImplementation()->getDoneActionName($this); } public function getCancelURI() { return $this->getImplementation()->getCancelURI($this); } public function getDescription() { return $this->getImplementation()->getDescription($this); } public function getDetailURI(PhortuneMerchant $authority = null) { if ($authority) { $prefix = 'merchant/'.$authority->getID().'/'; } else { $prefix = ''; } return '/phortune/'.$prefix.'cart/'.$this->getID().'/'; } public function getCheckoutURI() { return '/phortune/cart/'.$this->getID().'/checkout/'; } public function canCancelOrder() { try { $this->assertCanCancelOrder(); return true; } catch (Exception $ex) { return false; } } public function canRefundOrder() { try { $this->assertCanRefundOrder(); return true; } catch (Exception $ex) { return false; } } public function assertCanCancelOrder() { switch ($this->getStatus()) { case self::STATUS_BUILDING: throw new Exception( pht( 'This order can not be cancelled because the application has not '. 'finished building it yet.')); case self::STATUS_READY: throw new Exception( pht( 'This order can not be cancelled because it has not been placed.')); } return $this->getImplementation()->assertCanCancelOrder($this); } public function assertCanRefundOrder() { switch ($this->getStatus()) { case self::STATUS_BUILDING: throw new Exception( pht( 'This order can not be refunded because the application has not '. 'finished building it yet.')); case self::STATUS_READY: throw new Exception( pht( 'This order can not be refunded because it has not been placed.')); } return $this->getImplementation()->assertCanRefundOrder($this); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'text32', 'cartClass' => 'text128', 'mailKey' => 'bytes20', 'subscriptionPHID' => 'phid?', 'isInvoice' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_account' => array( 'columns' => array('accountPHID'), ), 'key_merchant' => array( 'columns' => array('merchantPHID'), ), 'key_subscription' => array( 'columns' => array('subscriptionPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortuneCartPHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function attachPurchases(array $purchases) { assert_instances_of($purchases, 'PhortunePurchase'); $this->purchases = $purchases; return $this; } public function getPurchases() { return $this->assertAttached($this->purchases); } public function attachAccount(PhortuneAccount $account) { $this->account = $account; return $this; } public function getAccount() { return $this->assertAttached($this->account); } public function attachMerchant(PhortuneMerchant $merchant) { $this->merchant = $merchant; return $this; } public function getMerchant() { return $this->assertAttached($this->merchant); } public function attachImplementation( PhortuneCartImplementation $implementation) { $this->implementation = $implementation; return $this; } public function getImplementation() { return $this->assertAttached($this->implementation); } public function getTotalPriceAsCurrency() { $prices = array(); foreach ($this->getPurchases() as $purchase) { $prices[] = $purchase->getTotalPriceAsCurrency(); } return PhortuneCurrency::newFromList($prices); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhortuneCartEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhortuneCartTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { // NOTE: Both view and edit use the account's edit policy. We punch a hole // through this for merchants, below. return $this ->getAccount() ->getPolicy(PhabricatorPolicyCapability::CAN_EDIT); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) { return true; } // If the viewer controls the merchant this order was placed with, they // can view the order. if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { $can_admin = PhabricatorPolicyFilter::hasCapability( $viewer, $this->getMerchant(), PhabricatorPolicyCapability::CAN_EDIT); if ($can_admin) { return true; } } return false; } public function describeAutomaticCapability($capability) { return array( pht('Orders inherit the policies of the associated account.'), pht('The merchant you placed an order with can review and manage it.'), ); } } diff --git a/src/applications/phragment/storage/PhragmentFragment.php b/src/applications/phragment/storage/PhragmentFragment.php index 3f5719178b..574283d7a7 100644 --- a/src/applications/phragment/storage/PhragmentFragment.php +++ b/src/applications/phragment/storage/PhragmentFragment.php @@ -1,351 +1,351 @@ <?php final class PhragmentFragment extends PhragmentDAO implements PhabricatorPolicyInterface { protected $path; protected $depth; protected $latestVersionPHID; protected $viewPolicy; protected $editPolicy; private $latestVersion = self::ATTACHABLE; protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'path' => 'text128', 'depth' => 'uint32', 'latestVersionPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_path' => array( 'columns' => array('path'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhragmentFragmentPHIDType::TYPECONST); } public function getURI() { return '/phragment/browse/'.$this->getPath(); } public function getName() { return basename($this->path); } public function getFile() { return $this->assertAttached($this->file); } public function attachFile(PhabricatorFile $file) { return $this->file = $file; } public function isDirectory() { return $this->latestVersionPHID === null; } public function isDeleted() { return $this->getLatestVersion()->getFilePHID() === null; } public function getLatestVersion() { if ($this->latestVersionPHID === null) { return null; } return $this->assertAttached($this->latestVersion); } public function attachLatestVersion(PhragmentFragmentVersion $version) { return $this->latestVersion = $version; } /* -( Updating ) --------------------------------------------------------- */ /** * Create a new fragment from a file. */ public static function createFromFile( PhabricatorUser $viewer, PhabricatorFile $file = null, $path, $view_policy, $edit_policy) { $fragment = id(new PhragmentFragment()); $fragment->setPath($path); $fragment->setDepth(count(explode('/', $path))); $fragment->setLatestVersionPHID(null); $fragment->setViewPolicy($view_policy); $fragment->setEditPolicy($edit_policy); $fragment->save(); // Directory fragments have no versions associated with them, so we // just return the fragment at this point. if ($file === null) { return $fragment; } if ($file->getMimeType() === 'application/zip') { $fragment->updateFromZIP($viewer, $file); } else { $fragment->updateFromFile($viewer, $file); } return $fragment; } /** * Set the specified file as the next version for the fragment. */ public function updateFromFile( PhabricatorUser $viewer, PhabricatorFile $file) { $existing = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($this->getPHID())) ->execute(); $sequence = count($existing); $this->openTransaction(); $version = id(new PhragmentFragmentVersion()); $version->setSequence($sequence); $version->setFragmentPHID($this->getPHID()); $version->setFilePHID($file->getPHID()); $version->save(); $this->setLatestVersionPHID($version->getPHID()); $this->save(); $this->saveTransaction(); $file->attachToObject($version->getPHID()); } /** * Apply the specified ZIP archive onto the fragment, removing * and creating fragments as needed. */ public function updateFromZIP( PhabricatorUser $viewer, PhabricatorFile $file) { if ($file->getMimeType() !== 'application/zip') { throw new Exception("File must have mimetype 'application/zip'"); } // First apply the ZIP as normal. $this->updateFromFile($viewer, $file); // Ensure we have ZIP support. $zip = null; try { $zip = new ZipArchive(); } catch (Exception $e) { // The server doesn't have php5-zip, so we can't do recursive updates. return; } $temp = new TempFile(); Filesystem::writeFile($temp, $file->loadFileData()); if (!$zip->open($temp)) { throw new Exception('Unable to open ZIP'); } // Get all of the paths and their data from the ZIP. $mappings = array(); for ($i = 0; $i < $zip->numFiles; $i++) { $path = trim($zip->getNameIndex($i), '/'); $stream = $zip->getStream($path); $data = null; // If the stream is false, then it is a directory entry. We leave // $data set to null for directories so we know not to create a // version entry for them. if ($stream !== false) { $data = stream_get_contents($stream); fclose($stream); } $mappings[$path] = $data; } // We need to detect any directories that are in the ZIP folder that // aren't explicitly noted in the ZIP. This can happen if the file // entries in the ZIP look like: // // * something/blah.png // * something/other.png // * test.png // // Where there is no explicit "something/" entry. foreach ($mappings as $path_key => $data) { if ($data === null) { continue; } $directory = dirname($path_key); while ($directory !== '.') { if (!array_key_exists($directory, $mappings)) { $mappings[$directory] = null; } if (dirname($directory) === $directory) { // dirname() will not reduce this directory any further; to // prevent infinite loop we just break out here. break; } $directory = dirname($directory); } } // Adjust the paths relative to this fragment so we can look existing // fragments up in the DB. $base_path = $this->getPath(); $paths = array(); foreach ($mappings as $p => $data) { $paths[] = $base_path.'/'.$p; } // FIXME: What happens when a child exists, but the current user // can't see it. We're going to create a new child with the exact // same path and then bad things will happen. $children = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->needLatestVersion(true) ->withLeadingPath($this->getPath().'/') ->execute(); $children = mpull($children, null, 'getPath'); // Iterate over the existing fragments. foreach ($children as $full_path => $child) { $path = substr($full_path, strlen($base_path) + 1); if (array_key_exists($path, $mappings)) { if ($child->isDirectory() && $mappings[$path] === null) { // Don't create a version entry for a directory // (unless it's been converted into a file). continue; } // The file is being updated. $file = PhabricatorFile::newFromFileData( $mappings[$path], array('name' => basename($path))); $child->updateFromFile($viewer, $file); } else { // The file is being deleted. $child->deleteFile($viewer); } } // Iterate over the mappings to find new files. foreach ($mappings as $path => $data) { if (!array_key_exists($base_path.'/'.$path, $children)) { // The file is being created. If the data is null, // then this is explicitly a directory being created. $file = null; if ($mappings[$path] !== null) { $file = PhabricatorFile::newFromFileData( $mappings[$path], array('name' => basename($path))); } - PhragmentFragment::createFromFile( + self::createFromFile( $viewer, $file, $base_path.'/'.$path, $this->getViewPolicy(), $this->getEditPolicy()); } } } /** * Delete the contents of the specified fragment. */ public function deleteFile(PhabricatorUser $viewer) { $existing = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($this->getPHID())) ->execute(); $sequence = count($existing); $this->openTransaction(); $version = id(new PhragmentFragmentVersion()); $version->setSequence($sequence); $version->setFragmentPHID($this->getPHID()); $version->setFilePHID(null); $version->save(); $this->setLatestVersionPHID($version->getPHID()); $this->save(); $this->saveTransaction(); } /* -( Utility ) ---------------------------------------------------------- */ public function getFragmentMappings( PhabricatorUser $viewer, $base_path) { $children = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->needLatestVersion(true) ->withLeadingPath($this->getPath().'/') ->withDepths(array($this->getDepth() + 1)) ->execute(); if (count($children) === 0) { $path = substr($this->getPath(), strlen($base_path) + 1); return array($path => $this); } else { $mappings = array(); foreach ($children as $child) { $child_mappings = $child->getFragmentMappings( $viewer, $base_path); foreach ($child_mappings as $key => $value) { $mappings[$key] = $value; } } return $mappings; } } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/policy/constants/PhabricatorPolicies.php b/src/applications/policy/constants/PhabricatorPolicies.php index 859010eca2..c33b9bf909 100644 --- a/src/applications/policy/constants/PhabricatorPolicies.php +++ b/src/applications/policy/constants/PhabricatorPolicies.php @@ -1,25 +1,25 @@ <?php final class PhabricatorPolicies extends PhabricatorPolicyConstants { const POLICY_PUBLIC = 'public'; const POLICY_USER = 'users'; const POLICY_ADMIN = 'admin'; const POLICY_NOONE = 'no-one'; /** * Returns the most public policy this install's configuration permits. * This is either "public" (if available) or "all users" (if not). * * @return const Most open working policy constant. */ public static function getMostOpenPolicy() { if (PhabricatorEnv::getEnvConfig('policy.allow-public')) { - return PhabricatorPolicies::POLICY_PUBLIC; + return self::POLICY_PUBLIC; } else { - return PhabricatorPolicies::POLICY_USER; + return self::POLICY_USER; } } } diff --git a/src/applications/project/icon/PhabricatorProjectIcon.php b/src/applications/project/icon/PhabricatorProjectIcon.php index ae6efa9b6e..9411baa6a9 100644 --- a/src/applications/project/icon/PhabricatorProjectIcon.php +++ b/src/applications/project/icon/PhabricatorProjectIcon.php @@ -1,59 +1,59 @@ <?php final class PhabricatorProjectIcon extends Phobject { public static function getIconMap() { return array( 'fa-briefcase' => pht('Briefcase'), 'fa-tags' => pht('Tag'), 'fa-folder' => pht('Folder'), 'fa-users' => pht('Team'), 'fa-bug' => pht('Bug'), 'fa-trash-o' => pht('Garbage'), 'fa-calendar' => pht('Deadline'), 'fa-flag-checkered' => pht('Goal'), 'fa-envelope' => pht('Communication'), 'fa-truck' => pht('Release'), 'fa-lock' => pht('Policy'), 'fa-umbrella' => pht('An Umbrella'), 'fa-cloud' => pht('The Cloud'), 'fa-building' => pht('Company'), 'fa-credit-card' => pht('Accounting'), 'fa-flask' => pht('Experimental'), ); } public static function getColorMap() { $shades = PHUITagView::getShadeMap(); $shades = array_select_keys( $shades, array(PhabricatorProject::DEFAULT_COLOR)) + $shades; unset($shades[PHUITagView::COLOR_DISABLED]); return $shades; } public static function getLabel($key) { $map = self::getIconMap(); return $map[$key]; } public static function getAPIName($key) { return substr($key, 3); } public static function renderIconForChooser($icon) { - $project_icons = PhabricatorProjectIcon::getIconMap(); + $project_icons = self::getIconMap(); return phutil_tag( 'span', array(), array( id(new PHUIIconView())->setIconFont($icon), ' ', idx($project_icons, $icon, pht('Unknown Icon')), )); } } diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index 8fcb6ade9c..63b8a8748f 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -1,197 +1,197 @@ <?php final class PhabricatorProjectColumn extends PhabricatorProjectDAO implements PhabricatorApplicationTransactionInterface, PhabricatorPolicyInterface, PhabricatorDestructibleInterface { const STATUS_ACTIVE = 0; const STATUS_HIDDEN = 1; const DEFAULT_ORDER = 'natural'; const ORDER_NATURAL = 'natural'; const ORDER_PRIORITY = 'priority'; protected $name; protected $status; protected $projectPHID; protected $sequence; protected $properties = array(); private $project = self::ATTACHABLE; public static function initializeNewColumn(PhabricatorUser $user) { return id(new PhabricatorProjectColumn()) ->setName('') ->setStatus(self::STATUS_ACTIVE); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'status' => 'uint32', 'sequence' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( 'columns' => array('projectPHID', 'status', 'sequence'), ), 'key_sequence' => array( 'columns' => array('projectPHID', 'sequence'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorProjectColumnPHIDType::TYPECONST); } public function attachProject(PhabricatorProject $project) { $this->project = $project; return $this; } public function getProject() { return $this->assertAttached($this->project); } public function isDefaultColumn() { return (bool)$this->getProperty('isDefault'); } public function isHidden() { return ($this->getStatus() == self::STATUS_HIDDEN); } public function getDisplayName() { $name = $this->getName(); if (strlen($name)) { return $name; } if ($this->isDefaultColumn()) { return pht('Backlog'); } return pht('Unnamed Column'); } public function getDisplayType() { if ($this->isDefaultColumn()) { return pht('(Default)'); } if ($this->isHidden()) { return pht('(Hidden)'); } return null; } public function getHeaderIcon() { $icon = null; if ($this->isHidden()) { $icon = 'fa-eye-slash'; $text = pht('Hidden'); } if ($icon) { return id(new PHUIIconView()) ->setIconFont($icon) ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => $text, - ));; + )); } return null; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getPointLimit() { return $this->getProperty('pointLimit'); } public function setPointLimit($limit) { $this->setProperty('pointLimit', $limit); return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorProjectColumnTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorProjectColumnTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getProject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getProject()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Users must be able to see a project to see its board.'); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/project/storage/PhabricatorProjectColumnTransaction.php b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php index dd4f95d5f6..ed4bfed8a6 100644 --- a/src/applications/project/storage/PhabricatorProjectColumnTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php @@ -1,82 +1,82 @@ <?php final class PhabricatorProjectColumnTransaction extends PhabricatorApplicationTransaction { const TYPE_NAME = 'project:col:name'; const TYPE_STATUS = 'project:col:status'; const TYPE_LIMIT = 'project:col:limit'; public function getApplicationName() { return 'project'; } public function getApplicationTransactionType() { return PhabricatorProjectColumnPHIDType::TYPECONST; } public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); $author_handle = $this->renderHandleLink($this->getAuthorPHID()); switch ($this->getTransactionType()) { - case PhabricatorProjectColumnTransaction::TYPE_NAME: + case self::TYPE_NAME: if ($old === null) { return pht( '%s created this column.', $author_handle); } else { if (!strlen($old)) { return pht( '%s named this column "%s".', $author_handle, $new); } else if (strlen($new)) { return pht( '%s renamed this column from "%s" to "%s".', $author_handle, $old, $new); } else { return pht( '%s removed the custom name of this column.', $author_handle); } } - case PhabricatorProjectColumnTransaction::TYPE_LIMIT: + case self::TYPE_LIMIT: if (!$old) { return pht( '%s set the point limit for this column to %s.', $author_handle, $new); } else if (!$new) { return pht( '%s removed the point limit for this column.', $author_handle); } else { return pht( '%s changed point limit for this column from %s to %s.', $author_handle, $old, $new); } - case PhabricatorProjectColumnTransaction::TYPE_STATUS: + case self::TYPE_STATUS: switch ($new) { case PhabricatorProjectColumn::STATUS_ACTIVE: return pht( '%s marked this column visible.', $author_handle); case PhabricatorProjectColumn::STATUS_HIDDEN: return pht( '%s marked this column hidden.', $author_handle); } break; } return parent::getTitle(); } } diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php index 9b0ff0c340..3dceb0934c 100644 --- a/src/applications/project/storage/PhabricatorProjectTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectTransaction.php @@ -1,352 +1,352 @@ <?php final class PhabricatorProjectTransaction extends PhabricatorApplicationTransaction { const TYPE_NAME = 'project:name'; const TYPE_SLUGS = 'project:slugs'; const TYPE_STATUS = 'project:status'; const TYPE_IMAGE = 'project:image'; const TYPE_ICON = 'project:icon'; const TYPE_COLOR = 'project:color'; const TYPE_LOCKED = 'project:locked'; // NOTE: This is deprecated, members are just a normal edge now. const TYPE_MEMBERS = 'project:members'; public function getApplicationName() { return 'project'; } public function getApplicationTransactionType() { return PhabricatorProjectProjectPHIDType::TYPECONST; } public function getRequiredHandlePHIDs() { $old = $this->getOldValue(); $new = $this->getNewValue(); $req_phids = array(); switch ($this->getTransactionType()) { - case PhabricatorProjectTransaction::TYPE_MEMBERS: + case self::TYPE_MEMBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); $req_phids = array_merge($add, $rem); break; - case PhabricatorProjectTransaction::TYPE_IMAGE: + case self::TYPE_IMAGE: $req_phids[] = $old; $req_phids[] = $new; break; } return array_merge($req_phids, parent::getRequiredHandlePHIDs()); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorProjectTransaction::TYPE_STATUS: + case self::TYPE_STATUS: if ($old == 0) { return 'red'; } else { return 'green'; } } return parent::getColor(); } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorProjectTransaction::TYPE_STATUS: + case self::TYPE_STATUS: if ($old == 0) { return 'fa-ban'; } else { return 'fa-check'; } - case PhabricatorProjectTransaction::TYPE_LOCKED: + case self::TYPE_LOCKED: if ($new) { return 'fa-lock'; } else { return 'fa-unlock'; } - case PhabricatorProjectTransaction::TYPE_ICON: + case self::TYPE_ICON: return $new; - case PhabricatorProjectTransaction::TYPE_IMAGE: + case self::TYPE_IMAGE: return 'fa-photo'; - case PhabricatorProjectTransaction::TYPE_MEMBERS: + case self::TYPE_MEMBERS: return 'fa-user'; - case PhabricatorProjectTransaction::TYPE_SLUGS: + case self::TYPE_SLUGS: return 'fa-tag'; } return parent::getIcon(); } public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); $author_handle = $this->renderHandleLink($this->getAuthorPHID()); switch ($this->getTransactionType()) { - case PhabricatorProjectTransaction::TYPE_NAME: + case self::TYPE_NAME: if ($old === null) { return pht( '%s created this project.', $author_handle); } else { return pht( '%s renamed this project from "%s" to "%s".', $author_handle, $old, $new); } - case PhabricatorProjectTransaction::TYPE_STATUS: + case self::TYPE_STATUS: if ($old == 0) { return pht( '%s archived this project.', $author_handle); } else { return pht( '%s activated this project.', $author_handle); } - case PhabricatorProjectTransaction::TYPE_IMAGE: + case self::TYPE_IMAGE: // TODO: Some day, it would be nice to show the images. if (!$old) { return pht( '%s set this project\'s image to %s.', $author_handle, $this->renderHandleLink($new)); } else if (!$new) { return pht( '%s removed this project\'s image.', $author_handle); } else { return pht( '%s updated this project\'s image from %s to %s.', $author_handle, $this->renderHandleLink($old), $this->renderHandleLink($new)); } - case PhabricatorProjectTransaction::TYPE_ICON: + case self::TYPE_ICON: return pht( '%s set this project\'s icon to %s.', $author_handle, PhabricatorProjectIcon::getLabel($new)); - case PhabricatorProjectTransaction::TYPE_COLOR: + case self::TYPE_COLOR: return pht( '%s set this project\'s color to %s.', $author_handle, PHUITagView::getShadeName($new)); - case PhabricatorProjectTransaction::TYPE_LOCKED: + case self::TYPE_LOCKED: if ($new) { return pht( '%s locked this project\'s membership.', $author_handle); } else { return pht( '%s unlocked this project\'s membership.', $author_handle); } - case PhabricatorProjectTransaction::TYPE_SLUGS: + case self::TYPE_SLUGS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s changed project hashtag(s), added %d: %s; removed %d: %s.', $author_handle, count($add), $this->renderSlugList($add), count($rem), $this->renderSlugList($rem)); } else if ($add) { return pht( '%s added %d project hashtag(s): %s.', $author_handle, count($add), $this->renderSlugList($add)); } else if ($rem) { return pht( '%s removed %d project hashtag(s): %s.', $author_handle, count($rem), $this->renderSlugList($rem)); } - case PhabricatorProjectTransaction::TYPE_MEMBERS: + case self::TYPE_MEMBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s changed project member(s), added %d: %s; removed %d: %s.', $author_handle, count($add), $this->renderHandleList($add), count($rem), $this->renderHandleList($rem)); } else if ($add) { if (count($add) == 1 && (head($add) == $this->getAuthorPHID())) { return pht( '%s joined this project.', $author_handle); } else { return pht( '%s added %d project member(s): %s.', $author_handle, count($add), $this->renderHandleList($add)); } } else if ($rem) { if (count($rem) == 1 && (head($rem) == $this->getAuthorPHID())) { return pht( '%s left this project.', $author_handle); } else { return pht( '%s removed %d project member(s): %s.', $author_handle, count($rem), $this->renderHandleList($rem)); } } } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $author_handle = $this->renderHandleLink($author_phid); $object_handle = $this->renderHandleLink($object_phid); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_NAME: if ($old === null) { return pht( '%s created %s.', $author_handle, $object_handle); } else { return pht( '%s renamed %s from "%s" to "%s".', $author_handle, $object_handle, $old, $new); } case self::TYPE_STATUS: if ($old == 0) { return pht( '%s archived %s.', $author_handle, $object_handle); } else { return pht( '%s activated %s.', $author_handle, $object_handle); } case self::TYPE_IMAGE: // TODO: Some day, it would be nice to show the images. if (!$old) { return pht( '%s set the image for %s to %s.', $author_handle, $object_handle, $this->renderHandleLink($new)); } else if (!$new) { return pht( '%s removed the image for %s.', $author_handle, $object_handle); } else { return pht( '%s updated the image for %s from %s to %s.', $author_handle, $object_handle, $this->renderHandleLink($old), $this->renderHandleLink($new)); } case self::TYPE_ICON: return pht( '%s set the icon for %s to %s.', $author_handle, $object_handle, PhabricatorProjectIcon::getLabel($new)); case self::TYPE_COLOR: return pht( '%s set the color for %s to %s.', $author_handle, $object_handle, PHUITagView::getShadeName($new)); case self::TYPE_LOCKED: if ($new) { return pht( '%s locked %s membership.', $author_handle, $object_handle); } else { return pht( '%s unlocked %s membership.', $author_handle, $object_handle); } case self::TYPE_SLUGS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s changed %s hashtag(s), added %d: %s; removed %d: %s.', $author_handle, $object_handle, count($add), $this->renderSlugList($add), count($rem), $this->renderSlugList($rem)); } else if ($add) { return pht( '%s added %d %s hashtag(s): %s.', $author_handle, count($add), $object_handle, $this->renderSlugList($add)); } else if ($rem) { return pht( '%s removed %d %s hashtag(s): %s.', $author_handle, count($rem), $object_handle, $this->renderSlugList($rem)); } } return parent::getTitleForFeed(); } private function renderSlugList($slugs) { return implode(', ', $slugs); } } diff --git a/src/applications/releeph/storage/ReleephRequest.php b/src/applications/releeph/storage/ReleephRequest.php index 7f3f190a6d..7f2487107a 100644 --- a/src/applications/releeph/storage/ReleephRequest.php +++ b/src/applications/releeph/storage/ReleephRequest.php @@ -1,373 +1,373 @@ <?php final class ReleephRequest extends ReleephDAO implements PhabricatorApplicationTransactionInterface, PhabricatorPolicyInterface, PhabricatorCustomFieldInterface { protected $branchID; protected $requestUserPHID; protected $details = array(); protected $userIntents = array(); protected $inBranch; protected $pickStatus; protected $mailKey; /** * The object which is being requested. Normally this is a commit, but it * might also be a revision. In the future, it could be a repository branch * or an external object (like a GitHub pull request). */ protected $requestedObjectPHID; // Information about the thing being requested protected $requestCommitPHID; // Information about the last commit to the releeph branch protected $commitIdentifier; protected $commitPHID; private $customFields = self::ATTACHABLE; private $branch = self::ATTACHABLE; private $requestedObject = self::ATTACHABLE; /* -( Constants and helper methods )--------------------------------------- */ const INTENT_WANT = 'want'; const INTENT_PASS = 'pass'; const PICK_PENDING = 1; // old const PICK_FAILED = 2; const PICK_OK = 3; const PICK_MANUAL = 4; // old const REVERT_OK = 5; const REVERT_FAILED = 6; public function shouldBeInBranch() { return $this->getPusherIntent() == self::INTENT_WANT && /** * We use "!= pass" instead of "== want" in case the requestor intent is * not present. In other words, only revert if the requestor explicitly * passed. */ $this->getRequestorIntent() != self::INTENT_PASS; } /** * Will return INTENT_WANT if any pusher wants this request, and no pusher * passes on this request. */ public function getPusherIntent() { $product = $this->getBranch()->getProduct(); if (!$product->getPushers()) { return self::INTENT_WANT; } $found_pusher_want = false; foreach ($this->userIntents as $phid => $intent) { if ($product->isAuthoritativePHID($phid)) { if ($intent == self::INTENT_PASS) { return self::INTENT_PASS; } $found_pusher_want = true; } } if ($found_pusher_want) { return self::INTENT_WANT; } else { return null; } } public function getRequestorIntent() { return idx($this->userIntents, $this->requestUserPHID); } public function getStatus() { return $this->calculateStatus(); } public function getMonogram() { return 'Y'.$this->getID(); } public function getBranch() { return $this->assertAttached($this->branch); } public function attachBranch(ReleephBranch $branch) { $this->branch = $branch; return $this; } public function getRequestedObject() { return $this->assertAttached($this->requestedObject); } public function attachRequestedObject($object) { $this->requestedObject = $object; return $this; } private function calculateStatus() { if ($this->shouldBeInBranch()) { if ($this->getInBranch()) { return ReleephRequestStatus::STATUS_PICKED; } else { return ReleephRequestStatus::STATUS_NEEDS_PICK; } } else { if ($this->getInBranch()) { return ReleephRequestStatus::STATUS_NEEDS_REVERT; } else { - $intent_pass = ReleephRequest::INTENT_PASS; - $intent_want = ReleephRequest::INTENT_WANT; + $intent_pass = self::INTENT_PASS; + $intent_want = self::INTENT_WANT; $has_been_in_branch = $this->getCommitIdentifier(); // Regardless of why we reverted something, always say reverted if it // was once in the branch. if ($has_been_in_branch) { return ReleephRequestStatus::STATUS_REVERTED; } else if ($this->getPusherIntent() === $intent_pass) { // Otherwise, if it has never been in the branch, explicitly say why: return ReleephRequestStatus::STATUS_REJECTED; } else if ($this->getRequestorIntent() === $intent_want) { return ReleephRequestStatus::STATUS_REQUESTED; } else { return ReleephRequestStatus::STATUS_ABANDONED; } } } } /* -( Lisk mechanics )----------------------------------------------------- */ protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, 'userIntents' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'requestCommitPHID' => 'phid?', 'commitIdentifier' => 'text40?', 'commitPHID' => 'phid?', 'pickStatus' => 'uint32?', 'inBranch' => 'bool', 'mailKey' => 'bytes20', 'userIntents' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'requestIdentifierBranch' => array( 'columns' => array('requestCommitPHID', 'branchID'), 'unique' => true, ), 'branchID' => array( 'columns' => array('branchID'), ), 'key_requestedObject' => array( 'columns' => array('requestedObjectPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( ReleephRequestPHIDType::TYPECONST); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } /* -( Helpful accessors )--------------------------------------------------- */ public function getDetail($key, $default = null) { return idx($this->getDetails(), $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } /** * Get the commit PHIDs this request is requesting. * * NOTE: For now, this always returns one PHID. * * @return list<phid> Commit PHIDs requested by this request. */ public function getCommitPHIDs() { return array( $this->requestCommitPHID, ); } public function getReason() { // Backward compatibility: reason used to be called comments $reason = $this->getDetail('reason'); if (!$reason) { return $this->getDetail('comments'); } return $reason; } /** * Allow a null summary, and fall back to the title of the commit. */ public function getSummaryForDisplay() { $summary = $this->getDetail('summary'); if (!strlen($summary)) { $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { $summary = $commit->getSummary(); } } if (!strlen($summary)) { $summary = pht('None'); } return $summary; } /* -( Loading external objects )------------------------------------------- */ public function loadPhabricatorRepositoryCommit() { return $this->loadOneRelative( new PhabricatorRepositoryCommit(), 'phid', 'getRequestCommitPHID'); } public function loadPhabricatorRepositoryCommitData() { $commit = $this->loadPhabricatorRepositoryCommit(); if ($commit) { return $commit->loadOneRelative( new PhabricatorRepositoryCommitData(), 'commitID'); } } /* -( State change helpers )----------------------------------------------- */ public function setUserIntent(PhabricatorUser $user, $intent) { $this->userIntents[$user->getPHID()] = $intent; return $this; } /* -( Migrating to status-less ReleephRequests )--------------------------- */ protected function didReadData() { if ($this->userIntents === null) { $this->userIntents = array(); } } public function setStatus($value) { throw new Exception('`status` is now deprecated!'); } /* -( Make magic Lisk methods private )------------------------------------ */ private function setUserIntents(array $ar) { return parent::setUserIntents($ar); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new ReleephRequestTransactionalEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new ReleephRequestTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBranch()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBranch()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Pull requests have the same policies as the branches they are '. 'requested against.'); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('releeph.fields'); } public function getCustomFieldBaseClass() { return 'ReleephFieldSpecification'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/applications/releeph/storage/ReleephRequestTransaction.php b/src/applications/releeph/storage/ReleephRequestTransaction.php index bd17ad43c9..f4a4720c78 100644 --- a/src/applications/releeph/storage/ReleephRequestTransaction.php +++ b/src/applications/releeph/storage/ReleephRequestTransaction.php @@ -1,279 +1,279 @@ <?php final class ReleephRequestTransaction extends PhabricatorApplicationTransaction { const TYPE_REQUEST = 'releeph:request'; const TYPE_USER_INTENT = 'releeph:user_intent'; const TYPE_EDIT_FIELD = 'releeph:edit_field'; const TYPE_PICK_STATUS = 'releeph:pick_status'; const TYPE_COMMIT = 'releeph:commit'; const TYPE_DISCOVERY = 'releeph:discovery'; const TYPE_MANUAL_IN_BRANCH = 'releeph:manual'; public function getApplicationName() { return 'releeph'; } public function getApplicationTransactionType() { return ReleephRequestPHIDType::TYPECONST; } public function getApplicationTransactionCommentObject() { return new ReleephRequestTransactionComment(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { default; break; } return parent::hasChangeDetails(); } public function getRequiredHandlePHIDs() { $phids = parent::getRequiredHandlePHIDs(); $phids[] = $this->getObjectPHID(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case ReleephRequestTransaction::TYPE_REQUEST: - case ReleephRequestTransaction::TYPE_DISCOVERY: + case self::TYPE_REQUEST: + case self::TYPE_DISCOVERY: $phids[] = $new; break; - case ReleephRequestTransaction::TYPE_EDIT_FIELD: + case self::TYPE_EDIT_FIELD: self::searchForPHIDs($this->getOldValue(), $phids); self::searchForPHIDs($this->getNewValue(), $phids); break; } return $phids; } public function getTitle() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case ReleephRequestTransaction::TYPE_REQUEST: + case self::TYPE_REQUEST: return pht( '%s requested %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); break; - case ReleephRequestTransaction::TYPE_USER_INTENT: + case self::TYPE_USER_INTENT: return $this->getIntentTitle(); break; - case ReleephRequestTransaction::TYPE_EDIT_FIELD: + case self::TYPE_EDIT_FIELD: $field = newv($this->getMetadataValue('fieldClass'), array()); $name = $field->getName(); $markup = $name; if ($this->getRenderingTarget() === PhabricatorApplicationTransaction::TARGET_HTML) { $markup = hsprintf('<em>%s</em>', $name); } return pht( '%s changed the %s to "%s"', $this->renderHandleLink($author_phid), $markup, $field->normalizeForTransactionView($this, $new)); break; - case ReleephRequestTransaction::TYPE_PICK_STATUS: + case self::TYPE_PICK_STATUS: switch ($new) { case ReleephRequest::PICK_OK: return pht('%s found this request picks without error', $this->renderHandleLink($author_phid)); case ReleephRequest::REVERT_OK: return pht('%s found this request reverts without error', $this->renderHandleLink($author_phid)); case ReleephRequest::PICK_FAILED: return pht("%s couldn't pick this request", $this->renderHandleLink($author_phid)); case ReleephRequest::REVERT_FAILED: return pht("%s couldn't revert this request", $this->renderHandleLink($author_phid)); } break; - case ReleephRequestTransaction::TYPE_COMMIT: + case self::TYPE_COMMIT: $action_type = $this->getMetadataValue('action'); switch ($action_type) { case 'pick': return pht( '%s picked this request and committed the result upstream', $this->renderHandleLink($author_phid)); break; case 'revert': return pht( '%s reverted this request and committed the result upstream', $this->renderHandleLink($author_phid)); break; } break; - case ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH: + case self::TYPE_MANUAL_IN_BRANCH: $action = $new ? pht('picked') : pht('reverted'); return pht( '%s marked this request as manually %s', $this->renderHandleLink($author_phid), $action); break; - case ReleephRequestTransaction::TYPE_DISCOVERY: + case self::TYPE_DISCOVERY: return pht('%s discovered this commit as %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); break; default: return parent::getTitle(); break; } } public function getActionStrength() { return parent::getActionStrength(); } public function getActionName() { switch ($this->getTransactionType()) { case self::TYPE_REQUEST: return pht('Requested'); case self::TYPE_COMMIT: $action_type = $this->getMetadataValue('action'); switch ($action_type) { case 'pick': return pht('Picked'); case 'revert': return pht('Reverted'); } } return parent::getActionName(); } public function getColor() { $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case ReleephRequestTransaction::TYPE_USER_INTENT: + case self::TYPE_USER_INTENT: switch ($new) { case ReleephRequest::INTENT_WANT: return PhabricatorTransactions::COLOR_GREEN; case ReleephRequest::INTENT_PASS: return PhabricatorTransactions::COLOR_RED; } } return parent::getColor(); } private static function searchForPHIDs($thing, array &$phids) { /** * To implement something like getRequiredHandlePHIDs() in a * ReleephFieldSpecification, we'd have to provide the field with its * ReleephRequest (so that it could load the PHIDs from the * ReleephRequest's storage, and return them.) * * We don't have fields initialized with their ReleephRequests, but we can * make a good guess at what handles will be needed for rendering the field * in this transaction by inspecting the old and new values. */ if (!is_array($thing)) { $thing = array($thing); } foreach ($thing as $value) { if (phid_get_type($value) !== PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { $phids[] = $value; } } } private function getIntentTitle() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $new = $this->getNewValue(); $is_pusher = $this->getMetadataValue('isPusher'); switch ($new) { case ReleephRequest::INTENT_WANT: if ($is_pusher) { return pht( '%s approved this request', $this->renderHandleLink($author_phid)); } else { return pht( '%s wanted this request', $this->renderHandleLink($author_phid)); } case ReleephRequest::INTENT_PASS: if ($is_pusher) { return pht( '%s rejected this request', $this->renderHandleLink($author_phid)); } else { return pht( '%s passed on this request', $this->renderHandleLink($author_phid)); } } } public function shouldHide() { $type = $this->getTransactionType(); - if ($type === ReleephRequestTransaction::TYPE_USER_INTENT && + if ($type === self::TYPE_USER_INTENT && $this->getMetadataValue('isRQCreate')) { return true; } if ($this->isBoringPickStatus()) { return true; } // ReleephSummaryFieldSpecification is usually blank when an RQ is created, // creating a transaction change from null to "". Hide these! - if ($type === ReleephRequestTransaction::TYPE_EDIT_FIELD) { + if ($type === self::TYPE_EDIT_FIELD) { if ($this->getOldValue() === null && $this->getNewValue() === '') { return true; } } return parent::shouldHide(); } public function isBoringPickStatus() { $type = $this->getTransactionType(); - if ($type === ReleephRequestTransaction::TYPE_PICK_STATUS) { + if ($type === self::TYPE_PICK_STATUS) { $new = $this->getNewValue(); if ($new === ReleephRequest::PICK_OK || $new === ReleephRequest::REVERT_OK) { return true; } } return false; } } diff --git a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php index 3131f9b9e6..21c7fe38ab 100644 --- a/src/applications/repository/query/PhabricatorRepositorySearchEngine.php +++ b/src/applications/repository/query/PhabricatorRepositorySearchEngine.php @@ -1,282 +1,282 @@ <?php final class PhabricatorRepositorySearchEngine extends PhabricatorApplicationSearchEngine { public function getResultTypeDescription() { return pht('Repositories'); } public function getApplicationClassName() { return 'PhabricatorDiffusionApplication'; } public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); $saved->setParameter('callsigns', $request->getStrList('callsigns')); $saved->setParameter('status', $request->getStr('status')); $saved->setParameter('order', $request->getStr('order')); $saved->setParameter('hosted', $request->getStr('hosted')); $saved->setParameter('types', $request->getArr('types')); $saved->setParameter('name', $request->getStr('name')); $saved->setParameter( 'projects', $this->readProjectsFromRequest($request, 'projects')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorRepositoryQuery()) ->needProjectPHIDs(true) ->needCommitCounts(true) ->needMostRecentCommits(true); $callsigns = $saved->getParameter('callsigns'); if ($callsigns) { $query->withCallsigns($callsigns); } $status = $saved->getParameter('status'); $status = idx($this->getStatusValues(), $status); if ($status) { $query->withStatus($status); } $this->setQueryOrder($query, $saved); $hosted = $saved->getParameter('hosted'); $hosted = idx($this->getHostedValues(), $hosted); if ($hosted) { $query->withHosted($hosted); } $types = $saved->getParameter('types'); if ($types) { $query->withTypes($types); } $name = $saved->getParameter('name'); if (strlen($name)) { $query->withNameContains($name); } $adjusted = clone $saved; $adjusted->setParameter('projects', $this->readProjectTokens($saved)); $this->setQueryProjects($query, $adjusted); return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved_query) { $callsigns = $saved_query->getParameter('callsigns', array()); $types = $saved_query->getParameter('types', array()); $types = array_fuse($types); $name = $saved_query->getParameter('name'); $projects = $this->readProjectTokens($saved_query); $form ->appendChild( id(new AphrontFormTextControl()) ->setName('callsigns') ->setLabel(pht('Callsigns')) ->setValue(implode(', ', $callsigns))) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name Contains')) ->setValue($name)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorProjectLogicalDatasource()) ->setName('projects') ->setLabel(pht('Projects')) ->setValue($projects)) ->appendChild( id(new AphrontFormSelectControl()) ->setName('status') ->setLabel(pht('Status')) ->setValue($saved_query->getParameter('status')) ->setOptions($this->getStatusOptions())) ->appendChild( id(new AphrontFormSelectControl()) ->setName('hosted') ->setLabel(pht('Hosted')) ->setValue($saved_query->getParameter('hosted')) ->setOptions($this->getHostedOptions())); $type_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Types')); $all_types = PhabricatorRepositoryType::getAllRepositoryTypes(); foreach ($all_types as $key => $name) { $type_control->addCheckbox( 'types[]', $key, $name, isset($types[$key])); } $form->appendChild($type_control); $this->appendOrderFieldsToForm( $form, $saved_query, new PhabricatorRepositoryQuery()); } protected function getURI($path) { return '/diffusion/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'active' => pht('Active Repositories'), 'all' => pht('All Repositories'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'active': return $query->setParameter('status', 'open'); case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } private function getStatusOptions() { return array( '' => pht('Active and Inactive Repositories'), 'open' => pht('Active Repositories'), 'closed' => pht('Inactive Repositories'), ); } private function getStatusValues() { return array( '' => PhabricatorRepositoryQuery::STATUS_ALL, 'open' => PhabricatorRepositoryQuery::STATUS_OPEN, 'closed' => PhabricatorRepositoryQuery::STATUS_CLOSED, ); } private function getHostedOptions() { return array( '' => pht('Hosted and Remote Repositories'), 'phabricator' => pht('Hosted Repositories'), 'remote' => pht('Remote Repositories'), ); } private function getHostedValues() { return array( '' => PhabricatorRepositoryQuery::HOSTED_ALL, 'phabricator' => PhabricatorRepositoryQuery::HOSTED_PHABRICATOR, 'remote' => PhabricatorRepositoryQuery::HOSTED_REMOTE, ); } protected function getRequiredHandlePHIDsForResultList( array $repositories, PhabricatorSavedQuery $query) { return array_mergev(mpull($repositories, 'getProjectPHIDs')); } protected function renderResultList( array $repositories, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($repositories, 'PhabricatorRepository'); - $viewer = $this->requireViewer();; + $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); foreach ($repositories as $repository) { $id = $repository->getID(); $item = id(new PHUIObjectItemView()) ->setUser($viewer) ->setHeader($repository->getName()) ->setObjectName('r'.$repository->getCallsign()) ->setHref($this->getApplicationURI($repository->getCallsign().'/')); $commit = $repository->getMostRecentCommit(); if ($commit) { $commit_link = DiffusionView::linkCommit( $repository, $commit->getCommitIdentifier(), $commit->getSummary()); $item->setSubhead($commit_link); $item->setEpoch($commit->getEpoch()); } $item->addIcon( 'none', PhabricatorRepositoryType::getNameForRepositoryType( $repository->getVersionControlSystem())); $size = $repository->getCommitCount(); if ($size) { $history_uri = DiffusionRequest::generateDiffusionURI( array( 'callsign' => $repository->getCallsign(), 'action' => 'history', )); $item->addAttribute( phutil_tag( 'a', array( 'href' => $history_uri, ), pht('%s Commit(s)', new PhutilNumber($size)))); } else { $item->addAttribute(pht('No Commits')); } $project_handles = array_select_keys( $handles, $repository->getProjectPHIDs()); if ($project_handles) { $item->addAttribute( id(new PHUIHandleTagListView()) ->setSlim(true) ->setHandles($project_handles)); } if (!$repository->isTracked()) { $item->setDisabled(true); $item->addIcon('disable-grey', pht('Inactive')); } $list->addItem($item); } return $list; } private function readProjectTokens(PhabricatorSavedQuery $saved) { $projects = $saved->getParameter('projects', array()); $any = $saved->getParameter('anyProjectPHIDs', array()); foreach ($any as $project) { $projects[] = 'any('.$project.')'; } return $projects; } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php index d5fb87fa8d..f16784bbf1 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php @@ -1,202 +1,202 @@ <?php /** * Records a push to a hosted repository. This allows us to store metadata * about who pushed commits, when, and from where. We can also record the * history of branches and tags, which is not normally persisted outside of * the reflog. * * This log is written by commit hooks installed into hosted repositories. * See @{class:DiffusionCommitHookEngine}. */ final class PhabricatorRepositoryPushLog extends PhabricatorRepositoryDAO implements PhabricatorPolicyInterface { const REFTYPE_BRANCH = 'branch'; const REFTYPE_TAG = 'tag'; const REFTYPE_BOOKMARK = 'bookmark'; const REFTYPE_COMMIT = 'commit'; const CHANGEFLAG_ADD = 1; const CHANGEFLAG_DELETE = 2; const CHANGEFLAG_APPEND = 4; const CHANGEFLAG_REWRITE = 8; const CHANGEFLAG_DANGEROUS = 16; const REJECT_ACCEPT = 0; const REJECT_DANGEROUS = 1; const REJECT_HERALD = 2; const REJECT_EXTERNAL = 3; const REJECT_BROKEN = 4; protected $repositoryPHID; protected $epoch; protected $pusherPHID; protected $pushEventPHID; protected $refType; protected $refNameHash; protected $refNameRaw; protected $refNameEncoding; protected $refOld; protected $refNew; protected $mergeBase; protected $changeFlags; private $dangerousChangeDescription = self::ATTACHABLE; private $pushEvent = self::ATTACHABLE; private $repository = self::ATTACHABLE; public static function initializeNewLog(PhabricatorUser $viewer) { return id(new PhabricatorRepositoryPushLog()) ->setPusherPHID($viewer->getPHID()); } public static function getHeraldChangeFlagConditionOptions() { return array( - PhabricatorRepositoryPushLog::CHANGEFLAG_ADD => + self::CHANGEFLAG_ADD => pht('change creates ref'), - PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE => + self::CHANGEFLAG_DELETE => pht('change deletes ref'), - PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE => + self::CHANGEFLAG_REWRITE => pht('change rewrites ref'), - PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS => + self::CHANGEFLAG_DANGEROUS => pht('dangerous change'), ); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_BINARY => array( 'refNameRaw' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'refType' => 'text12', 'refNameHash' => 'bytes12?', 'refNameRaw' => 'bytes?', 'refNameEncoding' => 'text16?', 'refOld' => 'text40?', 'refNew' => 'text40', 'mergeBase' => 'text40?', 'changeFlags' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_repository' => array( 'columns' => array('repositoryPHID'), ), 'key_ref' => array( 'columns' => array('repositoryPHID', 'refNew'), ), 'key_name' => array( 'columns' => array('repositoryPHID', 'refNameHash'), ), 'key_event' => array( 'columns' => array('pushEventPHID'), ), 'key_pusher' => array( 'columns' => array('pusherPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryPushLogPHIDType::TYPECONST); } public function attachPushEvent(PhabricatorRepositoryPushEvent $push_event) { $this->pushEvent = $push_event; return $this; } public function getPushEvent() { return $this->assertAttached($this->pushEvent); } public function getRefName() { return $this->getUTF8StringFromStorage( $this->getRefNameRaw(), $this->getRefNameEncoding()); } public function setRefName($ref_raw) { $this->setRefNameRaw($ref_raw); $this->setRefNameHash(PhabricatorHash::digestForIndex($ref_raw)); $this->setRefNameEncoding($this->detectEncodingForStorage($ref_raw)); return $this; } public function getRefOldShort() { if ($this->getRepository()->isSVN()) { return $this->getRefOld(); } return substr($this->getRefOld(), 0, 12); } public function getRefNewShort() { if ($this->getRepository()->isSVN()) { return $this->getRefNew(); } return substr($this->getRefNew(), 0, 12); } public function hasChangeFlags($mask) { return ($this->changeFlags & $mask); } public function attachDangerousChangeDescription($description) { $this->dangerousChangeDescription = $description; return $this; } public function getDangerousChangeDescription() { return $this->assertAttached($this->dangerousChangeDescription); } public function attachRepository(PhabricatorRepository $repository) { // NOTE: Some gymnastics around this because of object construction order // in the hook engine. Particularly, web build the logs before we build // their push event. $this->repository = $repository; return $this; } public function getRepository() { if ($this->repository == self::ATTACHABLE) { return $this->getPushEvent()->getRepository(); } return $this->assertAttached($this->repository); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // NOTE: We're passing through the repository rather than the push event // mostly because we need to do policy checks in Herald before we create // the event. The two approaches are equivalent in practice. return $this->getRepository()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( "A repository's push logs are visible to users who can see the ". "repository."); } } diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index e07a045dc2..3248dc8b2a 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -1,111 +1,111 @@ <?php final class PhabricatorUserPreferences extends PhabricatorUserDAO { const PREFERENCE_MONOSPACED = 'monospaced'; const PREFERENCE_DARK_CONSOLE = 'dark_console'; const PREFERENCE_EDITOR = 'editor'; const PREFERENCE_MULTIEDIT = 'multiedit'; const PREFERENCE_TITLES = 'titles'; const PREFERENCE_MONOSPACED_TEXTAREAS = 'monospaced-textareas'; const PREFERENCE_TIME_FORMAT = 'time-format'; const PREFERENCE_RE_PREFIX = 're-prefix'; const PREFERENCE_NO_SELF_MAIL = 'self-mail'; const PREFERENCE_NO_MAIL = 'no-mail'; const PREFERENCE_MAILTAGS = 'mailtags'; const PREFERENCE_VARY_SUBJECT = 'vary-subject'; const PREFERENCE_HTML_EMAILS = 'html-emails'; const PREFERENCE_SEARCHBAR_JUMP = 'searchbar-jump'; const PREFERENCE_SEARCH_SHORTCUT = 'search-shortcut'; const PREFERENCE_SEARCH_SCOPE = 'search-scope'; const PREFERENCE_DIFFUSION_BLAME = 'diffusion-blame'; const PREFERENCE_DIFFUSION_COLOR = 'diffusion-color'; const PREFERENCE_NAV_COLLAPSED = 'nav-collapsed'; const PREFERENCE_NAV_WIDTH = 'nav-width'; const PREFERENCE_APP_TILES = 'app-tiles'; const PREFERENCE_APP_PINNED = 'app-pinned'; const PREFERENCE_DIFF_UNIFIED = 'diff-unified'; const PREFERENCE_DIFF_FILETREE = 'diff-filetree'; const PREFERENCE_DIFF_GHOSTS = 'diff-ghosts'; const PREFERENCE_CONPH_NOTIFICATIONS = 'conph-notifications'; const PREFERENCE_CONPHERENCE_COLUMN = 'conpherence-column'; // These are in an unusual order for historic reasons. const MAILTAG_PREFERENCE_NOTIFY = 0; const MAILTAG_PREFERENCE_EMAIL = 1; const MAILTAG_PREFERENCE_IGNORE = 2; protected $userPHID; protected $preferences = array(); protected function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( 'preferences' => self::SERIALIZATION_JSON, ), self::CONFIG_TIMESTAMPS => false, self::CONFIG_KEY_SCHEMA => array( 'userPHID' => array( 'columns' => array('userPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getPreference($key, $default = null) { return idx($this->preferences, $key, $default); } public function setPreference($key, $value) { $this->preferences[$key] = $value; return $this; } public function unsetPreference($key) { unset($this->preferences[$key]); return $this; } public function getPinnedApplications(array $apps, PhabricatorUser $viewer) { - $pref_pinned = PhabricatorUserPreferences::PREFERENCE_APP_PINNED; + $pref_pinned = self::PREFERENCE_APP_PINNED; $pinned = $this->getPreference($pref_pinned); if ($pinned) { return $pinned; } - $pref_tiles = PhabricatorUserPreferences::PREFERENCE_APP_TILES; + $pref_tiles = self::PREFERENCE_APP_TILES; $tiles = $this->getPreference($pref_tiles, array()); $full_tile = 'full'; $large = array(); foreach ($apps as $app) { $show = $app->isPinnedByDefault($viewer); // TODO: This is legacy stuff, clean it up eventually. This approximately // retains the old "tiles" preference. if (isset($tiles[get_class($app)])) { $show = ($tiles[get_class($app)] == $full_tile); } if ($show) { $large[] = get_class($app); } } return $large; } public static function filterMonospacedCSSRule($monospaced) { // Prevent the user from doing dangerous things. return preg_replace('/[^a-z0-9 ,".]+/i', '', $monospaced); } } diff --git a/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php b/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php index abb73fd588..9abf1f5801 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvoteTransaction.php @@ -1,156 +1,156 @@ <?php final class PhabricatorSlowvoteTransaction extends PhabricatorApplicationTransaction { const TYPE_QUESTION = 'vote:question'; const TYPE_DESCRIPTION = 'vote:description'; const TYPE_RESPONSES = 'vote:responses'; const TYPE_SHUFFLE = 'vote:shuffle'; const TYPE_CLOSE = 'vote:close'; public function getApplicationName() { return 'slowvote'; } public function getApplicationTransactionType() { return PhabricatorSlowvotePollPHIDType::TYPECONST; } public function getApplicationTransactionCommentObject() { return new PhabricatorSlowvoteTransactionComment(); } public function shouldHide() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: - case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: - case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: - case PhabricatorSlowvoteTransaction::TYPE_CLOSE: + case self::TYPE_DESCRIPTION: + case self::TYPE_RESPONSES: + case self::TYPE_SHUFFLE: + case self::TYPE_CLOSE: return ($old === null); } return parent::shouldHide(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorSlowvoteTransaction::TYPE_QUESTION: + case self::TYPE_QUESTION: if ($old === null) { return pht( '%s created this poll.', $this->renderHandleLink($author_phid)); } else { return pht( '%s changed the poll question from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; - case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: + case self::TYPE_DESCRIPTION: return pht( '%s updated the description for this poll.', $this->renderHandleLink($author_phid)); - case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: + case self::TYPE_RESPONSES: // TODO: This could be more detailed return pht( '%s changed who can see the responses.', $this->renderHandleLink($author_phid)); - case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: + case self::TYPE_SHUFFLE: if ($new) { return pht( '%s made poll responses appear in a random order.', $this->renderHandleLink($author_phid)); } else { return pht( '%s made poll responses appear in a fixed order.', $this->renderHandleLink($author_phid)); } break; - case PhabricatorSlowvoteTransaction::TYPE_CLOSE: + case self::TYPE_CLOSE: if ($new) { return pht( '%s closed this poll.', $this->renderHandleLink($author_phid)); } else { return pht( '%s reopened this poll.', $this->renderHandleLink($author_phid)); } break; } return parent::getTitle(); } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorSlowvoteTransaction::TYPE_QUESTION: + case self::TYPE_QUESTION: if ($old === null) { return 'fa-plus'; } else { return 'fa-pencil'; } - case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: - case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: + case self::TYPE_DESCRIPTION: + case self::TYPE_RESPONSES: return 'fa-pencil'; - case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: + case self::TYPE_SHUFFLE: return 'fa-refresh'; - case PhabricatorSlowvoteTransaction::TYPE_CLOSE: + case self::TYPE_CLOSE: if ($new) { return 'fa-ban'; } else { return 'fa-pencil'; } } return parent::getIcon(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { - case PhabricatorSlowvoteTransaction::TYPE_QUESTION: - case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: - case PhabricatorSlowvoteTransaction::TYPE_RESPONSES: - case PhabricatorSlowvoteTransaction::TYPE_SHUFFLE: - case PhabricatorSlowvoteTransaction::TYPE_CLOSE: + case self::TYPE_QUESTION: + case self::TYPE_DESCRIPTION: + case self::TYPE_RESPONSES: + case self::TYPE_SHUFFLE: + case self::TYPE_CLOSE: return PhabricatorTransactions::COLOR_BLUE; } return parent::getColor(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { - case PhabricatorSlowvoteTransaction::TYPE_DESCRIPTION: + case self::TYPE_DESCRIPTION: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index c6755ccfcc..355f15c2a7 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1,1247 +1,1247 @@ <?php abstract class PhabricatorApplicationTransaction extends PhabricatorLiskDAO implements PhabricatorPolicyInterface, PhabricatorDestructibleInterface { const TARGET_TEXT = 'text'; const TARGET_HTML = 'html'; protected $phid; protected $objectPHID; protected $authorPHID; protected $viewPolicy; protected $editPolicy; protected $commentPHID; protected $commentVersion = 0; protected $transactionType; protected $oldValue; protected $newValue; protected $metadata = array(); protected $contentSource; private $comment; private $commentNotLoaded; private $handles; private $renderingTarget = self::TARGET_HTML; private $transactionGroup = array(); private $viewer = self::ATTACHABLE; private $object = self::ATTACHABLE; private $oldValueHasBeenSet = false; private $ignoreOnNoEffect; /** * Flag this transaction as a pure side-effect which should be ignored when * applying transactions if it has no effect, even if transaction application * would normally fail. This both provides users with better error messages * and allows transactions to perform optional side effects. */ public function setIgnoreOnNoEffect($ignore) { $this->ignoreOnNoEffect = $ignore; return $this; } public function getIgnoreOnNoEffect() { return $this->ignoreOnNoEffect; } public function shouldGenerateOldValue() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_BUILDABLE: case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_CUSTOMFIELD: case PhabricatorTransactions::TYPE_INLINESTATE: return false; } return true; } abstract public function getApplicationTransactionType(); private function getApplicationObjectTypeName() { $types = PhabricatorPHIDType::getAllTypes(); $type = idx($types, $this->getApplicationTransactionType()); if ($type) { return $type->getTypeName(); } return pht('Object'); } public function getApplicationTransactionCommentObject() { throw new PhutilMethodNotImplementedException(); } public function getApplicationTransactionViewObject() { return new PhabricatorApplicationTransactionView(); } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function generatePHID() { $type = PhabricatorApplicationTransactionTransactionPHIDType::TYPECONST; $subtype = $this->getApplicationTransactionType(); return PhabricatorPHID::generateNewPHID($type, $subtype); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'oldValue' => self::SERIALIZATION_JSON, 'newValue' => self::SERIALIZATION_JSON, 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'commentPHID' => 'phid?', 'commentVersion' => 'uint32', 'contentSource' => 'text', 'transactionType' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( 'columns' => array('objectPHID'), ), ), ) + parent::getConfiguration(); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function hasComment() { return $this->getComment() && strlen($this->getComment()->getContent()); } public function getComment() { if ($this->commentNotLoaded) { throw new Exception('Comment for this transaction was not loaded.'); } return $this->comment; } public function attachComment( PhabricatorApplicationTransactionComment $comment) { $this->comment = $comment; $this->commentNotLoaded = false; return $this; } public function setCommentNotLoaded($not_loaded) { $this->commentNotLoaded = $not_loaded; return $this; } public function attachObject($object) { $this->object = $object; return $this; } public function getObject() { return $this->assertAttached($this->object); } public function getRemarkupBlocks() { $blocks = array(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { $custom_blocks = $field->getApplicationTransactionRemarkupBlocks( $this); foreach ($custom_blocks as $custom_block) { $blocks[] = $custom_block; } } break; } if ($this->getComment()) { $blocks[] = $this->getComment()->getContent(); } return $blocks; } public function setOldValue($value) { $this->oldValueHasBeenSet = true; $this->writeField('oldValue', $value); return $this; } public function hasOldValue() { return $this->oldValueHasBeenSet; } /* -( Rendering )---------------------------------------------------------- */ public function setRenderingTarget($rendering_target) { $this->renderingTarget = $rendering_target; return $this; } public function getRenderingTarget() { return $this->renderingTarget; } public function attachViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->assertAttached($this->viewer); } public function getRequiredHandlePHIDs() { $phids = array(); $old = $this->getOldValue(); $new = $this->getNewValue(); $phids[] = array($this->getAuthorPHID()); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { $phids[] = $field->getApplicationTransactionRequiredHandlePHIDs( $this); } break; case PhabricatorTransactions::TYPE_SUBSCRIBERS: $phids[] = $old; $phids[] = $new; break; case PhabricatorTransactions::TYPE_EDGE: $phids[] = ipull($old, 'dst'); $phids[] = ipull($new, 'dst'); break; case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: if (!PhabricatorPolicyQuery::isGlobalPolicy($old)) { $phids[] = array($old); } if (!PhabricatorPolicyQuery::isGlobalPolicy($new)) { $phids[] = array($new); } break; case PhabricatorTransactions::TYPE_TOKEN: break; case PhabricatorTransactions::TYPE_BUILDABLE: $phid = $this->getMetadataValue('harbormaster:buildablePHID'); if ($phid) { $phids[] = array($phid); } break; } if ($this->getComment()) { $phids[] = array($this->getComment()->getAuthorPHID()); } return array_mergev($phids); } public function setHandles(array $handles) { $this->handles = $handles; return $this; } public function getHandle($phid) { if (empty($this->handles[$phid])) { throw new Exception( pht( 'Transaction ("%s", of type "%s") requires a handle ("%s") that it '. 'did not load.', $this->getPHID(), $this->getTransactionType(), $phid)); } return $this->handles[$phid]; } public function getHandleIfExists($phid) { return idx($this->handles, $phid); } public function getHandles() { if ($this->handles === null) { throw new Exception( 'Transaction requires handles and it did not load them.' ); } return $this->handles; } public function renderHandleLink($phid) { if ($this->renderingTarget == self::TARGET_HTML) { return $this->getHandle($phid)->renderLink(); } else { return $this->getHandle($phid)->getLinkName(); } } public function renderHandleList(array $phids) { $links = array(); foreach ($phids as $phid) { $links[] = $this->renderHandleLink($phid); } if ($this->renderingTarget == self::TARGET_HTML) { return phutil_implode_html(', ', $links); } else { return implode(', ', $links); } } private function renderSubscriberList(array $phids, $change_type) { if ($this->getRenderingTarget() == self::TARGET_TEXT) { return $this->renderHandleList($phids); } else { $handles = array_select_keys($this->getHandles(), $phids); return id(new SubscriptionListStringBuilder()) ->setHandles($handles) ->setObjectPHID($this->getPHID()) ->buildTransactionString($change_type); } } protected function renderPolicyName($phid, $state = 'old') { $policy = PhabricatorPolicy::newFromPolicyAndHandle( $phid, $this->getHandleIfExists($phid)); if ($this->renderingTarget == self::TARGET_HTML) { switch ($policy->getType()) { case PhabricatorPolicyType::TYPE_CUSTOM: $policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/'); $policy->setWorkflow(true); break; default: break; } $output = $policy->renderDescription(); } else { $output = hsprintf('%s', $policy->getFullName()); } return $output; } public function getIcon() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $comment = $this->getComment(); if ($comment && $comment->getIsRemoved()) { return 'fa-eraser'; } return 'fa-comment'; case PhabricatorTransactions::TYPE_SUBSCRIBERS: return 'fa-envelope'; case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: return 'fa-lock'; case PhabricatorTransactions::TYPE_EDGE: return 'fa-link'; case PhabricatorTransactions::TYPE_BUILDABLE: return 'fa-wrench'; case PhabricatorTransactions::TYPE_TOKEN: return 'fa-trophy'; } return 'fa-pencil'; } public function getToken() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_TOKEN: $old = $this->getOldValue(); $new = $this->getNewValue(); if ($new) { $icon = substr($new, 10); } else { $icon = substr($old, 10); } return array($icon, !$this->getNewValue()); } return array(null, null); } public function getColor() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT; $comment = $this->getComment(); if ($comment && $comment->getIsRemoved()) { return 'black'; } break; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_PASSED: return 'green'; case HarbormasterBuildable::STATUS_FAILED: return 'red'; } break; } return null; } protected function getTransactionCustomField() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $key = $this->getMetadataValue('customfield:key'); if (!$key) { return null; } $field = PhabricatorCustomField::getObjectField( $this->getObject(), PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, $key); if (!$field) { return null; } $field->setViewer($this->getViewer()); return $field; } return null; } public function shouldHide() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: if ($this->getOldValue() === null) { return true; } else { return false; } break; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->shouldHideInApplicationTransactions($this); } case PhabricatorTransactions::TYPE_EDGE: $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: return true; break; case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST: $new = ipull($this->getNewValue(), 'dst'); $old = ipull($this->getOldValue(), 'dst'); $add = array_diff($new, $old); $add_value = reset($add); $add_handle = $this->getHandle($add_value); if ($add_handle->getPolicyFiltered()) { return true; } return false; break; default: break; } break; } return false; } public function shouldHideForMail(array $xactions) { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_TOKEN: return true; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_FAILED: // For now, only ever send mail when builds fail. We might let // you customize this later, but in most cases this is probably // completely uninteresting. return false; } return true; case PhabricatorTransactions::TYPE_EDGE: $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST: return true; break; default: break; } break; } // If a transaction publishes an inline comment: // // - Don't show it if there are other kinds of transactions. The // rationale here is that application mail will make the presence // of inline comments obvious enough by including them prominently // in the body. We could change this in the future if the obviousness // needs to be increased. // - If there are only inline transactions, only show the first // transaction. The rationale is that seeing multiple "added an inline // comment" transactions is not useful. if ($this->isInlineCommentTransaction()) { foreach ($xactions as $xaction) { if (!$xaction->isInlineCommentTransaction()) { return true; } } return ($this !== head($xactions)); } return $this->shouldHide(); } public function shouldHideForFeed() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_TOKEN: return true; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_FAILED: // For now, don't notify on build passes either. These are pretty // high volume and annoying, with very little present value. We // might want to turn them back on in the specific case of // build successes on the current document? return false; } return true; case PhabricatorTransactions::TYPE_EDGE: $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST: return true; break; default: break; } break; case PhabricatorTransactions::TYPE_INLINESTATE: return true; } return $this->shouldHide(); } public function getTitleForMail() { return id(clone $this)->setRenderingTarget('text')->getTitle(); } public function getBodyForMail() { if ($this->isInlineCommentTransaction()) { // We don't return inline comment content as mail body content, because // applications need to contextualize it (by adding line numbers, for // example) in order for it to make sense. return null; } $comment = $this->getComment(); if ($comment && strlen($comment->getContent())) { return $comment->getContent(); } return null; } public function getNoEffectDescription() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht('You can not post an empty comment.'); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( 'This %s already has that view policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( 'This %s already has that edit policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( 'This %s already has that join policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht( 'All users are already subscribed to this %s.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_EDGE: return pht('Edges already exist; transaction has no effect.'); } return pht('Transaction has no effect.'); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht( '%s added a comment.', $this->renderHandleLink($author_phid)); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( '%s changed the visibility of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( '%s changed the edit policy of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( '%s changed the join policy of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old, 'old'), $this->renderPolicyName($new, 'new')); case PhabricatorTransactions::TYPE_SUBSCRIBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s edited subscriber(s), added %d: %s; removed %d: %s.', $this->renderHandleLink($author_phid), count($add), $this->renderSubscriberList($add, 'add'), count($rem), $this->renderSubscriberList($rem, 'rem')); } else if ($add) { return pht( '%s added %d subscriber(s): %s.', $this->renderHandleLink($author_phid), count($add), $this->renderSubscriberList($add, 'add')); } else if ($rem) { return pht( '%s removed %d subscriber(s): %s.', $this->renderHandleLink($author_phid), count($rem), $this->renderSubscriberList($rem, 'rem')); } else { // This is used when rendering previews, before the user actually // selects any CCs. return pht( '%s updated subscribers...', $this->renderHandleLink($author_phid)); } break; case PhabricatorTransactions::TYPE_EDGE: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); $add = array_diff($new, $old); $rem = array_diff($old, $new); $type = $this->getMetadata('edge:type'); $type = head($type); $type_obj = PhabricatorEdgeType::getByConstant($type); if ($add && $rem) { return $type_obj->getTransactionEditString( $this->renderHandleLink($author_phid), new PhutilNumber(count($add) + count($rem)), new PhutilNumber(count($add)), $this->renderHandleList($add), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else if ($add) { return $type_obj->getTransactionAddString( $this->renderHandleLink($author_phid), new PhutilNumber(count($add)), $this->renderHandleList($add)); } else if ($rem) { return $type_obj->getTransactionRemoveString( $this->renderHandleLink($author_phid), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else { return $type_obj->getTransactionPreviewString( $this->renderHandleLink($author_phid)); } case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionTitle($this); } else { return pht( '%s edited a custom field.', $this->renderHandleLink($author_phid)); } case PhabricatorTransactions::TYPE_TOKEN: if ($old && $new) { return pht( '%s updated a token.', $this->renderHandleLink($author_phid)); } else if ($old) { return pht( '%s rescinded a token.', $this->renderHandleLink($author_phid)); } else { return pht( '%s awarded a token.', $this->renderHandleLink($author_phid)); } case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_BUILDING: return pht( '%s started building %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); case HarbormasterBuildable::STATUS_PASSED: return pht( '%s completed building %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); case HarbormasterBuildable::STATUS_FAILED: return pht( '%s failed to build %s!', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID'))); default: return null; } case PhabricatorTransactions::TYPE_INLINESTATE: $done = 0; $undone = 0; foreach ($new as $phid => $state) { if ($state == PhabricatorInlineCommentInterface::STATE_DONE) { $done++; } else { $undone++; } } if ($done && $undone) { return pht( '%s marked %s inline comment(s) as done and %s inline comment(s) '. 'as not done.', $this->renderHandleLink($author_phid), new PhutilNumber($done), new PhutilNumber($undone)); } else if ($done) { return pht( '%s marked %s inline comment(s) as done.', $this->renderHandleLink($author_phid), new PhutilNumber($done)); } else { return pht( '%s marked %s inline comment(s) as not done.', $this->renderHandleLink($author_phid), new PhutilNumber($undone)); } break; default: return pht( '%s edited this %s.', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName()); } } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht( '%s added a comment to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( '%s changed the visibility for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( '%s changed the edit policy for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( '%s changed the join policy for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht( '%s updated subscribers of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_EDGE: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); $add = array_diff($new, $old); $rem = array_diff($old, $new); $type = $this->getMetadata('edge:type'); $type = head($type); $type_obj = PhabricatorEdgeType::getByConstant($type); if ($add && $rem) { return $type_obj->getFeedEditString( $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), new PhutilNumber(count($add) + count($rem)), new PhutilNumber(count($add)), $this->renderHandleList($add), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else if ($add) { return $type_obj->getFeedAddString( $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), new PhutilNumber(count($add)), $this->renderHandleList($add)); } else if ($rem) { return $type_obj->getFeedRemoveString( $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), new PhutilNumber(count($rem)), $this->renderHandleList($rem)); } else { return pht( '%s edited edge metadata for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionTitleForFeed($this); } else { return pht( '%s edited a custom field on %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_BUILDING: return pht( '%s started building %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); case HarbormasterBuildable::STATUS_PASSED: return pht( '%s completed building %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); case HarbormasterBuildable::STATUS_FAILED: return pht( '%s failed to build %s for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('harbormaster:buildablePHID')), $this->renderHandleLink($object_phid)); default: return null; } } return $this->getTitle(); } public function getMarkupFieldsForFeed(PhabricatorFeedStory $story) { $fields = array(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $text = $this->getComment()->getContent(); if (strlen($text)) { $fields[] = 'comment/'.$this->getID(); } break; } return $fields; } public function getMarkupTextForFeed(PhabricatorFeedStory $story, $field) { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $text = $this->getComment()->getContent(); return PhabricatorMarkupEngine::summarize($text); } return null; } public function getBodyForFeed(PhabricatorFeedStory $story) { $old = $this->getOldValue(); $new = $this->getNewValue(); $body = null; switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $text = $this->getComment()->getContent(); if (strlen($text)) { $body = $story->getMarkupFieldOutput('comment/'.$this->getID()); } break; } return $body; } public function getActionStrength() { if ($this->isInlineCommentTransaction()) { return 0.25; } switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return 0.5; case PhabricatorTransactions::TYPE_SUBSCRIBERS: $old = $this->getOldValue(); $new = $this->getNewValue(); $add = array_diff($old, $new); $rem = array_diff($new, $old); // If this action is the actor subscribing or unsubscribing themselves, // it is less interesting. In particular, if someone makes a comment and // also implicitly subscribes themselves, we should treat the // transaction group as "comment", not "subscribe". In this specific // case (one affected user, and that affected user it the actor), // decrease the action strength. if ((count($add) + count($rem)) != 1) { // Not exactly one CC change. break; } $affected_phid = head(array_merge($add, $rem)); if ($affected_phid != $this->getAuthorPHID()) { // Affected user is someone else. break; } // Make this weaker than TYPE_COMMENT. return 0.25; } return 1.0; } public function isCommentTransaction() { if ($this->hasComment()) { return true; } switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return true; } return false; } public function isInlineCommentTransaction() { return false; } public function getActionName() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht('Commented On'); case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht('Changed Policy'); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht('Changed Subscribers'); case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_PASSED: return pht('Build Passed'); case HarbormasterBuildable::STATUS_FAILED: return pht('Build Failed'); default: return pht('Build Status'); } default: return pht('Updated'); } } public function getMailTags() { return array(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionHasChangeDetails($this); } break; } return false; } public function renderChangeDetails(PhabricatorUser $viewer) { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_CUSTOMFIELD: $field = $this->getTransactionCustomField(); if ($field) { return $field->getApplicationTransactionChangeDetails($this, $viewer); } break; } return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } public function renderTextCorpusChangeDetails( PhabricatorUser $viewer, $old, $new) { require_celerity_resource('differential-changeset-view-css'); $view = id(new PhabricatorApplicationTransactionTextDiffDetailView()) ->setUser($viewer) ->setOldText($old) ->setNewText($new); return $view->render(); } public function attachTransactionGroup(array $group) { - assert_instances_of($group, 'PhabricatorApplicationTransaction'); + assert_instances_of($group, __CLASS__); $this->transactionGroup = $group; return $this; } public function getTransactionGroup() { return $this->transactionGroup; } /** * Should this transaction be visually grouped with an existing transaction * group? * * @param list<PhabricatorApplicationTransaction> List of transactions. * @return bool True to display in a group with the other transactions. */ public function shouldDisplayGroupWith(array $group) { $this_source = null; if ($this->getContentSource()) { $this_source = $this->getContentSource()->getSource(); } foreach ($group as $xaction) { // Don't group transactions by different authors. if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) { return false; } // Don't group transactions for different objects. if ($xaction->getObjectPHID() != $this->getObjectPHID()) { return false; } // Don't group anything into a group which already has a comment. if ($xaction->isCommentTransaction()) { return false; } // Don't group transactions from different content sources. $other_source = null; if ($xaction->getContentSource()) { $other_source = $xaction->getContentSource()->getSource(); } if ($other_source != $this_source) { return false; } // Don't group transactions which happened more than 2 minutes apart. $apart = abs($xaction->getDateCreated() - $this->getDateCreated()); if ($apart > (60 * 2)) { return false; } } return true; } public function renderExtraInformationLink() { $herald_xscript_id = $this->getMetadataValue('herald:transcriptID'); if ($herald_xscript_id) { return phutil_tag( 'a', array( 'href' => '/herald/transcript/'.$herald_xscript_id.'/', ), pht('View Herald Transcript')); } return null; } public function renderAsTextForDoorkeeper( DoorkeeperFeedStoryPublisher $publisher, PhabricatorFeedStory $story, array $xactions) { $text = array(); $body = array(); foreach ($xactions as $xaction) { $xaction_body = $xaction->getBodyForMail(); if ($xaction_body !== null) { $body[] = $xaction_body; } if ($xaction->shouldHideForMail($xactions)) { continue; } $old_target = $xaction->getRenderingTarget(); - $new_target = PhabricatorApplicationTransaction::TARGET_TEXT; + $new_target = self::TARGET_TEXT; $xaction->setRenderingTarget($new_target); if ($publisher->getRenderWithImpliedContext()) { $text[] = $xaction->getTitle(); } else { $text[] = $xaction->getTitleForFeed(); } $xaction->setRenderingTarget($old_target); } $text = implode("\n", $text); $body = implode("\n\n", $body); return rtrim($text."\n\n".$body); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht( 'Transactions are visible to users that can see the object which was '. 'acted upon. Some transactions - in particular, comments - are '. 'editable by the transaction author.'); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $comment_template = null; try { $comment_template = $this->getApplicationTransactionCommentObject(); } catch (Exception $ex) { // Continue; no comments for these transactions. } if ($comment_template) { $comments = $comment_template->loadAllWhere( 'transactionPHID = %s', $this->getPHID()); foreach ($comments as $comment) { $engine->destroyObject($comment); } } $this->delete(); $this->saveTransaction(); } } diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index ec5a02337e..28110e7334 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -1,1381 +1,1381 @@ <?php /** * @task apps Building Applications with Custom Fields * @task core Core Properties and Field Identity * @task proxy Field Proxies * @task context Contextual Data * @task render Rendering Utilities * @task storage Field Storage * @task edit Integration with Edit Views * @task view Integration with Property Views * @task list Integration with List views * @task appsearch Integration with ApplicationSearch * @task appxaction Integration with ApplicationTransactions * @task xactionmail Integration with Transaction Mail * @task globalsearch Integration with Global Search * @task herald Integration with Herald */ abstract class PhabricatorCustomField { private $viewer; private $object; private $proxy; const ROLE_APPLICATIONTRANSACTIONS = 'ApplicationTransactions'; const ROLE_TRANSACTIONMAIL = 'ApplicationTransactions.mail'; const ROLE_APPLICATIONSEARCH = 'ApplicationSearch'; const ROLE_STORAGE = 'storage'; const ROLE_DEFAULT = 'default'; const ROLE_EDIT = 'edit'; const ROLE_VIEW = 'view'; const ROLE_LIST = 'list'; const ROLE_GLOBALSEARCH = 'GlobalSearch'; const ROLE_CONDUIT = 'conduit'; const ROLE_HERALD = 'herald'; /* -( Building Applications with Custom Fields )--------------------------- */ /** * @task apps */ public static function getObjectFields( PhabricatorCustomFieldInterface $object, $role) { try { $attachment = $object->getCustomFields(); } catch (PhabricatorDataNotAttachedException $ex) { $attachment = new PhabricatorCustomFieldAttachment(); $object->attachCustomFields($attachment); } try { $field_list = $attachment->getCustomFieldList($role); } catch (PhabricatorCustomFieldNotAttachedException $ex) { $base_class = $object->getCustomFieldBaseClass(); $spec = $object->getCustomFieldSpecificationForRole($role); if (!is_array($spec)) { $obj_class = get_class($object); throw new Exception( "Expected an array from getCustomFieldSpecificationForRole() for ". "object of class '{$obj_class}'."); } - $fields = PhabricatorCustomField::buildFieldList( + $fields = self::buildFieldList( $base_class, $spec, $object); foreach ($fields as $key => $field) { if (!$field->shouldEnableForRole($role)) { unset($fields[$key]); } } foreach ($fields as $field) { $field->setObject($object); } $field_list = new PhabricatorCustomFieldList($fields); $attachment->addCustomFieldList($role, $field_list); } return $field_list; } /** * @task apps */ public static function getObjectField( PhabricatorCustomFieldInterface $object, $role, $field_key) { $fields = self::getObjectFields($object, $role)->getFields(); return idx($fields, $field_key); } /** * @task apps */ public static function buildFieldList($base_class, array $spec, $object) { $field_objects = id(new PhutilSymbolLoader()) ->setAncestorClass($base_class) ->loadObjects(); $fields = array(); $from_map = array(); foreach ($field_objects as $field_object) { $current_class = get_class($field_object); foreach ($field_object->createFields($object) as $field) { $key = $field->getFieldKey(); if (isset($fields[$key])) { $original_class = $from_map[$key]; throw new Exception( "Both '{$original_class}' and '{$current_class}' define a custom ". "field with field key '{$key}'. Field keys must be unique."); } $from_map[$key] = $current_class; $fields[$key] = $field; } } foreach ($fields as $key => $field) { if (!$field->isFieldEnabled()) { unset($fields[$key]); } } $fields = array_select_keys($fields, array_keys($spec)) + $fields; foreach ($spec as $key => $config) { if (empty($fields[$key])) { continue; } if (!empty($config['disabled'])) { if ($fields[$key]->canDisableField()) { unset($fields[$key]); } } } return $fields; } /* -( Core Properties and Field Identity )--------------------------------- */ /** * Return a key which uniquely identifies this field, like * "mycompany:dinosaur:count". Normally you should provide some level of * namespacing to prevent collisions. * * @return string String which uniquely identifies this field. * @task core */ public function getFieldKey() { if ($this->proxy) { return $this->proxy->getFieldKey(); } throw new PhabricatorCustomFieldImplementationIncompleteException( $this, $field_key_is_incomplete = true); } /** * Return a human-readable field name. * * @return string Human readable field name. * @task core */ public function getFieldName() { if ($this->proxy) { return $this->proxy->getFieldName(); } return $this->getFieldKey(); } /** * Return a short, human-readable description of the field's behavior. This * provides more context to administrators when they are customizing fields. * * @return string|null Optional human-readable description. * @task core */ public function getFieldDescription() { if ($this->proxy) { return $this->proxy->getFieldDescription(); } return null; } /** * Most field implementations are unique, in that one class corresponds to * one field. However, some field implementations are general and a single * implementation may drive several fields. * * For general implementations, the general field implementation can return * multiple field instances here. * * @param object The object to create fields for. * @return list<PhabricatorCustomField> List of fields. * @task core */ public function createFields($object) { return array($this); } /** * You can return `false` here if the field should not be enabled for any * role. For example, it might depend on something (like an application or * library) which isn't installed, or might have some global configuration * which allows it to be disabled. * * @return bool False to completely disable this field for all roles. * @task core */ public function isFieldEnabled() { if ($this->proxy) { return $this->proxy->isFieldEnabled(); } return true; } /** * Low level selector for field availability. Fields can appear in different * roles (like an edit view, a list view, etc.), but not every field needs * to appear everywhere. Fields that are disabled in a role won't appear in * that context within applications. * * Normally, you do not need to override this method. Instead, override the * methods specific to roles you want to enable. For example, implement * @{method:shouldUseStorage()} to activate the `'storage'` role. * * @return bool True to enable the field for the given role. * @task core */ public function shouldEnableForRole($role) { // NOTE: All of these calls proxy individually, so we don't need to // proxy this call as a whole. switch ($role) { case self::ROLE_APPLICATIONTRANSACTIONS: return $this->shouldAppearInApplicationTransactions(); case self::ROLE_APPLICATIONSEARCH: return $this->shouldAppearInApplicationSearch(); case self::ROLE_STORAGE: return $this->shouldUseStorage(); case self::ROLE_EDIT: return $this->shouldAppearInEditView(); case self::ROLE_VIEW: return $this->shouldAppearInPropertyView(); case self::ROLE_LIST: return $this->shouldAppearInListView(); case self::ROLE_GLOBALSEARCH: return $this->shouldAppearInGlobalSearch(); case self::ROLE_CONDUIT: return $this->shouldAppearInConduitDictionary(); case self::ROLE_TRANSACTIONMAIL: return $this->shouldAppearInTransactionMail(); case self::ROLE_HERALD: return $this->shouldAppearInHerald(); case self::ROLE_DEFAULT: return true; default: throw new Exception("Unknown field role '{$role}'!"); } } /** * Allow administrators to disable this field. Most fields should allow this, * but some are fundamental to the behavior of the application and can be * locked down to avoid chaos, disorder, and the decline of civilization. * * @return bool False to prevent this field from being disabled through * configuration. * @task core */ public function canDisableField() { return true; } public function shouldDisableByDefault() { return false; } /** * Return an index string which uniquely identifies this field. * * @return string Index string which uniquely identifies this field. * @task core */ final public function getFieldIndex() { return PhabricatorHash::digestForIndex($this->getFieldKey()); } /* -( Field Proxies )------------------------------------------------------ */ /** * Proxies allow a field to use some other field's implementation for most * of their behavior while still subclassing an application field. When a * proxy is set for a field with @{method:setProxy}, all of its methods will * call through to the proxy by default. * * This is most commonly used to implement configuration-driven custom fields * using @{class:PhabricatorStandardCustomField}. * * This method must be overridden to return `true` before a field can accept * proxies. * * @return bool True if you can @{method:setProxy} this field. * @task proxy */ public function canSetProxy() { if ($this instanceof PhabricatorStandardCustomFieldInterface) { return true; } return false; } /** * Set the proxy implementation for this field. See @{method:canSetProxy} for * discussion of field proxies. * * @param PhabricatorCustomField Field implementation. * @return this */ final public function setProxy(PhabricatorCustomField $proxy) { if (!$this->canSetProxy()) { throw new PhabricatorCustomFieldNotProxyException($this); } $this->proxy = $proxy; return $this; } /** * Get the field's proxy implementation, if any. For discussion, see * @{method:canSetProxy}. * * @return PhabricatorCustomField|null Proxy field, if one is set. */ final public function getProxy() { return $this->proxy; } /* -( Contextual Data )---------------------------------------------------- */ /** * Sets the object this field belongs to. * * @param PhabricatorCustomFieldInterface The object this field belongs to. * @return this * @task context */ final public function setObject(PhabricatorCustomFieldInterface $object) { if ($this->proxy) { $this->proxy->setObject($object); return $this; } $this->object = $object; $this->didSetObject($object); return $this; } /** * Read object data into local field storage, if applicable. * * @param PhabricatorCustomFieldInterface The object this field belongs to. * @return this * @task context */ public function readValueFromObject(PhabricatorCustomFieldInterface $object) { if ($this->proxy) { $this->proxy->readValueFromObject($object); } return $this; } /** * Get the object this field belongs to. * * @return PhabricatorCustomFieldInterface The object this field belongs to. * @task context */ final public function getObject() { if ($this->proxy) { return $this->proxy->getObject(); } return $this->object; } /** * This is a hook, primarily for subclasses to load object data. * * @return PhabricatorCustomFieldInterface The object this field belongs to. * @return void */ protected function didSetObject(PhabricatorCustomFieldInterface $object) { return; } /** * @task context */ final public function setViewer(PhabricatorUser $viewer) { if ($this->proxy) { $this->proxy->setViewer($viewer); return $this; } $this->viewer = $viewer; return $this; } /** * @task context */ final public function getViewer() { if ($this->proxy) { return $this->proxy->getViewer(); } return $this->viewer; } /** * @task context */ final protected function requireViewer() { if ($this->proxy) { return $this->proxy->requireViewer(); } if (!$this->viewer) { throw new PhabricatorCustomFieldDataNotAvailableException($this); } return $this->viewer; } /* -( Rendering Utilities )------------------------------------------------ */ /** * @task render */ protected function renderHandleList(array $handles) { if (!$handles) { return null; } $out = array(); foreach ($handles as $handle) { $out[] = $handle->renderLink(); } return phutil_implode_html(phutil_tag('br'), $out); } /* -( Storage )------------------------------------------------------------ */ /** * Return true to use field storage. * * Fields which can be edited by the user will most commonly use storage, * while some other types of fields (for instance, those which just display * information in some stylized way) may not. Many builtin fields do not use * storage because their data is available on the object itself. * * If you implement this, you must also implement @{method:getValueForStorage} * and @{method:setValueFromStorage}. * * @return bool True to use storage. * @task storage */ public function shouldUseStorage() { if ($this->proxy) { return $this->proxy->shouldUseStorage(); } return false; } /** * Return a new, empty storage object. This should be a subclass of * @{class:PhabricatorCustomFieldStorage} which is bound to the application's * database. * * @return PhabricatorCustomFieldStorage New empty storage object. * @task storage */ public function newStorageObject() { if ($this->proxy) { return $this->proxy->newStorageObject(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Return a serialized representation of the field value, appropriate for * storing in auxiliary field storage. You must implement this method if * you implement @{method:shouldUseStorage}. * * If the field value is a scalar, it can be returned unmodiifed. If not, * it should be serialized (for example, using JSON). * * @return string Serialized field value. * @task storage */ public function getValueForStorage() { if ($this->proxy) { return $this->proxy->getValueForStorage(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Set the field's value given a serialized storage value. This is called * when the field is loaded; if no data is available, the value will be * null. You must implement this method if you implement * @{method:shouldUseStorage}. * * Usually, the value can be loaded directly. If it isn't a scalar, you'll * need to undo whatever serialization you applied in * @{method:getValueForStorage}. * * @param string|null Serialized field representation (from * @{method:getValueForStorage}) or null if no value has * ever been stored. * @return this * @task storage */ public function setValueFromStorage($value) { if ($this->proxy) { return $this->proxy->setValueFromStorage($value); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( ApplicationSearch )-------------------------------------------------- */ /** * Appearing in ApplicationSearch allows a field to be indexed and searched * for. * * @return bool True to appear in ApplicationSearch. * @task appsearch */ public function shouldAppearInApplicationSearch() { if ($this->proxy) { return $this->proxy->shouldAppearInApplicationSearch(); } return false; } /** * Return one or more indexes which this field can meaningfully query against * to implement ApplicationSearch. * * Normally, you should build these using @{method:newStringIndex} and * @{method:newNumericIndex}. For example, if a field holds a numeric value * it might return a single numeric index: * * return array($this->newNumericIndex($this->getValue())); * * If a field holds a more complex value (like a list of users), it might * return several string indexes: * * $indexes = array(); * foreach ($this->getValue() as $phid) { * $indexes[] = $this->newStringIndex($phid); * } * return $indexes; * * @return list<PhabricatorCustomFieldIndexStorage> List of indexes. * @task appsearch */ public function buildFieldIndexes() { if ($this->proxy) { return $this->proxy->buildFieldIndexes(); } return array(); } /** * Return an index against which this field can be meaningfully ordered * against to implement ApplicationSearch. * * This should be a single index, normally built using * @{method:newStringIndex} and @{method:newNumericIndex}. * * The value of the index is not used. * * Return null from this method if the field can not be ordered. * * @return PhabricatorCustomFieldIndexStorage A single index to order by. * @task appsearch */ public function buildOrderIndex() { if ($this->proxy) { return $this->proxy->buildOrderIndex(); } return null; } /** * Build a new empty storage object for storing string indexes. Normally, * this should be a concrete subclass of * @{class:PhabricatorCustomFieldStringIndexStorage}. * * @return PhabricatorCustomFieldStringIndexStorage Storage object. * @task appsearch */ protected function newStringIndexStorage() { // NOTE: This intentionally isn't proxied, to avoid call cycles. throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Build a new empty storage object for storing string indexes. Normally, * this should be a concrete subclass of * @{class:PhabricatorCustomFieldStringIndexStorage}. * * @return PhabricatorCustomFieldStringIndexStorage Storage object. * @task appsearch */ protected function newNumericIndexStorage() { // NOTE: This intentionally isn't proxied, to avoid call cycles. throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Build and populate storage for a string index. * * @param string String to index. * @return PhabricatorCustomFieldStringIndexStorage Populated storage. * @task appsearch */ protected function newStringIndex($value) { if ($this->proxy) { return $this->proxy->newStringIndex(); } $key = $this->getFieldIndex(); return $this->newStringIndexStorage() ->setIndexKey($key) ->setIndexValue($value); } /** * Build and populate storage for a numeric index. * * @param string Numeric value to index. * @return PhabricatorCustomFieldNumericIndexStorage Populated storage. * @task appsearch */ protected function newNumericIndex($value) { if ($this->proxy) { return $this->proxy->newNumericIndex(); } $key = $this->getFieldIndex(); return $this->newNumericIndexStorage() ->setIndexKey($key) ->setIndexValue($value); } /** * Read a query value from a request, for storage in a saved query. Normally, * this method should, e.g., read a string out of the request. * * @param PhabricatorApplicationSearchEngine Engine building the query. * @param AphrontRequest Request to read from. * @return wild * @task appsearch */ public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { if ($this->proxy) { return $this->proxy->readApplicationSearchValueFromRequest( $engine, $request); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Constrain a query, given a field value. Generally, this method should * use `with...()` methods to apply filters or other constraints to the * query. * * @param PhabricatorApplicationSearchEngine Engine executing the query. * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. * @param wild Constraint provided by the user. * @return void * @task appsearch */ public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { if ($this->proxy) { return $this->proxy->applyApplicationSearchConstraintToQuery( $engine, $query, $value); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Append search controls to the interface. If you need handles, use * @{method:getRequiredHandlePHIDsForApplicationSearch} to get them. * * @param PhabricatorApplicationSearchEngine Engine constructing the form. * @param AphrontFormView The form to update. * @param wild Value from the saved query. * @param list<PhabricatorObjectHandle> List of handles. * @return void * @task appsearch */ public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, $value, array $handles) { if ($this->proxy) { return $this->proxy->appendToApplicationSearchForm( $engine, $form, $value, $handles); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Return a list of PHIDs which @{method:appendToApplicationSearchForm} needs * handles for. This is primarily useful if the field stores PHIDs and you * need to (for example) render a tokenizer control. * * @param wild Value from the saved query. * @return list<phid> List of PHIDs. * @task appsearch */ public function getRequiredHandlePHIDsForApplicationSearch($value) { if ($this->proxy) { return $this->proxy->getRequiredHandlePHIDsForApplicationSearch($value); } return array(); } /* -( ApplicationTransactions )-------------------------------------------- */ /** * Appearing in ApplicationTrasactions allows a field to be edited using * standard workflows. * * @return bool True to appear in ApplicationTransactions. * @task appxaction */ public function shouldAppearInApplicationTransactions() { if ($this->proxy) { return $this->proxy->shouldAppearInApplicationTransactions(); } return false; } /** * @task appxaction */ public function getApplicationTransactionType() { if ($this->proxy) { return $this->proxy->getApplicationTransactionType(); } return PhabricatorTransactions::TYPE_CUSTOMFIELD; } /** * @task appxaction */ public function getApplicationTransactionMetadata() { if ($this->proxy) { return $this->proxy->getApplicationTransactionMetadata(); } return array(); } /** * @task appxaction */ public function getOldValueForApplicationTransactions() { if ($this->proxy) { return $this->proxy->getOldValueForApplicationTransactions(); } return $this->getValueForStorage(); } /** * @task appxaction */ public function getNewValueForApplicationTransactions() { if ($this->proxy) { return $this->proxy->getNewValueForApplicationTransactions(); } return $this->getValueForStorage(); } /** * @task appxaction */ public function setValueFromApplicationTransactions($value) { if ($this->proxy) { return $this->proxy->setValueFromApplicationTransactions($value); } return $this->setValueFromStorage($value); } /** * @task appxaction */ public function getNewValueFromApplicationTransactions( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getNewValueFromApplicationTransactions($xaction); } return $xaction->getNewValue(); } /** * @task appxaction */ public function getApplicationTransactionHasEffect( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionHasEffect($xaction); } return ($xaction->getOldValue() !== $xaction->getNewValue()); } /** * @task appxaction */ public function applyApplicationTransactionInternalEffects( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->applyApplicationTransactionInternalEffects($xaction); } return; } /** * @task appxaction */ public function getApplicationTransactionRemarkupBlocks( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionRemarkupBlocks($xaction); } return array(); } /** * @task appxaction */ public function applyApplicationTransactionExternalEffects( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->applyApplicationTransactionExternalEffects($xaction); } if (!$this->shouldEnableForRole(self::ROLE_STORAGE)) { return; } $this->setValueFromApplicationTransactions($xaction->getNewValue()); $value = $this->getValueForStorage(); $table = $this->newStorageObject(); $conn_w = $table->establishConnection('w'); if ($value === null) { queryfx( $conn_w, 'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex = %s', $table->getTableName(), $this->getObject()->getPHID(), $this->getFieldIndex()); } else { queryfx( $conn_w, 'INSERT INTO %T (objectPHID, fieldIndex, fieldValue) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)', $table->getTableName(), $this->getObject()->getPHID(), $this->getFieldIndex(), $value); } return; } /** * Validate transactions for an object. This allows you to raise an error * when a transaction would set a field to an invalid value, or when a field * is required but no transactions provide value. * * @param PhabricatorLiskDAO Editor applying the transactions. * @param string Transaction type. This type is always * `PhabricatorTransactions::TYPE_CUSTOMFIELD`, it is provided for * convenience when constructing exceptions. * @param list<PhabricatorApplicationTransaction> Transactions being applied, * which may be empty if this field is not being edited. * @return list<PhabricatorApplicationTransactionValidationError> Validation * errors. * * @task appxaction */ public function validateApplicationTransactions( PhabricatorApplicationTransactionEditor $editor, $type, array $xactions) { if ($this->proxy) { return $this->proxy->validateApplicationTransactions( $editor, $type, $xactions); } return array(); } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionTitle( $xaction); } $author_phid = $xaction->getAuthorPHID(); return pht( '%s updated this object.', $xaction->renderHandleLink($author_phid)); } public function getApplicationTransactionTitleForFeed( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionTitleForFeed( $xaction); } $author_phid = $xaction->getAuthorPHID(); $object_phid = $xaction->getObjectPHID(); return pht( '%s updated %s.', $xaction->renderHandleLink($author_phid), $xaction->renderHandleLink($object_phid)); } public function getApplicationTransactionHasChangeDetails( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionHasChangeDetails( $xaction); } return false; } public function getApplicationTransactionChangeDetails( PhabricatorApplicationTransaction $xaction, PhabricatorUser $viewer) { if ($this->proxy) { return $this->proxy->getApplicationTransactionChangeDetails( $xaction, $viewer); } return null; } public function getApplicationTransactionRequiredHandlePHIDs( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->getApplicationTransactionRequiredHandlePHIDs( $xaction); } return array(); } public function shouldHideInApplicationTransactions( PhabricatorApplicationTransaction $xaction) { if ($this->proxy) { return $this->proxy->shouldHideInApplicationTransactions($xaction); } return false; } /** * TODO: this is only used by Diffusion right now and everything is completely * faked since Diffusion doesn't use ApplicationTransactions yet. This should * get fleshed out as we have more use cases. * * @task appxaction */ public function buildApplicationTransactionMailBody( PhabricatorApplicationTransaction $xaction, PhabricatorMetaMTAMailBody $body) { if ($this->proxy) { return $this->proxy->buildApplicationTransactionMailBody($xaction, $body); } return; } /* -( Transaction Mail )--------------------------------------------------- */ /** * @task xactionmail */ public function shouldAppearInTransactionMail() { if ($this->proxy) { return $this->proxy->shouldAppearInTransactionMail(); } return false; } /** * @task xactionmail */ public function updateTransactionMailBody( PhabricatorMetaMTAMailBody $body, PhabricatorApplicationTransactionEditor $editor, array $xactions) { if ($this->proxy) { return $this->proxy->updateTransactionMailBody($body, $editor, $xactions); } return; } /* -( Edit View )---------------------------------------------------------- */ /** * @task edit */ public function shouldAppearInEditView() { if ($this->proxy) { return $this->proxy->shouldAppearInEditView(); } return false; } /** * @task edit */ public function readValueFromRequest(AphrontRequest $request) { if ($this->proxy) { return $this->proxy->readValueFromRequest($request); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * @task edit */ public function getRequiredHandlePHIDsForEdit() { if ($this->proxy) { return $this->proxy->getRequiredHandlePHIDsForEdit(); } return array(); } /** * @task edit */ public function getInstructionsForEdit() { if ($this->proxy) { return $this->proxy->getInstructionsForEdit(); } return null; } /** * @task edit */ public function renderEditControl(array $handles) { if ($this->proxy) { return $this->proxy->renderEditControl($handles); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( Property View )------------------------------------------------------ */ /** * @task view */ public function shouldAppearInPropertyView() { if ($this->proxy) { return $this->proxy->shouldAppearInPropertyView(); } return false; } /** * @task view */ public function renderPropertyViewLabel() { if ($this->proxy) { return $this->proxy->renderPropertyViewLabel(); } return $this->getFieldName(); } /** * @task view */ public function renderPropertyViewValue(array $handles) { if ($this->proxy) { return $this->proxy->renderPropertyViewValue($handles); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * @task view */ public function getStyleForPropertyView() { if ($this->proxy) { return $this->proxy->getStyleForPropertyView(); } return 'property'; } /** * @task view */ public function getIconForPropertyView() { if ($this->proxy) { return $this->proxy->getIconForPropertyView(); } return null; } /** * @task view */ public function getRequiredHandlePHIDsForPropertyView() { if ($this->proxy) { return $this->proxy->getRequiredHandlePHIDsForPropertyView(); } return array(); } /* -( List View )---------------------------------------------------------- */ /** * @task list */ public function shouldAppearInListView() { if ($this->proxy) { return $this->proxy->shouldAppearInListView(); } return false; } /** * @task list */ public function renderOnListItem(PHUIObjectItemView $view) { if ($this->proxy) { return $this->proxy->renderOnListItem($view); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( Global Search )------------------------------------------------------ */ /** * @task globalsearch */ public function shouldAppearInGlobalSearch() { if ($this->proxy) { return $this->proxy->shouldAppearInGlobalSearch(); } return false; } /** * @task globalsearch */ public function updateAbstractDocument( PhabricatorSearchAbstractDocument $document) { if ($this->proxy) { return $this->proxy->updateAbstractDocument($document); } return $document; } /* -( Conduit )------------------------------------------------------------ */ /** * @task conduit */ public function shouldAppearInConduitDictionary() { if ($this->proxy) { return $this->proxy->shouldAppearInConduitDictionary(); } return false; } /** * @task conduit */ public function getConduitDictionaryValue() { if ($this->proxy) { return $this->proxy->getConduitDictionaryValue(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /* -( Herald )------------------------------------------------------------- */ /** * Return `true` to make this field available in Herald. * * @return bool True to expose the field in Herald. * @task herald */ public function shouldAppearInHerald() { if ($this->proxy) { return $this->proxy->shouldAppearInHerald(); } return false; } /** * Get the name of the field in Herald. By default, this uses the * normal field name. * * @return string Herald field name. * @task herald */ public function getHeraldFieldName() { if ($this->proxy) { return $this->proxy->getHeraldFieldName(); } return $this->getFieldName(); } /** * Get the field value for evaluation by Herald. * * @return wild Field value. * @task herald */ public function getHeraldFieldValue() { if ($this->proxy) { return $this->proxy->getHeraldFieldValue(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Get the available conditions for this field in Herald. * * @return list<const> List of Herald condition constants. * @task herald */ public function getHeraldFieldConditions() { if ($this->proxy) { return $this->proxy->getHeraldFieldConditions(); } throw new PhabricatorCustomFieldImplementationIncompleteException($this); } /** * Get the Herald value type for the given condition. * * @param const Herald condition constant. * @return const|null Herald value type, or null to use the default. * @task herald */ public function getHeraldFieldValueType($condition) { if ($this->proxy) { return $this->proxy->getHeraldFieldValueType($condition); } return null; } } diff --git a/src/infrastructure/daemon/bot/PhabricatorBot.php b/src/infrastructure/daemon/bot/PhabricatorBot.php index 18a0c74648..ef949cb9ff 100644 --- a/src/infrastructure/daemon/bot/PhabricatorBot.php +++ b/src/infrastructure/daemon/bot/PhabricatorBot.php @@ -1,160 +1,164 @@ <?php /** * Simple IRC bot which runs as a Phabricator daemon. Although this bot is * somewhat useful, it is also intended to serve as a demo of how to write * "system agents" which communicate with Phabricator over Conduit, so you can * script system interactions and integrate with other systems. * * NOTE: This is super janky and experimental right now. */ final class PhabricatorBot extends PhabricatorDaemon { private $handlers; private $conduit; private $config; private $pollFrequency; protected function run() { $argv = $this->getArgv(); if (count($argv) !== 1) { - throw new Exception('usage: PhabricatorBot <json_config_file>'); + throw new Exception( + pht( + 'Usage: %s %s', + __CLASS__, + '<json_config_file>')); } $json_raw = Filesystem::readFile($argv[0]); try { $config = phutil_json_decode($json_raw); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht("File '%s' is not valid JSON!", $argv[0]), $ex); } $nick = idx($config, 'nick', 'phabot'); $handlers = idx($config, 'handlers', array()); $protocol_adapter_class = idx( $config, 'protocol-adapter', 'PhabricatorIRCProtocolAdapter'); $this->pollFrequency = idx($config, 'poll-frequency', 1); $this->config = $config; foreach ($handlers as $handler) { $obj = newv($handler, array($this)); $this->handlers[] = $obj; } $ca_bundle = idx($config, 'https.cabundle'); if ($ca_bundle) { HTTPSFuture::setGlobalCABundleFromPath($ca_bundle); } $conduit_uri = idx($config, 'conduit.uri'); if ($conduit_uri) { $conduit_token = idx($config, 'conduit.token'); // Normalize the path component of the URI so users can enter the // domain without the "/api/" part. $conduit_uri = new PhutilURI($conduit_uri); $conduit_host = (string)$conduit_uri->setPath('/'); $conduit_uri = (string)$conduit_uri->setPath('/api/'); $conduit = new ConduitClient($conduit_uri); if ($conduit_token) { $conduit->setConduitToken($conduit_token); } else { $conduit_user = idx($config, 'conduit.user'); $conduit_cert = idx($config, 'conduit.cert'); $response = $conduit->callMethodSynchronous( 'conduit.connect', array( - 'client' => 'PhabricatorBot', + 'client' => __CLASS__, 'clientVersion' => '1.0', 'clientDescription' => php_uname('n').':'.$nick, 'host' => $conduit_host, 'user' => $conduit_user, 'certificate' => $conduit_cert, )); } $this->conduit = $conduit; } // Instantiate Protocol Adapter, for now follow same technique as // handler instantiation $this->protocolAdapter = newv($protocol_adapter_class, array()); $this->protocolAdapter ->setConfig($this->config) ->connect(); $this->runLoop(); $this->protocolAdapter->disconnect(); } public function getConfig($key, $default = null) { return idx($this->config, $key, $default); } private function runLoop() { do { $this->stillWorking(); $messages = $this->protocolAdapter->getNextMessages($this->pollFrequency); if (count($messages) > 0) { foreach ($messages as $message) { $this->routeMessage($message); } } foreach ($this->handlers as $handler) { $handler->runBackgroundTasks(); } } while (!$this->shouldExit()); } public function writeMessage(PhabricatorBotMessage $message) { return $this->protocolAdapter->writeMessage($message); } private function routeMessage(PhabricatorBotMessage $message) { $ignore = $this->getConfig('ignore'); if ($ignore) { $sender = $message->getSender(); if ($sender && in_array($sender->getName(), $ignore)) { return; } } if ($message->getCommand() == 'LOG') { $this->log('[LOG] '.$message->getBody()); } foreach ($this->handlers as $handler) { try { $handler->receiveMessage($message); } catch (Exception $ex) { phlog($ex); } } } public function getAdapter() { return $this->protocolAdapter; } public function getConduit() { if (empty($this->conduit)) { throw new Exception( "This bot is not configured with a Conduit uplink. Set 'conduit.uri', ". "'conduit.user' and 'conduit.cert' in the configuration to connect."); } return $this->conduit; } } diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php index 34cab99afa..c569f0c695 100644 --- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php +++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php @@ -1,233 +1,233 @@ <?php final class PhabricatorWorkerTriggerQuery extends PhabricatorPolicyAwareQuery { // NOTE: This is a PolicyAware query so it can work with other infrastructure // like handles; triggers themselves are low-level and do not have // meaninguful policies. const ORDER_ID = 'id'; const ORDER_EXECUTION = 'execution'; const ORDER_VERSION = 'version'; private $ids; private $phids; private $versionMin; private $versionMax; private $nextEpochMin; private $nextEpochMax; private $needEvents; private $order = self::ORDER_ID; public function getQueryApplicationClass() { return null; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withVersionBetween($min, $max) { $this->versionMin = $min; $this->versionMax = $max; return $this; } public function withNextEventBetween($min, $max) { $this->nextEpochMin = $min; $this->nextEpochMax = $max; return $this; } public function needEvents($need_events) { $this->needEvents = $need_events; return $this; } /** * Set the result order. * * Note that using `ORDER_EXECUTION` will also filter results to include only * triggers which have been scheduled to execute. You should not use this * ordering when querying for specific triggers, e.g. by ID or PHID. * * @param const Result order. * @return this */ public function setOrder($order) { $this->order = $order; return $this; } protected function nextPage(array $page) { // NOTE: We don't implement paging because we don't currently ever need // it and paging ORDER_EXCUTION is a hassle. throw new PhutilMethodNotImplementedException(); } protected function loadPage() { $task_table = new PhabricatorWorkerTrigger(); $conn_r = $task_table->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT t.* FROM %T t %Q %Q %Q %Q', $task_table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $triggers = $task_table->loadAllFromArray($rows); if ($triggers) { if ($this->needEvents) { $ids = mpull($triggers, 'getID'); $events = id(new PhabricatorWorkerTriggerEvent())->loadAllWhere( 'triggerID IN (%Ld)', $ids); $events = mpull($events, null, 'getTriggerID'); foreach ($triggers as $key => $trigger) { $event = idx($events, $trigger->getID()); $trigger->attachEvent($event); } } foreach ($triggers as $key => $trigger) { $clock_class = $trigger->getClockClass(); if (!is_subclass_of($clock_class, 'PhabricatorTriggerClock')) { unset($triggers[$key]); continue; } try { $argv = array($trigger->getClockProperties()); $clock = newv($clock_class, $argv); } catch (Exception $ex) { unset($triggers[$key]); continue; } $trigger->attachClock($clock); } foreach ($triggers as $key => $trigger) { $action_class = $trigger->getActionClass(); if (!is_subclass_of($action_class, 'PhabricatorTriggerAction')) { unset($triggers[$key]); continue; } try { $argv = array($trigger->getActionProperties()); $action = newv($action_class, $argv); } catch (Exception $ex) { unset($triggers[$key]); continue; } $trigger->attachAction($action); } } return $triggers; } protected function buildJoinClause(AphrontDatabaseConnection $conn_r) { $joins = array(); if (($this->nextEpochMin !== null) || ($this->nextEpochMax !== null) || - ($this->order == PhabricatorWorkerTriggerQuery::ORDER_EXECUTION)) { + ($this->order == self::ORDER_EXECUTION)) { $joins[] = qsprintf( $conn_r, 'JOIN %T e ON e.triggerID = t.id', id(new PhabricatorWorkerTriggerEvent())->getTableName()); } return implode(' ', $joins); } protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 't.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn_r, 't.phid IN (%Ls)', $this->phids); } if ($this->versionMin !== null) { $where[] = qsprintf( $conn_r, 't.triggerVersion >= %d', $this->versionMin); } if ($this->versionMax !== null) { $where[] = qsprintf( $conn_r, 't.triggerVersion <= %d', $this->versionMax); } if ($this->nextEpochMin !== null) { $where[] = qsprintf( $conn_r, 'e.nextEventEpoch >= %d', $this->nextEpochMin); } if ($this->nextEpochMax !== null) { $where[] = qsprintf( $conn_r, 'e.nextEventEpoch <= %d', $this->nextEpochMax); } return $this->formatWhereClause($where); } private function buildOrderClause(AphrontDatabaseConnection $conn_r) { switch ($this->order) { case self::ORDER_ID: return qsprintf( $conn_r, 'ORDER BY id DESC'); case self::ORDER_EXECUTION: return qsprintf( $conn_r, 'ORDER BY e.nextEventEpoch ASC, e.id ASC'); case self::ORDER_VERSION: return qsprintf( $conn_r, 'ORDER BY t.triggerVersion ASC'); default: throw new Exception( pht( 'Unsupported order "%s".', $this->order)); } } } diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 05ab7c677d..39f911e3c5 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -1,876 +1,876 @@ <?php /** * Manages the execution environment configuration, exposing APIs to read * configuration settings and other similar values that are derived directly * from configuration settings. * * * = Reading Configuration = * * The primary role of this class is to provide an API for reading * Phabricator configuration, @{method:getEnvConfig}: * * $value = PhabricatorEnv::getEnvConfig('some.key', $default); * * The class also handles some URI construction based on configuration, via * the methods @{method:getURI}, @{method:getProductionURI}, * @{method:getCDNURI}, and @{method:getDoclink}. * * For configuration which allows you to choose a class to be responsible for * some functionality (e.g., which mail adapter to use to deliver email), * @{method:newObjectFromConfig} provides a simple interface that validates * the configured value. * * * = Unit Test Support = * * In unit tests, you can use @{method:beginScopedEnv} to create a temporary, * mutable environment. The method returns a scope guard object which restores * the environment when it is destroyed. For example: * * public function testExample() { * $env = PhabricatorEnv::beginScopedEnv(); * $env->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 { private static $sourceStack; private static $repairSource; private static $overrideSource; private static $requestBaseURI; private static $cache; private static $localeCode; /** * @phutil-external-symbol class PhabricatorStartup */ public static function initializeWebEnvironment() { self::initializeCommonEnvironment(); } public static function initializeScriptEnvironment() { self::initializeCommonEnvironment(); // 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() { PhutilErrorHandler::initialize(); self::buildConfigurationSourceStack(); // Force a valid timezone. If both PHP and Phabricator configuration are // invalid, use UTC. - $tz = PhabricatorEnv::getEnvConfig('phabricator.timezone'); + $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 = PhabricatorEnv::getEnvConfig('environment.append-paths'); + $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 = PhabricatorEnv::getEnvConfig('cluster.instance'); + $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 setLocaleCode($locale_code) { if ($locale_code == self::$localeCode) { return; } try { $locale = PhutilLocale::loadLocale($locale_code); $translations = PhutilTranslation::getTranslationMapForLocale( $locale_code); - $override = PhabricatorEnv::getEnvConfig('translation.override'); + $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() { 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 (PhabricatorEnv::getEnvConfig('load-libraries') as $library) { + foreach (self::getEnvConfig('load-libraries') as $library) { phutil_load_library($library); } // 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 PhutilSymbolLoader()) ->setAncestorClass('PhabricatorConfigSiteSource') ->loadObjects(); $site_sources = msort($site_sources, 'getPriority'); foreach ($site_sources as $site_source) { $stack->pushSource($site_source); } try { $stack->pushSource( id(new PhabricatorConfigDatabaseSource('default')) ->setName(pht('Database'))); } catch (AphrontQueryException $exception) { // If the database is not available, just skip this configuration // source. This happens during `bin/storage upgrade`, `bin/conf` before // schema setup, etc. } } 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; } public static function calculateEnvironmentHash() { $keys = self::getKeysForConsistencyCheck(); $values = array(); foreach ($keys as $key) { $values[$key] = self::getEnvConfigIfExists($key); } return PhabricatorHash::digest(json_encode($values)); } /** * Returns a summary of non-default configuration settings to allow the * "daemons and web have different config" setup check to list divergent * keys. */ public static function calculateEnvironmentInfo() { $keys = self::getKeysForConsistencyCheck(); $info = array(); $defaults = id(new PhabricatorConfigDefaultSource())->getAllKeys(); foreach ($keys as $key) { $current = self::getEnvConfigIfExists($key); $default = idx($defaults, $key, null); if ($current !== $default) { $info[$key] = PhabricatorHash::digestForIndex(json_encode($current)); } } $keys_hash = array_keys($defaults); sort($keys_hash); $keys_hash = implode("\0", $keys_hash); $keys_hash = PhabricatorHash::digestForIndex($keys_hash); return array( 'version' => 1, 'keys' => $keys_hash, 'values' => $info, ); } /** * Compare two environment info summaries to generate a human-readable * list of discrepancies. */ public static function compareEnvironmentInfo(array $u, array $v) { $issues = array(); $uversion = idx($u, 'version'); $vversion = idx($v, 'version'); if ($uversion != $vversion) { $issues[] = pht( 'The two configurations were generated by different versions '. 'of Phabricator.'); // These may not be comparable, so stop here. return $issues; } if ($u['keys'] !== $v['keys']) { $issues[] = pht( 'The two configurations have different keys. This usually means '. 'that they are running different versions of Phabricator.'); } $uval = idx($u, 'values', array()); $vval = idx($v, 'values', array()); $all_keys = array_keys($uval + $vval); foreach ($all_keys as $key) { $uv = idx($uval, $key); $vv = idx($vval, $key); if ($uv !== $vv) { if ($uv && $vv) { $issues[] = pht( 'The configuration key "%s" is set in both configurations, but '. 'set to different values.', $key); } else { $issues[] = pht( 'The configuration key "%s" is set in only one configuration.', $key); } } } return $issues; } private static function getKeysForConsistencyCheck() { $keys = array_keys(self::getAllConfigKeys()); sort($keys); $skip_keys = self::getEnvConfig('phd.variant-config'); return array_diff($keys, $skip_keys); } /* -( 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 (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("No config value specified for key '{$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 getAllowedURIs($path) { $uri = new PhutilURI($path); if ($uri->getDomain()) { return $path; } $allowed_uris = self::getEnvConfig('phabricator.allowed-uris'); $return = array(); foreach ($allowed_uris as $allowed_uri) { $return[] = rtrim($allowed_uri, '/').$path; } return $return; } /** * 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; } /** * 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( "Define 'phabricator.base-uri' in your configuration to continue."); } return $base_uri; } public static function getRequestBaseURI() { return self::$requestBaseURI; } public static function setRequestBaseURI($uri) { self::$requestBaseURI = $uri; } /* -( 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( 'Scoped environments were destroyed in a diffent 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 idenfies 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($uri) { $uri = new PhutilURI($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.', $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.', $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.', $uri)); } } /** * Detect if a URI identifies a valid fetchable remote resource. * * @param string URI to test. * @param list<string> 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<string> Allowed protocols. * @return pair<string, string> Pre-resolved URI and domain. * @task uri */ public static function requireValidRemoteURIForFetch( $uri, array $protocols) { $uri = new PhutilURI($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.', $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.', $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.', $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.', $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.', $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() { $address = idx($_SERVER, 'REMOTE_ADDR'); if (!$address) { throw new Exception( pht( 'Unable to test remote address against cluster whitelist: '. 'REMOTE_ADDR is not defined.')); } return self::isClusterAddress($address); } public static function isClusterAddress($address) { - $cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses'); + $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); } /* -( 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(); } } diff --git a/src/infrastructure/events/PhabricatorExampleEventListener.php b/src/infrastructure/events/PhabricatorExampleEventListener.php index 0a7bd699df..fb85678da1 100644 --- a/src/infrastructure/events/PhabricatorExampleEventListener.php +++ b/src/infrastructure/events/PhabricatorExampleEventListener.php @@ -1,28 +1,31 @@ <?php /** * Example event listener. For details about installing Phabricator event hooks, * refer to @{article:Events User Guide: Installing Event Listeners}. */ final class PhabricatorExampleEventListener extends PhabricatorEventListener { public function register() { // When your listener is installed, its register() method will be called. // You should listen() to any events you are interested in here. $this->listen(PhabricatorEventType::TYPE_TEST_DIDRUNTEST); } public function handleEvent(PhutilEvent $event) { // When an event you have called listen() for in your register() method // occurs, this method will be invoked. You should respond to the event. // In this case, we just echo a message out so the event test script will // do something visible. $console = PhutilConsole::getConsole(); $console->writeOut( - "PhabricatorExampleEventListener got test event at %d\n", - $event->getValue('time')); + "%s\n", + pht( + '% got test event at %d', + __CLASS__, + $event->getValue('time'))); } } diff --git a/src/infrastructure/sms/storage/PhabricatorSMS.php b/src/infrastructure/sms/storage/PhabricatorSMS.php index c17d8f16da..7b6c85f159 100644 --- a/src/infrastructure/sms/storage/PhabricatorSMS.php +++ b/src/infrastructure/sms/storage/PhabricatorSMS.php @@ -1,75 +1,75 @@ <?php final class PhabricatorSMS extends PhabricatorSMSDAO { const MAXIMUM_SEND_TRIES = 5; /** * Status constants should be 16 characters or less. See status entries * for details on what they indicate about the underlying SMS. */ // in the beginning, all SMS are unsent const STATUS_UNSENT = 'unsent'; // that nebulous time when we've sent it from Phabricator but haven't // heard anything from the external API const STATUS_SENT_UNCONFIRMED = 'sent-unconfirmed'; // "success" const STATUS_SENT = 'sent'; // "fail" but we'll try again const STATUS_FAILED = 'failed'; // we're giving up on our external API partner const STATUS_FAILED_PERMANENTLY = 'permafailed'; const SHORTNAME_PLACEHOLDER = 'phabricator'; protected $providerShortName; protected $providerSMSID; // numbers can be up to 20 digits long protected $toNumber; protected $fromNumber; protected $body; protected $sendStatus; public static function initializeNewSMS($body) { // NOTE: these values will be updated to correct values when the // SMS is sent for the first time. In particular, the ProviderShortName // and ProviderSMSID are totally garbage data before a send it attempted. return id(new PhabricatorSMS()) ->setBody($body) - ->setSendStatus(PhabricatorSMS::STATUS_UNSENT) - ->setProviderShortName(PhabricatorSMS::SHORTNAME_PLACEHOLDER) + ->setSendStatus(self::STATUS_UNSENT) + ->setProviderShortName(self::SHORTNAME_PLACEHOLDER) ->setProviderSMSID(Filesystem::readRandomCharacters(40)); } protected function getConfiguration() { return array( self::CONFIG_COLUMN_SCHEMA => array( 'providerShortName' => 'text16', 'providerSMSID' => 'text40', 'toNumber' => 'text20', 'fromNumber' => 'text20?', 'body' => 'text', 'sendStatus' => 'text16?', ), self::CONFIG_KEY_SCHEMA => array( 'key_provider' => array( 'columns' => array('providerSMSID', 'providerShortName'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getTableName() { // Slightly non-standard, but otherwise this class needs "MetaMTA" in its // name. :/ return 'sms'; } public function hasBeenSentAtLeastOnce() { return ($this->getProviderShortName() != - PhabricatorSMS::SHORTNAME_PLACEHOLDER); + self::SHORTNAME_PLACEHOLDER); } } diff --git a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php index 0eefe68069..390d591a07 100644 --- a/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorSQLPatchList.php @@ -1,181 +1,181 @@ <?php abstract class PhabricatorSQLPatchList { public abstract function getNamespace(); public abstract function getPatches(); /** * Examine a directory for `.php` and `.sql` files and build patch * specifications for them. */ protected function buildPatchesFromDirectory($directory) { $patch_list = Filesystem::listDirectory( $directory, $include_hidden = false); sort($patch_list); $patches = array(); foreach ($patch_list as $patch) { $matches = null; if (!preg_match('/\.(sql|php)$/', $patch, $matches)) { throw new Exception( pht( 'Unknown patch "%s" in "%s", expected ".php" or ".sql" suffix.', $patch, $directory)); } $patches[$patch] = array( 'type' => $matches[1], 'name' => rtrim($directory, '/').'/'.$patch, ); } return $patches; } final public static function buildAllPatches() { $patch_lists = id(new PhutilSymbolLoader()) - ->setAncestorClass('PhabricatorSQLPatchList') + ->setAncestorClass(__CLASS__) ->setConcreteOnly(true) ->selectAndLoadSymbols(); $specs = array(); $seen_namespaces = array(); foreach ($patch_lists as $patch_class) { $patch_class = $patch_class['name']; $patch_list = newv($patch_class, array()); $namespace = $patch_list->getNamespace(); if (isset($seen_namespaces[$namespace])) { $prior = $seen_namespaces[$namespace]; throw new Exception( "PatchList '{$patch_class}' has the same namespace, '{$namespace}', ". "as another patch list class, '{$prior}'. Each patch list MUST have ". "a unique namespace."); } $last_key = null; foreach ($patch_list->getPatches() as $key => $patch) { if (!is_array($patch)) { throw new Exception( "PatchList '{$patch_class}' has a patch '{$key}' which is not ". "an array."); } $valid = array( 'type' => true, 'name' => true, 'after' => true, 'legacy' => true, 'dead' => true, ); foreach ($patch as $pkey => $pval) { if (empty($valid[$pkey])) { throw new Exception( "PatchList '{$patch_class}' has a patch, '{$key}', with an ". "unknown property, '{$pkey}'. Patches must have only valid ". "keys: ".implode(', ', array_keys($valid)).'.'); } } if (is_numeric($key)) { throw new Exception( "PatchList '{$patch_class}' has a patch with a numeric key, ". "'{$key}'. Patches must use string keys."); } if (strpos($key, ':') !== false) { throw new Exception( "PatchList '{$patch_class}' has a patch with a colon in the ". "key name, '{$key}'. Patch keys may not contain colons."); } $full_key = "{$namespace}:{$key}"; if (isset($specs[$full_key])) { throw new Exception( "PatchList '{$patch_class}' has a patch '{$key}' which ". "duplicates an existing patch key."); } $patch['key'] = $key; $patch['fullKey'] = $full_key; $patch['dead'] = (bool)idx($patch, 'dead', false); if (isset($patch['legacy'])) { if ($namespace != 'phabricator') { throw new Exception( "Only patches in the 'phabricator' namespace may contain ". "'legacy' keys."); } } else { $patch['legacy'] = false; } if (!array_key_exists('after', $patch)) { if ($last_key === null) { throw new Exception( "Patch '{$full_key}' is missing key 'after', and is the first ". "patch in the patch list '{$patch_class}', so its application ". "order can not be determined implicitly. The first patch in a ". "patch list must list the patch or patches it depends on ". "explicitly."); } else { $patch['after'] = array($last_key); } } $last_key = $full_key; foreach ($patch['after'] as $after_key => $after) { if (strpos($after, ':') === false) { $patch['after'][$after_key] = $namespace.':'.$after; } } $type = idx($patch, 'type'); if (!$type) { throw new Exception( "Patch '{$namespace}:{$key}' is missing key 'type'. Every patch ". "must have a type."); } switch ($type) { case 'db': case 'sql': case 'php': break; default: throw new Exception( "Patch '{$namespace}:{$key}' has unknown patch type '{$type}'."); } $specs[$full_key] = $patch; } } foreach ($specs as $key => $patch) { foreach ($patch['after'] as $after) { if (empty($specs[$after])) { throw new Exception( "Patch '{$key}' references nonexistent dependency, '{$after}'. ". "Patches may only depend on patches which actually exist."); } } } $patches = array(); foreach ($specs as $full_key => $spec) { $patches[$full_key] = new PhabricatorStoragePatch($spec); } // TODO: Detect cycles? return $patches; } } diff --git a/src/infrastructure/time/PhabricatorTime.php b/src/infrastructure/time/PhabricatorTime.php index d2866d99c2..03495d8412 100644 --- a/src/infrastructure/time/PhabricatorTime.php +++ b/src/infrastructure/time/PhabricatorTime.php @@ -1,72 +1,75 @@ <?php final class PhabricatorTime { private static $stack = array(); private static $originalZone; public static function pushTime($epoch, $timezone) { if (empty(self::$stack)) { self::$originalZone = date_default_timezone_get(); } $ok = date_default_timezone_set($timezone); if (!$ok) { throw new Exception("Invalid timezone '{$timezone}'!"); } self::$stack[] = array( 'epoch' => $epoch, 'timezone' => $timezone, ); return new PhabricatorTimeGuard(last_key(self::$stack)); } public static function popTime($key) { if ($key !== last_key(self::$stack)) { - throw new Exception('PhabricatorTime::popTime with bad key.'); + throw new Exception( + pht( + '%s with bad key.', + __METHOD__)); } array_pop(self::$stack); if (empty(self::$stack)) { date_default_timezone_set(self::$originalZone); } else { $frame = end(self::$stack); date_default_timezone_set($frame['timezone']); } } public static function getNow() { if (self::$stack) { $frame = end(self::$stack); return $frame['epoch']; } return time(); } public static function parseLocalTime($time, PhabricatorUser $user) { $old_zone = date_default_timezone_get(); date_default_timezone_set($user->getTimezoneIdentifier()); - $timestamp = (int)strtotime($time, PhabricatorTime::getNow()); + $timestamp = (int)strtotime($time, self::getNow()); if ($timestamp <= 0) { $timestamp = null; } date_default_timezone_set($old_zone); return $timestamp; } public static function getTodayMidnightDateTime($viewer) { $timezone = new DateTimeZone($viewer->getTimezoneIdentifier()); $today = new DateTime('@'.time()); $today->setTimeZone($timezone); $year = $today->format('Y'); $month = $today->format('m'); $day = $today->format('d'); $today = new DateTime("{$year}-{$month}-{$day}", $timezone); return $today; } } diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php index dd158d1168..df1fbfa08b 100644 --- a/src/infrastructure/util/PhabricatorHash.php +++ b/src/infrastructure/util/PhabricatorHash.php @@ -1,124 +1,124 @@ <?php final class PhabricatorHash extends Phobject { const INDEX_DIGEST_LENGTH = 12; /** * Digest a string for general use, including use which relates to security. * * @param string Input string. * @return string 32-byte hexidecimal SHA1+HMAC hash. */ public static function digest($string, $key = null) { if ($key === null) { $key = PhabricatorEnv::getEnvConfig('security.hmac-key'); } if (!$key) { throw new Exception( "Set a 'security.hmac-key' in your Phabricator configuration!"); } return hash_hmac('sha1', $string, $key); } /** * Digest a string into a password hash. This is similar to @{method:digest}, * but requires a salt and iterates the hash to increase cost. */ public static function digestPassword(PhutilOpaqueEnvelope $envelope, $salt) { $result = $envelope->openEnvelope(); if (!$result) { throw new Exception('Trying to digest empty password!'); } for ($ii = 0; $ii < 1000; $ii++) { - $result = PhabricatorHash::digest($result, $salt); + $result = self::digest($result, $salt); } return $result; } /** * Digest a string for use in, e.g., a MySQL index. This produces a short * (12-byte), case-sensitive alphanumeric string with 72 bits of entropy, * which is generally safe in most contexts (notably, URLs). * * This method emphasizes compactness, and should not be used for security * related hashing (for general purpose hashing, see @{method:digest}). * * @param string Input string. * @return string 12-byte, case-sensitive alphanumeric hash of the string * which */ public static function digestForIndex($string) { $hash = sha1($string, $raw_output = true); static $map; if ($map === null) { $map = '0123456789'. 'abcdefghij'. 'klmnopqrst'. 'uvwxyzABCD'. 'EFGHIJKLMN'. 'OPQRSTUVWX'. 'YZ._'; } $result = ''; for ($ii = 0; $ii < self::INDEX_DIGEST_LENGTH; $ii++) { $result .= $map[(ord($hash[$ii]) & 0x3F)]; } return $result; } /** * Shorten a string to a maximum byte length in a collision-resistant way * while retaining some degree of human-readability. * * This function converts an input string into a prefix plus a hash. For * example, a very long string beginning with "crabapplepie..." might be * digested to something like "crabapp-N1wM1Nz3U84k". * * This allows the maximum length of identifiers to be fixed while * maintaining a high degree of collision resistance and a moderate degree * of human readability. * * @param string The string to shorten. * @param int Maximum length of the result. * @return string String shortened in a collision-resistant way. */ public static function digestToLength($string, $length) { // We need at least two more characters than the hash length to fit in a // a 1-character prefix and a separator. $min_length = self::INDEX_DIGEST_LENGTH + 2; if ($length < $min_length) { throw new Exception( pht( 'Length parameter in digestToLength() must be at least %s, '. 'but %s was provided.', new PhutilNumber($min_length), new PhutilNumber($length))); } // We could conceivably return the string unmodified if it's shorter than // the specified length. Instead, always hash it. This makes the output of // the method more recognizable and consistent (no surprising new behavior // once you hit a string longer than `$length`) and prevents an attacker // who can control the inputs from intentionally using the hashed form // of a string to cause a collision. - $hash = PhabricatorHash::digestForIndex($string); + $hash = self::digestForIndex($string); $prefix = substr($string, 0, ($length - ($min_length - 1))); return $prefix.'-'.$hash; } } diff --git a/src/infrastructure/util/password/PhabricatorPasswordHasher.php b/src/infrastructure/util/password/PhabricatorPasswordHasher.php index 4409163509..3b136b4ec3 100644 --- a/src/infrastructure/util/password/PhabricatorPasswordHasher.php +++ b/src/infrastructure/util/password/PhabricatorPasswordHasher.php @@ -1,431 +1,431 @@ <?php /** * Provides a mechanism for hashing passwords, like "iterated md5", "bcrypt", * "scrypt", etc. * * Hashers define suitability and strength, and the system automatically * chooses the strongest available hasher and can prompt users to upgrade as * soon as a stronger hasher is available. * * @task hasher Implementing a Hasher * @task hashing Using Hashers */ abstract class PhabricatorPasswordHasher extends Phobject { const MAXIMUM_STORAGE_SIZE = 128; /* -( Implementing a Hasher )---------------------------------------------- */ /** * Return a human-readable description of this hasher, like "Iterated MD5". * * @return string Human readable hash name. * @task hasher */ abstract public function getHumanReadableName(); /** * Return a short, unique, key identifying this hasher, like "md5" or * "bcrypt". This identifier should not be translated. * * @return string Short, unique hash name. * @task hasher */ abstract public function getHashName(); /** * Return the maximum byte length of hashes produced by this hasher. This is * used to prevent storage overflows. * * @return int Maximum number of bytes in hashes this class produces. * @task hasher */ abstract public function getHashLength(); /** * Return `true` to indicate that any required extensions or dependencies * are available, and this hasher is able to perform hashing. * * @return bool True if this hasher can execute. * @task hasher */ abstract public function canHashPasswords(); /** * Return a human-readable string describing why this hasher is unable * to operate. For example, "To use bcrypt, upgrade to PHP 5.5.0 or newer.". * * @return string Human-readable description of how to enable this hasher. * @task hasher */ abstract public function getInstallInstructions(); /** * Return an indicator of this hasher's strength. When choosing to hash * new passwords, the strongest available hasher which is usuable for new * passwords will be used, and the presence of a stronger hasher will * prompt users to update their hashes. * * Generally, this method should return a larger number than hashers it is * preferable to, but a smaller number than hashers which are better than it * is. This number does not need to correspond directly with the actual hash * strength. * * @return float Strength of this hasher. * @task hasher */ abstract public function getStrength(); /** * Return a short human-readable indicator of this hasher's strength, like * "Weak", "Okay", or "Good". * * This is only used to help administrators make decisions about * configuration. * * @return string Short human-readable description of hash strength. * @task hasher */ abstract public function getHumanReadableStrength(); /** * Produce a password hash. * * @param PhutilOpaqueEnvelope Text to be hashed. * @return PhutilOpaqueEnvelope Hashed text. * @task hasher */ abstract protected function getPasswordHash(PhutilOpaqueEnvelope $envelope); /** * Verify that a password matches a hash. * * The default implementation checks for equality; if a hasher embeds salt in * hashes it should override this method and perform a salt-aware comparison. * * @param PhutilOpaqueEnvelope Password to compare. * @param PhutilOpaqueEnvelope Bare password hash. * @return bool True if the passwords match. * @task hasher */ protected function verifyPassword( PhutilOpaqueEnvelope $password, PhutilOpaqueEnvelope $hash) { $actual_hash = $this->getPasswordHash($password)->openEnvelope(); $expect_hash = $hash->openEnvelope(); return ($actual_hash === $expect_hash); } /** * Check if an existing hash created by this algorithm is upgradeable. * * The default implementation returns `false`. However, hash algorithms which * have (for example) an internal cost function may be able to upgrade an * existing hash to a stronger one with a higher cost. * * @param PhutilOpaqueEnvelope Bare hash. * @return bool True if the hash can be upgraded without * changing the algorithm (for example, to a * higher cost). * @task hasher */ protected function canUpgradeInternalHash(PhutilOpaqueEnvelope $hash) { return false; } /* -( Using Hashers )------------------------------------------------------ */ /** * Get the hash of a password for storage. * * @param PhutilOpaqueEnvelope Password text. * @return PhutilOpaqueEnvelope Hashed text. * @task hashing */ final public function getPasswordHashForStorage( PhutilOpaqueEnvelope $envelope) { $name = $this->getHashName(); $hash = $this->getPasswordHash($envelope); $actual_len = strlen($hash->openEnvelope()); $expect_len = $this->getHashLength(); if ($actual_len > $expect_len) { throw new Exception( pht( "Password hash '%s' produced a hash of length %d, but a ". "maximum length of %d was expected.", $name, new PhutilNumber($actual_len), new PhutilNumber($expect_len))); } return new PhutilOpaqueEnvelope($name.':'.$hash->openEnvelope()); } /** * Parse a storage hash into its components, like the hash type and hash * data. * * @return map Dictionary of information about the hash. * @task hashing */ private static function parseHashFromStorage(PhutilOpaqueEnvelope $hash) { $raw_hash = $hash->openEnvelope(); if (strpos($raw_hash, ':') === false) { throw new Exception( pht( 'Malformed password hash, expected "name:hash".')); } list($name, $hash) = explode(':', $raw_hash); return array( 'name' => $name, 'hash' => new PhutilOpaqueEnvelope($hash), ); } /** * Get all available password hashers. This may include hashers which can not * actually be used (for example, a required extension is missing). * * @return list<PhabicatorPasswordHasher> Hasher objects. * @task hashing */ public static function getAllHashers() { $objects = id(new PhutilSymbolLoader()) - ->setAncestorClass('PhabricatorPasswordHasher') + ->setAncestorClass(__CLASS__) ->loadObjects(); $map = array(); foreach ($objects as $object) { $name = $object->getHashName(); $potential_length = strlen($name) + $object->getHashLength() + 1; $maximum_length = self::MAXIMUM_STORAGE_SIZE; if ($potential_length > $maximum_length) { throw new Exception( pht( 'Hasher "%s" may produce hashes which are too long to fit in '. 'storage. %d characters are available, but its hashes may be '. 'up to %d characters in length.', $name, $maximum_length, $potential_length)); } if (isset($map[$name])) { throw new Exception( pht( 'Two hashers use the same hash name ("%s"), "%s" and "%s". Each '. 'hasher must have a unique name.', $name, get_class($object), get_class($map[$name]))); } $map[$name] = $object; } return $map; } /** * Get all usable password hashers. This may include hashers which are * not desirable or advisable. * * @return list<PhabicatorPasswordHasher> Hasher objects. * @task hashing */ public static function getAllUsableHashers() { $hashers = self::getAllHashers(); foreach ($hashers as $key => $hasher) { if (!$hasher->canHashPasswords()) { unset($hashers[$key]); } } return $hashers; } /** * Get the best (strongest) available hasher. * * @return PhabicatorPasswordHasher Best hasher. * @task hashing */ public static function getBestHasher() { $hashers = self::getAllUsableHashers(); $hashers = msort($hashers, 'getStrength'); $hasher = last($hashers); if (!$hasher) { throw new PhabricatorPasswordHasherUnavailableException( pht( 'There are no password hashers available which are usable for '. 'new passwords.')); } return $hasher; } /** * Get the hashser for a given stored hash. * * @return PhabicatorPasswordHasher Corresponding hasher. * @task hashing */ public static function getHasherForHash(PhutilOpaqueEnvelope $hash) { $info = self::parseHashFromStorage($hash); $name = $info['name']; $usable = self::getAllUsableHashers(); if (isset($usable[$name])) { return $usable[$name]; } $all = self::getAllHashers(); if (isset($all[$name])) { throw new PhabricatorPasswordHasherUnavailableException( pht( 'Attempting to compare a password saved with the "%s" hash. The '. 'hasher exists, but is not currently usable. %s', $name, $all[$name]->getInstallInstructions())); } throw new PhabricatorPasswordHasherUnavailableException( pht( 'Attempting to compare a password saved with the "%s" hash. No such '. 'hasher is known to Phabricator.', $name)); } /** * Test if a password is using an weaker hash than the strongest available * hash. This can be used to prompt users to upgrade, or automatically upgrade * on login. * * @return bool True to indicate that rehashing this password will improve * the hash strength. * @task hashing */ public static function canUpgradeHash(PhutilOpaqueEnvelope $hash) { if (!strlen($hash->openEnvelope())) { throw new Exception( pht('Expected a password hash, received nothing!')); } $current_hasher = self::getHasherForHash($hash); $best_hasher = self::getBestHasher(); if ($current_hasher->getHashName() != $best_hasher->getHashName()) { // If the algorithm isn't the best one, we can upgrade. return true; } $info = self::parseHashFromStorage($hash); if ($current_hasher->canUpgradeInternalHash($info['hash'])) { // If the algorithm provides an internal upgrade, we can also upgrade. return true; } // Already on the best algorithm with the best settings. return false; } /** * Generate a new hash for a password, using the best available hasher. * * @param PhutilOpaqueEnvelope Password to hash. * @return PhutilOpaqueEnvelope Hashed password, using best available * hasher. * @task hashing */ public static function generateNewPasswordHash( PhutilOpaqueEnvelope $password) { $hasher = self::getBestHasher(); return $hasher->getPasswordHashForStorage($password); } /** * Compare a password to a stored hash. * * @param PhutilOpaqueEnvelope Password to compare. * @param PhutilOpaqueEnvelope Stored password hash. * @return bool True if the passwords match. * @task hashing */ public static function comparePassword( PhutilOpaqueEnvelope $password, PhutilOpaqueEnvelope $hash) { $hasher = self::getHasherForHash($hash); $parts = self::parseHashFromStorage($hash); return $hasher->verifyPassword($password, $parts['hash']); } /** * Get the human-readable algorithm name for a given hash. * * @param PhutilOpaqueEnvelope Storage hash. * @return string Human-readable algorithm name. */ public static function getCurrentAlgorithmName(PhutilOpaqueEnvelope $hash) { $raw_hash = $hash->openEnvelope(); if (!strlen($raw_hash)) { return pht('None'); } try { - $current_hasher = PhabricatorPasswordHasher::getHasherForHash($hash); + $current_hasher = self::getHasherForHash($hash); return $current_hasher->getHumanReadableName(); } catch (Exception $ex) { $info = self::parseHashFromStorage($hash); $name = $info['name']; return pht('Unknown ("%s")', $name); } } /** * Get the human-readable algorithm name for the best available hash. * * @return string Human-readable name for best hash. */ public static function getBestAlgorithmName() { try { - $best_hasher = PhabricatorPasswordHasher::getBestHasher(); + $best_hasher = self::getBestHasher(); return $best_hasher->getHumanReadableName(); } catch (Exception $ex) { return pht('Unknown'); } } } diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php index 3d79197bd8..925d76a565 100644 --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -1,355 +1,358 @@ <?php final class AphrontDialogView extends AphrontView { private $title; private $shortTitle; private $submitButton; private $cancelURI; private $cancelText = 'Cancel'; private $submitURI; private $hidden = array(); private $class; private $renderAsForm = true; private $formID; private $headerColor = PHUIActionHeaderView::HEADER_LIGHTBLUE; private $footers = array(); private $isStandalone; private $method = 'POST'; private $disableWorkflowOnSubmit; private $disableWorkflowOnCancel; private $width = 'default'; private $errors = array(); private $flush; private $validationException; const WIDTH_DEFAULT = 'default'; const WIDTH_FORM = 'form'; const WIDTH_FULL = 'full'; public function setMethod($method) { $this->method = $method; return $this; } public function setIsStandalone($is_standalone) { $this->isStandalone = $is_standalone; return $this; } public function setErrors(array $errors) { $this->errors = $errors; return $this; } public function getIsStandalone() { return $this->isStandalone; } public function setSubmitURI($uri) { $this->submitURI = $uri; return $this; } public function setTitle($title) { $this->title = $title; return $this; } public function getTitle() { return $this->title; } public function setShortTitle($short_title) { $this->shortTitle = $short_title; return $this; } public function getShortTitle() { return $this->shortTitle; } public function addSubmitButton($text = null) { if (!$text) { $text = pht('Okay'); } $this->submitButton = $text; return $this; } public function addCancelButton($uri, $text = null) { if (!$text) { $text = pht('Cancel'); } $this->cancelURI = $uri; $this->cancelText = $text; return $this; } public function addFooter($footer) { $this->footers[] = $footer; return $this; } public function addHiddenInput($key, $value) { if (is_array($value)) { foreach ($value as $hidden_key => $hidden_value) { $this->hidden[] = array($key.'['.$hidden_key.']', $hidden_value); } } else { $this->hidden[] = array($key, $value); } return $this; } public function setClass($class) { $this->class = $class; return $this; } public function setFlush($flush) { $this->flush = $flush; return $this; } public function setRenderDialogAsDiv() { // TODO: This API is awkward. $this->renderAsForm = false; return $this; } public function setFormID($id) { $this->formID = $id; return $this; } public function setWidth($width) { $this->width = $width; return $this; } public function setHeaderColor($color) { $this->headerColor = $color; return $this; } public function appendParagraph($paragraph) { return $this->appendChild( phutil_tag( 'p', array( 'class' => 'aphront-dialog-view-paragraph', ), $paragraph)); } public function appendForm(AphrontFormView $form) { return $this->appendChild($form->buildLayoutView()); } public function setDisableWorkflowOnSubmit($disable_workflow_on_submit) { $this->disableWorkflowOnSubmit = $disable_workflow_on_submit; return $this; } public function getDisableWorkflowOnSubmit() { return $this->disableWorkflowOnSubmit; } public function setDisableWorkflowOnCancel($disable_workflow_on_cancel) { $this->disableWorkflowOnCancel = $disable_workflow_on_cancel; return $this; } public function getDisableWorkflowOnCancel() { return $this->disableWorkflowOnCancel; } public function setValidationException( PhabricatorApplicationTransactionValidationException $ex = null) { $this->validationException = $ex; return $this; } final public function render() { require_celerity_resource('aphront-dialog-view-css'); $buttons = array(); if ($this->submitButton) { $meta = array(); if ($this->disableWorkflowOnSubmit) { $meta['disableWorkflow'] = true; } $buttons[] = javelin_tag( 'button', array( 'name' => '__submit__', 'sigil' => '__default__', 'type' => 'submit', 'meta' => $meta, ), $this->submitButton); } if ($this->cancelURI) { $meta = array(); if ($this->disableWorkflowOnCancel) { $meta['disableWorkflow'] = true; } $buttons[] = javelin_tag( 'a', array( 'href' => $this->cancelURI, 'class' => 'button grey', 'name' => '__cancel__', 'sigil' => 'jx-workflow-button', 'meta' => $meta, ), $this->cancelText); } if (!$this->user) { throw new Exception( - pht('You must call setUser() when rendering an AphrontDialogView.')); + pht( + 'You must call %s when rendering an %s.', + 'setUser()', + __CLASS__)); } $more = $this->class; if ($this->flush) { $more .= ' aphront-dialog-flush'; } switch ($this->width) { case self::WIDTH_FORM: case self::WIDTH_FULL: $more .= ' aphront-dialog-view-width-'.$this->width; break; case self::WIDTH_DEFAULT: break; default: throw new Exception("Unknown dialog width '{$this->width}'!"); } if ($this->isStandalone) { $more .= ' aphront-dialog-view-standalone'; } $attributes = array( 'class' => 'aphront-dialog-view '.$more, 'sigil' => 'jx-dialog', ); $form_attributes = array( 'action' => $this->submitURI, 'method' => $this->method, 'id' => $this->formID, ); $hidden_inputs = array(); $hidden_inputs[] = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => '__dialog__', 'value' => '1', )); foreach ($this->hidden as $desc) { list($key, $value) = $desc; $hidden_inputs[] = javelin_tag( 'input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value, 'sigil' => 'aphront-dialog-application-input', )); } if (!$this->renderAsForm) { $buttons = array(phabricator_form( $this->user, $form_attributes, array_merge($hidden_inputs, $buttons)), ); } $children = $this->renderChildren(); $errors = $this->errors; $ex = $this->validationException; $exception_errors = null; if ($ex) { foreach ($ex->getErrors() as $error) { $errors[] = $error->getMessage(); } } if ($errors) { $children = array( id(new PHUIInfoView())->setErrors($errors), $children, ); } $header = new PHUIActionHeaderView(); $header->setHeaderTitle($this->title); $header->setHeaderColor($this->headerColor); $footer = null; if ($this->footers) { $footer = phutil_tag( 'div', array( 'class' => 'aphront-dialog-foot', ), $this->footers); } $tail = null; if ($buttons || $footer) { $tail = phutil_tag( 'div', array( 'class' => 'aphront-dialog-tail grouped', ), array( $buttons, $footer, )); } $content = array( phutil_tag( 'div', array( 'class' => 'aphront-dialog-head', ), $header), phutil_tag('div', array( 'class' => 'aphront-dialog-body grouped', ), $children), $tail, ); if ($this->renderAsForm) { return phabricator_form( $this->user, $form_attributes + $attributes, array($hidden_inputs, $content)); } else { return javelin_tag( 'div', $attributes, $content); } } } diff --git a/src/view/form/AphrontFormView.php b/src/view/form/AphrontFormView.php index 6b30656131..f9f281ff20 100644 --- a/src/view/form/AphrontFormView.php +++ b/src/view/form/AphrontFormView.php @@ -1,168 +1,171 @@ <?php final class AphrontFormView extends AphrontView { private $action; private $method = 'POST'; private $header; private $data = array(); private $encType; private $workflow; private $id; private $shaded = false; private $sigils = array(); private $metadata; private $controls = array(); private $fullWidth = false; public function setMetadata($metadata) { $this->metadata = $metadata; return $this; } public function getMetadata() { return $this->metadata; } public function setID($id) { $this->id = $id; return $this; } public function setAction($action) { $this->action = $action; return $this; } public function setMethod($method) { $this->method = $method; return $this; } public function setEncType($enc_type) { $this->encType = $enc_type; return $this; } public function setShaded($shaded) { $this->shaded = $shaded; return $this; } public function addHiddenInput($key, $value) { $this->data[$key] = $value; return $this; } public function setWorkflow($workflow) { $this->workflow = $workflow; return $this; } public function addSigil($sigil) { $this->sigils[] = $sigil; return $this; } public function setFullWidth($full_width) { $this->fullWidth = $full_width; return $this; } public function getFullWidth() { return $this->fullWidth; } public function appendInstructions($text) { return $this->appendChild( phutil_tag( 'div', array( 'class' => 'aphront-form-instructions', ), $text)); } public function appendRemarkupInstructions($remarkup) { return $this->appendInstructions( PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($remarkup), 'default', $this->getUser())); } public function buildLayoutView() { foreach ($this->controls as $control) { $control->setUser($this->getUser()); $control->willRender(); } return id(new PHUIFormLayoutView()) ->setFullWidth($this->getFullWidth()) ->appendChild($this->renderDataInputs()) ->appendChild($this->renderChildren()); } /** * Append a control to the form. * * This method behaves like @{method:appendChild}, but it only takes * controls. It will propagate some information from the form to the * control to simplify rendering. * * @param AphrontFormControl Control to append. * @return this */ public function appendControl(AphrontFormControl $control) { $this->controls[] = $control; return $this->appendChild($control); } public function render() { require_celerity_resource('phui-form-view-css'); $layout = $this->buildLayoutView(); if (!$this->user) { - throw new Exception(pht('You must pass the user to AphrontFormView.')); + throw new Exception( + pht( + 'You must pass the user to %s.', + __CLASS__)); } $sigils = $this->sigils; if ($this->workflow) { $sigils[] = 'workflow'; } return phabricator_form( $this->user, array( 'class' => $this->shaded ? 'phui-form-shaded' : null, 'action' => $this->action, 'method' => $this->method, 'enctype' => $this->encType, 'sigil' => $sigils ? implode(' ', $sigils) : null, 'meta' => $this->metadata, 'id' => $this->id, ), $layout->render()); } private function renderDataInputs() { $inputs = array(); foreach ($this->data as $key => $value) { if ($value === null) { continue; } $inputs[] = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value, )); } return $inputs; } } diff --git a/src/view/form/PHUIFormLayoutView.php b/src/view/form/PHUIFormLayoutView.php index 87e35e03c1..3e7941257e 100644 --- a/src/view/form/PHUIFormLayoutView.php +++ b/src/view/form/PHUIFormLayoutView.php @@ -1,62 +1,65 @@ <?php /** * This provides the layout of an AphrontFormView without actually providing * the <form /> tag. Useful on its own for creating forms in other forms (like * dialogs) or forms which aren't submittable. */ final class PHUIFormLayoutView extends AphrontView { private $classes = array(); private $fullWidth; public function setFullWidth($width) { $this->fullWidth = $width; return $this; } public function addClass($class) { $this->classes[] = $class; return $this; } public function appendInstructions($text) { return $this->appendChild( phutil_tag( 'div', array( 'class' => 'aphront-form-instructions', ), $text)); } public function appendRemarkupInstructions($remarkup) { if ($this->getUser() === null) { throw new Exception( - 'Call `setUser` before appending Remarkup to PHUIFormLayoutView.'); + pht( + 'Call %s before appending Remarkup to %s.', + 'setUser()', + __CLASS__)); } return $this->appendInstructions( PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($remarkup), 'default', $this->getUser())); } public function render() { $classes = $this->classes; $classes[] = 'phui-form-view'; if ($this->fullWidth) { $classes[] = 'phui-form-full-width'; } return phutil_tag( 'div', array( 'class' => implode(' ', $classes), ), $this->renderChildren()); } } diff --git a/src/view/form/PHUIInfoView.php b/src/view/form/PHUIInfoView.php index a079e3f6ca..497fcf3f78 100644 --- a/src/view/form/PHUIInfoView.php +++ b/src/view/form/PHUIInfoView.php @@ -1,123 +1,122 @@ <?php final class PHUIInfoView extends AphrontView { const SEVERITY_ERROR = 'error'; const SEVERITY_WARNING = 'warning'; const SEVERITY_NOTICE = 'notice'; const SEVERITY_NODATA = 'nodata'; const SEVERITY_SUCCESS = 'success'; private $title; private $errors; private $severity; private $id; private $buttons = array(); public function setTitle($title) { $this->title = $title; return $this; } public function setSeverity($severity) { $this->severity = $severity; return $this; } public function setErrors(array $errors) { $this->errors = $errors; return $this; } public function setID($id) { $this->id = $id; return $this; } public function addButton(PHUIButtonView $button) { $this->buttons[] = $button; return $this; } final public function render() { - require_celerity_resource('phui-info-view-css'); $errors = $this->errors; if ($errors) { $list = array(); foreach ($errors as $error) { $list[] = phutil_tag( 'li', array(), $error); } $list = phutil_tag( 'ul', array( 'class' => 'phui-info-view-list', ), $list); } else { $list = null; } $title = $this->title; if (strlen($title)) { $title = phutil_tag( 'h1', array( 'class' => 'phui-info-view-head', ), $title); } else { $title = null; } $this->severity = nonempty($this->severity, self::SEVERITY_ERROR); $classes = array(); $classes[] = 'phui-info-view'; $classes[] = 'phui-info-severity-'.$this->severity; $classes[] = 'grouped'; $classes = implode(' ', $classes); $children = $this->renderChildren(); if ($list) { $children[] = $list; } $body = null; if (!empty($children)) { $body = phutil_tag( 'div', array( 'class' => 'phui-info-view-body', ), $children); } $buttons = null; if (!empty($this->buttons)) { $buttons = phutil_tag( 'div', array( 'class' => 'phui-info-view-actions', ), $this->buttons); } return phutil_tag( 'div', array( 'id' => $this->id, 'class' => $classes, ), array( $buttons, $title, $body, )); } } diff --git a/src/view/form/PHUIPagedFormView.php b/src/view/form/PHUIPagedFormView.php index 82e58c7f58..ff72f67ca0 100644 --- a/src/view/form/PHUIPagedFormView.php +++ b/src/view/form/PHUIPagedFormView.php @@ -1,278 +1,278 @@ <?php /** * @task page Managing Pages */ final class PHUIPagedFormView extends AphrontView { private $name = 'pages'; private $pages = array(); private $selectedPage; private $choosePage; private $nextPage; private $prevPage; private $complete; private $cancelURI; protected function canAppendChild() { return false; } /* -( Managing Pages )----------------------------------------------------- */ /** * @task page */ public function addPage($key, PHUIFormPageView $page) { if (isset($this->pages[$key])) { throw new Exception("Duplicate page with key '{$key}'!"); } $this->pages[$key] = $page; $page->setPagedFormView($this, $key); $this->selectedPage = null; $this->complete = null; return $this; } /** * @task page */ public function getPage($key) { if (!$this->pageExists($key)) { throw new Exception("No page '{$key}' exists!"); } return $this->pages[$key]; } /** * @task page */ public function pageExists($key) { return isset($this->pages[$key]); } /** * @task page */ protected function getPageIndex($key) { $page = $this->getPage($key); $index = 0; foreach ($this->pages as $target_page) { if ($page === $target_page) { break; } $index++; } return $index; } /** * @task page */ protected function getPageByIndex($index) { foreach ($this->pages as $page) { if (!$index) { return $page; } $index--; } throw new Exception("Requesting out-of-bounds page '{$index}'."); } protected function getLastIndex() { return count($this->pages) - 1; } protected function isFirstPage(PHUIFormPageView $page) { return ($this->getPageIndex($page->getKey()) === 0); } protected function isLastPage(PHUIFormPageView $page) { return ($this->getPageIndex($page->getKey()) === (count($this->pages) - 1)); } public function getSelectedPage() { return $this->selectedPage; } public function readFromObject($object) { return $this->processForm($is_request = false, $object); } public function writeToResponse($response) { foreach ($this->pages as $page) { $page->validateResponseType($response); $response = $page->writeToResponse($page); } return $response; } public function readFromRequest(AphrontRequest $request) { $this->choosePage = $request->getStr($this->getRequestKey('page')); $this->nextPage = $request->getStr('__submit__'); $this->prevPage = $request->getStr('__back__'); return $this->processForm($is_request = true, $request); } public function setName($name) { $this->name = $name; return $this; } public function getValue($page, $key, $default = null) { return $this->getPage($page)->getValue($key, $default); } public function setValue($page, $key, $value) { $this->getPage($page)->setValue($key, $value); return $this; } private function processForm($is_request, $source) { if ($this->pageExists($this->choosePage)) { $selected = $this->getPage($this->choosePage); } else { $selected = $this->getPageByIndex(0); } $is_attempt_complete = false; if ($this->prevPage) { - $prev_index = $this->getPageIndex($selected->getKey()) - 1;; + $prev_index = $this->getPageIndex($selected->getKey()) - 1; $index = max(0, $prev_index); $selected = $this->getPageByIndex($index); } else if ($this->nextPage) { $next_index = $this->getPageIndex($selected->getKey()) + 1; if ($next_index > $this->getLastIndex()) { $is_attempt_complete = true; } $index = min($this->getLastIndex(), $next_index); $selected = $this->getPageByIndex($index); } $validation_error = false; $found_selected = false; foreach ($this->pages as $key => $page) { if ($is_request) { if ($key === $this->choosePage) { $page->readFromRequest($source); } else { $page->readSerializedValues($source); } } else { $page->readFromObject($source); } if (!$found_selected) { $page->adjustFormPage(); } if ($page === $selected) { $found_selected = true; } if (!$found_selected || $is_attempt_complete) { if (!$page->isValid()) { $selected = $page; $validation_error = true; break; } } } if ($is_attempt_complete && !$validation_error) { $this->complete = true; } else { $this->selectedPage = $selected; } return $this; } public function isComplete() { return $this->complete; } public function getRequestKey($key) { return $this->name.':'.$key; } public function setCancelURI($cancel_uri) { $this->cancelURI = $cancel_uri; return $this; } public function getCancelURI() { return $this->cancelURI; } public function render() { $form = id(new AphrontFormView()) ->setUser($this->getUser()); $selected_page = $this->getSelectedPage(); if (!$selected_page) { throw new Exception('No selected page!'); } $form->addHiddenInput( $this->getRequestKey('page'), $selected_page->getKey()); $errors = array(); foreach ($this->pages as $page) { if ($page == $selected_page) { $errors = $page->getPageErrors(); continue; } foreach ($page->getSerializedValues() as $key => $value) { $form->addHiddenInput($key, $value); } } $submit = id(new PHUIFormMultiSubmitControl()); if (!$this->isFirstPage($selected_page)) { $submit->addBackButton(); } else if ($this->getCancelURI()) { $submit->addCancelButton($this->getCancelURI()); } if ($this->isLastPage($selected_page)) { $submit->addSubmitButton(pht('Save')); } else { $submit->addSubmitButton(pht("Continue \xC2\xBB")); } $form->appendChild($selected_page); $form->appendChild($submit); $box = id(new PHUIObjectBoxView()) ->setFormErrors($errors) ->setForm($form); if ($selected_page->getPageName()) { $header = id(new PHUIHeaderView()) ->setHeader($selected_page->getPageName()); $box->setHeader($header); } return $box; } } diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php index 7d5451a1b2..b6ed3b5e0c 100644 --- a/src/view/form/control/PhabricatorRemarkupControl.php +++ b/src/view/form/control/PhabricatorRemarkupControl.php @@ -1,227 +1,230 @@ <?php final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl { private $disableMacro = false; private $disableFullScreen = false; public function setDisableMacros($disable) { $this->disableMacro = $disable; return $this; } public function setDisableFullScreen($disable) { $this->disableFullScreen = $disable; return $this; } protected function renderInput() { $id = $this->getID(); if (!$id) { $id = celerity_generate_unique_node_id(); $this->setID($id); } $viewer = $this->getUser(); if (!$viewer) { throw new Exception( - pht('Call setUser() before rendering a PhabricatorRemarkupControl!')); + pht( + 'Call %s before rendering a %s!', + 'setUser()', + __CLASS__)); } // We need to have this if previews render images, since Ajax can not // currently ship JS or CSS. require_celerity_resource('lightbox-attachment-css'); Javelin::initBehavior( 'aphront-drag-and-drop-textarea', array( 'target' => $id, 'activatedClass' => 'aphront-textarea-drag-and-drop', 'uri' => '/file/dropupload/', 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), )); Javelin::initBehavior( 'phabricator-remarkup-assist', array( 'pht' => array( 'bold text' => pht('bold text'), 'italic text' => pht('italic text'), 'monospaced text' => pht('monospaced text'), 'List Item' => pht('List Item'), 'data' => pht('data'), 'name' => pht('name'), 'URL' => pht('URL'), ), )); Javelin::initBehavior('phabricator-tooltips', array()); $actions = array( 'fa-bold' => array( 'tip' => pht('Bold'), ), 'fa-italic' => array( 'tip' => pht('Italics'), ), 'fa-text-width' => array( 'tip' => pht('Monospaced'), ), 'fa-link' => array( 'tip' => pht('Link'), ), array( 'spacer' => true, ), 'fa-list-ul' => array( 'tip' => pht('Bulleted List'), ), 'fa-list-ol' => array( 'tip' => pht('Numbered List'), ), 'fa-code' => array( 'tip' => pht('Code Block'), ), 'fa-table' => array( 'tip' => pht('Table'), ), 'fa-cloud-upload' => array( 'tip' => pht('Upload File'), ), ); $can_use_macros = (!$this->disableMacro) && (function_exists('imagettftext')); if ($can_use_macros) { $can_use_macros = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorMacroApplication', $viewer); } if ($can_use_macros) { $actions[] = array( 'spacer' => true, ); $actions['fa-meh-o'] = array( 'tip' => pht('Meme'), ); } $actions['fa-life-bouy'] = array( 'tip' => pht('Help'), 'align' => 'right', 'href' => PhabricatorEnv::getDoclink('Remarkup Reference'), ); if (!$this->disableFullScreen) { $actions[] = array( 'spacer' => true, 'align' => 'right', ); $actions['fa-arrows-alt'] = array( 'tip' => pht('Fullscreen Mode'), 'align' => 'right', ); } $buttons = array(); foreach ($actions as $action => $spec) { $classes = array(); if (idx($spec, 'align') == 'right') { $classes[] = 'remarkup-assist-right'; } if (idx($spec, 'spacer')) { $classes[] = 'remarkup-assist-separator'; $buttons[] = phutil_tag( 'span', array( 'class' => implode(' ', $classes), ), ''); continue; } else { $classes[] = 'remarkup-assist-button'; } $href = idx($spec, 'href', '#'); if ($href == '#') { $meta = array('action' => $action); $mustcapture = true; $target = null; } else { $meta = array(); $mustcapture = null; $target = '_blank'; } $content = null; $tip = idx($spec, 'tip'); if ($tip) { $meta['tip'] = $tip; $content = javelin_tag( 'span', array( 'aural' => true, ), $tip); } $buttons[] = javelin_tag( 'a', array( 'class' => implode(' ', $classes), 'href' => $href, 'sigil' => 'remarkup-assist has-tooltip', 'meta' => $meta, 'mustcapture' => $mustcapture, 'target' => $target, 'tabindex' => -1, ), phutil_tag( 'div', array( 'class' => 'remarkup-assist phui-icon-view phui-font-fa bluegrey '.$action, ), $content)); } $buttons = phutil_tag( 'div', array( 'class' => 'remarkup-assist-bar', ), $buttons); $monospaced_textareas = null; $monospaced_textareas_class = null; $monospaced_textareas = $viewer ->loadPreferences() ->getPreference( PhabricatorUserPreferences::PREFERENCE_MONOSPACED_TEXTAREAS); if ($monospaced_textareas == 'enabled') { $monospaced_textareas_class = 'PhabricatorMonospaced'; } $this->setCustomClass( 'remarkup-assist-textarea '.$monospaced_textareas_class); return javelin_tag( 'div', array( 'sigil' => 'remarkup-assist-control', ), array( $buttons, parent::renderInput(), )); } } diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 3c8891ba80..7c5dcaf1cb 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -1,722 +1,722 @@ <?php /** * This is a standard Phabricator page with menus, Javelin, DarkConsole, and * basic styles. - * */ final class PhabricatorStandardPageView extends PhabricatorBarePageView { private $baseURI; private $applicationName; private $glyph; private $menuContent; private $showChrome = true; private $disableConsole; private $pageObjects = array(); private $applicationMenu; private $showFooter = true; private $showDurableColumn = true; public function setShowFooter($show_footer) { $this->showFooter = $show_footer; return $this; } public function getShowFooter() { return $this->showFooter; } public function setApplicationMenu(PHUIListView $application_menu) { $this->applicationMenu = $application_menu; return $this; } public function getApplicationMenu() { return $this->applicationMenu; } public function setApplicationName($application_name) { $this->applicationName = $application_name; return $this; } public function setDisableConsole($disable) { $this->disableConsole = $disable; return $this; } public function getApplicationName() { return $this->applicationName; } public function setBaseURI($base_uri) { $this->baseURI = $base_uri; return $this; } public function getBaseURI() { return $this->baseURI; } public function setShowChrome($show_chrome) { $this->showChrome = $show_chrome; return $this; } public function getShowChrome() { return $this->showChrome; } public function appendPageObjects(array $objs) { foreach ($objs as $obj) { $this->pageObjects[] = $obj; } } public function setShowDurableColumn($show) { $this->showDurableColumn = $show; return $this; } public function getShowDurableColumn() { $request = $this->getRequest(); if (!$request) { return false; } $viewer = $request->getUser(); if (!$viewer->isLoggedIn()) { return false; } $conpherence_installed = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorConpherenceApplication', $viewer); if (!$conpherence_installed) { return false; } if ($this->isQuicksandBlacklistURI()) { return false; } return true; } private function isQuicksandBlacklistURI() { $request = $this->getRequest(); if (!$request) { return false; } $patterns = $this->getQuicksandURIPatternBlacklist(); $path = $request->getRequestURI()->getPath(); foreach ($patterns as $pattern) { if (preg_match('(^'.$pattern.'$)', $path)) { return true; } } return false; } public function getDurableColumnVisible() { $column_key = PhabricatorUserPreferences::PREFERENCE_CONPHERENCE_COLUMN; return (bool)$this->getUserPreference($column_key, 0); } public function getTitle() { $glyph_key = PhabricatorUserPreferences::PREFERENCE_TITLES; if ($this->getUserPreference($glyph_key) == 'text') { $use_glyph = false; } else { $use_glyph = true; } $title = parent::getTitle(); $prefix = null; if ($use_glyph) { $prefix = $this->getGlyph(); } else { $application_name = $this->getApplicationName(); if (strlen($application_name)) { $prefix = '['.$application_name.']'; } } if (strlen($prefix)) { $title = $prefix.' '.$title; } return $title; } protected function willRenderPage() { parent::willRenderPage(); if (!$this->getRequest()) { throw new Exception( pht( - 'You must set the Request to render a PhabricatorStandardPageView.')); + 'You must set the Request to render a %s.', + __CLASS__)); } $console = $this->getConsole(); require_celerity_resource('phabricator-core-css'); require_celerity_resource('phabricator-zindex-css'); require_celerity_resource('phui-button-css'); require_celerity_resource('phui-spacing-css'); require_celerity_resource('phui-form-css'); require_celerity_resource('sprite-gradient-css'); require_celerity_resource('phabricator-standard-page-view'); require_celerity_resource('conpherence-durable-column-view'); Javelin::initBehavior('workflow', array()); $request = $this->getRequest(); $user = null; if ($request) { $user = $request->getUser(); } if ($user) { $default_img_uri = celerity_get_resource_uri( 'rsrc/image/icon/fatcow/document_black.png'); $download_form = phabricator_form( $user, array( 'action' => '#', 'method' => 'POST', 'class' => 'lightbox-download-form', 'sigil' => 'download', ), phutil_tag( 'button', array(), pht('Download'))); Javelin::initBehavior( 'lightbox-attachments', array( 'defaultImageUri' => $default_img_uri, 'downloadForm' => $download_form, )); } Javelin::initBehavior('aphront-form-disable-on-submit'); Javelin::initBehavior('toggle-class', array()); Javelin::initBehavior('history-install'); Javelin::initBehavior('phabricator-gesture'); $current_token = null; if ($user) { $current_token = $user->getCSRFToken(); } Javelin::initBehavior( 'refresh-csrf', array( 'tokenName' => AphrontRequest::getCSRFTokenName(), 'header' => AphrontRequest::getCSRFHeaderName(), 'current' => $current_token, )); Javelin::initBehavior('device'); Javelin::initBehavior( 'high-security-warning', $this->getHighSecurityWarningConfig()); if ($console) { require_celerity_resource('aphront-dark-console-css'); $headers = array(); if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) { $headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page'; } if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) { $headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true; } Javelin::initBehavior( 'dark-console', $this->getConsoleConfig()); // Change this to initBehavior when there is some behavior to initialize require_celerity_resource('javelin-behavior-error-log'); } if ($user) { $viewer = $user; } else { $viewer = new PhabricatorUser(); } $menu = id(new PhabricatorMainMenuView()) ->setUser($viewer); if ($this->getController()) { $menu->setController($this->getController()); } if ($this->getApplicationMenu()) { $menu->setApplicationMenu($this->getApplicationMenu()); } $this->menuContent = $menu->render(); } protected function getHead() { $monospaced = null; $request = $this->getRequest(); if ($request) { $user = $request->getUser(); if ($user) { $monospaced = $user->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_MONOSPACED); } } $response = CelerityAPI::getStaticResourceResponse(); $font_css = null; if (!empty($monospaced)) { // We can't print this normally because escaping quotation marks will // break the CSS. Instead, filter it strictly and then mark it as safe. $monospaced = new PhutilSafeHTML( PhabricatorUserPreferences::filterMonospacedCSSRule( $monospaced)); $font_css = hsprintf( '<style type="text/css">'. '.PhabricatorMonospaced, '. '.phabricator-remarkup .remarkup-code-block '. '.remarkup-code { font: %s !important; } '. '</style>', $monospaced); } return hsprintf( '%s%s%s', parent::getHead(), $font_css, $response->renderSingleResource('javelin-magical-init', 'phabricator')); } public function setGlyph($glyph) { $this->glyph = $glyph; return $this; } public function getGlyph() { return $this->glyph; } protected function willSendResponse($response) { $request = $this->getRequest(); $response = parent::willSendResponse($response); $console = $request->getApplicationConfiguration()->getConsole(); if ($console) { $response = PhutilSafeHTML::applyFunction( 'str_replace', hsprintf('<darkconsole />'), $console->render($request), $response); } return $response; } protected function getBody() { $user = null; $request = $this->getRequest(); if ($request) { $user = $request->getUser(); } $header_chrome = null; if ($this->getShowChrome()) { $header_chrome = $this->menuContent; } $classes = array(); $classes[] = 'main-page-frame'; $developer_warning = null; if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') && DarkConsoleErrorLogPluginAPI::getErrors()) { $developer_warning = phutil_tag_div( 'aphront-developer-error-callout', pht( 'This page raised PHP errors. Find them in DarkConsole '. 'or the error log.')); } // Render the "you have unresolved setup issues..." warning. $setup_warning = null; if ($user && $user->getIsAdmin()) { $open = PhabricatorSetupCheck::getOpenSetupIssueKeys(); if ($open) { $classes[] = 'page-has-warning'; $setup_warning = phutil_tag_div( 'setup-warning-callout', phutil_tag( 'a', array( 'href' => '/config/issue/', 'title' => implode(', ', $open), ), pht('You have %d unresolved setup issue(s)...', count($open)))); } } $main_page = phutil_tag( 'div', array( 'id' => 'phabricator-standard-page', 'class' => 'phabricator-standard-page', ), array( $developer_warning, $header_chrome, $setup_warning, phutil_tag( 'div', array( 'id' => 'phabricator-standard-page-body', 'class' => 'phabricator-standard-page-body', ), $this->renderPageBodyContent()), )); $durable_column = null; if ($this->getShowDurableColumn()) { $is_visible = $this->getDurableColumnVisible(); $durable_column = id(new ConpherenceDurableColumnView()) ->setSelectedConpherence(null) ->setUser($user) ->setQuicksandConfig($this->buildQuicksandConfig()) ->setVisible($is_visible) ->setInitialLoad(true); } Javelin::initBehavior('quicksand-blacklist', array( 'patterns' => $this->getQuicksandURIPatternBlacklist(), )); return phutil_tag( 'div', array( 'class' => implode(' ', $classes), ), array( $main_page, $durable_column, )); } private function renderPageBodyContent() { $console = $this->getConsole(); return array( ($console ? hsprintf('<darkconsole />') : null), parent::getBody(), $this->renderFooter(), ); } protected function getTail() { $request = $this->getRequest(); $user = $request->getUser(); $tail = array( parent::getTail(), ); $response = CelerityAPI::getStaticResourceResponse(); if (PhabricatorEnv::getEnvConfig('notification.enabled')) { if ($user && $user->isLoggedIn()) { $client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri'); $client_uri = new PhutilURI($client_uri); if ($client_uri->getDomain() == 'localhost') { $this_host = $this->getRequest()->getHost(); $this_host = new PhutilURI('http://'.$this_host.'/'); $client_uri->setDomain($this_host->getDomain()); } if ($request->isHTTPS()) { $client_uri->setProtocol('wss'); } else { $client_uri->setProtocol('ws'); } Javelin::initBehavior( 'aphlict-listen', array( 'websocketURI' => (string)$client_uri, ) + $this->buildAphlictListenConfigData()); } } $tail[] = $response->renderHTMLFooter(); return $tail; } protected function getBodyClasses() { $classes = array(); if (!$this->getShowChrome()) { $classes[] = 'phabricator-chromeless-page'; } $agent = AphrontRequest::getHTTPHeader('User-Agent'); // Try to guess the device resolution based on UA strings to avoid a flash // of incorrectly-styled content. $device_guess = 'device-desktop'; if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) { $device_guess = 'device-phone device'; } else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) { $device_guess = 'device-tablet device'; } $classes[] = $device_guess; if (preg_match('@Windows@', $agent)) { $classes[] = 'platform-windows'; } else if (preg_match('@Macintosh@', $agent)) { $classes[] = 'platform-mac'; } else if (preg_match('@X11@', $agent)) { $classes[] = 'platform-linux'; } if ($this->getRequest()->getStr('__print__')) { $classes[] = 'printable'; } if ($this->getRequest()->getStr('__aural__')) { $classes[] = 'audible'; } return implode(' ', $classes); } private function getConsole() { if ($this->disableConsole) { return null; } return $this->getRequest()->getApplicationConfiguration()->getConsole(); } private function getConsoleConfig() { $user = $this->getRequest()->getUser(); $headers = array(); if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) { $headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page'; } if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) { $headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true; } return array( // NOTE: We use a generic label here to prevent input reflection // and mitigate compression attacks like BREACH. See discussion in // T3684. 'uri' => pht('Main Request'), 'selected' => $user ? $user->getConsoleTab() : null, 'visible' => $user ? (int)$user->getConsoleVisible() : true, 'headers' => $headers, ); } private function getHighSecurityWarningConfig() { $user = $this->getRequest()->getUser(); $show = false; if ($user->hasSession()) { $hisec = ($user->getSession()->getHighSecurityUntil() - time()); if ($hisec > 0) { $show = true; } } return array( 'show' => $show, 'uri' => '/auth/session/downgrade/', 'message' => pht( 'Your session is in high security mode. When you '. 'finish using it, click here to leave.'), ); } private function renderFooter() { if (!$this->getShowChrome()) { return null; } if (!$this->getShowFooter()) { return null; } $items = PhabricatorEnv::getEnvConfig('ui.footer-items'); if (!$items) { return null; } $foot = array(); foreach ($items as $item) { $name = idx($item, 'name', pht('Unnamed Footer Item')); $href = idx($item, 'href'); if (!PhabricatorEnv::isValidURIForLink($href)) { $href = null; } if ($href !== null) { $tag = 'a'; } else { $tag = 'span'; } $foot[] = phutil_tag( $tag, array( 'href' => $href, ), $name); } $foot = phutil_implode_html(" \xC2\xB7 ", $foot); return phutil_tag( 'div', array( 'class' => 'phabricator-standard-page-footer grouped', ), $foot); } public function renderForQuicksand(array $extra_config) { parent::willRenderPage(); $response = $this->renderPageBodyContent(); $response = $this->willSendResponse($response); return array( 'content' => hsprintf('%s', $response), ) + $this->buildQuicksandConfig() + $extra_config; } private function buildQuicksandConfig() { $viewer = $this->getRequest()->getUser(); $controller = $this->getController(); $dropdown_query = id(new AphlictDropdownDataQuery()) ->setViewer($viewer); $dropdown_query->execute(); $rendered_dropdowns = array(); $applications = array( 'PhabricatorHelpApplication', ); foreach ($applications as $application_class) { if (!PhabricatorApplication::isClassInstalledForViewer( $application_class, $viewer)) { continue; } $application = PhabricatorApplication::getByClass($application_class); $rendered_dropdowns[$application_class] = $application->buildMainMenuExtraNodes( $viewer, $controller); } $hisec_warning_config = $this->getHighSecurityWarningConfig(); $console_config = null; $console = $this->getConsole(); if ($console) { $console_config = $this->getConsoleConfig(); } $upload_enabled = false; if ($controller) { $upload_enabled = $controller->isGlobalDragAndDropUploadEnabled(); } $application_class = null; $application_search_icon = null; $controller = $this->getController(); if ($controller) { $application = $controller->getCurrentApplication(); if ($application) { $application_class = get_class($application); if ($application->getApplicationSearchDocumentTypes()) { $application_search_icon = $application->getFontIcon(); } } } return array( 'title' => $this->getTitle(), 'aphlictDropdownData' => array( $dropdown_query->getNotificationData(), $dropdown_query->getConpherenceData(), ), 'globalDragAndDrop' => $upload_enabled, 'aphlictDropdowns' => $rendered_dropdowns, 'hisecWarningConfig' => $hisec_warning_config, 'consoleConfig' => $console_config, 'applicationClass' => $application_class, 'applicationSearchIcon' => $application_search_icon, ) + $this->buildAphlictListenConfigData(); } private function buildAphlictListenConfigData() { $user = $this->getRequest()->getUser(); $subscriptions = $this->pageObjects; $subscriptions[] = $user->getPHID(); return array( 'pageObjects' => array_fill_keys($this->pageObjects, true), 'subscriptions' => $subscriptions, ); } private function getQuicksandURIPatternBlacklist() { $applications = PhabricatorApplication::getAllApplications(); $blacklist = array(); foreach ($applications as $application) { $blacklist[] = $application->getQuicksandURIPatternBlacklist(); } return array_mergev($blacklist); } private function getUserPreference($key, $default = null) { $request = $this->getRequest(); if (!$request) { return $default; } $user = $request->getUser(); if (!$user) { return $default; } return $user->loadPreferences()->getPreference($key, $default); } } diff --git a/src/view/phui/PHUIDocumentView.php b/src/view/phui/PHUIDocumentView.php index 833bb48fe5..a27805e1c7 100644 --- a/src/view/phui/PHUIDocumentView.php +++ b/src/view/phui/PHUIDocumentView.php @@ -1,227 +1,227 @@ <?php final class PHUIDocumentView extends AphrontTagView { /* For mobile displays, where do you want the sidebar */ const NAV_BOTTOM = 'nav_bottom'; const NAV_TOP = 'nav_top'; const FONT_SOURCE_SANS = 'source-sans'; private $offset; private $header; private $sidenav; private $topnav; private $crumbs; private $bookname; private $bookdescription; private $mobileview; private $fontKit; private $actionListID; private $fluid; public function setOffset($offset) { $this->offset = $offset; return $this; } public function setHeader(PHUIHeaderView $header) { $header->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE); $this->header = $header; return $this; } public function setSideNav(PHUIListView $list, $display = self::NAV_BOTTOM) { $list->setType(PHUIListView::SIDENAV_LIST); $this->sidenav = $list; $this->mobileview = $display; return $this; } public function setTopNav(PHUIListView $list) { $list->setType(PHUIListView::NAVBAR_LIST); $this->topnav = $list; return $this; } public function setCrumbs(PHUIListView $list) { $this->crumbs = $list; return $this; } public function setBook($name, $description) { $this->bookname = $name; $this->bookdescription = $description; return $this; } public function setFontKit($kit) { $this->fontKit = $kit; return $this; } public function setActionListID($id) { $this->actionListID = $id; return $this; } public function setFluid($fluid) { $this->fluid = $fluid; return $this; } protected function getTagAttributes() { $classes = array(); if ($this->offset) { $classes[] = 'phui-document-offset'; - }; + } if ($this->fluid) { $classes[] = 'phui-document-fluid'; } return array( 'class' => $classes, ); } protected function getTagContent() { require_celerity_resource('phui-document-view-css'); if ($this->fontKit) { require_celerity_resource('phui-fontkit-css'); } switch ($this->fontKit) { case self::FONT_SOURCE_SANS: require_celerity_resource('font-source-sans-pro'); break; } $classes = array(); $classes[] = 'phui-document-view'; if ($this->offset) { $classes[] = 'phui-offset-view'; } if ($this->sidenav) { $classes[] = 'phui-sidenav-view'; } $sidenav = null; if ($this->sidenav) { $sidenav = phutil_tag( 'div', array( 'class' => 'phui-document-sidenav', ), $this->sidenav); } $book = null; if ($this->bookname) { $book = phutil_tag( 'div', array( 'class' => 'phui-document-bookname grouped', ), array( phutil_tag( 'span', array('class' => 'bookname'), $this->bookname), phutil_tag( 'span', array('class' => 'bookdescription'), $this->bookdescription), )); } $topnav = null; if ($this->topnav) { $topnav = phutil_tag( 'div', array( 'class' => 'phui-document-topnav', ), $this->topnav); } $crumbs = null; if ($this->crumbs) { $crumbs = phutil_tag( 'div', array( 'class' => 'phui-document-crumbs', ), $this->bookName); } if ($this->fontKit) { $main_content = phutil_tag( 'div', array( 'class' => 'phui-font-'.$this->fontKit, ), $this->renderChildren()); } else { $main_content = $this->renderChildren(); } if ($this->actionListID) { $icon_id = celerity_generate_unique_node_id(); $icon = id(new PHUIIconView()) ->setIconFont('fa-bars'); $meta = array( 'map' => array( $this->actionListID => 'phabricator-action-list-toggle', $icon_id => 'phuix-dropdown-open', ), ); $mobile_menu = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Actions')) ->setHref('#') ->setIcon($icon) ->addClass('phui-mobile-menu') ->setID($icon_id) ->addSigil('jx-toggle-class') ->setMetadata($meta); $this->header->addActionLink($mobile_menu); } $content_inner = phutil_tag( 'div', array( 'class' => 'phui-document-inner', ), array( $book, $this->header, $topnav, $main_content, $crumbs, )); if ($this->mobileview == self::NAV_BOTTOM) { $order = array($content_inner, $sidenav); } else { $order = array($sidenav, $content_inner); } $content = phutil_tag( 'div', array( 'class' => 'phui-document-content', ), $order); $view = phutil_tag( 'div', array( 'class' => implode(' ', $classes), ), $content); return $view; } } diff --git a/support/PhabricatorStartup.php b/support/PhabricatorStartup.php index 3945049ffb..30e8f3c937 100644 --- a/support/PhabricatorStartup.php +++ b/support/PhabricatorStartup.php @@ -1,885 +1,885 @@ <?php /** * Handle request startup, before loading the environment or libraries. This * class bootstraps the request state up to the point where we can enter * Phabricator code. * * NOTE: This class MUST NOT have any dependencies. It runs before libraries * load. * * Rate Limiting * ============= * * Phabricator limits the rate at which clients can request pages, and issues * HTTP 429 "Too Many Requests" responses if clients request too many pages too * quickly. Although this is not a complete defense against high-volume attacks, * it can protect an install against aggressive crawlers, security scanners, * and some types of malicious activity. * * To perform rate limiting, each page increments a score counter for the * requesting user's IP. The page can give the IP more points for an expensive * request, or fewer for an authetnicated request. * * Score counters are kept in buckets, and writes move to a new bucket every * minute. After a few minutes (defined by @{method:getRateLimitBucketCount}), * the oldest bucket is discarded. This provides a simple mechanism for keeping * track of scores without needing to store, access, or read very much data. * * Users are allowed to accumulate up to 1000 points per minute, averaged across * all of the tracked buckets. * * @task info Accessing Request Information * @task hook Startup Hooks * @task apocalypse In Case Of Apocalypse * @task validation Validation * @task ratelimit Rate Limiting */ final class PhabricatorStartup { private static $startTime; private static $debugTimeLimit; private static $globals = array(); private static $capturingOutput; private static $rawInput; private static $oldMemoryLimit; // TODO: For now, disable rate limiting entirely by default. We need to // iterate on it a bit for Conduit, some of the specific score levels, and // to deal with NAT'd offices. private static $maximumRate = 0; /* -( Accessing Request Information )-------------------------------------- */ /** * @task info */ public static function getStartTime() { return self::$startTime; } /** * @task info */ public static function getMicrosecondsSinceStart() { return (int)(1000000 * (microtime(true) - self::getStartTime())); } /** * @task info */ public static function setGlobal($key, $value) { self::validateGlobal($key); self::$globals[$key] = $value; } /** * @task info */ public static function getGlobal($key, $default = null) { self::validateGlobal($key); if (!array_key_exists($key, self::$globals)) { return $default; } return self::$globals[$key]; } /** * @task info */ public static function getRawInput() { return self::$rawInput; } /* -( Startup Hooks )------------------------------------------------------ */ /** * @task hook */ public static function didStartup() { self::$startTime = microtime(true); self::$globals = array(); static $registered; if (!$registered) { // NOTE: This protects us against multiple calls to didStartup() in the // same request, but also against repeated requests to the same // interpreter state, which we may implement in the future. register_shutdown_function(array(__CLASS__, 'didShutdown')); $registered = true; } self::setupPHP(); self::verifyPHP(); // If we've made it this far, the environment isn't completely broken so // we can switch over to relying on our own exception recovery mechanisms. ini_set('display_errors', 0); if (isset($_SERVER['REMOTE_ADDR'])) { self::rateLimitRequest($_SERVER['REMOTE_ADDR']); } self::normalizeInput(); self::verifyRewriteRules(); self::detectPostMaxSizeTriggered(); self::beginOutputCapture(); self::$rawInput = (string)file_get_contents('php://input'); } /** * @task hook */ public static function didShutdown() { $event = error_get_last(); if (!$event) { return; } switch ($event['type']) { case E_ERROR: case E_PARSE: case E_COMPILE_ERROR: break; default: return; } $msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n"; if ($event) { // Even though we should be emitting this as text-plain, escape things // just to be sure since we can't really be sure what the program state // is when we get here. $msg .= htmlspecialchars( $event['message']."\n\n".$event['file'].':'.$event['line'], ENT_QUOTES, 'UTF-8'); } // flip dem tables $msg .= "\n\n\n"; $msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf". "\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20". "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb"; self::didFatal($msg); } public static function loadCoreLibraries() { $phabricator_root = dirname(dirname(__FILE__)); $libraries_root = dirname($phabricator_root); $root = null; if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) { $root = $_SERVER['PHUTIL_LIBRARY_ROOT']; } ini_set( 'include_path', $libraries_root.PATH_SEPARATOR.ini_get('include_path')); @include_once $root.'libphutil/src/__phutil_library_init__.php'; if (!@constant('__LIBPHUTIL__')) { self::didFatal( "Unable to load libphutil. Put libphutil/ next to phabricator/, or ". "update your PHP 'include_path' to include the parent directory of ". "libphutil/."); } phutil_load_library('arcanist/src'); // Load Phabricator itself using the absolute path, so we never end up doing // anything surprising (loading index.php and libraries from different // directories). phutil_load_library($phabricator_root.'/src'); } /* -( Output Capture )----------------------------------------------------- */ public static function beginOutputCapture() { if (self::$capturingOutput) { self::didFatal('Already capturing output!'); } self::$capturingOutput = true; ob_start(); } public static function endOutputCapture() { if (!self::$capturingOutput) { return null; } self::$capturingOutput = false; return ob_get_clean(); } /* -( Debug Time Limit )--------------------------------------------------- */ /** * Set a time limit (in seconds) for the current script. After time expires, * the script fatals. * * This works like `max_execution_time`, but prints out a useful stack trace * when the time limit expires. This is primarily intended to make it easier * to debug pages which hang by allowing extraction of a stack trace: set a * short debug limit, then use the trace to figure out what's happening. * * The limit is implemented with a tick function, so enabling it implies * some accounting overhead. * * @param int Time limit in seconds. * @return void */ public static function setDebugTimeLimit($limit) { self::$debugTimeLimit = $limit; static $initialized; if (!$initialized) { declare(ticks=1); - register_tick_function(array('PhabricatorStartup', 'onDebugTick')); + register_tick_function(array(__CLASS__, 'onDebugTick')); } } /** * Callback tick function used by @{method:setDebugTimeLimit}. * * Fatals with a useful stack trace after the time limit expires. * * @return void */ public static function onDebugTick() { $limit = self::$debugTimeLimit; if (!$limit) { return; } $elapsed = (microtime(true) - self::getStartTime()); if ($elapsed > $limit) { $frames = array(); foreach (debug_backtrace() as $frame) { $file = isset($frame['file']) ? $frame['file'] : '-'; $file = basename($file); $line = isset($frame['line']) ? $frame['line'] : '-'; $class = isset($frame['class']) ? $frame['class'].'->' : null; $func = isset($frame['function']) ? $frame['function'].'()' : '?'; $frames[] = "{$file}:{$line} {$class}{$func}"; } self::didFatal( "Request aborted by debug time limit after {$limit} seconds.\n\n". "STACK TRACE\n". implode("\n", $frames)); } } /* -( In Case of Apocalypse )---------------------------------------------- */ /** * Fatal the request completely in response to an exception, sending a plain * text message to the client. Calls @{method:didFatal} internally. * * @param string Brief description of the exception context, like * `"Rendering Exception"`. * @param Exception The exception itself. * @param bool True if it's okay to show the exception's stack trace * to the user. The trace will always be logged. * @return exit This method **does not return**. * * @task apocalypse */ public static function didEncounterFatalException( $note, Exception $ex, $show_trace) { $message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage(); $full_message = $message; $full_message .= "\n\n"; $full_message .= $ex->getTraceAsString(); if ($show_trace) { $message = $full_message; } self::didFatal($message, $full_message); } /** * Fatal the request completely, sending a plain text message to the client. * * @param string Plain text message to send to the client. * @param string Plain text message to send to the error log. If not * provided, the client message is used. You can pass a more * detailed message here (e.g., with stack traces) to avoid * showing it to users. * @return exit This method **does not return**. * * @task apocalypse */ public static function didFatal($message, $log_message = null) { if ($log_message === null) { $log_message = $message; } self::endOutputCapture(); $access_log = self::getGlobal('log.access'); if ($access_log) { // We may end up here before the access log is initialized, e.g. from // verifyPHP(). $access_log->setData( array( 'c' => 500, )); $access_log->write(); } header( 'Content-Type: text/plain; charset=utf-8', $replace = true, $http_error = 500); error_log($log_message); echo $message; exit(1); } /* -( Validation )--------------------------------------------------------- */ /** * @task validation */ private static function setupPHP() { error_reporting(E_ALL | E_STRICT); self::$oldMemoryLimit = ini_get('memory_limit'); ini_set('memory_limit', -1); // If we have libxml, disable the incredibly dangerous entity loader. if (function_exists('libxml_disable_entity_loader')) { libxml_disable_entity_loader(true); } } /** * @task validation */ public static function getOldMemoryLimit() { return self::$oldMemoryLimit; } /** * @task validation */ private static function normalizeInput() { // Replace superglobals with unfiltered versions, disrespect php.ini (we // filter ourselves) $filter = array(INPUT_GET, INPUT_POST, INPUT_SERVER, INPUT_ENV, INPUT_COOKIE, ); foreach ($filter as $type) { $filtered = filter_input_array($type, FILTER_UNSAFE_RAW); if (!is_array($filtered)) { continue; } switch ($type) { case INPUT_SERVER: $_SERVER = array_merge($_SERVER, $filtered); break; case INPUT_GET: $_GET = array_merge($_GET, $filtered); break; case INPUT_COOKIE: $_COOKIE = array_merge($_COOKIE, $filtered); break; case INPUT_POST: $_POST = array_merge($_POST, $filtered); break; case INPUT_ENV; $env = array_merge($_ENV, $filtered); $_ENV = self::filterEnvSuperglobal($env); break; } } // rebuild $_REQUEST, respecting order declared in ini files $order = ini_get('request_order'); if (!$order) { $order = ini_get('variables_order'); } if (!$order) { // $_REQUEST will be empty, leave it alone return; } $_REQUEST = array(); for ($i = 0; $i < strlen($order); $i++) { switch ($order[$i]) { case 'G': $_REQUEST = array_merge($_REQUEST, $_GET); break; case 'P': $_REQUEST = array_merge($_REQUEST, $_POST); break; case 'C': $_REQUEST = array_merge($_REQUEST, $_COOKIE); break; default: // $_ENV and $_SERVER never go into $_REQUEST break; } } } /** * Adjust `$_ENV` before execution. * * Adjustments here primarily impact the environment as seen by subprocesses. * The environment is forwarded explicitly by @{class:ExecFuture}. * * @param map<string, wild> Input `$_ENV`. * @return map<string, string> Suitable `$_ENV`. * @task validation */ private static function filterEnvSuperglobal(array $env) { // In some configurations, we may get "argc" and "argv" set in $_ENV. // These are not real environmental variables, and "argv" may have an array // value which can not be forwarded to subprocesses. Remove these from the // environment if they are present. unset($env['argc']); unset($env['argv']); return $env; } /** * @task validation */ private static function verifyPHP() { $required_version = '5.2.3'; if (version_compare(PHP_VERSION, $required_version) < 0) { self::didFatal( "You are running PHP version '".PHP_VERSION."', which is older than ". "the minimum version, '{$required_version}'. Update to at least ". "'{$required_version}'."); } if (get_magic_quotes_gpc()) { self::didFatal( "Your server is configured with PHP 'magic_quotes_gpc' enabled. This ". "feature is 'highly discouraged' by PHP's developers and you must ". "disable it to run Phabricator. Consult the PHP manual for ". "instructions."); } if (extension_loaded('apc')) { $apc_version = phpversion('apc'); $known_bad = array( '3.1.14' => true, '3.1.15' => true, '3.1.15-dev' => true, ); if (isset($known_bad[$apc_version])) { self::didFatal( "You have APC {$apc_version} installed. This version of APC is ". "known to be bad, and does not work with Phabricator (it will ". "cause Phabricator to fatal unrecoverably with nonsense errors). ". "Downgrade to version 3.1.13."); } } } /** * @task validation */ private static function verifyRewriteRules() { if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) { return; } if (php_sapi_name() == 'cli-server') { // Compatibility with PHP 5.4+ built-in web server. $url = parse_url($_SERVER['REQUEST_URI']); $_REQUEST['__path__'] = $url['path']; return; } if (!isset($_REQUEST['__path__'])) { self::didFatal( "Request parameter '__path__' is not set. Your rewrite rules ". "are not configured correctly."); } if (!strlen($_REQUEST['__path__'])) { self::didFatal( "Request parameter '__path__' is set, but empty. Your rewrite rules ". "are not configured correctly. The '__path__' should always ". "begin with a '/'."); } } /** * @task validation */ private static function validateGlobal($key) { static $globals = array( 'log.access' => true, 'csrf.salt' => true, ); if (empty($globals[$key])) { throw new Exception("Access to unknown startup global '{$key}'!"); } } /** * Detect if this request has had its POST data stripped by exceeding the * 'post_max_size' PHP configuration limit. * * PHP has a setting called 'post_max_size'. If a POST request arrives with * a body larger than the limit, PHP doesn't generate $_POST but processes * the request anyway, and provides no formal way to detect that this * happened. * * We can still read the entire body out of `php://input`. However according * to the documentation the stream isn't available for "multipart/form-data" * (on nginx + php-fpm it appears that it is available, though, at least) so * any attempt to generate $_POST would be fragile. * * @task validation */ private static function detectPostMaxSizeTriggered() { // If this wasn't a POST, we're fine. if ($_SERVER['REQUEST_METHOD'] != 'POST') { return; } // If there's POST data, clearly we're in good shape. if ($_POST) { return; } // For HTML5 drag-and-drop file uploads, Safari submits the data as // "application/x-www-form-urlencoded". For most files this generates // something in POST because most files decode to some nonempty (albeit // meaningless) value. However, some files (particularly small images) // don't decode to anything. If we know this is a drag-and-drop upload, // we can skip this check. if (isset($_REQUEST['__upload__'])) { return; } // PHP generates $_POST only for two content types. This routing happens // in `main/php_content_types.c` in PHP. Normally, all forms use one of // these content types, but some requests may not -- for example, Firefox // submits files sent over HTML5 XMLHTTPRequest APIs with the Content-Type // of the file itself. If we don't have a recognized content type, we // don't need $_POST. // // NOTE: We use strncmp() because the actual content type may be something // like "multipart/form-data; boundary=...". // // NOTE: Chrome sometimes omits this header, see some discussion in T1762 // and http://code.google.com/p/chromium/issues/detail?id=6800 $content_type = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : ''; $parsed_types = array( 'application/x-www-form-urlencoded', 'multipart/form-data', ); $is_parsed_type = false; foreach ($parsed_types as $parsed_type) { if (strncmp($content_type, $parsed_type, strlen($parsed_type)) === 0) { $is_parsed_type = true; break; } } if (!$is_parsed_type) { return; } // Check for 'Content-Length'. If there's no data, we don't expect $_POST // to exist. $length = (int)$_SERVER['CONTENT_LENGTH']; if (!$length) { return; } // Time to fatal: we know this was a POST with data that should have been // populated into $_POST, but it wasn't. $config = ini_get('post_max_size'); - PhabricatorStartup::didFatal( + self::didFatal( "As received by the server, this request had a nonzero content length ". "but no POST data.\n\n". "Normally, this indicates that it exceeds the 'post_max_size' setting ". "in the PHP configuration on the server. Increase the 'post_max_size' ". "setting or reduce the size of the request.\n\n". "Request size according to 'Content-Length' was '{$length}', ". "'post_max_size' is set to '{$config}'."); } /* -( Rate Limiting )------------------------------------------------------ */ /** * Adjust the permissible rate limit score. * * By default, the limit is `1000`. You can use this method to set it to * a larger or smaller value. If you set it to `2000`, users may make twice * as many requests before rate limiting. * * @param int Maximum score before rate limiting. * @return void * @task ratelimit */ public static function setMaximumRate($rate) { self::$maximumRate = $rate; } /** * Check if the user (identified by `$user_identity`) has issued too many * requests recently. If they have, end the request with a 429 error code. * * The key just needs to identify the user. Phabricator uses both user PHIDs * and user IPs as keys, tracking logged-in and logged-out users separately * and enforcing different limits. * * @param string Some key which identifies the user making the request. * @return void If the user has exceeded the rate limit, this method * does not return. * @task ratelimit */ public static function rateLimitRequest($user_identity) { if (!self::canRateLimit()) { return; } $score = self::getRateLimitScore($user_identity); if ($score > (self::$maximumRate * self::getRateLimitBucketCount())) { // Give the user some bonus points for getting rate limited. This keeps // bad actors who keep slamming the 429 page locked out completely, // instead of letting them get a burst of requests through every minute // after a bucket expires. self::addRateLimitScore($user_identity, 50); self::didRateLimit($user_identity); } } /** * Add points to the rate limit score for some user. * * If users have earned more than 1000 points per minute across all the * buckets they'll be locked out of the application, so awarding 1 point per * request roughly corresponds to allowing 1000 requests per second, while * awarding 50 points roughly corresponds to allowing 20 requests per second. * * @param string Some key which identifies the user making the request. * @param float The cost for this request; more points pushes them toward * the limit faster. * @return void * @task ratelimit */ public static function addRateLimitScore($user_identity, $score) { if (!self::canRateLimit()) { return; } $current = self::getRateLimitBucket(); // There's a bit of a race here, if a second process reads the bucket before // this one writes it, but it's fine if we occasionally fail to record a // user's score. If they're making requests fast enough to hit rate // limiting, we'll get them soon. $bucket_key = self::getRateLimitBucketKey($current); $bucket = apc_fetch($bucket_key); if (!is_array($bucket)) { $bucket = array(); } if (empty($bucket[$user_identity])) { $bucket[$user_identity] = 0; } $bucket[$user_identity] += $score; apc_store($bucket_key, $bucket); } /** * Determine if rate limiting is available. * * Rate limiting depends on APC, and isn't available unless the APC user * cache is available. * * @return bool True if rate limiting is available. * @task ratelimit */ private static function canRateLimit() { if (!self::$maximumRate) { return false; } if (!function_exists('apc_fetch')) { return false; } return true; } /** * Get the current bucket for storing rate limit scores. * * @return int The current bucket. * @task ratelimit */ private static function getRateLimitBucket() { return (int)(time() / 60); } /** * Get the total number of rate limit buckets to retain. * * @return int Total number of rate limit buckets to retain. * @task ratelimit */ private static function getRateLimitBucketCount() { return 5; } /** * Get the APC key for a given bucket. * * @param int Bucket to get the key for. * @return string APC key for the bucket. * @task ratelimit */ private static function getRateLimitBucketKey($bucket) { return 'rate:bucket:'.$bucket; } /** * Get the APC key for the smallest stored bucket. * * @return string APC key for the smallest stored bucket. * @task ratelimit */ private static function getRateLimitMinKey() { return 'rate:min'; } /** * Get the current rate limit score for a given user. * * @param string Unique key identifying the user. * @return float The user's current score. * @task ratelimit */ private static function getRateLimitScore($user_identity) { $min_key = self::getRateLimitMinKey(); // Identify the oldest bucket stored in APC. $cur = self::getRateLimitBucket(); $min = apc_fetch($min_key); // If we don't have any buckets stored yet, store the current bucket as // the oldest bucket. if (!$min) { apc_store($min_key, $cur); $min = $cur; } // Destroy any buckets that are older than the minimum bucket we're keeping // track of. Under load this normally shouldn't do anything, but will clean // up an old bucket once per minute. $count = self::getRateLimitBucketCount(); for ($cursor = $min; $cursor < ($cur - $count); $cursor++) { apc_delete(self::getRateLimitBucketKey($cursor)); apc_store($min_key, $cursor + 1); } // Now, sum up the user's scores in all of the active buckets. $score = 0; for (; $cursor <= $cur; $cursor++) { $bucket = apc_fetch(self::getRateLimitBucketKey($cursor)); if (isset($bucket[$user_identity])) { $score += $bucket[$user_identity]; } } return $score; } /** * Emit an HTTP 429 "Too Many Requests" response (indicating that the user * has exceeded application rate limits) and exit. * * @return exit This method **does not return**. * @task ratelimit */ private static function didRateLimit() { $message = "TOO MANY REQUESTS\n". "You are issuing too many requests too quickly.\n". "To adjust limits, see \"Configuring a Preamble Script\" in the ". "documentation."; header( 'Content-Type: text/plain; charset=utf-8', $replace = true, $http_error = 429); echo $message; exit(1); } }