diff --git a/scripts/symbols/generate_php_symbols.php b/scripts/symbols/generate_php_symbols.php index f8888d1efa..087898fdda 100755 --- a/scripts/symbols/generate_php_symbols.php +++ b/scripts/symbols/generate_php_symbols.php @@ -1,113 +1,113 @@ #!/usr/bin/env php limit(8) as $file => $future) { $tree = XHPASTTree::newFromDataAndResolvedExecFuture( $data[$file], $future->resolve()); $root = $tree->getRootNode(); $scopes = array(); $functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION'); foreach ($functions as $function) { $name = $function->getChildByIndex(2); // Skip anonymous functions if (!$name->getConcreteString()) { continue; } print_symbol($file, 'function', $name); } $classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION'); foreach ($classes as $class) { $class_name = $class->getChildByIndex(1); print_symbol($file, 'class', $class_name); $scopes[] = array($class, $class_name); } $interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION'); foreach ($interfaces as $interface) { $interface_name = $interface->getChildByIndex(1); // We don't differentiate classes and interfaces in highlighters. print_symbol($file, 'class', $interface_name); $scopes[] = array($interface, $interface_name); } $constants = $root->selectDescendantsOfType('n_CONSTANT_DECLARATION_LIST'); foreach ($constants as $constant_list) { foreach ($constant_list->getChildren() as $constant) { $constant_name = $constant->getChildByIndex(0); print_symbol($file, 'constant', $constant_name); } } foreach ($scopes as $scope) { // this prints duplicate symbols in the case of nested classes // luckily, PHP doesn't allow those list($class, $class_name) = $scope; $consts = $class->selectDescendantsOfType( 'n_CLASS_CONSTANT_DECLARATION_LIST'); foreach ($consts as $const_list) { foreach ($const_list->getChildren() as $const) { $const_name = $const->getChildByIndex(0); print_symbol($file, 'class_const', $const_name, $class_name); } } $members = $class->selectDescendantsOfType( 'n_CLASS_MEMBER_DECLARATION_LIST'); foreach ($members as $member_list) { foreach ($member_list->getChildren() as $member) { if ($member->getTypeName() == 'n_CLASS_MEMBER_MODIFIER_LIST') { continue; } $member_name = $member->getChildByIndex(0); print_symbol($file, 'member', $member_name, $class_name); } } $methods = $class->selectDescendantsOfType('n_METHOD_DECLARATION'); foreach ($methods as $method) { $method_name = $method->getChildByIndex(2); print_symbol($file, 'method', $method_name, $class_name); } } } -function print_symbol($file, $type, $token, $context=null) { +function print_symbol($file, $type, $token, $context = null) { $parts = array( $context ? $context->getConcreteString() : '', // variable tokens are `$name`, not just `name`, so strip the $ off of // class field names ltrim($token->getConcreteString(), '$'), $type, 'php', $token->getLineNumber(), '/'.ltrim($file, './'), ); echo implode(' ', $parts)."\n"; } diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php index aa8c12260e..147f9849b6 100644 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php @@ -1,312 +1,313 @@ getResourceURIMapRules() + array( '/~/' => array( '' => 'DarkConsoleController', 'data/(?P[^/]+)/' => 'DarkConsoleDataController', ), ); } protected function getResourceURIMapRules() { $extensions = CelerityResourceController::getSupportedResourceTypes(); $extensions = array_keys($extensions); $extensions = implode('|', $extensions); return array( '/res/' => array( '(?:(?P[0-9]+)T/)?'. '(?P[^/]+)/'. '(?P[a-f0-9]{8})/'. '(?P.+\.(?:'.$extensions.'))' => 'CelerityPhabricatorResourceController', ), ); } /** * @phutil-external-symbol class PhabricatorStartup */ public function buildRequest() { $parser = new PhutilQueryStringParser(); $data = array(); // If the request has "multipart/form-data" content, we can't use // PhutilQueryStringParser to parse it, and the raw data supposedly is not // available anyway (according to the PHP documentation, "php://input" is // not available for "multipart/form-data" requests). However, it is // available at least some of the time (see T3673), so double check that // we aren't trying to parse data we won't be able to parse correctly by // examining the Content-Type header. $content_type = idx($_SERVER, 'CONTENT_TYPE'); $is_form_data = preg_match('@^multipart/form-data@i', $content_type); $raw_input = PhabricatorStartup::getRawInput(); if (strlen($raw_input) && !$is_form_data) { $data += $parser->parseQueryString($raw_input); } else if ($_POST) { $data += $_POST; } $data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', '')); $cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix'); $request = new AphrontRequest($this->getHost(), $this->getPath()); $request->setRequestData($data); $request->setApplicationConfiguration($this); $request->setCookiePrefix($cookie_prefix); return $request; } public function handleException(Exception $ex) { $request = $this->getRequest(); // For Conduit requests, return a Conduit response. if ($request->isConduit()) { $response = new ConduitAPIResponse(); $response->setErrorCode(get_class($ex)); $response->setErrorInfo($ex->getMessage()); return id(new AphrontJSONResponse()) ->setAddJSONShield(false) ->setContent($response->toDictionary()); } // For non-workflow requests, return a Ajax response. if ($request->isAjax() && !$request->isJavelinWorkflow()) { // Log these; they don't get shown on the client and can be difficult // to debug. phlog($ex); $response = new AphrontAjaxResponse(); $response->setError( array( 'code' => get_class($ex), 'info' => $ex->getMessage(), )); return $response; } $user = $request->getUser(); if (!$user) { // If we hit an exception very early, we won't have a user. $user = new PhabricatorUser(); } if ($ex instanceof PhabricatorSystemActionRateLimitException) { $dialog = id(new AphrontDialogView()) ->setTitle(pht('Slow Down!')) ->setUser($user) ->setErrors(array(pht('You are being rate limited.'))) ->appendParagraph($ex->getMessage()) ->appendParagraph($ex->getRateExplanation()) ->addCancelButton('/', pht('Okaaaaaaaaaaaaaay...')); $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } if ($ex instanceof PhabricatorAuthHighSecurityRequiredException) { $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm( $ex->getFactors(), $ex->getFactorValidationResults(), $user, $request); $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Entering High Security')) ->setShortTitle(pht('Security Checkpoint')) ->setWidth(AphrontDialogView::WIDTH_FORM) ->addHiddenInput(AphrontRequest::TYPE_HISEC, true) ->setErrors( array( pht( 'You are taking an action which requires you to enter '. 'high security.'), )) ->appendParagraph( pht( 'High security mode helps protect your account from security '. 'threats, like session theft or someone messing with your stuff '. 'while you\'re grabbing a coffee. To enter high security mode, '. 'confirm your credentials.')) ->appendChild($form->buildLayoutView()) ->appendParagraph( pht( 'Your account will remain in high security mode for a short '. 'period of time. When you are finished taking sensitive '. 'actions, you should leave high security.')) ->setSubmitURI($request->getPath()) ->addCancelButton($ex->getCancelURI()) ->addSubmitButton(pht('Enter High Security')); foreach ($request->getPassthroughRequestParameters() as $key => $value) { $dialog->addHiddenInput($key, $value); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } if ($ex instanceof PhabricatorPolicyException) { if (!$user->isLoggedIn()) { // If the user isn't logged in, just give them a login form. This is // probably a generally more useful response than a policy dialog that // they have to click through to get a login form. // // Possibly we should add a header here like "you need to login to see // the thing you are trying to look at". $login_controller = new PhabricatorAuthStartController($request); $auth_app_class = 'PhabricatorAuthApplication'; $auth_app = PhabricatorApplication::getByClass($auth_app_class); $login_controller->setCurrentApplication($auth_app); return $login_controller->processRequest(); } $list = $ex->getMoreInfo(); foreach ($list as $key => $item) { $list[$key] = phutil_tag('li', array(), $item); } if ($list) { $list = phutil_tag('ul', array(), $list); } $content = array( phutil_tag( 'div', array( 'class' => 'aphront-policy-rejection', ), $ex->getRejection()), phutil_tag( 'div', array( 'class' => 'aphront-capability-details', ), pht('Users with the "%s" capability:', $ex->getCapabilityName())), $list, ); $dialog = new AphrontDialogView(); $dialog ->setTitle($ex->getTitle()) ->setClass('aphront-access-dialog') ->setUser($user) ->appendChild($content); if ($this->getRequest()->isAjax()) { $dialog->addCancelButton('/', pht('Close')); } else { $dialog->addCancelButton('/', pht('OK')); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } if ($ex instanceof AphrontUsageException) { $error = new AphrontErrorView(); $error->setTitle($ex->getTitle()); $error->appendChild($ex->getMessage()); $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->appendChild($error); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); $response->setHTTPResponseCode(500); return $response; } // Always log the unhandled exception. phlog($ex); $class = get_class($ex); $message = $ex->getMessage(); if ($ex instanceof AphrontSchemaQueryException) { $message .= "\n\n". "NOTE: This usually indicates that the MySQL schema has not been ". "properly upgraded. Run 'bin/storage upgrade' to ensure your ". "schema is up to date."; } if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { $trace = id(new AphrontStackTraceView()) ->setUser($user) ->setTrace($ex->getTrace()); } else { $trace = null; } $content = phutil_tag( 'div', array('class' => 'aphront-unhandled-exception'), array( phutil_tag('div', array('class' => 'exception-message'), $message), $trace, )); $dialog = new AphrontDialogView(); $dialog ->setTitle('Unhandled Exception ("'.$class.'")') ->setClass('aphront-exception-dialog') ->setUser($user) ->appendChild($content); if ($this->getRequest()->isAjax()) { $dialog->addCancelButton('/', 'Close'); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); $response->setHTTPResponseCode(500); return $response; } public function willSendResponse(AphrontResponse $response) { return $response; } public function build404Controller() { return array(new Phabricator404Controller($this->getRequest()), array()); } public function buildRedirectController($uri, $external) { return array( new PhabricatorRedirectController($this->getRequest()), array( 'uri' => $uri, 'external' => $external, - )); + ), + ); } } diff --git a/src/aphront/console/plugin/DarkConsoleRequestPlugin.php b/src/aphront/console/plugin/DarkConsoleRequestPlugin.php index d6d9687883..b354b328c2 100644 --- a/src/aphront/console/plugin/DarkConsoleRequestPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleRequestPlugin.php @@ -1,75 +1,76 @@ $_REQUEST, 'Server' => $_SERVER, ); } public function renderPanel() { $data = $this->getData(); $sections = array( 'Basics' => array( 'Machine' => php_uname('n'), ), ); // NOTE: This may not be present for some SAPIs, like php-fpm. if (!empty($data['Server']['SERVER_ADDR'])) { $addr = $data['Server']['SERVER_ADDR']; $sections['Basics']['Host'] = $addr; $sections['Basics']['Hostname'] = @gethostbyaddr($addr); } $sections = array_merge($sections, $data); $mask = array( 'HTTP_COOKIE' => true, 'HTTP_X_PHABRICATOR_CSRF' => true, ); $out = array(); foreach ($sections as $header => $map) { $rows = array(); foreach ($map as $key => $value) { if (isset($mask[$key])) { $rows[] = array( $key, - phutil_tag('em', array(), '(Masked)')); + phutil_tag('em', array(), '(Masked)'), + ); } else { $rows[] = array( $key, (is_array($value) ? json_encode($value) : $value), ); } } $table = new AphrontTableView($rows); $table->setHeaders( array( $header, null, )); $table->setColumnClasses( array( 'header', 'wide wrap', )); $out[] = $table->render(); } return phutil_implode_html("\n", $out); } } diff --git a/src/aphront/response/AphrontFileResponse.php b/src/aphront/response/AphrontFileResponse.php index fae9f8af17..a5a7e90aa1 100644 --- a/src/aphront/response/AphrontFileResponse.php +++ b/src/aphront/response/AphrontFileResponse.php @@ -1,92 +1,93 @@ allowOrigins[] = $origin; return $this; } public function setDownload($download) { $download = preg_replace('/[^A-Za-z0-9_.-]/', '_', $download); if (!strlen($download)) { $download = 'untitled_document.txt'; } $this->download = $download; return $this; } public function getDownload() { return $this->download; } public function setMimeType($mime_type) { $this->mimeType = $mime_type; return $this; } public function getMimeType() { return $this->mimeType; } public function setContent($content) { $this->content = $content; return $this; } public function buildResponseString() { if ($this->rangeMin || $this->rangeMax) { $length = ($this->rangeMax - $this->rangeMin) + 1; return substr($this->content, $this->rangeMin, $length); } else { return $this->content; } } public function setRange($min, $max) { $this->rangeMin = $min; $this->rangeMax = $max; return $this; } public function getHeaders() { $headers = array( array('Content-Type', $this->getMimeType()), array('Content-Length', strlen($this->buildResponseString())), ); if ($this->rangeMin || $this->rangeMax) { $len = strlen($this->content); $min = $this->rangeMin; $max = $this->rangeMax; $headers[] = array('Content-Range', "bytes {$min}-{$max}/{$len}"); } if (strlen($this->getDownload())) { $headers[] = array('X-Download-Options', 'noopen'); $filename = $this->getDownload(); $headers[] = array( 'Content-Disposition', 'attachment; filename='.$filename, ); } if ($this->allowOrigins) { $headers[] = array( 'Access-Control-Allow-Origin', - implode(',', $this->allowOrigins)); + implode(',', $this->allowOrigins), + ); } $headers = array_merge(parent::getHeaders(), $headers); return $headers; } } diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php index 759b03b091..e0f0730081 100644 --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -1,147 +1,152 @@ request = $request; return $this; } public function getRequest() { return $this->request; } public function getHeaders() { $headers = array(); if (!$this->frameable) { $headers[] = array('X-Frame-Options', 'Deny'); } 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() { 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')); $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 ') !== false) { throw new Exception( 'Literal is not allowed inside inline script.'); } if (strpos($data, ' because it is ignored by HTML parsers. We // would need to send the document with XHTML content type. return phutil_tag( 'script', array('type' => 'text/javascript'), phutil_safe_html($data)); } public function buildAjaxResponse($payload, $error = null) { $response = array( 'error' => $error, 'payload' => $payload, ); if ($this->metadata) { $response['javelin_metadata'] = $this->metadata; $this->metadata = array(); } if ($this->behaviors) { $response['javelin_behaviors'] = $this->behaviors; $this->behaviors = array(); } $this->resolveResources(); $resources = array(); foreach ($this->packaged as $source_name => $resource_names) { $map = CelerityResourceMap::getNamedInstance($source_name); foreach ($resource_names as $resource_name) { $resources[] = $this->getURI($map, $resource_name); } } if ($resources) { $response['javelin_resources'] = $resources; } return $response; } public function getURI( CelerityResourceMap $map, $name, $use_primary_domain = false) { $uri = $map->getURIForName($name); // In developer mode, we dump file modification times into the URI. When a // page is reloaded in the browser, any resources brought in by Ajax calls // do not trigger revalidation, so without this it's very difficult to get // changes to Ajaxed-in CSS to work (you must clear your cache or rerun // the map script). In production, we can assume the map script gets run // after changes, and safely skip this. if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { $mtime = $map->getModifiedTimeForName($name); $uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri); } if ($use_primary_domain) { return PhabricatorEnv::getURI($uri); } else { return PhabricatorEnv::getCDNURI($uri); } } } diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php index 25df41bedd..3a5102568c 100644 --- a/src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php +++ b/src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php @@ -1,92 +1,93 @@ getConfig('flowdock.organization'); if (empty($organization)) { $this->getConfig('organization'); } if (empty($organization)) { throw new Exception( '"flowdock.organization" configuration variable not set'); } $ssl = $this->getConfig('ssl'); $url = ($ssl) ? 'https://' : 'http://'; $url .= "{$this->authtoken}@stream.flowdock.com"; $url .= "/flows/{$organization}/{$channel}"; return $url; } protected function processMessage($m_obj) { $command = null; switch ($m_obj['event']) { case 'message': $command = 'MESSAGE'; break; default: // For now, ignore anything which we don't otherwise know about. break; } if ($command === null) { return false; } // TODO: These should be usernames, not user IDs. $sender = id(new PhabricatorBotUser()) ->setName($m_obj['user']); $target = id(new PhabricatorBotChannel()) ->setName($m_obj['flow']); return id(new PhabricatorBotMessage()) ->setCommand($command) ->setSender($sender) ->setTarget($target) ->setBody($m_obj['content']); } public function writeMessage(PhabricatorBotMessage $message) { switch ($message->getCommand()) { case 'MESSAGE': $this->speak( $message->getBody(), $message->getTarget()); break; } } private function speak( $body, PhabricatorBotTarget $flow) { // The $flow->getName() returns the flow's UUID, // as such, the Flowdock API does not require the organization // to be specified in the URI $this->performPost( '/messages', array( 'flow' => $flow->getName(), 'event' => 'message', - 'content' => $body)); + 'content' => $body, + )); } public function __destruct() { if ($this->readHandles) { foreach ($this->readHandles as $read_handle) { curl_multi_remove_handle($this->multiHandle, $read_handle); curl_close($read_handle); } } curl_multi_close($this->multiHandle); } } diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php index 42054ec723..008ed9d7d6 100644 --- a/src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php +++ b/src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php @@ -1,112 +1,114 @@ getConfig('ssl'); $url = ($ssl) ? 'https://' : 'http://'; $url .= "streaming.campfirenow.com/room/{$channel}/live.json"; return $url; } protected function processMessage($m_obj) { $command = null; switch ($m_obj['type']) { case 'TextMessage': $command = 'MESSAGE'; break; case 'PasteMessage': $command = 'PASTE'; break; default: // For now, ignore anything which we don't otherwise know about. break; } if ($command === null) { return false; } // TODO: These should be usernames, not user IDs. $sender = id(new PhabricatorBotUser()) ->setName($m_obj['user_id']); $target = id(new PhabricatorBotChannel()) ->setName($m_obj['room_id']); return id(new PhabricatorBotMessage()) ->setCommand($command) ->setSender($sender) ->setTarget($target) ->setBody($m_obj['body']); } public function writeMessage(PhabricatorBotMessage $message) { switch ($message->getCommand()) { case 'MESSAGE': $this->speak( $message->getBody(), $message->getTarget()); break; case 'SOUND': $this->speak( $message->getBody(), $message->getTarget(), 'SoundMessage'); break; case 'PASTE': $this->speak( $message->getBody(), $message->getTarget(), 'PasteMessage'); break; } } protected function joinRoom($room_id) { $this->performPost("/room/{$room_id}/join.json"); $this->inRooms[$room_id] = true; } private function leaveRoom($room_id) { $this->performPost("/room/{$room_id}/leave.json"); unset($this->inRooms[$room_id]); } private function speak( $message, PhabricatorBotTarget $channel, $type = 'TextMessage') { $room_id = $channel->getName(); $this->performPost( "/room/{$room_id}/speak.json", array( 'message' => array( 'type' => $type, - 'body' => $message))); + 'body' => $message, + ), + )); } public function __destruct() { foreach ($this->inRooms as $room_id => $ignored) { $this->leaveRoom($room_id); } if ($this->readHandles) { foreach ($this->readHandles as $read_handle) { curl_multi_remove_handle($this->multiHandle, $read_handle); curl_close($read_handle); } } curl_multi_close($this->multiHandle); } } diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php index 504605982c..c5c711593f 100644 --- a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php +++ b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php @@ -1,278 +1,281 @@ getConfig('network', $this->getConfig('server')); } // Hash map of command translations public static $commandTranslations = array( - 'PRIVMSG' => 'MESSAGE'); + 'PRIVMSG' => 'MESSAGE', + ); public function connect() { $nick = $this->getConfig('nick', 'phabot'); $server = $this->getConfig('server'); $port = $this->getConfig('port', 6667); $pass = $this->getConfig('pass'); $ssl = $this->getConfig('ssl', false); $user = $this->getConfig('user', $nick); if (!preg_match('/^[A-Za-z0-9_`[{}^|\]\\-]+$/', $nick)) { throw new Exception( "Nickname '{$nick}' is invalid!"); } $errno = null; $error = null; if (!$ssl) { $socket = fsockopen($server, $port, $errno, $error); } else { $socket = fsockopen('ssl://'.$server, $port, $errno, $error); } if (!$socket) { throw new Exception("Failed to connect, #{$errno}: {$error}"); } $ok = stream_set_blocking($socket, false); if (!$ok) { throw new Exception('Failed to set stream nonblocking.'); } $this->socket = $socket; if ($pass) { $this->write("PASS {$pass}"); } $this->write("NICK {$nick}"); $this->write("USER {$user} 0 * :{$user}"); } public function getNextMessages($poll_frequency) { $messages = array(); $read = array($this->socket); if (strlen($this->writeBuffer)) { $write = array($this->socket); } else { $write = array(); } $except = array(); $ok = @stream_select($read, $write, $except, $timeout_sec = 1); if ($ok === false) { // We may have been interrupted by a signal, like a SIGINT. Try // selecting again. If the second select works, conclude that the failure // was most likely because we were signaled. $ok = @stream_select($read, $write, $except, $timeout_sec = 0); if ($ok === false) { throw new Exception('stream_select() failed!'); } } if ($read) { // Test for connection termination; in PHP, fread() off a nonblocking, // closed socket is empty string. if (feof($this->socket)) { // This indicates the connection was terminated on the other side, // just exit via exception and let the overseer restart us after a // delay so we can reconnect. throw new Exception('Remote host closed connection.'); } do { $data = fread($this->socket, 4096); if ($data === false) { throw new Exception('fread() failed!'); } else { $messages[] = id(new PhabricatorBotMessage()) ->setCommand('LOG') ->setBody('>>> '.$data); $this->readBuffer .= $data; } } while (strlen($data)); } if ($write) { do { $len = fwrite($this->socket, $this->writeBuffer); if ($len === false) { throw new Exception('fwrite() failed!'); } else if ($len === 0) { break; } else { $messages[] = id(new PhabricatorBotMessage()) ->setCommand('LOG') ->setBody('>>> '.substr($this->writeBuffer, 0, $len)); $this->writeBuffer = substr($this->writeBuffer, $len); } } while (strlen($this->writeBuffer)); } while (($m = $this->processReadBuffer()) !== false) { if ($m !== null) { $messages[] = $m; } } return $messages; } private function write($message) { $this->writeBuffer .= $message."\r\n"; return $this; } public function writeMessage(PhabricatorBotMessage $message) { switch ($message->getCommand()) { case 'MESSAGE': case 'PASTE': $name = $message->getTarget()->getName(); $body = $message->getBody(); $this->write("PRIVMSG {$name} :{$body}"); return true; default: return false; } } private function processReadBuffer() { $until = strpos($this->readBuffer, "\r\n"); if ($until === false) { return false; } $message = substr($this->readBuffer, 0, $until); $this->readBuffer = substr($this->readBuffer, $until + 2); $pattern = '/^'. '(?::(?P(\S+?))(?:!\S*)? )?'. // This may not be present. '(?P[A-Z0-9]+) '. '(?P.*)'. '$/'; $matches = null; if (!preg_match($pattern, $message, $matches)) { throw new Exception("Unexpected message from server: {$message}"); } if ($this->handleIRCProtocol($matches)) { return null; } $command = $this->getBotCommand($matches['command']); list($target, $body) = $this->parseMessageData($command, $matches['data']); if (!strlen($matches['sender'])) { $sender = null; } else { $sender = id(new PhabricatorBotUser()) ->setName($matches['sender']); } $bot_message = id(new PhabricatorBotMessage()) ->setSender($sender) ->setCommand($command) ->setTarget($target) ->setBody($body); return $bot_message; } private function handleIRCProtocol(array $matches) { $data = $matches['data']; switch ($matches['command']) { case '433': // Nickname already in use // If we receive this error, try appending "-1", "-2", etc. to the nick $this->nickIncrement++; $nick = $this->getConfig('nick', 'phabot').'-'.$this->nickIncrement; $this->write("NICK {$nick}"); return true; case '422': // Error - no MOTD case '376': // End of MOTD $nickpass = $this->getConfig('nickpass'); if ($nickpass) { $this->write("PRIVMSG nickserv :IDENTIFY {$nickpass}"); } $join = $this->getConfig('join'); if (!$join) { throw new Exception('Not configured to join any channels!'); } foreach ($join as $channel) { $this->write("JOIN {$channel}"); } return true; case 'PING': $this->write("PONG {$data}"); return true; } return false; } private function getBotCommand($irc_command) { if (isset(self::$commandTranslations[$irc_command])) { return self::$commandTranslations[$irc_command]; } // We have no translation for this command, use as-is return $irc_command; } private function parseMessageData($command, $data) { switch ($command) { case 'MESSAGE': $matches = null; if (preg_match('/^(\S+)\s+:?(.*)$/', $data, $matches)) { $target_name = $matches[1]; if (strncmp($target_name, '#', 1) === 0) { $target = id(new PhabricatorBotChannel()) ->setName($target_name); } else { $target = id(new PhabricatorBotUser()) ->setName($target_name); } return array( $target, - rtrim($matches[2], "\r\n")); + rtrim($matches[2], "\r\n"), + ); } break; } // By default we assume there is no target, only a body return array( null, - $data); + $data, + ); } public function disconnect() { // NOTE: FreeNode doesn't show quit messages if you've recently joined a // channel, presumably to prevent some kind of abuse. If you're testing // this, you may need to stay connected to the network for a few minutes // before it works. If you disconnect too quickly, the server will replace // your message with a "Client Quit" message. $quit = $this->getConfig('quit', pht('Shutting down.')); $this->write("QUIT :{$quit}"); // Flush the write buffer. while (strlen($this->writeBuffer)) { $this->getNextMessages(0); } @fclose($this->socket); $this->socket = null; } } diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php index 59a921555c..cc1cda2f77 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php @@ -1,43 +1,43 @@ getCommand()) { case 'MESSAGE': $message_body = $message->getBody(); $now = time(); $prompt = '~what( i|\')?s new\?~i'; if (preg_match($prompt, $message_body)) { if ($now < $this->floodblock) { return; } $this->floodblock = $now + 60; $this->reportNew($message); } break; } } public function reportNew(PhabricatorBotMessage $message) { $latest = $this->getConduit()->callMethodSynchronous( 'feed.query', array( 'limit' => 5, - 'view' => 'text' + 'view' => 'text', )); foreach ($latest as $feed_item) { if (isset($feed_item['text'])) { $this->replyTo($message, html_entity_decode($feed_item['text'])); } } } } diff --git a/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php b/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php index ab40c38264..8a3aad0413 100644 --- a/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php +++ b/src/infrastructure/diff/view/PhabricatorInlineSummaryView.php @@ -1,116 +1,118 @@ groups[$name])) { $this->groups[$name] = $items; } else { $this->groups[$name] = array_merge($this->groups[$name], $items); } return $this; } public function render() { require_celerity_resource('inline-comment-summary-css'); return hsprintf('%s%s', $this->renderHeader(), $this->renderTable()); } private function renderHeader() { $icon = id(new PHUIIconView()) ->setIconFont('fa-comment bluegrey msr'); $header = phutil_tag_div( 'phabricator-inline-summary', array( $icon, - pht('Inline Comments'))); + pht('Inline Comments'), + )); return $header; } private function renderTable() { $rows = array(); foreach ($this->groups as $group => $items) { $has_where = false; foreach ($items as $item) { if (!empty($item['where'])) { $has_where = true; break; } } $rows[] = phutil_tag( 'tr', array(), phutil_tag('th', array('colspan' => 3), $group)); foreach ($items as $item) { $line = $item['line']; $length = $item['length']; if ($length) { $lines = $line."\xE2\x80\x93".($line + $length); } else { $lines = $line; } if (isset($item['href'])) { $href = $item['href']; $target = '_blank'; $tail = " \xE2\x86\x97"; } else { $href = '#inline-'.$item['id']; $target = null; $tail = null; } if ($href) { $icon = id(new PHUIIconView()) ->setIconFont('fa-share white msr'); $lines = phutil_tag( 'a', array( 'href' => $href, 'target' => $target, 'class' => 'num', ), array( $icon, $lines, $tail, )); } $where = idx($item, 'where'); $colspan = ($has_where ? null : 2); $rows[] = phutil_tag( 'tr', array(), array( phutil_tag('td', array('class' => 'inline-line-number'), $lines), ($has_where ? phutil_tag('td', array('class' => 'inline-which-diff'), $where) : null), phutil_tag( 'td', array( 'class' => 'inline-summary-content', 'colspan' => $colspan, ), - phutil_tag_div('phabricator-remarkup', $item['content'])))); + phutil_tag_div('phabricator-remarkup', $item['content'])), + )); } } return phutil_tag( 'table', array( 'class' => 'phabricator-inline-summary-table', ), phutil_implode_html("\n", $rows)); } } diff --git a/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php index ccf452215a..855cf62130 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorBaseEnglishTranslation.php @@ -1,1101 +1,1101 @@ array( 'No daemon with id %s exists!', 'No daemons with ids %s exist!', ), 'These %d configuration value(s) are related:' => array( 'This configuration value is related:', 'These configuration values are related:', ), 'Differential Revision(s)' => array( 'Differential Revision', 'Differential Revisions', ), 'file(s)' => array('file', 'files'), 'Maniphest Task(s)' => array('Maniphest Task', 'Maniphest Tasks'), 'Task(s)' => array('Task', 'Tasks'), 'Please fix these errors and try again.' => array( 'Please fix this error and try again.', 'Please fix these errors and try again.', ), '%d Error(s)' => array('%d Error', '%d Errors'), '%d Warning(s)' => array('%d Warning', '%d Warnings'), '%d Auto-Fix(es)' => array('%d Auto-Fix', '%d Auto-Fixes'), '%d Advice(s)' => array('%d Advice', '%d Pieces of Advice'), '%d Detail(s)' => array('%d Detail', '%d Details'), '(%d line(s))' => array('(%d line)', '(%d lines)'), 'COMMIT(S)' => array('COMMIT', 'COMMITS'), '%d line(s)' => array('%d line', '%d lines'), '%d path(s)' => array('%d path', '%d paths'), '%d diff(s)' => array('%d diff', '%d diffs'), 'added %d commit(s): %s' => array( 'added commit: %2$s', 'added commits: %2$s', ), 'removed %d commit(s): %s' => array( 'removed commit: %2$s', 'removed commits: %2$s', ), 'changed %d commit(s), added %d: %s; removed %d: %s' => 'changed commits, added: %3$s; removed: %5$s', 'ATTACHED %d COMMIT(S)' => array( 'ATTACHED COMMIT', 'ATTACHED COMMITS', ), 'added %d mock(s): %s' => array( 'added a mock: %2$s', 'added mocks: %2$s', ), 'removed %d mock(s): %s' => array( 'removed a mock: %2$s', 'removed mocks: %2$s', ), 'changed %d mock(s), added %d: %s; removed %d: %s' => 'changed mocks, added: %3$s; removed: %5$s', 'ATTACHED %d MOCK(S)' => array( 'ATTACHED MOCK', 'ATTACHED MOCKS', ), 'added %d dependencie(s): %s' => array( 'added dependency: %2$s', 'added dependencies: %2$s', ), 'added %d dependent task(s): %s' => array( 'added dependent task: %2$s', 'added dependent tasks: %2$s', ), 'removed %d dependencie(s): %s' => array( 'removed dependency: %2$s', 'removed dependencies: %2$s', ), 'removed %d dependent task(s): %s' => array( 'removed dependent task: %2$s', 'removed dependent tasks: %2$s', ), 'changed %d dependencie(s), added %d: %s; removed %d: %s' => 'changed dependencies, added: %3$s; removed: %5$s', 'changed %d dependent task(s), added %d: %s; removed %d: %s', 'changed dependent tasks, added: %3$s; removed: %5$s', 'DEPENDENT %d TASK(s)' => array( 'DEPENDENT TASK', 'DEPENDENT TASKS', ), 'DEPENDS ON %d TASK(S)' => array( 'DEPENDS ON TASK', 'DEPENDS ON TASKS', ), 'DIFFERENTIAL %d REVISION(S)' => array( 'DIFFERENTIAL REVISION', 'DIFFERENTIAL REVISIONS', ), 'added %d revision(s): %s' => array( 'added revision: %2$s', 'added revisions: %2$s', ), 'removed %d revision(s): %s' => array( 'removed revision: %2$s', 'removed revisions: %2$s', ), 'changed %d revision(s), added %d: %s; removed %d: %s' => 'changed revisions, added %3$s; removed %5$s', '%s edited revision(s), added %d: %s; removed %d: %s.' => '%s edited revisions, added: %3$s; removed: %5$s', 'There are %d raw fact(s) in storage.' => array( 'There is %d raw fact in storage.', 'There are %d raw facts in storage.', ), 'There are %d aggregate fact(s) in storage.' => array( 'There is %d aggregate fact in storage.', 'There are %d aggregate facts in storage.', ), '%d Commit(s) Awaiting Audit' => array( '%d Commit Awaiting Audit', '%d Commits Awaiting Audit', ), '%d Problem Commit(s)' => array( '%d Problem Commit', '%d Problem Commits', ), '%d Review(s) Blocking Others' => array( '%d Review Blocking Others', '%d Reviews Blocking Others', ), '%d Review(s) Need Attention' => array( '%d Review Needs Attention', '%d Reviews Need Attention', ), '%d Review(s) Waiting on Others' => array( '%d Review Waiting on Others', '%d Reviews Waiting on Others', ), '%d Flagged Object(s)' => array( '%d Flagged Object', '%d Flagged Objects', ), '%d Unbreak Now Task(s)!' => array( '%d Unbreak Now Task!', '%d Unbreak Now Tasks!', ), '%d Assigned Task(s)' => array( '%d Assigned Task', '%d Assigned Tasks', ), 'Show %d Lint Message(s)' => array( 'Show %d Lint Message', 'Show %d Lint Messages', ), 'Hide %d Lint Message(s)' => array( 'Hide %d Lint Message', 'Hide %d Lint Messages', ), 'Switch for %d Lint Message(s)' => array( 'Switch for %d Lint Message', 'Switch for %d Lint Messages', ), '%d Lint Message(s)' => array( '%d Lint Message', '%d Lint Messages', ), 'This is a binary file. It is %s byte(s) in length.' => array( 'This is a binary file. It is %s byte in length.', 'This is a binary file. It is %s bytes in length.', ), '%d Action(s) Have No Effect' => array( 'Action Has No Effect', 'Actions Have No Effect', ), '%d Action(s) With No Effect' => array( 'Action With No Effect', 'Actions With No Effect', ), 'Some of your %d action(s) have no effect:' => array( 'One of your actions has no effect:', 'Some of your actions have no effect:', ), 'Apply remaining %d action(s)?' => array( 'Apply remaining action?', 'Apply remaining actions?', ), 'Apply %d Other Action(s)' => array( 'Apply Remaining Action', 'Apply Remaining Actions', ), 'The %d action(s) you are taking have no effect:' => array( 'The action you are taking has no effect:', 'The actions you are taking have no effect:', ), '%s edited post(s), added %d: %s; removed %d: %s.' => '%s edited posts, added: %3$s; removed: %5$s', '%s added %d post(s): %s.' => array( array( '%s added a post: %3$s.', '%s added posts: %3$s.', ), ), '%s removed %d post(s): %s.' => array( array( '%s removed a post: %3$s.', '%s removed posts: %3$s.', ), ), '%s edited blog(s), added %d: %s; removed %d: %s.' => '%s edited blogs, added: %3$s; removed: %5$s', '%s added %d blog(s): %s.' => array( array( '%s added a blog: %3$s.', '%s added blogs: %3$s.', ), ), '%s removed %d blog(s): %s.' => array( array( '%s removed a blog: %3$s.', '%s removed blogs: %3$s.', ), ), '%s edited blogger(s), added %d: %s; removed %d: %s.' => '%s edited bloggers, added: %3$s; removed: %5$s', '%s added %d blogger(s): %s.' => array( array( '%s added a blogger: %3$s.', '%s added bloggers: %3$s.', ), ), '%s removed %d blogger(s): %s.' => array( array( '%s removed a blogger: %3$s.', '%s removed bloggers: %3$s.', ), ), '%s edited member(s), added %d: %s; removed %d: %s.' => '%s edited members, added: %3$s; removed: %5$s', '%s added %d member(s): %s.' => array( array( '%s added a member: %3$s.', '%s added members: %3$s.', ), ), '%s removed %d member(s): %s.' => array( array( '%s removed a member: %3$s.', '%s removed members: %3$s.', ), ), '%s edited project(s), added %d: %s; removed %d: %s.' => '%s edited projects, added: %3$s; removed: %5$s', '%s added %d project(s): %s.' => array( array( '%s added a project: %3$s.', '%s added projects: %3$s.', ), ), '%s removed %d project(s): %s.' => array( array( '%s removed a project: %3$s.', '%s removed projects: %3$s.', ), ), '%s changed project(s) of %s, added %d: %s; removed %d: %s' => '%s changed projects of %s, added: %4$s; removed: %6$s', '%s added %d project(s) to %s: %s' => array( array( '%s added a project to %3$s: %4$s', '%s added projects to %3$s: %4$s', ), ), '%s removed %d project(s) from %s: %s' => array( array( '%s removed a project from %3$s: %4$s', '%s removed projects from %3$s: %4$s', ), ), '%s merged %d task(s): %s.' => array( array( '%s merged a task: %3$s.', '%s merged tasks: %3$s.', ), ), '%s merged %d task(s) %s into %s.' => array( array( '%s merged %3$s into %4$s.', '%s merged tasks %3$s into %4$s.', ), ), '%s edited voting user(s), added %d: %s; removed %d: %s.' => '%s edited voting users, added: %3$s; removed: %5$s', '%s added %d voting user(s): %s.' => array( array( '%s added a voting user: %3$s.', '%s added voting users: %3$s.', ), ), '%s removed %d voting user(s): %s.' => array( array( '%s removed a voting user: %3$s.', '%s removed voting users: %3$s.', ), ), '%s added %d blocking task(s): %s.' => array( array( '%s added a blocking task: %3$s.', - '%s added blocking tasks: %3$s.' + '%s added blocking tasks: %3$s.', ), ), '%s added %d blocked task(s): %s.' => array( array( '%s added a blocked task: %3$s.', - '%s added blocked tasks: %3$s.' - ) + '%s added blocked tasks: %3$s.', + ), ), '%s removed %d blocking task(s): %s.' => array( array( '%s removed a blocking task: %3$s.', - '%s removed blocking tasks: %3$s.' + '%s removed blocking tasks: %3$s.', ), ), '%s removed %d blocked task(s): %s.' => array( array( '%s removed a blocked task: %3$s.', - '%s removed blocked tasks: %3$s.' - ) + '%s removed blocked tasks: %3$s.', + ), ), '%s edited answer(s), added %d: %s; removed %d: %s.' => '%s edited answers, added: %3$s; removed: %5$s', '%s added %d answer(s): %s.' => array( array( '%s added an answer: %3$s.', '%s added answers: %3$s.', ), ), '%s removed %d answer(s): %s.' => array( array( '%s removed a answer: %3$s.', '%s removed answers: %3$s.', ), ), '%s edited question(s), added %d: %s; removed %d: %s.' => '%s edited questions, added: %3$s; removed: %5$s', '%s added %d question(s): %s.' => array( array( '%s added a question: %3$s.', '%s added questions: %3$s.', ), ), '%s removed %d question(s): %s.' => array( array( '%s removed a question: %3$s.', '%s removed questions: %3$s.', ), ), '%s edited mock(s), added %d: %s; removed %d: %s.' => '%s edited mocks, added: %3$s; removed: %5$s', '%s added %d mock(s): %s.' => array( array( '%s added a mock: %3$s.', '%s added mocks: %3$s.', ), ), '%s removed %d mock(s): %s.' => array( array( '%s removed a mock: %3$s.', '%s removed mocks: %3$s.', ), ), '%s edited task(s), added %d: %s; removed %d: %s.' => '%s edited tasks, added: %3$s; removed: %5$s', '%s added %d task(s): %s.' => array( array( '%s added a task: %3$s.', '%s added tasks: %3$s.', ), ), '%s removed %d task(s): %s.' => array( array( '%s removed a task: %3$s.', '%s removed tasks: %3$s.', ), ), '%s edited file(s), added %d: %s; removed %d: %s.' => '%s edited files, added: %3$s; removed: %5$s', '%s added %d file(s): %s.' => array( array( '%s added a file: %3$s.', '%s added files: %3$s.', ), ), '%s removed %d file(s): %s.' => array( array( '%s removed a file: %3$s.', '%s removed files: %3$s.', ), ), '%s edited account(s), added %d: %s; removed %d: %s.' => '%s edited accounts, added: %3$s; removed: %5$s', '%s added %d account(s): %s.' => array( array( '%s added a account: %3$s.', '%s added accounts: %3$s.', ), ), '%s removed %d account(s): %s.' => array( array( '%s removed a account: %3$s.', '%s removed accounts: %3$s.', ), ), '%s edited charge(s), added %d: %s; removed %d: %s.' => '%s edited charges, added: %3$s; removed: %5$s', '%s added %d charge(s): %s.' => array( array( '%s added a charge: %3$s.', '%s added charges: %3$s.', ), ), '%s removed %d charge(s): %s.' => array( array( '%s removed a charge: %3$s.', '%s removed charges: %3$s.', ), ), '%s edited purchase(s), added %d: %s; removed %d: %s.' => '%s edited purchases, added: %3$s; removed: %5$s', '%s added %d purchase(s): %s.' => array( array( '%s added a purchase: %3$s.', '%s added purchases: %3$s.', ), ), '%s removed %d purchase(s): %s.' => array( array( '%s removed a purchase: %3$s.', '%s removed purchases: %3$s.', ), ), '%s edited contributor(s), added %d: %s; removed %d: %s.' => '%s edited contributors, added: %3$s; removed: %5$s', '%s added %d contributor(s): %s.' => array( array( '%s added a contributor: %3$s.', '%s added contributors: %3$s.', ), ), '%s removed %d contributor(s): %s.' => array( array( '%s removed a contributor: %3$s.', '%s removed contributors: %3$s.', ), ), '%s edited reviewer(s), added %d: %s; removed %d: %s.' => '%s edited reviewers, added: %3$s; removed: %5$s', '%s added %d reviewer(s): %s.' => array( array( '%s added a reviewer: %3$s.', '%s added reviewers: %3$s.', ), ), '%s removed %d reviewer(s): %s.' => array( array( '%s removed a reviewer: %3$s.', '%s removed reviewers: %3$s.', ), ), '%s edited object(s), added %d: %s; removed %d: %s.' => '%s edited objects, added: %3$s; removed: %5$s', '%s added %d object(s): %s.' => array( array( '%s added a object: %3$s.', '%s added objects: %3$s.', ), ), '%s removed %d object(s): %s.' => array( array( '%s removed a object: %3$s.', '%s removed objects: %3$s.', ), ), '%d other(s)' => array( '1 other', '%d others', ), '%s edited subscriber(s), added %d: %s; removed %d: %s.' => '%s edited subscribers, added: %3$s; removed: %5$s', '%s added %d subscriber(s): %s.' => array( array( '%s added a subscriber: %3$s.', '%s added subscribers: %3$s.', ), ), '%s removed %d subscriber(s): %s.' => array( array( '%s removed a subscriber: %3$s.', '%s removed subscribers: %3$s.', ), ), '%s edited unsubscriber(s), added %d: %s; removed %d: %s.' => '%s edited unsubscribers, added: %3$s; removed: %5$s', '%s added %d unsubscriber(s): %s.' => array( array( '%s added a unsubscriber: %3$s.', '%s added unsubscribers: %3$s.', ), ), '%s removed %d unsubscriber(s): %s.' => array( array( '%s removed a unsubscriber: %3$s.', '%s removed unsubscribers: %3$s.', ), ), '%s edited participant(s), added %d: %s; removed %d: %s.' => '%s edited participants, added: %3$s; removed: %5$s', '%s added %d participant(s): %s.' => array( array( '%s added a participant: %3$s.', '%s added participants: %3$s.', ), ), '%s removed %d participant(s): %s.' => array( array( '%s removed a participant: %3$s.', '%s removed participants: %3$s.', ), ), '%s edited image(s), added %d: %s; removed %d: %s.' => '%s edited images, added: %3$s; removed: %5$s', '%s added %d image(s): %s.' => array( array( '%s added an image: %3$s.', '%s added images: %3$s.', ), ), '%s removed %d image(s): %s.' => array( array( '%s removed an image: %3$s.', '%s removed images: %3$s.', ), ), '%d people(s)' => array( array( '%d person', '%d people', ), ), '%s Line(s)' => array( '%s Line', '%s Lines', ), 'Indexing %d object(s) of type %s.' => array( 'Indexing %d object of type %s.', 'Indexing %d object of type %s.', ), 'Run these %d command(s):' => array( 'Run this command:', 'Run these commands:', ), 'Install these %d PHP extension(s):' => array( 'Install this PHP extension:', 'Install these PHP extensions:', ), 'The current Phabricator configuration has these %d value(s):' => array( 'The current Phabricator configuration has this value:', 'The current Phabricator configuration has these values:', ), 'The current MySQL configuration has these %d value(s):' => array( 'The current MySQL configuration has this value:', 'The current MySQL configuration has these values:', ), 'To update these %d value(s), run these command(s) from the command line:' => array( 'To update this value, run this command from the command line:', 'To update these values, run these commands from the command line:', ), 'You can update these %d value(s) here:' => array( 'You can update this value here:', 'You can update these values here:', ), 'The current PHP configuration has these %d value(s):' => array( 'The current PHP configuration has this value:', 'The current PHP configuration has these values:', ), 'To update these %d value(s), edit your PHP configuration file.' => array( 'To update this %d value, edit your PHP configuration file.', 'To update these %d values, edit your PHP configuration file.', ), 'To update these %d value(s), edit your PHP configuration file, located '. 'here:' => array( 'To update this value, edit your PHP configuration file, located '. 'here:', 'To update these values, edit your PHP configuration file, located '. 'here:', ), 'PHP also loaded these configuration file(s):' => array( 'PHP also loaded this configuration file:', 'PHP also loaded these configuration files:', ), 'You have %d unresolved setup issue(s)...' => array( 'You have an unresolved setup issue...', 'You have %d unresolved setup issues...', ), '%s added %d inline comment(s).' => array( array( '%s added an inline comment.', '%s added inline comments.', ), ), '%d comment(s)' => array('%d comment', '%d comments'), '%d rejection(s)' => array('%d rejection', '%d rejections'), '%d update(s)' => array('%d update', '%d updates'), 'This configuration value is defined in these %d '. 'configuration source(s): %s.' => array( 'This configuration value is defined in this '. 'configuration source: %2$s.', 'This configuration value is defined in these %d '. 'configuration sources: %s.', ), '%d Open Pull Request(s)' => array( '%d Open Pull Request', '%d Open Pull Requests', ), 'Stale (%s day(s))' => array( 'Stale (%s day)', 'Stale (%s days)', ), 'Old (%s day(s))' => array( 'Old (%s day)', 'Old (%s days)', ), '%s Commit(s)' => array( '%s Commit', '%s Commits', ), '%s added %d project(s): %s' => array( array( '%s added a project: %3$s', '%s added projects: %3$s', ), ), '%s removed %d project(s): %s' => array( array( '%s removed a project: %3$s', '%s removed projects: %3$s', ), ), '%s changed project(s), added %d: %s; removed %d: %s' => '%s changed projects, added: %3$s; removed: %5$s', '%s attached %d file(s): %s' => array( array( '%s attached a file: %3$s', '%s attached files: %3$s', ), ), '%s detached %d file(s): %s' => array( array( '%s detached a file: %3$s', '%s detached files: %3$s', ), ), '%s changed file(s), attached %d: %s; detached %d: %s' => '%s changed files, attached: %3$s; detached: %5$s', '%s added %d dependencie(s): %s.' => array( array( '%s added a dependency: %3$s', '%s added dependencies: %3$s', ), ), '%s added %d dependent task(s): %s.' => array( array( '%s added a dependent task: %3$s', '%s added dependent tasks: %3$s', ), ), '%s removed %d dependencie(s): %s.' => array( array( '%s removed a dependency: %3$s.', '%s removed dependencies: %3$s.', ), ), '%s removed %d dependent task(s): %s.' => array( array( '%s removed a dependent task: %3$s.', '%s removed dependent tasks: %3$s.', ), ), '%s added %d revision(s): %s.' => array( array( '%s added a revision: %3$s.', '%s added revisions: %3$s.', ), ), '%s removed %d revision(s): %s.' => array( array( '%s removed a revision: %3$s.', '%s removed revisions: %3$s.', ), ), '%s added %d commit(s): %s.' => array( array( '%s added a commit: %3$s.', '%s added commits: %3$s.', ), ), '%s removed %d commit(s): %s.' => array( array( '%s removed a commit: %3$s.', '%s removed commits: %3$s.', ), ), '%s edited commit(s), added %d: %s; removed %d: %s.' => '%s edited commits, added %3$s; removed %5$s.', '%s changed project member(s), added %d: %s; removed %d: %s' => '%s changed project members, added %3$s; removed %5$s', '%s added %d project member(s): %s' => array( array( '%s added a member: %3$s', '%s added members: %3$s', ), ), '%s removed %d project member(s): %s' => array( array( '%s removed a member: %3$s', '%s removed members: %3$s', ), ), '%d project hashtag(s) are already used: %s' => array( 'Project hashtag %2$s is already used.', '%d project hashtags are already used: %2$s', ), '%s changed project hashtag(s), added %d: %s; removed %d: %s' => '%s changed project hashtags, added %3$s; removed %5$s', '%s added %d project hashtag(s): %s' => array( array( '%s added a hashtag: %3$s', '%s added hashtags: %3$s', ), ), '%s removed %d project hashtag(s): %s' => array( array( '%s removed a hashtag: %3$s', '%s removed hashtags: %3$s', ), ), '%d User(s) Need Approval' => array( '%d User Needs Approval', '%d Users Need Approval', ), '%s older changes(s) are hidden.' => array( '%d older change is hidden.', '%d older changes are hidden.', ), '%s, %s line(s)' => array( '%s, %s line', '%s, %s lines', ), '%s pushed %d commit(s) to %s.' => array( array( '%s pushed a commit to %3$s.', '%s pushed %d commits to %s.', ), ), '%s commit(s)' => array( '1 commit', '%s commits', ), '%s removed %d JIRA issue(s): %s.' => array( array( '%s removed a JIRA issue: %3$s.', '%s removed JIRA issues: %3$s.', ), ), '%s added %d JIRA issue(s): %s.' => array( array( '%s added a JIRA issue: %3$s.', '%s added JIRA issues: %3$s.', ), ), '%s added %d required legal document(s): %s.' => array( array( '%s added a required legal document: %3$s.', '%s added required legal documents: %3$s.', ), ), '%s updated JIRA issue(s): added %d %s; removed %d %s.' => '%s updated JIRA issues: added %3$s; removed %5$s.', '%s added %s task(s): %s.' => array( array( '%s added a task: %3$s.', '%s added tasks: %3$s.', ), ), '%s removed %s task(s): %s.' => array( array( '%s removed a task: %3$s.', '%s removed tasks: %3$s.', ), ), '%s edited %s task(s), added %s: %s; removed %s: %s.' => '%s edited tasks, added %4$s; removed %6$s.', '%s added %s task(s) to %s: %s.' => array( array( '%s added a task to %3$s: %4$s.', '%s added tasks to %3$s: %4$s.', ), ), '%s removed %s task(s) from %s: %s.' => array( array( '%s removed a task from %3$s: %4$s.', '%s removed tasks from %3$s: %4$s.', ), ), '%s edited %s task(s) for %s, added %s: %s; removed %s: %s.' => '%s edited tasks for %3$s, added: %5$s; removed %7$s.', '%s added %s commit(s): %s.' => array( array( '%s added a commit: %3$s.', '%s added commits: %3$s.', ), ), '%s removed %s commit(s): %s.' => array( array( '%s removed a commit: %3$s.', '%s removed commits: %3$s.', ), ), '%s edited %s commit(s), added %s: %s; removed %s: %s.' => '%s edited commits, added %4$s; removed %6$s.', '%s added %s commit(s) to %s: %s.' => array( array( '%s added a commit to %3$s: %4$s.', '%s added commits to %3$s: %4$s.', ), ), '%s removed %s commit(s) from %s: %s.' => array( array( '%s removed a commit from %3$s: %4$s.', '%s removed commits from %3$s: %4$s.', ), ), '%s edited %s commit(s) for %s, added %s: %s; removed %s: %s.' => '%s edited commits for %3$s, added: %5$s; removed %7$s.', '%s added %s revision(s): %s.' => array( array( '%s added a revision: %3$s.', '%s added revisions: %3$s.', ), ), '%s removed %s revision(s): %s.' => array( array( '%s removed a revision: %3$s.', '%s removed revisions: %3$s.', ), ), '%s edited %s revision(s), added %s: %s; removed %s: %s.' => '%s edited revisions, added %4$s; removed %6$s.', '%s added %s revision(s) to %s: %s.' => array( array( '%s added a revision to %3$s: %4$s.', '%s added revisions to %3$s: %4$s.', ), ), '%s removed %s revision(s) from %s: %s.' => array( array( '%s removed a revision from %3$s: %4$s.', '%s removed revisions from %3$s: %4$s.', ), ), '%s edited %s revision(s) for %s, added %s: %s; removed %s: %s.' => '%s edited revisions for %3$s, added: %5$s; removed %7$s.', '%s added %s project(s): %s.' => array( array( '%s added a project: %3$s.', '%s added projects: %3$s.', ), ), '%s removed %s project(s): %s.' => array( array( '%s removed a project: %3$s.', '%s removed projects: %3$s.', ), ), '%s edited %s project(s), added %s: %s; removed %s: %s.' => '%s edited projects, added %4$s; removed %6$s.', '%s added %s project(s) to %s: %s.' => array( array( '%s added a project to %3$s: %4$s.', '%s added projects to %3$s: %4$s.', ), ), '%s removed %s project(s) from %s: %s.' => array( array( '%s removed a project from %3$s: %4$s.', '%s removed projects from %3$s: %4$s.', ), ), '%s edited %s project(s) for %s, added %s: %s; removed %s: %s.' => '%s edited projects for %3$s, added: %5$s; removed %7$s.', ); } } diff --git a/src/infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php b/src/infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php index 1cec169180..317ea7146b 100644 --- a/src/infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php +++ b/src/infrastructure/sms/adapter/PhabricatorSMSImplementationTestBlackholeAdapter.php @@ -1,30 +1,31 @@ getID()) { return PhabricatorSMS::STATUS_SENT; } return PhabricatorSMS::STATUS_SENT_UNCONFIRMED; } } diff --git a/src/infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php b/src/infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php index fae9c9515d..7715166a7e 100644 --- a/src/infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php +++ b/src/infrastructure/sms/worker/PhabricatorSMSDemultiplexWorker.php @@ -1,30 +1,31 @@ getTaskData(); $to_numbers = idx($task_data, 'toNumbers'); if (!$to_numbers) { // If we don't have any to numbers, don't send any sms. return; } foreach ($to_numbers as $number) { // NOTE: we will set the fromNumber and the proper provider data // in the `PhabricatorSMSSendWorker`. $sms = PhabricatorSMS::initializeNewSMS($task_data['body']); $sms->setToNumber($number); $sms->save(); $this->queueTask( 'PhabricatorSMSSendWorker', array( - 'smsID' => $sms->getID())); + 'smsID' => $sms->getID(), + )); } } } diff --git a/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php b/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php index 91e50a81dd..720784cfd5 100644 --- a/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php +++ b/src/infrastructure/storage/lisk/__tests__/LiskChunkTestCase.php @@ -1,55 +1,55 @@ assertEqual( array( 'aa', 'bb', 'ccc', 'dd', 'e', ), PhabricatorLiskDAO::chunkSQL($fragments, '', 2)); $fragments = array( - 'a', 'a', 'a', 'XX', 'a', 'a', 'a', 'a' + 'a', 'a', 'a', 'XX', 'a', 'a', 'a', 'a', ); $this->assertEqual( array( 'a, a, a', 'XX, a, a', 'a, a', ), PhabricatorLiskDAO::chunkSQL($fragments, ', ', 8)); $fragments = array( 'xxxxxxxxxx', 'yyyyyyyyyy', 'a', 'b', 'c', 'zzzzzzzzzz', ); $this->assertEqual( array( 'xxxxxxxxxx', 'yyyyyyyyyy', 'a, b, c', 'zzzzzzzzzz', ), PhabricatorLiskDAO::chunkSQL($fragments, ', ', 8)); } } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php index 46ef6d3cf3..9aa38d2c3d 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php @@ -1,80 +1,81 @@ setName('destroy') ->setExamples('**destroy** [__options__]') ->setSynopsis('Permanently destroy all storage and data.') ->setArguments( array( array( 'name' => 'unittest-fixtures', 'help' => 'Restrict **destroy** operations to databases created '. 'by PhabricatorTestCase test fixtures.', - ))); + ), + )); } public function execute(PhutilArgumentParser $args) { $is_dry = $args->getArg('dryrun'); $is_force = $args->getArg('force'); if (!$is_dry && !$is_force) { echo phutil_console_wrap( 'Are you completely sure you really want to permanently destroy all '. 'storage for Phabricator data? This operation can not be undone and '. 'your data will not be recoverable if you proceed.'); if (!phutil_console_confirm('Permanently destroy all data?')) { echo "Cancelled.\n"; exit(1); } if (!phutil_console_confirm('Really destroy all data forever?')) { echo "Cancelled.\n"; exit(1); } } $api = $this->getAPI(); $patches = $this->getPatches(); if ($args->getArg('unittest-fixtures')) { $conn = $api->getConn(null); $databases = queryfx_all( $conn, 'SELECT DISTINCT(TABLE_SCHEMA) AS db '. 'FROM INFORMATION_SCHEMA.TABLES '. 'WHERE TABLE_SCHEMA LIKE %>', PhabricatorTestCase::NAMESPACE_PREFIX); $databases = ipull($databases, 'db'); } else { $databases = $api->getDatabaseList($patches); $databases[] = $api->getDatabaseName('meta_data'); // These are legacy databases that were dropped long ago. See T2237. $databases[] = $api->getDatabaseName('phid'); $databases[] = $api->getDatabaseName('directory'); } foreach ($databases as $database) { if ($is_dry) { echo "DRYRUN: Would drop database '{$database}'.\n"; } else { echo "Dropping database '{$database}'...\n"; queryfx( $api->getConn(null), 'DROP DATABASE IF EXISTS %T', $database); } } if (!$is_dry) { echo "Storage was destroyed.\n"; } return 0; } } diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php index 4f4e8e5f3b..879999468c 100644 --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -1,344 +1,346 @@ 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 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.')); } $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' + 'sigil' => 'aphront-dialog-application-input', )); } if (!$this->renderAsForm) { $buttons = array(phabricator_form( $this->user, $form_attributes, - array_merge($hidden_inputs, $buttons))); + 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 AphrontErrorView())->setErrors($errors), - $children); + $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); } $content = array( phutil_tag( 'div', array( 'class' => 'aphront-dialog-head', ), $header), phutil_tag('div', array( 'class' => 'aphront-dialog-body grouped', ), $children), phutil_tag( 'div', array( 'class' => 'aphront-dialog-tail grouped', ), array( $buttons, $footer, )), ); 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/control/AphrontTokenizerTemplateView.php b/src/view/control/AphrontTokenizerTemplateView.php index dc2dedf37d..db3b35dc22 100644 --- a/src/view/control/AphrontTokenizerTemplateView.php +++ b/src/view/control/AphrontTokenizerTemplateView.php @@ -1,107 +1,108 @@ id = $id; return $this; } public function setValue(array $value) { assert_instances_of($value, 'PhabricatorObjectHandle'); $this->value = $value; return $this; } public function getValue() { return $this->value; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function render() { require_celerity_resource('aphront-tokenizer-control-css'); $id = $this->id; $name = $this->getName(); $values = nonempty($this->getValue(), array()); $tokens = array(); foreach ($values as $key => $value) { $tokens[] = $this->renderToken( $value->getPHID(), $value->getFullName(), $value->getType()); } $input = javelin_tag( 'input', array( 'mustcapture' => true, 'name' => $name, 'class' => 'jx-tokenizer-input', 'sigil' => 'tokenizer-input', 'style' => 'width: 0px;', 'disabled' => 'disabled', 'type' => 'text', )); $content = $tokens; $content[] = $input; $content[] = phutil_tag('div', array('style' => 'clear: both;'), ''); return phutil_tag( 'div', array( 'id' => $id, 'class' => 'jx-tokenizer-container', ), $content); } private function renderToken($key, $value, $icon) { $input_name = $this->getName(); if ($input_name) { $input_name .= '[]'; } if ($icon) { $value = array( phutil_tag( 'span', array( 'class' => 'phui-icon-view phui-font-fa bluetext '.$icon, )), - $value); + $value, + ); } return phutil_tag( 'a', array( 'class' => 'jx-tokenizer-token', ), array( $value, phutil_tag( 'input', array( 'type' => 'hidden', 'name' => $input_name, 'value' => $key, )), phutil_tag('span', array('class' => 'jx-tokenizer-x-placeholder'), ''), )); } } diff --git a/src/view/form/control/AphrontFormCheckboxControl.php b/src/view/form/control/AphrontFormCheckboxControl.php index 6e575fd533..673c1c6d82 100644 --- a/src/view/form/control/AphrontFormCheckboxControl.php +++ b/src/view/form/control/AphrontFormCheckboxControl.php @@ -1,52 +1,52 @@ boxes[] = array( 'name' => $name, 'value' => $value, 'label' => $label, 'checked' => $checked, ); return $this; } protected function getCustomControlClass() { return 'aphront-form-control-checkbox'; } protected function renderInput() { $rows = array(); foreach ($this->boxes as $box) { $id = celerity_generate_unique_node_id(); $checkbox = phutil_tag( 'input', array( 'id' => $id, 'type' => 'checkbox', 'name' => $box['name'], 'value' => $box['value'], 'checked' => $box['checked'] ? 'checked' : null, 'disabled' => $this->getDisabled() ? 'disabled' : null, )); $label = phutil_tag( 'label', array( 'for' => $id, ), $box['label']); $rows[] = phutil_tag('tr', array(), array( phutil_tag('td', array(), $checkbox), - phutil_tag('th', array(), $label) + phutil_tag('th', array(), $label), )); } return phutil_tag( 'table', array('class' => 'aphront-form-control-checkbox-layout'), $rows); } } diff --git a/src/view/form/control/AphrontFormCropControl.php b/src/view/form/control/AphrontFormCropControl.php index 7d1bd7d601..6096520f3b 100644 --- a/src/view/form/control/AphrontFormCropControl.php +++ b/src/view/form/control/AphrontFormCropControl.php @@ -1,94 +1,94 @@ height = $height; return $this; } public function getHeight() { return $this->height; } public function setWidth($width) { $this->width = $width; return $this; } public function getWidth() { return $this->width; } protected function getCustomControlClass() { return 'aphront-form-crop'; } protected function renderInput() { $file = $this->getValue(); if ($file === null) { return phutil_tag( 'img', array( - 'src' => PhabricatorUser::getDefaultProfileImageURI() + 'src' => PhabricatorUser::getDefaultProfileImageURI(), ), ''); } $c_id = celerity_generate_unique_node_id(); $metadata = $file->getMetadata(); $scale = PhabricatorImageTransformer::getScaleForCrop( $file, $this->getWidth(), $this->getHeight()); Javelin::initBehavior( 'aphront-crop', array( 'cropBoxID' => $c_id, 'width' => $this->getWidth(), 'height' => $this->getHeight(), 'scale' => $scale, 'imageH' => $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT], 'imageW' => $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH], )); return javelin_tag( 'div', array( 'id' => $c_id, 'sigil' => 'crop-box', 'mustcapture' => true, - 'class' => 'crop-box' + 'class' => 'crop-box', ), array( javelin_tag( 'img', array( 'src' => $file->getBestURI(), 'class' => 'crop-image', - 'sigil' => 'crop-image' + 'sigil' => 'crop-image', ), ''), javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'image_x', 'sigil' => 'crop-x', ), ''), javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'image_y', 'sigil' => 'crop-y', ), ''), )); } } diff --git a/src/view/form/control/AphrontFormTextControl.php b/src/view/form/control/AphrontFormTextControl.php index a507b0e74e..581f22682d 100644 --- a/src/view/form/control/AphrontFormTextControl.php +++ b/src/view/form/control/AphrontFormTextControl.php @@ -1,55 +1,55 @@ disableAutocomplete = $disable; return $this; } private function getDisableAutocomplete() { return $this->disableAutocomplete; } public function getPlaceholder() { return $this->placeholder; } public function setPlaceholder($placeholder) { $this->placeholder = $placeholder; return $this; } public function getSigil() { return $this->sigil; } public function setSigil($sigil) { $this->sigil = $sigil; return $this; } protected function getCustomControlClass() { return 'aphront-form-control-text'; } protected function renderInput() { return javelin_tag( 'input', array( 'type' => 'text', 'name' => $this->getName(), 'value' => $this->getValue(), 'disabled' => $this->getDisabled() ? 'disabled' : null, 'autocomplete' => $this->getDisableAutocomplete() ? 'off' : null, 'id' => $this->getID(), 'sigil' => $this->getSigil(), - 'placeholder' => $this->getPlaceholder() + 'placeholder' => $this->getPlaceholder(), )); } } diff --git a/src/view/form/control/AphrontFormTextWithSubmitControl.php b/src/view/form/control/AphrontFormTextWithSubmitControl.php index eeaa597c89..07b872d1fe 100644 --- a/src/view/form/control/AphrontFormTextWithSubmitControl.php +++ b/src/view/form/control/AphrontFormTextWithSubmitControl.php @@ -1,57 +1,57 @@ submitLabel = $submit_label; return $this; } public function getSubmitLabel() { return $this->submitLabel; } protected function getCustomControlClass() { return 'aphront-form-control-text-with-submit'; } protected function renderInput() { return phutil_tag( 'div', array( 'class' => 'text-with-submit-control-outer-bounds', ), array( phutil_tag( 'div', array( 'class' => 'text-with-submit-control-text-bounds', ), javelin_tag( 'input', array( 'type' => 'text', 'class' => 'text-with-submit-control-text', 'name' => $this->getName(), 'value' => $this->getValue(), 'disabled' => $this->getDisabled() ? 'disabled' : null, 'id' => $this->getID(), ))), phutil_tag( 'div', array( 'class' => 'text-with-submit-control-submit-bounds', ), javelin_tag( 'input', array( 'type' => 'submit', 'class' => 'text-with-submit-control-submit grey', - 'value' => coalesce($this->getSubmitLabel(), pht('Submit')) + 'value' => coalesce($this->getSubmitLabel(), pht('Submit')), ))), )); } } diff --git a/src/view/layout/AphrontListFilterView.php b/src/view/layout/AphrontListFilterView.php index 11d6631214..7a6be9c3d2 100644 --- a/src/view/layout/AphrontListFilterView.php +++ b/src/view/layout/AphrontListFilterView.php @@ -1,118 +1,118 @@ showAction = $show; $this->hideAction = $hide; $this->showHideDescription = $description; $this->showHideHref = $href; return $this; } public function render() { $content = $this->renderChildren(); if (!$content) { return null; } require_celerity_resource('aphront-list-filter-view-css'); $content = phutil_tag( 'div', array( 'class' => 'aphront-list-filter-view-content', ), $content); $classes = array(); $classes[] = 'aphront-list-filter-view'; if ($this->showAction !== null) { $classes[] = 'aphront-list-filter-view-collapsible'; Javelin::initBehavior('phabricator-reveal-content'); $hide_action_id = celerity_generate_unique_node_id(); $show_action_id = celerity_generate_unique_node_id(); $content_id = celerity_generate_unique_node_id(); $hide_action = javelin_tag( 'a', array( 'class' => 'button grey', 'sigil' => 'reveal-content', 'id' => $hide_action_id, 'href' => $this->showHideHref, 'meta' => array( 'hideIDs' => array($hide_action_id), 'showIDs' => array($content_id, $show_action_id), ), ), $this->showAction); $content_description = phutil_tag( 'div', array( 'class' => 'aphront-list-filter-description', ), $this->showHideDescription); $show_action = javelin_tag( 'a', array( 'class' => 'button grey', 'sigil' => 'reveal-content', 'style' => 'display: none;', 'href' => '#', 'id' => $show_action_id, 'meta' => array( 'hideIDs' => array($content_id, $show_action_id), 'showIDs' => array($hide_action_id), ), ), $this->hideAction); $reveal_block = phutil_tag( 'div', array( 'class' => 'aphront-list-filter-reveal', ), array( $content_description, $hide_action, $show_action, )); $content = array( $reveal_block, phutil_tag( 'div', array( 'id' => $content_id, 'style' => 'display: none;', ), $content), ); } $content = phutil_tag( 'div', array( 'class' => implode(' ', $classes), ), $content); return phutil_tag( 'div', array( - 'class' => 'aphront-list-filter-wrap' + 'class' => 'aphront-list-filter-wrap', ), $content); } } diff --git a/src/view/layout/AphrontMultiColumnView.php b/src/view/layout/AphrontMultiColumnView.php index a5fa4d99d8..6705b89126 100644 --- a/src/view/layout/AphrontMultiColumnView.php +++ b/src/view/layout/AphrontMultiColumnView.php @@ -1,153 +1,156 @@ id = $id; return $this; } public function getID() { return $this->id; } public function addColumn( $column, $class = null, $sigil = null, $metadata = null) { $this->columns[] = array( 'column' => $column, 'class' => $class, 'sigil' => $sigil, - 'metadata' => $metadata); + 'metadata' => $metadata, + ); return $this; } public function setFluidlayout($layout) { $this->fluidLayout = $layout; return $this; } public function setFluidishLayout($layout) { $this->fluidLayout = true; $this->fluidishLayout = $layout; return $this; } public function setGutter($gutter) { $this->gutter = $gutter; return $this; } public function setBorder($border) { $this->border = $border; return $this; } public function render() { require_celerity_resource('aphront-multi-column-view-css'); $classes = array(); $classes[] = 'aphront-multi-column-inner'; $classes[] = 'grouped'; if ($this->fluidishLayout || $this->fluidLayout) { // we only support seven columns for now for fluid views; see T4054 if (count($this->columns) > 7) { throw new Exception('No more than 7 columns per view.'); } } $classes[] = 'aphront-multi-column-'.count($this->columns).'-up'; $columns = array(); $i = 0; foreach ($this->columns as $column_data) { $column_class = array('aphront-multi-column-column'); if ($this->gutter) { $column_class[] = $this->gutter; } $outer_class = array('aphront-multi-column-column-outer'); if (++$i === count($this->columns)) { $column_class[] = 'aphront-multi-column-column-last'; $outer_class[] = 'aphront-multi-colum-column-outer-last'; } $column = $column_data['column']; if ($column_data['class']) { $outer_class[] = $column_data['class']; } $column_sigil = idx($column_data, 'sigil'); $column_metadata = idx($column_data, 'metadata'); $column_inner = javelin_tag( 'div', array( 'class' => implode(' ', $column_class), 'sigil' => $column_sigil, - 'meta' => $column_metadata), + 'meta' => $column_metadata, + ), $column); $columns[] = phutil_tag( 'div', array( - 'class' => implode(' ', $outer_class)), + 'class' => implode(' ', $outer_class), + ), $column_inner); } $view = phutil_tag( 'div', array( 'class' => implode(' ', $classes), ), array( $columns, )); $classes = array(); $classes[] = 'aphront-multi-column-outer'; if ($this->fluidLayout) { $classes[] = 'aphront-multi-column-fluid'; if ($this->fluidishLayout) { $classes[] = 'aphront-multi-column-fluidish'; } } else { $classes[] = 'aphront-multi-column-fixed'; } $board = phutil_tag( 'div', array( - 'class' => implode(' ', $classes) + 'class' => implode(' ', $classes), ), $view); if ($this->border) { $board = id(new PHUIBoxView()) ->setBorder(true) ->appendChild($board) ->addPadding(PHUI::PADDING_MEDIUM_TOP) ->addPadding(PHUI::PADDING_MEDIUM_BOTTOM); } return javelin_tag( 'div', array( 'class' => 'aphront-multi-column-view', 'id' => $this->getID(), // TODO: It would be nice to convert this to an AphrontTagView and // use addSigil() from Workboards instead of hard-coding this. 'sigil' => 'aphront-multi-column-view', ), $board); } } diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php index 058ff15e98..0a0554b37f 100644 --- a/src/view/layout/AphrontSideNavFilterView.php +++ b/src/view/layout/AphrontSideNavFilterView.php @@ -1,301 +1,301 @@ setBaseURI($some_uri) * ->addLabel('Cats') * ->addFilter('meow', 'Meow') * ->addFilter('purr', 'Purr') * ->addLabel('Dogs') * ->addFilter('woof', 'Woof') * ->addFilter('bark', 'Bark'); * $valid_filter = $nav->selectFilter($user_selection, $default = 'meow'); * */ final class AphrontSideNavFilterView extends AphrontView { private $items = array(); private $baseURI; private $selectedFilter = false; private $flexible; private $collapsed = false; private $active; private $menu; private $crumbs; private $classes = array(); private $menuID; public function setMenuID($menu_id) { $this->menuID = $menu_id; return $this; } public function getMenuID() { return $this->menuID; } public function __construct() { $this->menu = new PHUIListView(); } public function addClass($class) { $this->classes[] = $class; return $this; } public static function newFromMenu(PHUIListView $menu) { $object = new AphrontSideNavFilterView(); $object->setBaseURI(new PhutilURI('/')); $object->menu = $menu; return $object; } public function setCrumbs(PhabricatorCrumbsView $crumbs) { $this->crumbs = $crumbs; return $this; } public function getCrumbs() { return $this->crumbs; } public function setActive($active) { $this->active = $active; return $this; } public function setFlexible($flexible) { $this->flexible = $flexible; return $this; } public function setCollapsed($collapsed) { $this->collapsed = $collapsed; return $this; } public function getMenuView() { return $this->menu; } public function addMenuItem(PHUIListItemView $item) { $this->menu->addMenuItem($item); return $this; } public function getMenu() { return $this->menu; } public function addFilter($key, $name, $uri = null) { return $this->addThing( $key, $name, $uri, PHUIListItemView::TYPE_LINK); } public function addButton($key, $name, $uri = null) { return $this->addThing( $key, $name, $uri, PHUIListItemView::TYPE_BUTTON); } private function addThing( $key, $name, $uri = null, $type) { $item = id(new PHUIListItemView()) ->setName($name) ->setType($type); if (strlen($key)) { $item->setKey($key); } if ($uri) { $item->setHref($uri); } else { $href = clone $this->baseURI; $href->setPath(rtrim($href->getPath().$key, '/').'/'); $href = (string)$href; $item->setHref($href); } return $this->addMenuItem($item); } public function addCustomBlock($block) { $this->menu->addMenuItem( id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_CUSTOM) ->appendChild($block)); return $this; } public function addLabel($name) { return $this->addMenuItem( id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_LABEL) ->setName($name)); } public function setBaseURI(PhutilURI $uri) { $this->baseURI = $uri; return $this; } public function getBaseURI() { return $this->baseURI; } public function selectFilter($key, $default = null) { $this->selectedFilter = $default; if ($this->menu->getItem($key) && strlen($key)) { $this->selectedFilter = $key; } return $this->selectedFilter; } public function getSelectedFilter() { return $this->selectedFilter; } public function render() { if ($this->menu->getItems()) { if (!$this->baseURI) { throw new Exception(pht('Call setBaseURI() before render()!')); } if ($this->selectedFilter === false) { throw new Exception(pht('Call selectFilter() before render()!')); } } if ($this->selectedFilter !== null) { $selected_item = $this->menu->getItem($this->selectedFilter); if ($selected_item) { $selected_item->addClass('phui-list-item-selected'); } } require_celerity_resource('phabricator-side-menu-view-css'); return $this->renderFlexNav(); } private function renderFlexNav() { $user = $this->user; require_celerity_resource('phabricator-nav-view-css'); $nav_classes = array(); $nav_classes[] = 'phabricator-nav'; $nav_id = null; $drag_id = null; $content_id = celerity_generate_unique_node_id(); $local_id = null; $background_id = null; $local_menu = null; $main_id = celerity_generate_unique_node_id(); if ($this->flexible) { $drag_id = celerity_generate_unique_node_id(); $flex_bar = phutil_tag( 'div', array( 'class' => 'phabricator-nav-drag', 'id' => $drag_id, ), ''); } else { $flex_bar = null; } $nav_menu = null; if ($this->menu->getItems()) { $local_id = celerity_generate_unique_node_id(); $background_id = celerity_generate_unique_node_id(); if (!$this->collapsed) { $nav_classes[] = 'has-local-nav'; } $menu_background = phutil_tag( 'div', array( 'class' => 'phabricator-nav-column-background', 'id' => $background_id, ), ''); $local_menu = array( $menu_background, phutil_tag( 'div', array( 'class' => 'phabricator-nav-local phabricator-side-menu', 'id' => $local_id, ), $this->menu->setID($this->getMenuID())), ); } $crumbs = null; if ($this->crumbs) { $crumbs = $this->crumbs->render(); $nav_classes[] = 'has-crumbs'; } if ($this->flexible) { if (!$this->collapsed) { $nav_classes[] = 'has-drag-nav'; } Javelin::initBehavior( 'phabricator-nav', array( 'mainID' => $main_id, 'localID' => $local_id, 'dragID' => $drag_id, 'contentID' => $content_id, 'backgroundID' => $background_id, 'collapsed' => $this->collapsed, )); if ($this->active) { Javelin::initBehavior( 'phabricator-active-nav', array( 'localID' => $local_id, )); } } $nav_classes = array_merge($nav_classes, $this->classes); return phutil_tag( 'div', array( 'class' => implode(' ', $nav_classes), 'id' => $main_id, ), array( $local_menu, $flex_bar, phutil_tag( 'div', array( 'class' => 'phabricator-nav-content mlb', 'id' => $content_id, ), array( $crumbs, $this->renderChildren(), - )) + )), )); } } diff --git a/src/view/layout/AphrontTwoColumnView.php b/src/view/layout/AphrontTwoColumnView.php index a206d6c836..c29a658ff5 100644 --- a/src/view/layout/AphrontTwoColumnView.php +++ b/src/view/layout/AphrontTwoColumnView.php @@ -1,66 +1,66 @@ mainColumn = $main; return $this; } public function setSideColumn($side) { $this->sideColumn = $side; return $this; } public function setCentered($centered) { $this->centered = $centered; return $this; } public function setNoPadding($padding) { $this->padding = $padding; return $this; } public function render() { require_celerity_resource('aphront-two-column-view-css'); $main = phutil_tag( 'div', array( - 'class' => 'aphront-main-column' + 'class' => 'aphront-main-column', ), $this->mainColumn); $side = phutil_tag( 'div', array( - 'class' => 'aphront-side-column' + 'class' => 'aphront-side-column', ), $this->sideColumn); $classes = array('aphront-two-column'); if ($this->centered) { $classes = array('aphront-two-column-centered'); } if ($this->padding) { $classes[] = 'aphront-two-column-padded'; } return phutil_tag( 'div', array( - 'class' => implode(' ', $classes) + 'class' => implode(' ', $classes), ), array( $main, $side, )); } } diff --git a/src/view/layout/PhabricatorActionListView.php b/src/view/layout/PhabricatorActionListView.php index 866cd45492..6db62c129e 100644 --- a/src/view/layout/PhabricatorActionListView.php +++ b/src/view/layout/PhabricatorActionListView.php @@ -1,66 +1,66 @@ object = $object; return $this; } public function setObjectURI($uri) { $this->objectURI = $uri; return $this; } public function addAction(PhabricatorActionView $view) { $this->actions[] = $view; return $this; } public function setID($id) { $this->id = $id; return $this; } public function render() { if (!$this->user) { throw new Exception(pht('Call setUser() before render()!')); } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS, array( 'object' => $this->object, 'actions' => $this->actions, )); $event->setUser($this->user); PhutilEventEngine::dispatchEvent($event); $actions = $event->getValue('actions'); if (!$actions) { return null; } foreach ($actions as $action) { $action->setObjectURI($this->objectURI); $action->setUser($this->user); } require_celerity_resource('phabricator-action-list-view-css'); return phutil_tag( 'ul', array( 'class' => 'phabricator-action-list-view', - 'id' => $this->id + 'id' => $this->id, ), $actions); } } diff --git a/src/view/layout/PhabricatorCrumbsView.php b/src/view/layout/PhabricatorCrumbsView.php index 2dc1523687..c17095603a 100644 --- a/src/view/layout/PhabricatorCrumbsView.php +++ b/src/view/layout/PhabricatorCrumbsView.php @@ -1,151 +1,151 @@ addCrumb( id(new PhabricatorCrumbView()) ->setName($text) ->setHref($href)); } public function addCrumb(PhabricatorCrumbView $crumb) { $this->crumbs[] = $crumb; return $this; } public function addAction(PHUIListItemView $action) { $this->actions[] = $action; return $this; } public function setActionList(PhabricatorActionListView $list) { $this->actionListID = celerity_generate_unique_node_id(); $list->setId($this->actionListID); return $this; } public function render() { require_celerity_resource('phabricator-crumbs-view-css'); $action_view = null; if (($this->actions) || ($this->actionListID)) { $actions = array(); foreach ($this->actions as $action) { $icon = null; if ($action->getIcon()) { $icon_name = $action->getIcon(); if ($action->getDisabled()) { $icon_name .= ' lightgreytext'; } $icon = id(new PHUIIconView()) ->setIconFont($icon_name); } $name = phutil_tag( 'span', array( - 'class' => 'phabricator-crumbs-action-name' + 'class' => 'phabricator-crumbs-action-name', ), $action->getName()); $action_sigils = $action->getSigils(); if ($action->getWorkflow()) { $action_sigils[] = 'workflow'; } $action_classes = $action->getClasses(); $action_classes[] = 'phabricator-crumbs-action'; if ($action->getDisabled()) { $action_classes[] = 'phabricator-crumbs-action-disabled'; } $actions[] = javelin_tag( 'a', array( 'href' => $action->getHref(), 'class' => implode(' ', $action_classes), 'sigil' => implode(' ', $action_sigils), - 'style' => $action->getStyle() + 'style' => $action->getStyle(), ), array( $icon, $name, )); } if ($this->actionListID) { $icon_id = celerity_generate_unique_node_id(); $icon = id(new PHUIIconView()) ->setIconFont('fa-bars'); $name = phutil_tag( 'span', array( - 'class' => 'phabricator-crumbs-action-name' + 'class' => 'phabricator-crumbs-action-name', ), pht('Actions')); $actions[] = javelin_tag( 'a', array( 'href' => '#', 'class' => 'phabricator-crumbs-action phabricator-crumbs-action-menu', 'sigil' => 'jx-toggle-class', 'id' => $icon_id, 'meta' => array( 'map' => array( $this->actionListID => 'phabricator-action-list-toggle', - $icon_id => 'phabricator-crumbs-action-menu-open' + $icon_id => 'phabricator-crumbs-action-menu-open', ), ), ), array( $icon, $name, )); } $action_view = phutil_tag( 'div', array( 'class' => 'phabricator-crumbs-actions', ), $actions); } if ($this->crumbs) { last($this->crumbs)->setIsLastCrumb(true); } return phutil_tag( 'div', array( 'class' => 'phabricator-crumbs-view '. 'sprite-gradient gradient-breadcrumbs', ), array( $action_view, $this->crumbs, )); } } diff --git a/src/view/layout/PhabricatorSourceCodeView.php b/src/view/layout/PhabricatorSourceCodeView.php index 4eaa6356df..9a9454cbfa 100644 --- a/src/view/layout/PhabricatorSourceCodeView.php +++ b/src/view/layout/PhabricatorSourceCodeView.php @@ -1,131 +1,132 @@ limit = $limit; return $this; } public function setLines(array $lines) { $this->lines = $lines; return $this; } public function setURI(PhutilURI $uri) { $this->uri = $uri; return $this; } public function setHighlights(array $array) { $this->highlights = array_fuse($array); return $this; } public function disableHighlightOnClick() { $this->canClickHighlight = false; return $this; } public function render() { require_celerity_resource('phabricator-source-code-view-css'); require_celerity_resource('syntax-highlighting-css'); Javelin::initBehavior('phabricator-oncopy', array()); if ($this->canClickHighlight) { Javelin::initBehavior('phabricator-line-linker'); } $line_number = 1; $rows = array(); foreach ($this->lines as $line) { $hit_limit = $this->limit && ($line_number == $this->limit) && (count($this->lines) != $this->limit); if ($hit_limit) { $content_number = ''; $content_line = phutil_tag( 'span', array( 'class' => 'c', ), pht('...')); } else { $content_number = $line_number; // NOTE: See phabricator-oncopy behavior. $content_line = hsprintf("\xE2\x80\x8B%s", $line); } $row_attributes = array(); if (isset($this->highlights[$line_number])) { $row_attributes['class'] = 'phabricator-source-highlight'; } if ($this->canClickHighlight) { $line_uri = $this->uri.'$'.$line_number; $line_href = (string) new PhutilURI($line_uri); $tag_number = javelin_tag( 'a', array( - 'href' => $line_href + 'href' => $line_href, ), $line_number); } else { $tag_number = javelin_tag( 'span', array(), $line_number); } $rows[] = phutil_tag( 'tr', $row_attributes, array( javelin_tag( 'th', array( 'class' => 'phabricator-source-line', - 'sigil' => 'phabricator-source-line' + 'sigil' => 'phabricator-source-line', ), $tag_number), phutil_tag( 'td', array( - 'class' => 'phabricator-source-code' + 'class' => 'phabricator-source-code', ), - $content_line))); + $content_line), + )); if ($hit_limit) { break; } $line_number++; } $classes = array(); $classes[] = 'phabricator-source-code-view'; $classes[] = 'remarkup-code'; $classes[] = 'PhabricatorMonospaced'; return phutil_tag_div( 'phabricator-source-code-container', javelin_tag( 'table', array( 'class' => implode(' ', $classes), - 'sigil' => 'phabricator-source' + 'sigil' => 'phabricator-source', ), phutil_implode_html('', $rows))); } } diff --git a/src/view/page/PhabricatorBarePageView.php b/src/view/page/PhabricatorBarePageView.php index 2519226509..11f3848da2 100644 --- a/src/view/page/PhabricatorBarePageView.php +++ b/src/view/page/PhabricatorBarePageView.php @@ -1,122 +1,122 @@ controller = $controller; return $this; } public function getController() { return $this->controller; } public function setRequest(AphrontRequest $request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } public function setFrameable($frameable) { $this->frameable = $frameable; return $this; } public function getFrameable() { return $this->frameable; } public function setDeviceReady($device_ready) { $this->deviceReady = $device_ready; return $this; } public function getDeviceReady() { return $this->deviceReady; } protected function willRenderPage() { // We render this now to resolve static resources so they can appear in the // document head. $this->bodyContent = phutil_implode_html('', $this->renderChildren()); } protected function getHead() { $framebust = null; if (!$this->getFrameable()) { $framebust = '(top == self) || top.location.replace(self.location.href);'; } $viewport_tag = null; if ($this->getDeviceReady()) { $viewport_tag = phutil_tag( 'meta', array( 'name' => 'viewport', 'content' => 'width=device-width, '. 'initial-scale=1, '. 'maximum-scale=1', )); } $icon_tag = phutil_tag( 'link', array( 'rel' => 'apple-touch-icon', - 'href' => celerity_get_resource_uri('/rsrc/image/apple-touch-icon.png') + 'href' => celerity_get_resource_uri('/rsrc/image/apple-touch-icon.png'), )); $apple_tag = phutil_tag( 'meta', array( 'name' => 'apple-mobile-web-app-status-bar-style', - 'content' => 'black-translucent' + 'content' => 'black-translucent', )); $referrer_tag = phutil_tag( 'meta', array( 'name' => 'referrer', 'content' => 'never', )); $response = CelerityAPI::getStaticResourceResponse(); $developer = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); return hsprintf( '%s%s%s%s%s%s', $viewport_tag, $icon_tag, $apple_tag, $referrer_tag, CelerityStaticResourceResponse::renderInlineScript( $framebust.jsprintf('window.__DEV__=%d;', ($developer ? 1 : 0))), $response->renderResourcesOfType('css')); } protected function getBody() { return $this->bodyContent; } protected function getTail() { $response = CelerityAPI::getStaticResourceResponse(); return $response->renderResourcesOfType('js'); } } diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php index d94b34eca9..a5a8c362a8 100644 --- a/src/view/page/menu/PhabricatorMainMenuView.php +++ b/src/view/page/menu/PhabricatorMainMenuView.php @@ -1,479 +1,480 @@ applicationMenu = $application_menu; return $this; } public function getApplicationMenu() { return $this->applicationMenu; } public function setController(PhabricatorController $controller) { $this->controller = $controller; return $this; } public function getController() { return $this->controller; } public function render() { $user = $this->user; require_celerity_resource('phabricator-main-menu-view'); require_celerity_resource('sprite-main-header-css'); $header_id = celerity_generate_unique_node_id(); $menus = array(); $alerts = array(); $search_button = ''; $app_button = ''; $aural = null; if ($user->isLoggedIn() && $user->isUserActivated()) { list($menu, $dropdowns, $aural) = $this->renderNotificationMenu(); if (array_filter($menu)) { $alerts[] = $menu; } $menus = array_merge($menus, $dropdowns); $app_button = $this->renderApplicationMenuButton($header_id); $search_button = $this->renderSearchMenuButton($header_id); } else { $app_button = $this->renderApplicationMenuButton($header_id); if (PhabricatorEnv::getEnvConfig('policy.allow-public')) { $search_button = $this->renderSearchMenuButton($header_id); } } $search_menu = $this->renderPhabricatorSearchMenu(); if ($alerts) { $alerts = javelin_tag( 'div', array( 'class' => 'phabricator-main-menu-alerts', 'aural' => false, ), $alerts); } if ($aural) { $aural = javelin_tag( 'span', array( 'aural' => true, ), phutil_implode_html(' ', $aural)); } $application_menu = $this->renderApplicationMenu(); $classes = array(); $classes[] = 'phabricator-main-menu'; $classes[] = 'sprite-main-header'; $classes[] = 'main-header-'.PhabricatorEnv::getEnvConfig('ui.header-color'); return phutil_tag( 'div', array( 'class' => implode(' ', $classes), 'id' => $header_id, ), array( $app_button, $search_button, $this->renderPhabricatorLogo(), $alerts, $aural, $application_menu, $search_menu, $menus, )); } private function renderSearch() { $user = $this->user; $result = null; $keyboard_config = array( 'helpURI' => '/help/keyboardshortcut/', ); if ($user->isLoggedIn()) { $show_search = $user->isUserActivated(); } else { $show_search = PhabricatorEnv::getEnvConfig('policy.allow-public'); } if ($show_search) { $search = new PhabricatorMainMenuSearchView(); $search->setUser($user); $result = $search; $pref_shortcut = PhabricatorUserPreferences::PREFERENCE_SEARCH_SHORTCUT; if ($user->loadPreferences()->getPreference($pref_shortcut, true)) { $keyboard_config['searchID'] = $search->getID(); } } Javelin::initBehavior('phabricator-keyboard-shortcuts', $keyboard_config); if ($result) { $result = id(new PHUIListItemView()) ->addClass('phabricator-main-menu-search') ->appendChild($result); } return $result; } public function renderApplicationMenuButton($header_id) { $button_id = celerity_generate_unique_node_id(); return javelin_tag( 'a', array( 'class' => 'phabricator-main-menu-expand-button '. 'phabricator-expand-search-menu', 'sigil' => 'jx-toggle-class', 'meta' => array( 'map' => array( $header_id => 'phabricator-application-menu-expanded', $button_id => 'menu-icon-app-blue', ), ), ), phutil_tag( 'span', array( 'class' => 'phabricator-menu-button-icon sprite-menu menu-icon-app', 'id' => $button_id, ), '')); } public function renderApplicationMenu() { $user = $this->getUser(); $controller = $this->getController(); $applications = PhabricatorApplication::getAllInstalledApplications(); $actions = array(); foreach ($applications as $application) { $app_actions = $application->buildMainMenuItems($user, $controller); foreach ($app_actions as $action) { $actions[] = $action; } } $actions = msort($actions, 'getOrder'); $view = $this->getApplicationMenu(); if (!$view) { $view = new PHUIListView(); } $view->addClass('phabricator-dark-menu'); $view->addClass('phabricator-application-menu'); if ($actions) { $view->addMenuItem( id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_LABEL) ->setName(pht('Actions'))); foreach ($actions as $action) { $icon = $action->getIcon(); if ($icon) { $action->appendChild($this->renderMenuIcon($icon)); } $view->addMenuItem($action); } } return $view; } public function renderSearchMenuButton($header_id) { $button_id = celerity_generate_unique_node_id(); return javelin_tag( 'a', array( 'class' => 'phabricator-main-menu-search-button '. 'phabricator-expand-application-menu', 'sigil' => 'jx-toggle-class', 'meta' => array( 'map' => array( $header_id => 'phabricator-search-menu-expanded', $button_id => 'menu-icon-search-blue', ), ), ), phutil_tag( 'span', array( 'class' => 'phabricator-menu-button-icon sprite-menu menu-icon-search', 'id' => $button_id, ), '')); } private function renderPhabricatorSearchMenu() { $view = new PHUIListView(); $view->addClass('phabricator-dark-menu'); $view->addClass('phabricator-search-menu'); $search = $this->renderSearch(); if ($search) { $view->addMenuItem($search); } return $view; } private function renderPhabricatorLogo() { $class = 'phabricator-main-menu-logo-image'; return phutil_tag( 'a', array( 'class' => 'phabricator-main-menu-logo', 'href' => '/', ), array( javelin_tag( 'span', array( 'aural' => true, ), pht('Home')), phutil_tag( 'span', array( 'class' => 'sprite-menu menu-logo-image '.$class, ), ''), )); } private function renderNotificationMenu() { $user = $this->user; require_celerity_resource('phabricator-notification-css'); require_celerity_resource('phabricator-notification-menu-css'); require_celerity_resource('sprite-menu-css'); $container_classes = array( 'sprite-menu', 'alert-notifications', ); $aural = array(); $message_tag = ''; $message_notification_dropdown = ''; $conpherence = 'PhabricatorConpherenceApplication'; if (PhabricatorApplication::isClassInstalledForViewer( $conpherence, $user)) { $message_id = celerity_generate_unique_node_id(); $message_count_id = celerity_generate_unique_node_id(); $message_dropdown_id = celerity_generate_unique_node_id(); $unread_status = ConpherenceParticipationStatus::BEHIND; $unread = id(new ConpherenceParticipantCountQuery()) ->withParticipantPHIDs(array($user->getPHID())) ->withParticipationStatus($unread_status) ->execute(); $message_count_number = idx($unread, $user->getPHID(), 0); if ($message_count_number) { $aural[] = phutil_tag( 'a', array( 'href' => '/conpherence/', ), pht( '%s unread messages.', new PhutilNumber($message_count_number))); } else { $aural[] = pht('No messages.'); } if ($message_count_number > 999) { $message_count_number = "\xE2\x88\x9E"; } $message_count_tag = phutil_tag( 'span', array( 'id' => $message_count_id, - 'class' => 'phabricator-main-menu-message-count' + 'class' => 'phabricator-main-menu-message-count', ), $message_count_number); $message_icon_tag = phutil_tag( 'span', array( 'class' => 'sprite-menu phabricator-main-menu-message-icon', ), ''); if ($message_count_number) { $container_classes[] = 'message-unread'; } $message_tag = phutil_tag( 'a', array( 'href' => '/conpherence/', 'class' => implode(' ', $container_classes), 'id' => $message_id, ), array( $message_icon_tag, $message_count_tag, )); Javelin::initBehavior( 'aphlict-dropdown', array( 'bubbleID' => $message_id, 'countID' => $message_count_id, 'dropdownID' => $message_dropdown_id, 'loadingText' => pht('Loading...'), 'uri' => '/conpherence/panel/', 'countType' => 'messages', 'countNumber' => $message_count_number, )); $message_notification_dropdown = javelin_tag( 'div', array( 'id' => $message_dropdown_id, 'class' => 'phabricator-notification-menu', 'sigil' => 'phabricator-notification-menu', 'style' => 'display: none;', ), ''); } $bubble_tag = ''; $notification_dropdown = ''; $notification_app = 'PhabricatorNotificationsApplication'; if (PhabricatorApplication::isClassInstalledForViewer( $notification_app, $user)) { $count_id = celerity_generate_unique_node_id(); $dropdown_id = celerity_generate_unique_node_id(); $bubble_id = celerity_generate_unique_node_id(); $count_number = id(new PhabricatorFeedStoryNotification()) ->countUnread($user); if ($count_number) { $aural[] = phutil_tag( 'a', array( 'href' => '/notification/', ), pht( '%s unread notifications.', new PhutilNumber($count_number))); } else { $aural[] = pht('No notifications.'); } if ($count_number > 999) { $count_number = "\xE2\x88\x9E"; } $count_tag = phutil_tag( 'span', array( 'id' => $count_id, - 'class' => 'phabricator-main-menu-alert-count' + 'class' => 'phabricator-main-menu-alert-count', ), $count_number); $icon_tag = phutil_tag( 'span', array( 'class' => 'sprite-menu phabricator-main-menu-alert-icon', ), ''); if ($count_number) { $container_classes[] = 'alert-unread'; } $bubble_tag = phutil_tag( 'a', array( 'href' => '/notification/', 'class' => implode(' ', $container_classes), 'id' => $bubble_id, ), array($icon_tag, $count_tag)); Javelin::initBehavior( 'aphlict-dropdown', array( 'bubbleID' => $bubble_id, 'countID' => $count_id, 'dropdownID' => $dropdown_id, 'loadingText' => pht('Loading...'), 'uri' => '/notification/panel/', 'countType' => 'notifications', 'countNumber' => $count_number, )); $notification_dropdown = javelin_tag( 'div', array( 'id' => $dropdown_id, 'class' => 'phabricator-notification-menu', 'sigil' => 'phabricator-notification-menu', 'style' => 'display: none;', ), ''); } $dropdowns = array( $notification_dropdown, - $message_notification_dropdown); + $message_notification_dropdown, + ); $applications = PhabricatorApplication::getAllInstalledApplications(); foreach ($applications as $application) { $dropdowns[] = $application->buildMainMenuExtraNodes( $this->getUser(), $this->getController()); } return array( array( $bubble_tag, $message_tag, ), $dropdowns, $aural, ); } private function renderMenuIcon($name) { return phutil_tag( 'span', array( 'class' => 'phabricator-core-menu-icon '. 'sprite-menu menu-icon-'.$name, ), ''); } } diff --git a/src/view/phui/PHUIActionHeaderView.php b/src/view/phui/PHUIActionHeaderView.php index c9d0404914..0639203fbc 100644 --- a/src/view/phui/PHUIActionHeaderView.php +++ b/src/view/phui/PHUIActionHeaderView.php @@ -1,163 +1,164 @@ dropdown = $dropdown; return $this; } public function addAction(PHUIIconView $action) { $this->actions[] = $action; return $this; } public function setTag(PHUITagView $tag) { $this->tag = $tag; return $this; } public function setHeaderTitle($header) { $this->headerTitle = $header; return $this; } public function setHeaderHref($href) { $this->headerHref = $href; return $this; } public function addHeaderSigil($sigil) { $this->headerSigils[] = $sigil; return $this; } public function setHeaderIcon(PHUIIconView $icon) { $this->headerIcon = $icon; return $this; } public function setHeaderColor($color) { $this->headerColor = $color; return $this; } private function getIconColor() { switch ($this->headerColor) { case self::HEADER_GREY: return 'lightgreytext'; case self::HEADER_DARK_GREY: return 'lightgreytext'; case self::HEADER_LIGHTGREEN: return 'bluegrey'; case self::HEADER_LIGHTRED: return 'bluegrey'; case self::HEADER_LIGHTVIOLET: return 'bluegrey'; case self::HEADER_LIGHTBLUE: return 'bluegrey'; } } public function render() { require_celerity_resource('phui-action-header-view-css'); $classes = array(); $classes[] = 'phui-action-header'; if ($this->headerColor) { $classes[] = 'sprite-gradient'; $classes[] = 'gradient-'.$this->headerColor.'-header'; } if ($this->dropdown) { $classes[] = 'dropdown'; } $action_list = array(); if (nonempty($this->actions)) { foreach ($this->actions as $action) { $action->addClass($this->getIconColor()); $action_list[] = phutil_tag( 'li', array( - 'class' => 'phui-action-header-icon-item' + 'class' => 'phui-action-header-icon-item', ), $action); } } if ($this->tag) { $action_list[] = phutil_tag( 'li', array( - 'class' => 'phui-action-header-icon-item' + 'class' => 'phui-action-header-icon-item', ), $this->tag); } $header_icon = null; if ($this->headerIcon) { $header_icon = $this->headerIcon; } $header_title = $this->headerTitle; if ($this->headerHref) { $header_title = javelin_tag( 'a', array( 'class' => 'phui-action-header-link', 'href' => $this->headerHref, - 'sigil' => implode(' ', $this->headerSigils) + 'sigil' => implode(' ', $this->headerSigils), ), $this->headerTitle); } $header = phutil_tag( 'h3', array( - 'class' => 'phui-action-header-title' + 'class' => 'phui-action-header-title', ), array( $header_icon, - $header_title)); + $header_title, + )); $icons = ''; if (nonempty($action_list)) { $icons = phutil_tag( 'ul', array( - 'class' => 'phui-action-header-icon-list' + 'class' => 'phui-action-header-icon-list', ), $action_list); } return phutil_tag( 'div', array( - 'class' => implode(' ', $classes) + 'class' => implode(' ', $classes), ), array( $header, - $icons + $icons, )); } } diff --git a/src/view/phui/PHUIDocumentView.php b/src/view/phui/PHUIDocumentView.php index 1c36500f52..60f099730b 100644 --- a/src/view/phui/PHUIDocumentView.php +++ b/src/view/phui/PHUIDocumentView.php @@ -1,188 +1,189 @@ 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 getTagAttributes() { $classes = array(); if ($this->offset) { $classes[] = 'phui-document-offset'; }; return array( 'class' => $classes, ); } public 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' + 'class' => 'phui-document-sidenav', ), $this->sidenav); } $book = null; if ($this->bookname) { $book = phutil_tag( 'div', array( - 'class' => 'phui-document-bookname grouped' + 'class' => 'phui-document-bookname grouped', ), array( phutil_tag( 'span', array('class' => 'bookname'), $this->bookname), phutil_tag( 'span', array('class' => 'bookdescription'), - $this->bookdescription))); + $this->bookdescription), + )); } $topnav = null; if ($this->topnav) { $topnav = phutil_tag( 'div', array( - 'class' => 'phui-document-topnav' + 'class' => 'phui-document-topnav', ), $this->topnav); } $crumbs = null; if ($this->crumbs) { $crumbs = phutil_tag( 'div', array( - 'class' => 'phui-document-crumbs' + 'class' => 'phui-document-crumbs', ), $this->bookName); } if ($this->fontKit) { $main_content = phutil_tag( 'div', array( - 'class' => 'phui-font-'.$this->fontKit + 'class' => 'phui-font-'.$this->fontKit, ), $this->renderChildren()); } else { $main_content = $this->renderChildren(); } $content_inner = phutil_tag( 'div', array( 'class' => 'phui-document-inner', ), array( $book, $this->header, $topnav, $main_content, - $crumbs + $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/src/view/phui/PHUIFeedStoryView.php b/src/view/phui/PHUIFeedStoryView.php index de717e25d1..d5d0fd35b3 100644 --- a/src/view/phui/PHUIFeedStoryView.php +++ b/src/view/phui/PHUIFeedStoryView.php @@ -1,294 +1,297 @@ tags = $tags; return $this; } public function getTags() { return $this->tags; } public function setChronologicalKey($chronological_key) { $this->chronologicalKey = $chronological_key; return $this; } public function getChronologicalKey() { return $this->chronologicalKey; } public function setTitle($title) { $this->title = $title; return $this; } public function getTitle() { return $this->title; } public function setEpoch($epoch) { $this->epoch = $epoch; return $this; } public function setImage($image) { $this->image = $image; return $this; } public function setImageHref($image_href) { $this->imageHref = $image_href; return $this; } public function setAppIcon($icon) { $this->appIcon = $icon; return $this; } public function setViewed($viewed) { $this->viewed = $viewed; return $this; } public function getViewed() { return $this->viewed; } public function setHref($href) { $this->href = $href; return $this; } public function setTokenBar(array $tokens) { $this->tokenBar = $tokens; return $this; } public function addProject($project) { $this->projects[] = $project; return $this; } public function addAction(PHUIIconView $action) { $this->actions[] = $action; return $this; } public function setPontification($text, $title = null) { if ($title) { $title = phutil_tag('h3', array(), $title); } $copy = phutil_tag( 'div', array( 'class' => 'phui-feed-story-bigtext-post', ), array( $title, - $text)); + $text, + )); $this->appendChild($copy); return $this; } public function getHref() { return $this->href; } public function renderNotification($user) { $classes = array( 'phabricator-notification', ); if (!$this->viewed) { $classes[] = 'phabricator-notification-unread'; } if ($this->epoch) { if ($user) { $foot = phabricator_datetime($this->epoch, $user); $foot = phutil_tag( 'span', array( - 'class' => 'phabricator-notification-date'), + 'class' => 'phabricator-notification-date', + ), $foot); } else { $foot = null; } } else { $foot = pht('No time specified.'); } return javelin_tag( 'div', array( 'class' => implode(' ', $classes), 'sigil' => 'notification', 'meta' => array( 'href' => $this->getHref(), ), ), array($this->title, $foot)); } public function render() { require_celerity_resource('phui-feed-story-css'); Javelin::initBehavior('phabricator-hovercards'); $body = null; $foot = null; $image_style = null; $actor = ''; if ($this->image) { $actor = new PHUIIconView(); $actor->setImage($this->image); $actor->addClass('phui-feed-story-actor-image'); if ($this->imageHref) { $actor->setHref($this->imageHref); } } if ($this->epoch) { // TODO: This is really bad; when rendering through Conduit and via // renderText() we don't have a user. if ($this->user) { $foot = phabricator_datetime($this->epoch, $this->user); } else { $foot = null; } } else { $foot = pht('No time specified.'); } if ($this->chronologicalKey) { $foot = phutil_tag( 'a', array( 'href' => '/feed/'.$this->chronologicalKey.'/', ), $foot); } $icon = null; if ($this->appIcon) { $icon = new PHUIIconView(); $icon->setSpriteIcon($this->appIcon); $icon->setSpriteSheet(PHUIIconView::SPRITE_APPS); } $action_list = array(); $icons = null; foreach ($this->actions as $action) { $action_list[] = phutil_tag( 'li', array( - 'class' => 'phui-feed-story-action-item' - ), - $action); + 'class' => 'phui-feed-story-action-item', + ), + $action); } if (!empty($action_list)) { $icons = phutil_tag( 'ul', array( - 'class' => 'phui-feed-story-action-list' + 'class' => 'phui-feed-story-action-list', ), $action_list); } $head = phutil_tag( 'div', array( 'class' => 'phui-feed-story-head', ), array( $actor, nonempty($this->title, pht('Untitled Story')), $icons, )); if (!empty($this->tokenBar)) { $tokenview = phutil_tag( 'div', array( - 'class' => 'phui-feed-token-bar' + 'class' => 'phui-feed-token-bar', ), $this->tokenBar); $this->appendChild($tokenview); } $body_content = $this->renderChildren(); if ($body_content) { $body = phutil_tag( 'div', array( 'class' => 'phui-feed-story-body', ), $body_content); } $tags = null; if ($this->tags) { $tags = array( " \xC2\xB7 ", - $this->tags); + $this->tags, + ); } $foot = phutil_tag( 'div', array( 'class' => 'phui-feed-story-foot', ), array( $icon, $foot, $tags, )); $classes = array('phui-feed-story'); return id(new PHUIBoxView()) ->addClass(implode(' ', $classes)) ->setBorder(true) ->addMargin(PHUI::MARGIN_MEDIUM_BOTTOM) ->appendChild(array($head, $body, $foot)); } public function setAppIconFromPHID($phid) { switch (phid_get_type($phid)) { case PholioMockPHIDType::TYPECONST: $this->setAppIcon('pholio-dark'); break; case PhabricatorMacroMacroPHIDType::TYPECONST: $this->setAppIcon('macro-dark'); break; case ManiphestTaskPHIDType::TYPECONST: $this->setAppIcon('maniphest-dark'); break; case DifferentialRevisionPHIDType::TYPECONST: $this->setAppIcon('differential-dark'); break; case PhabricatorCalendarEventPHIDType::TYPECONST: $this->setAppIcon('calendar-dark'); break; } } } diff --git a/src/view/phui/PHUIObjectItemListView.php b/src/view/phui/PHUIObjectItemListView.php index 87d333044c..0a29e62ce9 100644 --- a/src/view/phui/PHUIObjectItemListView.php +++ b/src/view/phui/PHUIObjectItemListView.php @@ -1,135 +1,136 @@ allowEmptyList = $allow_empty_list; return $this; } public function getAllowEmptyList() { return $this->allowEmptyList; } public function setFlush($flush) { $this->flush = $flush; return $this; } public function setPlain($plain) { $this->plain = $plain; return $this; } public function setHeader($header) { $this->header = $header; return $this; } public function setPager($pager) { $this->pager = $pager; return $this; } public function setNoDataString($no_data_string) { $this->noDataString = $no_data_string; return $this; } public function addItem(PHUIObjectItemView $item) { $this->items[] = $item; return $this; } public function setStackable($stackable) { $this->stackable = $stackable; return $this; } public function setStates($states) { $this->states = $states; return $this; } protected function getTagName() { return 'ul'; } protected function getTagAttributes() { $classes = array(); $classes[] = 'phui-object-item-list-view'; if ($this->stackable) { $classes[] = 'phui-object-list-stackable'; } if ($this->states) { $classes[] = 'phui-object-list-states'; $classes[] = 'phui-object-list-stackable'; } if ($this->flush) { $classes[] = 'phui-object-list-flush'; } if ($this->plain) { $classes[] = 'phui-object-list-plain'; } return array( 'class' => $classes, ); } protected function getTagContent() { require_celerity_resource('phui-object-item-list-view-css'); $header = null; if (strlen($this->header)) { $header = phutil_tag( 'h1', array( 'class' => 'phui-object-item-list-header', ), $this->header); } if ($this->items) { $items = $this->items; } else if ($this->allowEmptyList) { $items = null; } else { $string = nonempty($this->noDataString, pht('No data.')); $string = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NODATA) ->appendChild($string); $items = phutil_tag( 'li', array( - 'class' => 'phui-object-item-empty'), + 'class' => 'phui-object-item-empty', + ), $string); } $pager = null; if ($this->pager) { $pager = $this->pager; } return array( $header, $items, $pager, $this->renderChildren(), ); } } diff --git a/src/view/phui/PHUIPropertyListView.php b/src/view/phui/PHUIPropertyListView.php index 4569d3209d..95d0528571 100644 --- a/src/view/phui/PHUIPropertyListView.php +++ b/src/view/phui/PHUIPropertyListView.php @@ -1,288 +1,288 @@ object = $object; return $this; } public function setActionList(PhabricatorActionListView $list) { $this->actionList = $list; return $this; } public function setStacked($stacked) { $this->stacked = $stacked; return $this; } public function addClass($class) { $this->classes[] = $class; return $this; } public function setHasKeyboardShortcuts($has_keyboard_shortcuts) { $this->hasKeyboardShortcuts = $has_keyboard_shortcuts; return $this; } public function addProperty($key, $value) { $current = array_pop($this->parts); if (!$current || $current['type'] != 'property') { if ($current) { $this->parts[] = $current; } $current = array( 'type' => 'property', 'list' => array(), ); } $current['list'][] = array( 'key' => $key, 'value' => $value, ); $this->parts[] = $current; return $this; } - public function addSectionHeader($name, $icon=null) { + public function addSectionHeader($name, $icon = null) { $this->parts[] = array( 'type' => 'section', 'name' => $name, 'icon' => $icon, ); return $this; } public function addTextContent($content) { $this->parts[] = array( 'type' => 'text', 'content' => $content, ); return $this; } public function addRawContent($content) { $this->parts[] = array( 'type' => 'raw', 'content' => $content, ); return $this; } public function addImageContent($content) { $this->parts[] = array( 'type' => 'image', 'content' => $content, ); return $this; } public function invokeWillRenderEvent() { if ($this->object && $this->getUser() && !$this->invokedWillRenderEvent) { $event = new PhabricatorEvent( PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES, array( 'object' => $this->object, 'view' => $this, )); $event->setUser($this->getUser()); PhutilEventEngine::dispatchEvent($event); } $this->invokedWillRenderEvent = true; } public function render() { $this->invokeWillRenderEvent(); require_celerity_resource('phui-property-list-view-css'); $items = array(); $parts = $this->parts; // If we have an action list, make sure we render a property part, even // if there are no properties. Otherwise, the action list won't render. if ($this->actionList) { $have_property_part = false; foreach ($this->parts as $part) { if ($part['type'] == 'property') { $have_property_part = true; break; } } if (!$have_property_part) { $parts[] = array( 'type' => 'property', 'list' => array(), ); } } foreach ($parts as $part) { $type = $part['type']; switch ($type) { case 'property': $items[] = $this->renderPropertyPart($part); break; case 'section': $items[] = $this->renderSectionPart($part); break; case 'text': case 'image': $items[] = $this->renderTextPart($part); break; case 'raw': $items[] = $this->renderRawPart($part); break; default: throw new Exception(pht("Unknown part type '%s'!", $type)); } } $this->classes[] = 'phui-property-list-section'; $classes = implode(' ', $this->classes); return phutil_tag( 'div', array( 'class' => $classes, ), array( $items, )); } private function renderPropertyPart(array $part) { $items = array(); foreach ($part['list'] as $spec) { $key = $spec['key']; $value = $spec['value']; // NOTE: We append a space to each value to improve the behavior when the // user double-clicks a property value (like a URI) to select it. Without // the space, the label is also selected. $items[] = phutil_tag( 'dt', array( 'class' => 'phui-property-list-key', ), array($key, ' ')); $items[] = phutil_tag( 'dd', array( 'class' => 'phui-property-list-value', ), array($value, ' ')); } $stacked = ''; if ($this->stacked) { $stacked = 'phui-property-list-stacked'; } $list = phutil_tag( 'dl', array( 'class' => 'phui-property-list-properties '.$stacked, ), $items); $shortcuts = null; if ($this->hasKeyboardShortcuts) { $shortcuts = new AphrontKeyboardShortcutsAvailableView(); } $list = phutil_tag( 'div', array( 'class' => 'phui-property-list-properties-wrap', ), array($shortcuts, $list)); $action_list = null; if ($this->actionList) { $action_list = phutil_tag( 'div', array( 'class' => 'phui-property-list-actions', ), $this->actionList); $this->actionList = null; } return phutil_tag( 'div', array( 'class' => 'phui-property-list-container grouped', ), array($action_list, $list)); } private function renderSectionPart(array $part) { $name = $part['name']; if ($part['icon']) { $icon = id(new PHUIIconView()) ->setIconFont($part['icon']); $name = phutil_tag( 'span', array( 'class' => 'phui-property-list-section-header-icon', ), array($icon, $name)); } return phutil_tag( 'div', array( 'class' => 'phui-property-list-section-header', ), $name); } private function renderTextPart(array $part) { $classes = array(); $classes[] = 'phui-property-list-text-content'; if ($part['type'] == 'image') { $classes[] = 'phui-property-list-image-content'; } return phutil_tag( 'div', array( 'class' => implode($classes, ' '), ), $part['content']); } private function renderRawPart(array $part) { $classes = array(); $classes[] = 'phui-property-list-raw-content'; return phutil_tag( 'div', array( 'class' => implode($classes, ' '), ), $part['content']); } } diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php index c1a026c6d7..9f308a0a4c 100644 --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -1,585 +1,585 @@ quoteRef = $quote_ref; return $this; } public function getQuoteRef() { return $this->quoteRef; } public function setQuoteTargetID($quote_target_id) { $this->quoteTargetID = $quote_target_id; return $this; } public function getQuoteTargetID() { return $this->quoteTargetID; } public function setHideByDefault($hide_by_default) { $this->hideByDefault = $hide_by_default; return $this; } public function getHideByDefault() { return $this->hideByDefault; } public function setTransactionPHID($transaction_phid) { $this->transactionPHID = $transaction_phid; return $this; } public function getTransactionPHID() { return $this->transactionPHID; } public function setIsEdited($is_edited) { $this->isEdited = $is_edited; return $this; } public function getIsEdited() { return $this->isEdited; } public function setIsPreview($is_preview) { $this->isPreview = $is_preview; return $this; } public function getIsPreview() { return $this->isPreview; } public function setIsEditable($is_editable) { $this->isEditable = $is_editable; return $this; } public function getIsEditable() { return $this->isEditable; } public function setIsRemovable($is_removable) { $this->isRemovable = $is_removable; return $this; } public function getIsRemovable() { return $this->isRemovable; } public function setDateCreated($date_created) { $this->dateCreated = $date_created; return $this; } public function getDateCreated() { return $this->dateCreated; } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function getContentSource() { return $this->contentSource; } public function setUserHandle(PhabricatorObjectHandle $handle) { $this->userHandle = $handle; return $this; } public function setAnchor($anchor) { $this->anchor = $anchor; return $this; } public function getAnchor() { return $this->anchor; } public function setTitle($title) { $this->title = $title; return $this; } public function addClass($class) { $this->classes[] = $class; return $this; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function setColor($color) { $this->color = $color; return $this; } - public function setToken($token, $removed=false) { + public function setToken($token, $removed = false) { $this->token = $token; $this->tokenRemoved = $removed; return $this; } public function getEventGroup() { return array_merge(array($this), $this->eventGroup); } public function addEventToGroup(PHUITimelineEventView $event) { $this->eventGroup[] = $event; return $this; } protected function shouldRenderEventTitle() { if ($this->title === null) { return false; } return true; } protected function renderEventTitle($force_icon, $has_menu, $extra) { $title = $this->title; $title_classes = array(); $title_classes[] = 'phui-timeline-title'; $icon = null; if ($this->icon || $force_icon) { $title_classes[] = 'phui-timeline-title-with-icon'; } if ($has_menu) { $title_classes[] = 'phui-timeline-title-with-menu'; } if ($this->icon) { $fill_classes = array(); $fill_classes[] = 'phui-timeline-icon-fill'; if ($this->color) { $fill_classes[] = 'phui-timeline-icon-fill-'.$this->color; } $icon = id(new PHUIIconView()) ->setIconFont($this->icon.' white') ->addClass('phui-timeline-icon'); $icon = phutil_tag( 'span', array( 'class' => implode(' ', $fill_classes), ), $icon); } $token = null; if ($this->token) { $token = id(new PHUIIconView()) ->addClass('phui-timeline-token') ->setSpriteSheet(PHUIIconView::SPRITE_TOKENS) ->setSpriteIcon($this->token); if ($this->tokenRemoved) { $token->addClass('strikethrough'); } } $title = phutil_tag( 'div', array( 'class' => implode(' ', $title_classes), ), array($icon, $token, $title, $extra)); return $title; } public function render() { $events = $this->getEventGroup(); // Move events with icons first. $icon_keys = array(); foreach ($this->getEventGroup() as $key => $event) { if ($event->icon) { $icon_keys[] = $key; } } $events = array_select_keys($events, $icon_keys) + $events; $force_icon = (bool)$icon_keys; $menu = null; $items = array(); $has_menu = false; if (!$this->getIsPreview()) { foreach ($this->getEventGroup() as $event) { $items[] = $event->getMenuItems($this->anchor); if ($event->hasChildren()) { $has_menu = true; } } $items = array_mergev($items); } if ($items || $has_menu) { $icon = id(new PHUIIconView()) ->setIconFont('fa-caret-down'); $aural = javelin_tag( 'span', array( 'aural' => true, ), pht('Comment Actions')); if ($items) { $sigil = 'phui-timeline-menu'; Javelin::initBehavior('phui-timeline-dropdown-menu'); } else { $sigil = null; } $action_list = id(new PhabricatorActionListView()) ->setUser($this->getUser()); foreach ($items as $item) { $action_list->addAction($item); } $menu = javelin_tag( $items ? 'a' : 'span', array( 'href' => '#', 'class' => 'phui-timeline-menu', 'sigil' => $sigil, 'aria-haspopup' => 'true', 'aria-expanded' => 'false', 'meta' => array( 'items' => hsprintf('%s', $action_list), ), ), array( $aural, $icon, )); $has_menu = true; } // Render "extra" information (timestamp, etc). $extra = $this->renderExtra($events); $group_titles = array(); $group_items = array(); $group_children = array(); foreach ($events as $event) { if ($event->shouldRenderEventTitle()) { $group_titles[] = $event->renderEventTitle( $force_icon, $has_menu, $extra); // Don't render this information more than once. $extra = null; } if ($event->hasChildren()) { $group_children[] = $event->renderChildren(); } } $image_uri = $this->userHandle->getImageURI(); $wedge = phutil_tag( 'div', array( 'class' => 'phui-timeline-wedge phui-timeline-border', 'style' => (nonempty($image_uri)) ? '' : 'display: none;', ), ''); $image = phutil_tag( 'div', array( 'style' => 'background-image: url('.$image_uri.')', 'class' => 'phui-timeline-image', ), ''); $content_classes = array(); $content_classes[] = 'phui-timeline-content'; $classes = array(); $classes[] = 'phui-timeline-event-view'; if ($group_children) { $classes[] = 'phui-timeline-major-event'; $content = phutil_tag( 'div', array( 'class' => 'phui-timeline-inner-content', ), array( $group_titles, $menu, phutil_tag( 'div', array( 'class' => 'phui-timeline-core-content', ), $group_children), )); } else { $classes[] = 'phui-timeline-minor-event'; $content = $group_titles; } $content = phutil_tag( 'div', array( 'class' => 'phui-timeline-group phui-timeline-border', ), $content); $content = phutil_tag( 'div', array( 'class' => implode(' ', $content_classes), ), array($image, $wedge, $content)); $outer_classes = $this->classes; $outer_classes[] = 'phui-timeline-shell'; $color = null; foreach ($this->getEventGroup() as $event) { if ($event->color) { $color = $event->color; break; } } if ($color) { $outer_classes[] = 'phui-timeline-'.$color; } $sigil = null; $meta = null; if ($this->getTransactionPHID()) { $sigil = 'transaction'; $meta = array( 'phid' => $this->getTransactionPHID(), 'anchor' => $this->anchor, ); } return javelin_tag( 'div', array( 'class' => implode(' ', $outer_classes), 'id' => $this->anchor ? 'anchor-'.$this->anchor : null, 'sigil' => $sigil, 'meta' => $meta, ), phutil_tag( 'div', array( 'class' => implode(' ', $classes), ), $content)); } private function renderExtra(array $events) { $extra = array(); if ($this->getIsPreview()) { $extra[] = pht('PREVIEW'); } else { foreach ($events as $event) { if ($event->getIsEdited()) { $extra[] = pht('Edited'); break; } } $source = $this->getContentSource(); if ($source) { $extra[] = id(new PhabricatorContentSourceView()) ->setContentSource($source) ->setUser($this->getUser()) ->render(); } $date_created = null; foreach ($events as $event) { if ($event->getDateCreated()) { if ($date_created === null) { $date_created = $event->getDateCreated(); } else { $date_created = min($event->getDateCreated(), $date_created); } } } if ($date_created) { $date = phabricator_datetime( $date_created, $this->getUser()); if ($this->anchor) { Javelin::initBehavior('phabricator-watch-anchor'); $anchor = id(new PhabricatorAnchorView()) ->setAnchorName($this->anchor) ->render(); $date = array( $anchor, phutil_tag( 'a', array( 'href' => '#'.$this->anchor, ), $date), ); } $extra[] = $date; } } $extra = javelin_tag( 'span', array( 'class' => 'phui-timeline-extra', ), phutil_implode_html( javelin_tag( 'span', array( 'aural' => false, ), self::DELIMITER), $extra)); return $extra; } private function getMenuItems($anchor) { $xaction_phid = $this->getTransactionPHID(); $items = array(); if ($this->getIsEditable()) { $items[] = id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setHref('/transactions/edit/'.$xaction_phid.'/') ->setName(pht('Edit Comment')) ->addSigil('transaction-edit') ->setMetadata( array( 'anchor' => $anchor, )); } if ($this->getQuoteTargetID()) { $ref = null; if ($this->getQuoteRef()) { $ref = $this->getQuoteRef(); if ($anchor) { $ref = $ref.'#'.$anchor; } } $items[] = id(new PhabricatorActionView()) ->setIcon('fa-quote-left') ->setHref('#') ->setName(pht('Quote')) ->addSigil('transaction-quote') ->setMetadata( array( 'targetID' => $this->getQuoteTargetID(), 'uri' => '/transactions/quote/'.$xaction_phid.'/', 'ref' => $ref, )); // if there is something to quote then there is something to view raw $items[] = id(new PhabricatorActionView()) ->setIcon('fa-cutlery') ->setHref('/transactions/raw/'.$xaction_phid.'/') ->setName(pht('View Raw')) ->addSigil('transaction-raw') ->setMetadata( array( 'anchor' => $anchor, )); $content_source = $this->getContentSource(); $source_email = PhabricatorContentSource::SOURCE_EMAIL; if ($content_source->getSource() == $source_email) { $source_id = $content_source->getParam('id'); if ($source_id) { $items[] = id(new PhabricatorActionView()) ->setIcon('fa-envelope-o') ->setHref('/transactions/raw/'.$xaction_phid.'/?email') ->setName(pht('View Email Body')) ->addSigil('transaction-raw') ->setMetadata( array( 'anchor' => $anchor, )); } } } if ($this->getIsRemovable()) { $items[] = id(new PhabricatorActionView()) ->setIcon('fa-times') ->setHref('/transactions/remove/'.$xaction_phid.'/') ->setName(pht('Remove Comment')) ->addSigil('transaction-remove') ->setMetadata( array( 'anchor' => $anchor, )); } if ($this->getIsEdited()) { $items[] = id(new PhabricatorActionView()) ->setIcon('fa-list') ->setHref('/transactions/history/'.$xaction_phid.'/') ->setName(pht('View Edit History')) ->setWorkflow(true); } return $items; } } diff --git a/src/view/phui/PHUIWorkboardView.php b/src/view/phui/PHUIWorkboardView.php index 06ee373b6a..948d1c5740 100644 --- a/src/view/phui/PHUIWorkboardView.php +++ b/src/view/phui/PHUIWorkboardView.php @@ -1,82 +1,82 @@ panels[] = $panel; return $this; } public function setFluidLayout($layout) { $this->fluidLayout = $layout; return $this; } public function setFluidishLayout($layout) { $this->fluidishLayout = $layout; return $this; } public function addAction(PHUIIconView $action) { $this->actions[] = $action; return $this; } public function getTagAttributes() { return array( 'class' => 'phui-workboard-view', ); } public function getTagContent() { require_celerity_resource('phui-workboard-view-css'); $action_list = null; if (!empty($this->actions)) { $items = array(); foreach ($this->actions as $action) { $items[] = phutil_tag( 'li', array( - 'class' => 'phui-workboard-action-item' + 'class' => 'phui-workboard-action-item', ), $action); } $action_list = phutil_tag( 'ul', array( - 'class' => 'phui-workboard-action-list' + 'class' => 'phui-workboard-action-list', ), $items); } $view = new AphrontMultiColumnView(); $view->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM); if ($this->fluidLayout) { $view->setFluidLayout($this->fluidLayout); } if ($this->fluidishLayout) { $view->setFluidishLayout($this->fluidishLayout); } foreach ($this->panels as $panel) { $view->addColumn($panel); } $board = phutil_tag( 'div', array( - 'class' => 'phui-workboard-view-shadow' + 'class' => 'phui-workboard-view-shadow', ), $view); return array( $action_list, $board, ); } } diff --git a/src/view/phui/PHUIWorkpanelView.php b/src/view/phui/PHUIWorkpanelView.php index ef420b7056..1842c9d08b 100644 --- a/src/view/phui/PHUIWorkpanelView.php +++ b/src/view/phui/PHUIWorkpanelView.php @@ -1,112 +1,112 @@ headerIcon = $header_icon; return $this; } public function getHeaderIcon() { return $this->headerIcon; } public function setCards(PHUIObjectItemListView $cards) { $this->cards[] = $cards; return $this; } public function setHeader($header) { $this->header = $header; return $this; } public function setFooterAction(PHUIListItemView $footer_action) { $this->footerAction = $footer_action; return $this; } public function setHeaderColor($header_color) { $this->headerColor = $header_color; return $this; } public function addHeaderAction(PHUIIconView $action) { $this->headerActions[] = $action; return $this; } public function setHeaderTag(PHUITagView $tag) { $this->headerTag = $tag; return $this; } public function getTagAttributes() { return array( 'class' => 'phui-workpanel-view', ); } public function getTagContent() { require_celerity_resource('phui-workpanel-view-css'); $classes = array(); $classes[] = 'phui-workpanel-view-inner'; $footer = ''; if ($this->footerAction) { $footer_tag = $this->footerAction; $footer = phutil_tag( 'ul', array( - 'class' => 'phui-workpanel-footer-action mst ps' + 'class' => 'phui-workpanel-footer-action mst ps', ), $footer_tag); } $header = id(new PHUIActionHeaderView()) ->setHeaderTitle($this->header) ->setHeaderColor($this->headerColor); if ($this->headerIcon) { $header->setHeaderIcon($this->headerIcon); } if ($this->headerTag) { $header->setTag($this->headerTag); } foreach ($this->headerActions as $action) { $header->addAction($action); } $classes[] = 'phui-workpanel-'.$this->headerColor; $body = phutil_tag( 'div', array( - 'class' => 'phui-workpanel-body' + 'class' => 'phui-workpanel-body', ), $this->cards); $view = phutil_tag( 'div', array( 'class' => implode(' ', $classes), ), array( $header, $body, $footer, )); return $view; } } diff --git a/src/view/phui/calendar/PHUICalendarListView.php b/src/view/phui/calendar/PHUICalendarListView.php index 70761ba063..953abc6d1a 100644 --- a/src/view/phui/calendar/PHUICalendarListView.php +++ b/src/view/phui/calendar/PHUICalendarListView.php @@ -1,126 +1,130 @@ events[] = $event; return $this; } public function showBlankState($state) { $this->blankState = $state; return $this; } public function getTagName() { return 'div'; } public function getTagAttributes() { require_celerity_resource('phui-calendar-css'); require_celerity_resource('phui-calendar-list-css'); return array('class' => 'phui-calendar-day-list'); } protected function getTagContent() { if (!$this->blankState && empty($this->events)) { return ''; } $events = msort($this->events, 'getEpochStart'); $singletons = array(); $allday = false; foreach ($events as $event) { $color = $event->getColor(); if ($event->getAllDay()) { $timelabel = pht('All Day'); } else { $timelabel = phabricator_time( $event->getEpochStart(), $this->getUser()); } $dot = phutil_tag( 'span', array( - 'class' => 'phui-calendar-list-dot'), + 'class' => 'phui-calendar-list-dot', + ), ''); $title = phutil_tag( 'span', array( - 'class' => 'phui-calendar-list-title'), + 'class' => 'phui-calendar-list-title', + ), $this->renderEventLink($event, $allday)); $time = phutil_tag( 'span', array( - 'class' => 'phui-calendar-list-time'), + 'class' => 'phui-calendar-list-time', + ), $timelabel); $singletons[] = phutil_tag( 'li', array( - 'class' => 'phui-calendar-list-item phui-calendar-'.$color + 'class' => 'phui-calendar-list-item phui-calendar-'.$color, ), array( $dot, $title, - $time)); + $time, + )); } if (empty($singletons)) { $singletons[] = phutil_tag( 'li', array( - 'class' => 'phui-calendar-list-item-empty' - ), + 'class' => 'phui-calendar-list-item-empty', + ), pht('Clear sailing ahead.')); } $list = phutil_tag( 'ul', array( - 'class' => 'phui-calendar-list' + 'class' => 'phui-calendar-list', ), $singletons); return $list; } private function renderEventLink($event) { Javelin::initBehavior('phabricator-tooltips'); if ($event->getMultiDay()) { $tip = pht('%s, Until: %s', $event->getName(), phabricator_date($event->getEpochEnd(), $this->getUser())); } else { $tip = pht('%s, Until: %s', $event->getName(), phabricator_time($event->getEpochEnd(), $this->getUser())); } $description = $event->getDescription(); if (strlen($description) == 0) { $description = pht('(%s)', $event->getName()); } $anchor = javelin_tag( 'a', array( 'sigil' => 'has-tooltip', 'class' => 'phui-calendar-item-link', 'href' => '/calendar/event/view/'.$event->getEventID().'/', 'meta' => array( 'tip' => $tip, 'size' => 200, ), ), $description); return $anchor; } } diff --git a/src/view/phui/calendar/PHUICalendarMonthView.php b/src/view/phui/calendar/PHUICalendarMonthView.php index 0b63643af1..17e9fd420b 100644 --- a/src/view/phui/calendar/PHUICalendarMonthView.php +++ b/src/view/phui/calendar/PHUICalendarMonthView.php @@ -1,308 +1,309 @@ browseURI = $browse_uri; return $this; } private function getBrowseURI() { return $this->browseURI; } public function addEvent(AphrontCalendarEventView $event) { $this->events[] = $event; return $this; } public function setImage($uri) { $this->image = $uri; return $this; } public function setHolidays(array $holidays) { assert_instances_of($holidays, 'PhabricatorCalendarHoliday'); $this->holidays = mpull($holidays, null, 'getDay'); return $this; } public function __construct($month, $year, $day = null) { $this->day = $day; $this->month = $month; $this->year = $year; } public function render() { if (empty($this->user)) { throw new Exception('Call setUser() before render()!'); } $events = msort($this->events, 'getEpochStart'); $days = $this->getDatesInMonth(); require_celerity_resource('phui-calendar-month-css'); $first = reset($days); $empty = $first->format('w'); $markup = array(); $empty_box = phutil_tag( 'div', array('class' => 'phui-calendar-day phui-calendar-empty'), ''); for ($ii = 0; $ii < $empty; $ii++) { $markup[] = $empty_box; } $show_events = array(); foreach ($days as $day) { $day_number = $day->format('j'); $holiday = idx($this->holidays, $day->format('Y-m-d')); $class = 'phui-calendar-day'; $weekday = $day->format('w'); if ($day_number == $this->day) { $class .= ' phui-calendar-today'; } if ($holiday || $weekday == 0 || $weekday == 6) { $class .= ' phui-calendar-not-work-day'; } $day->setTime(0, 0, 0); $epoch_start = $day->format('U'); $day->modify('+1 day'); $epoch_end = $day->format('U'); if ($weekday == 0) { $show_events = array(); } else { $show_events = array_fill_keys( array_keys($show_events), phutil_tag_div( 'phui-calendar-event phui-calendar-event-empty', "\xC2\xA0")); //   } $list_events = array(); foreach ($events as $event) { if ($event->getEpochStart() >= $epoch_end) { // This list is sorted, so we can stop looking. break; } if ($event->getEpochStart() < $epoch_end && $event->getEpochEnd() > $epoch_start) { $list_events[] = $event; } } $list = new PHUICalendarListView(); $list->setUser($this->user); foreach ($list_events as $item) { $list->addEvent($item); } $holiday_markup = null; if ($holiday) { $name = $holiday->getName(); $holiday_markup = phutil_tag( 'div', array( 'class' => 'phui-calendar-holiday', 'title' => $name, ), $name); } $markup[] = phutil_tag_div( $class, array( phutil_tag_div('phui-calendar-date-number', $day_number), $holiday_markup, $list, )); } $table = array(); $rows = array_chunk($markup, 7); foreach ($rows as $row) { $cells = array(); while (count($row) < 7) { $row[] = $empty_box; } $j = 0; foreach ($row as $cell) { if ($j == 0) { $cells[] = phutil_tag( 'td', array( - 'class' => 'phui-calendar-month-weekstart'), + 'class' => 'phui-calendar-month-weekstart', + ), $cell); } else { $cells[] = phutil_tag('td', array(), $cell); } $j++; } $table[] = phutil_tag('tr', array(), $cells); } $header = phutil_tag( 'tr', array('class' => 'phui-calendar-day-of-week-header'), array( phutil_tag('th', array(), pht('Sun')), phutil_tag('th', array(), pht('Mon')), phutil_tag('th', array(), pht('Tue')), phutil_tag('th', array(), pht('Wed')), phutil_tag('th', array(), pht('Thu')), phutil_tag('th', array(), pht('Fri')), phutil_tag('th', array(), pht('Sat')), )); $table = phutil_tag( 'table', array('class' => 'phui-calendar-view'), array( $header, phutil_implode_html("\n", $table), )); $box = id(new PHUIObjectBoxView()) ->setHeader($this->renderCalendarHeader($first)) ->appendChild($table); return $box; } private function renderCalendarHeader(DateTime $date) { $button_bar = null; // check for a browseURI, which means we need "fancy" prev / next UI $uri = $this->getBrowseURI(); if ($uri) { $uri = new PhutilURI($uri); list($prev_year, $prev_month) = $this->getPrevYearAndMonth(); $query = array('year' => $prev_year, 'month' => $prev_month); $prev_uri = (string) $uri->setQueryParams($query); list($next_year, $next_month) = $this->getNextYearAndMonth(); $query = array('year' => $next_year, 'month' => $next_month); $next_uri = (string) $uri->setQueryParams($query); $button_bar = new PHUIButtonBarView(); $left_icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-left bluegrey'); $left = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setHref($prev_uri) ->setTitle(pht('Previous Month')) ->setIcon($left_icon); $right_icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-right bluegrey'); $right = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setHref($next_uri) ->setTitle(pht('Next Month')) ->setIcon($right_icon); $button_bar->addButton($left); $button_bar->addButton($right); } $header = id(new PHUIHeaderView()) ->setHeader($date->format('F Y')); if ($button_bar) { $header->setButtonBar($button_bar); } if ($this->image) { $header->setImage($this->image); } return $header; } private function getNextYearAndMonth() { $month = $this->month; $year = $this->year; $next_year = $year; $next_month = $month + 1; if ($next_month == 13) { $next_year = $year + 1; $next_month = 1; } return array($next_year, $next_month); } private function getPrevYearAndMonth() { $month = $this->month; $year = $this->year; $prev_year = $year; $prev_month = $month - 1; if ($prev_month == 0) { $prev_year = $year - 1; $prev_month = 12; } return array($prev_year, $prev_month); } /** * Return a DateTime object representing the first moment in each day in the * month, according to the user's locale. * * @return list List of DateTimes, one for each day. */ private function getDatesInMonth() { $user = $this->user; $timezone = new DateTimeZone($user->getTimezoneIdentifier()); $month = $this->month; $year = $this->year; // Get the year and month numbers of the following month, so we can // determine when this month ends. list($next_year, $next_month) = $this->getNextYearAndMonth(); $end_date = new DateTime("{$next_year}-{$next_month}-01", $timezone); $end_epoch = $end_date->format('U'); $days = array(); for ($day = 1; $day <= 31; $day++) { $day_date = new DateTime("{$year}-{$month}-{$day}", $timezone); $day_epoch = $day_date->format('U'); if ($day_epoch >= $end_epoch) { break; } else { $days[] = $day_date; } } return $days; } } diff --git a/src/view/widget/bars/AphrontGlyphBarView.php b/src/view/widget/bars/AphrontGlyphBarView.php index c013d18b72..d75fc932c4 100644 --- a/src/view/widget/bars/AphrontGlyphBarView.php +++ b/src/view/widget/bars/AphrontGlyphBarView.php @@ -1,102 +1,102 @@ value = $value; return $this; } public function setMax($max) { $this->max = $max; return $this; } public function setNumGlyphs($nn) { $this->numGlyphs = $nn; return $this; } public function setGlyph(PhutilSafeHTML $fg_glyph) { $this->fgGlyph = $fg_glyph; return $this; } public function setBackgroundGlyph(PhutilSafeHTML $bg_glyph) { $this->bgGlyph = $bg_glyph; return $this; } protected function getRatio() { return min($this->value, $this->max) / $this->max; } public function render() { require_celerity_resource('aphront-bars'); $ratio = $this->getRatio(); $percentage = 100 * $ratio; $is_star = false; if ($this->fgGlyph) { $fg_glyph = $this->fgGlyph; if ($this->bgGlyph) { $bg_glyph = $this->bgGlyph; } else { $bg_glyph = $fg_glyph; } } else { $is_star = true; $fg_glyph = self::BLACK_STAR; $bg_glyph = self::WHITE_STAR; } $fg_glyphs = array_fill(0, $this->numGlyphs, $fg_glyph); $bg_glyphs = array_fill(0, $this->numGlyphs, $bg_glyph); $color = $this->getColor(); return phutil_tag( 'div', array( 'class' => "aphront-bar glyph color-{$color}", ), array( phutil_tag( 'div', array( 'class' => 'glyphs'.($is_star ? ' starstar' : ''), ), array( phutil_tag( 'div', array( 'class' => 'fg', 'style' => "width: {$percentage}%;", ), $fg_glyphs), phutil_tag( 'div', array(), - $bg_glyphs) + $bg_glyphs), )), phutil_tag( 'div', array('class' => 'caption'), - $this->getCaption()) + $this->getCaption()), )); } } diff --git a/src/view/widget/bars/AphrontProgressBarView.php b/src/view/widget/bars/AphrontProgressBarView.php index ef63cd0ccd..9987c68955 100644 --- a/src/view/widget/bars/AphrontProgressBarView.php +++ b/src/view/widget/bars/AphrontProgressBarView.php @@ -1,57 +1,58 @@ value = $value; return $this; } public function setMax($max) { $this->max = $max; return $this; } public function setAlt($text) { $this->alt = $text; return $this; } protected function getRatio() { return min($this->value, $this->max) / $this->max; } public function render() { require_celerity_resource('aphront-bars'); $ratio = $this->getRatio(); $width = self::WIDTH * $ratio; $color = $this->getColor(); return phutil_tag_div( "aphront-bar progress color-{$color}", array( phutil_tag( 'div', array('title' => $this->alt), phutil_tag( 'div', array('style' => "width: {$width}px;"), '')), phutil_tag( 'span', array(), - $this->getCaption()))); + $this->getCaption()), + )); } } diff --git a/src/view/widget/hovercard/PhabricatorHovercardView.php b/src/view/widget/hovercard/PhabricatorHovercardView.php index 09b05a984d..40cb6fa30e 100644 --- a/src/view/widget/hovercard/PhabricatorHovercardView.php +++ b/src/view/widget/hovercard/PhabricatorHovercardView.php @@ -1,170 +1,171 @@ handle = $handle; return $this; } public function setTitle($title) { $this->title = $title; return $this; } public function setDetail($detail) { $this->detail = $detail; return $this; } public function addField($label, $value) { $this->fields[] = array( 'label' => $label, 'value' => $value, ); return $this; } public function addAction($label, $uri, $workflow = false) { $this->actions[] = array( 'label' => $label, 'uri' => $uri, 'workflow' => $workflow, ); return $this; } public function addTag(PHUITagView $tag) { $this->tags[] = $tag; return $this; } public function setColor($color) { $this->color = $color; return $this; } public function render() { if (!$this->handle) { throw new Exception('Call setObjectHandle() before calling render()!'); } $handle = $this->handle; require_celerity_resource('phabricator-hovercard-view-css'); $title = pht('%s: %s', $handle->getTypeName(), $this->title ? $this->title : $handle->getName()); $header = new PHUIActionHeaderView(); $header->setHeaderColor($this->color); $header->setHeaderTitle($title); if ($this->tags) { foreach ($this->tags as $tag) { $header->setTag($tag); } } $body = array(); if ($this->detail) { $body_title = $this->detail; } else { // Fallback for object handles $body_title = $handle->getFullName(); } $body[] = phutil_tag_div('phabricator-hovercard-body-header', $body_title); foreach ($this->fields as $field) { $item = array( phutil_tag('strong', array(), $field['label']), ' ', phutil_tag('span', array(), $field['value']), ); $body[] = phutil_tag_div('phabricator-hovercard-body-item', $item); } if ($handle->getImageURI()) { // Probably a user, we don't need to assume something else // "Prepend" the image by appending $body $body = phutil_tag( 'div', array( - 'class' => 'phabricator-hovercard-body-image'), - phutil_tag( - 'div', - array( - 'class' => 'profile-header-picture-frame', - 'style' => 'background-image: url('.$handle->getImageURI().');', - ), - '')) - ->appendHTML( - phutil_tag( - 'div', - array( - 'class' => 'phabricator-hovercard-body-details', - ), - $body)); + 'class' => 'phabricator-hovercard-body-image', + ), + phutil_tag( + 'div', + array( + 'class' => 'profile-header-picture-frame', + 'style' => 'background-image: url('.$handle->getImageURI().');', + ), + '')) + ->appendHTML( + phutil_tag( + 'div', + array( + 'class' => 'phabricator-hovercard-body-details', + ), + $body)); } $buttons = array(); foreach ($this->actions as $action) { $options = array( 'class' => 'button grey', 'href' => $action['uri'], ); if ($action['workflow']) { $options['sigil'] = 'workflow'; $buttons[] = javelin_tag( 'a', $options, $action['label']); } else { $buttons[] = phutil_tag( 'a', $options, $action['label']); } } $tail = null; if ($buttons) { $tail = phutil_tag_div('phabricator-hovercard-tail', $buttons); } // Assemble container // TODO: Add color support $hovercard = phutil_tag_div( 'phabricator-hovercard-container', array( phutil_tag_div('phabricator-hovercard-head', $header), phutil_tag_div('phabricator-hovercard-body grouped', $body), $tail, )); // Wrap for thick border // and later the tip at the bottom return phutil_tag_div('phabricator-hovercard-wrapper', $hovercard); } } diff --git a/support/PhabricatorStartup.php b/support/PhabricatorStartup.php index 62b28c85c5..51e9be02f2 100644 --- a/support/PhabricatorStartup.php +++ b/support/PhabricatorStartup.php @@ -1,855 +1,856 @@ >> 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')); } } /** * 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); + 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); 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; } } } /** * @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( "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); } } diff --git a/webroot/index.php b/webroot/index.php index 05a19d5d6b..09a62b5e4a 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -1,181 +1,182 @@ setData( array( 'R' => AphrontRequest::getHTTPHeader('Referer', '-'), 'r' => idx($_SERVER, 'REMOTE_ADDR', '-'), 'M' => idx($_SERVER, 'REQUEST_METHOD', '-'), )); DarkConsoleXHProfPluginAPI::hookProfiler(); DarkConsoleErrorLogPluginAPI::registerErrorHandler(); $sink = new AphrontPHPHTTPSink(); $response = PhabricatorSetupCheck::willProcessRequest(); if ($response) { PhabricatorStartup::endOutputCapture(); $sink->writeResponse($response); return; } $host = AphrontRequest::getHTTPHeader('Host'); $path = $_REQUEST['__path__']; switch ($host) { default: $config_key = 'aphront.default-application-configuration-class'; $application = PhabricatorEnv::newObjectFromConfig($config_key); break; } $application->setHost($host); $application->setPath($path); $application->willBuildRequest(); $request = $application->buildRequest(); // Until an administrator sets "phabricator.base-uri", assume it is the same // as the request URI. This will work fine in most cases, it just breaks down // when daemons need to do things. $request_protocol = ($request->isHTTPS() ? 'https' : 'http'); $request_base_uri = "{$request_protocol}://{$host}/"; PhabricatorEnv::setRequestBaseURI($request_base_uri); $write_guard = new AphrontWriteGuard(array($request, 'validateCSRF')); $application->setRequest($request); list($controller, $uri_data) = $application->buildController(); $access_log->setData( array( 'U' => (string)$request->getRequestURI()->getPath(), 'C' => get_class($controller), )); // If execution throws an exception and then trying to render that exception // throws another exception, we want to show the original exception, as it is // likely the root cause of the rendering exception. $original_exception = null; try { $response = $controller->willBeginExecution(); if ($request->getUser() && $request->getUser()->getPHID()) { $access_log->setData( array( 'u' => $request->getUser()->getUserName(), 'P' => $request->getUser()->getPHID(), )); } if (!$response) { $controller->willProcessRequest($uri_data); $response = $controller->processRequest(); } } catch (Exception $ex) { $original_exception = $ex; $response = $application->handleException($ex); } try { $response = $controller->didProcessRequest($response); $response = $application->willSendResponse($response, $controller); $response->setRequest($request); $unexpected_output = PhabricatorStartup::endOutputCapture(); if ($unexpected_output) { $unexpected_output = "Unexpected output:\n\n{$unexpected_output}"; phlog($unexpected_output); if ($response instanceof AphrontWebpageResponse) { echo phutil_tag( 'div', array('style' => 'background: #eeddff;'. 'white-space: pre-wrap;'. 'z-index: 200000;'. 'position: relative;'. 'padding: 8px;'. - 'font-family: monospace'), + 'font-family: monospace', + ), $unexpected_output); } } $sink->writeResponse($response); } catch (Exception $ex) { $write_guard->dispose(); $access_log->write(); if ($original_exception) { $ex = new PhutilAggregateException( 'Multiple exceptions during processing and rendering.', array( $original_exception, $ex, )); } PhabricatorStartup::didEncounterFatalException( 'Rendering Exception', $ex, $show_unexpected_traces); } $write_guard->dispose(); $access_log->setData( array( 'c' => $response->getHTTPResponseCode(), 'T' => PhabricatorStartup::getMicrosecondsSinceStart(), )); DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log); // Add points to the rate limits for this request. if (isset($_SERVER['REMOTE_ADDR'])) { $user_ip = $_SERVER['REMOTE_ADDR']; // The base score for a request allows users to make 30 requests per // minute. $score = (1000 / 30); // If the user was logged in, let them make more requests. if ($request->getUser() && $request->getUser()->getPHID()) { $score = $score / 5; } PhabricatorStartup::addRateLimitScore($user_ip, $score); } } catch (Exception $ex) { PhabricatorStartup::didEncounterFatalException( 'Core Exception', $ex, $show_unexpected_traces); }