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 <?php $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; if ($argc !== 1 || posix_isatty(STDIN)) { echo phutil_console_format( "usage: find . -type f -name '*.php' | ./generate_php_symbols.php\n"); exit(1); } $input = file_get_contents('php://stdin'); $input = trim($input); $input = explode("\n", $input); $data = array(); $futures = array(); foreach ($input as $file) { $file = Filesystem::readablePath($file); $data[$file] = Filesystem::readFile($file); $futures[$file] = xhpast_get_parser_future($data[$file]); } foreach (Futures($futures)->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 @@ <?php /** * NOTE: Do not extend this! * * @concrete-extensible */ class AphrontDefaultApplicationConfiguration extends AphrontApplicationConfiguration { public function __construct() {} public function getApplicationName() { return 'aphront-default'; } public function getURIMap() { return $this->getResourceURIMapRules() + array( '/~/' => array( '' => 'DarkConsoleController', 'data/(?P<key>[^/]+)/' => 'DarkConsoleDataController', ), ); } protected function getResourceURIMapRules() { $extensions = CelerityResourceController::getSupportedResourceTypes(); $extensions = array_keys($extensions); $extensions = implode('|', $extensions); return array( '/res/' => array( '(?:(?P<mtime>[0-9]+)T/)?'. '(?P<library>[^/]+)/'. '(?P<hash>[a-f0-9]{8})/'. '(?P<path>.+\.(?:'.$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 @@ <?php final class DarkConsoleRequestPlugin extends DarkConsolePlugin { public function getName() { return 'Request'; } public function getDescription() { return 'Information about $_REQUEST and $_SERVER.'; } public function generateData() { return array( 'Request' => $_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 @@ <?php final class AphrontFileResponse extends AphrontResponse { private $content; private $mimeType; private $download; private $rangeMin; private $rangeMax; private $allowOrigins = array(); public function addAllowOrigin($origin) { $this->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 @@ <?php abstract class AphrontResponse { private $request; private $cacheable = false; private $responseCode = 200; private $lastModified = null; protected $frameable; public function setRequest($request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } 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 <script /> tag and then uses // Object.prototype.__defineSetter__() or similar to read response data. // This header causes the browser to loop infinitely instead of handing over // sensitive data. $shield = 'for (;;);'; $response = $shield.$json_response; return $response; } public function getCacheHeaders() { $headers = array(); if ($this->cacheable) { $headers[] = array( 'Expires', - $this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable)); + $this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable), + ); } else { $headers[] = array( 'Cache-Control', - 'private, no-cache, no-store, must-revalidate'); + 'private, no-cache, no-store, must-revalidate', + ); $headers[] = array( 'Pragma', - 'no-cache'); + 'no-cache', + ); $headers[] = array( 'Expires', - 'Sat, 01 Jan 2000 00:00:00 GMT'); + 'Sat, 01 Jan 2000 00:00:00 GMT', + ); } if ($this->lastModified) { $headers[] = array( 'Last-Modified', - $this->formatEpochTimestampForHTTPHeader($this->lastModified)); + $this->formatEpochTimestampForHTTPHeader($this->lastModified), + ); } // IE has a feature where it may override an explicit Content-Type // declaration by inferring a content type. This can be a security risk // and we always explicitly transmit the correct Content-Type header, so // prevent IE from using inferred content types. This only offers protection // on recent versions of IE; IE6/7 and Opera currently ignore this header. $headers[] = array('X-Content-Type-Options', 'nosniff'); return $headers; } private function formatEpochTimestampForHTTPHeader($epoch_timestamp) { return gmdate('D, d M Y H:i:s', $epoch_timestamp).' GMT'; } abstract public function buildResponseString(); } diff --git a/src/applications/audit/query/PhabricatorCommitSearchEngine.php b/src/applications/audit/query/PhabricatorCommitSearchEngine.php index 259cf486aa..959c364696 100644 --- a/src/applications/audit/query/PhabricatorCommitSearchEngine.php +++ b/src/applications/audit/query/PhabricatorCommitSearchEngine.php @@ -1,220 +1,221 @@ <?php final class PhabricatorCommitSearchEngine extends PhabricatorApplicationSearchEngine { public function getResultTypeDescription() { return pht('Commits'); } public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); $saved->setParameter( 'auditorPHIDs', $this->readPHIDsFromRequest($request, 'auditorPHIDs')); $saved->setParameter( 'commitAuthorPHIDs', $this->readUsersFromRequest($request, 'authors')); $saved->setParameter( 'auditStatus', $request->getStr('auditStatus')); $saved->setParameter( 'repositoryPHIDs', $this->readPHIDsFromRequest($request, 'repositoryPHIDs')); // -- TODO - T4173 - file location return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new DiffusionCommitQuery()) ->needAuditRequests(true) ->needCommitData(true); $auditor_phids = $saved->getParameter('auditorPHIDs', array()); if ($auditor_phids) { $query->withAuditorPHIDs($auditor_phids); } $commit_author_phids = $saved->getParameter('commitAuthorPHIDs', array()); if ($commit_author_phids) { $query->withAuthorPHIDs($commit_author_phids); } $audit_status = $saved->getParameter('auditStatus', null); if ($audit_status) { $query->withAuditStatus($audit_status); } $awaiting_user_phid = $saved->getParameter('awaitingUserPHID', null); if ($awaiting_user_phid) { // This is used only for the built-in "needs attention" filter, // so cheat and just use the already-loaded viewer rather than reloading // it. $query->withAuditAwaitingUser($this->requireViewer()); } $repository_phids = $saved->getParameter('repositoryPHIDs', array()); if ($repository_phids) { $query->withRepositoryPHIDs($repository_phids); } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $auditor_phids = $saved->getParameter('auditorPHIDs', array()); $commit_author_phids = $saved->getParameter( 'commitAuthorPHIDs', array()); $audit_status = $saved->getParameter('auditStatus', null); $repository_phids = $saved->getParameter('repositoryPHIDs', array()); $phids = array_mergev( array( $auditor_phids, $commit_author_phids, - $repository_phids)); + $repository_phids, + )); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->withPHIDs($phids) ->execute(); $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource(new DiffusionAuditorDatasource()) ->setName('auditorPHIDs') ->setLabel(pht('Auditors')) ->setValue(array_select_keys($handles, $auditor_phids))) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('authors') ->setLabel(pht('Commit Authors')) ->setValue(array_select_keys($handles, $commit_author_phids))) ->appendChild( id(new AphrontFormSelectControl()) ->setName('auditStatus') ->setLabel(pht('Audit Status')) ->setOptions($this->getAuditStatusOptions()) ->setValue($audit_status)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Repositories')) ->setName('repositoryPHIDs') ->setDatasource(new DiffusionRepositoryDatasource()) ->setValue(array_select_keys($handles, $repository_phids))); } protected function getURI($path) { return '/audit/'.$path; } public function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['need'] = pht('Need Attention'); $names['problem'] = pht('Problem Commits'); } $names['open'] = pht('Open Audits'); if ($this->requireViewer()->isLoggedIn()) { $names['authored'] = pht('Authored Commits'); } $names['all'] = pht('All Commits'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer = $this->requireViewer(); switch ($query_key) { case 'all': return $query; case 'open': $query->setParameter( 'auditStatus', DiffusionCommitQuery::AUDIT_STATUS_OPEN); return $query; case 'need': $query->setParameter('awaitingUserPHID', $viewer->getPHID()); $query->setParameter( 'auditStatus', DiffusionCommitQuery::AUDIT_STATUS_OPEN); $query->setParameter( 'auditorPHIDs', PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($viewer)); return $query; case 'authored': $query->setParameter('commitAuthorPHIDs', array($viewer->getPHID())); return $query; case 'problem': $query->setParameter('commitAuthorPHIDs', array($viewer->getPHID())); $query->setParameter( 'auditStatus', DiffusionCommitQuery::AUDIT_STATUS_CONCERN); return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } private function getAuditStatusOptions() { return array( DiffusionCommitQuery::AUDIT_STATUS_ANY => pht('Any'), DiffusionCommitQuery::AUDIT_STATUS_OPEN => pht('Open'), DiffusionCommitQuery::AUDIT_STATUS_CONCERN => pht('Concern Raised'), DiffusionCommitQuery::AUDIT_STATUS_ACCEPTED => pht('Accepted'), DiffusionCommitQuery::AUDIT_STATUS_PARTIAL => pht('Partially Audited'), ); } protected function renderResultList( array $commits, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($commits, 'PhabricatorRepositoryCommit'); $viewer = $this->requireViewer(); $nodata = pht('No matching audits.'); $view = id(new PhabricatorAuditListView()) ->setUser($viewer) ->setCommits($commits) ->setAuthorityPHIDs( PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($viewer)) ->setNoDataString($nodata); $phids = $view->getRequiredHandlePHIDs(); if ($phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); } else { $handles = array(); } $view->setHandles($handles); return $view->buildList(); } } diff --git a/src/applications/auth/controller/PhabricatorAuthValidateController.php b/src/applications/auth/controller/PhabricatorAuthValidateController.php index 445c466556..ebb7a41ba7 100644 --- a/src/applications/auth/controller/PhabricatorAuthValidateController.php +++ b/src/applications/auth/controller/PhabricatorAuthValidateController.php @@ -1,71 +1,72 @@ <?php final class PhabricatorAuthValidateController extends PhabricatorAuthController { public function shouldRequireLogin() { return false; } public function shouldAllowPartialSessions() { return true; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $failures = array(); if (!strlen($request->getStr('expect'))) { return $this->renderErrors( array( pht( 'Login validation is missing expected parameter ("%s").', - 'phusr'))); + 'phusr'), + )); } $expect_phusr = $request->getStr('expect'); $actual_phusr = $request->getCookie(PhabricatorCookies::COOKIE_USERNAME); if ($actual_phusr != $expect_phusr) { if ($actual_phusr) { $failures[] = pht( "Attempted to set '%s' cookie to '%s', but your browser sent back ". "a cookie with the value '%s'. Clear your browser's cookies and ". "try again.", 'phusr', $expect_phusr, $actual_phusr); } else { $failures[] = pht( "Attempted to set '%s' cookie to '%s', but your browser did not ". "accept the cookie. Check that cookies are enabled, clear them, ". "and try again.", 'phusr', $expect_phusr); } } if (!$failures) { if (!$viewer->getPHID()) { $failures[] = pht( 'Login cookie was set correctly, but your login session is not '. 'valid. Try clearing cookies and logging in again.'); } } if ($failures) { return $this->renderErrors($failures); } $finish_uri = $this->getApplicationURI('finish/'); return id(new AphrontRedirectResponse())->setURI($finish_uri); } private function renderErrors(array $messages) { return $this->renderErrorPage( pht('Login Failure'), $messages); } } diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php index 9daae24df1..6c34babacb 100644 --- a/src/applications/auth/provider/PhabricatorAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -1,486 +1,486 @@ <?php abstract class PhabricatorAuthProvider { private $providerConfig; public function attachProviderConfig(PhabricatorAuthProviderConfig $config) { $this->providerConfig = $config; return $this; } public function hasProviderConfig() { return (bool)$this->providerConfig; } public function getProviderConfig() { if ($this->providerConfig === null) { throw new Exception( 'Call attachProviderConfig() before getProviderConfig()!'); } return $this->providerConfig; } public function getConfigurationHelp() { return null; } public function getDefaultProviderConfig() { return id(new PhabricatorAuthProviderConfig()) ->setProviderClass(get_class($this)) ->setIsEnabled(1) ->setShouldAllowLogin(1) ->setShouldAllowRegistration(1) ->setShouldAllowLink(1) ->setShouldAllowUnlink(1); } public function getNameForCreate() { return $this->getProviderName(); } public function getDescriptionForCreate() { return null; } public function getProviderKey() { return $this->getAdapter()->getAdapterKey(); } public function getProviderType() { return $this->getAdapter()->getAdapterType(); } public function getProviderDomain() { return $this->getAdapter()->getAdapterDomain(); } public static function getAllBaseProviders() { static $providers; if ($providers === null) { $objects = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); $providers = $objects; } return $providers; } public static function getAllProviders() { static $providers; if ($providers === null) { $objects = self::getAllBaseProviders(); $configs = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->execute(); $providers = array(); foreach ($configs as $config) { if (!isset($objects[$config->getProviderClass()])) { // This configuration is for a provider which is not installed. continue; } $object = clone $objects[$config->getProviderClass()]; $object->attachProviderConfig($config); $key = $object->getProviderKey(); if (isset($providers[$key])) { throw new Exception( pht( "Two authentication providers use the same provider key ". "('%s'). Each provider must be identified by a unique key.", $key)); } $providers[$key] = $object; } } return $providers; } public static function getAllEnabledProviders() { $providers = self::getAllProviders(); foreach ($providers as $key => $provider) { if (!$provider->isEnabled()) { unset($providers[$key]); } } return $providers; } public static function getEnabledProviderByKey($provider_key) { return idx(self::getAllEnabledProviders(), $provider_key); } abstract public function getProviderName(); abstract public function getAdapter(); public function isEnabled() { return $this->getProviderConfig()->getIsEnabled(); } public function shouldAllowLogin() { return $this->getProviderConfig()->getShouldAllowLogin(); } public function shouldAllowRegistration() { return $this->getProviderConfig()->getShouldAllowRegistration(); } public function shouldAllowAccountLink() { return $this->getProviderConfig()->getShouldAllowLink(); } public function shouldAllowAccountUnlink() { return $this->getProviderConfig()->getShouldAllowUnlink(); } public function shouldTrustEmails() { return $this->shouldAllowEmailTrustConfiguration() && $this->getProviderConfig()->getShouldTrustEmails(); } /** * Should we allow the adapter to be marked as "trusted". This is true for * all adapters except those that allow the user to type in emails (see * @{class:PhabricatorPasswordAuthProvider}). */ public function shouldAllowEmailTrustConfiguration() { return true; } public function buildLoginForm(PhabricatorAuthStartController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'start'); } abstract public function processLoginRequest( PhabricatorAuthLoginController $controller); public function buildLinkForm(PhabricatorAuthLinkController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'link'); } public function shouldAllowAccountRefresh() { return true; } public function buildRefreshForm( PhabricatorAuthLinkController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh'); } protected function renderLoginForm(AphrontRequest $request, $mode) { throw new PhutilMethodNotImplementedException(); } public function createProviders() { return array($this); } protected function willSaveAccount(PhabricatorExternalAccount $account) { return; } public function willRegisterAccount(PhabricatorExternalAccount $account) { return; } protected function loadOrCreateAccount($account_id) { if (!strlen($account_id)) { throw new Exception('loadOrCreateAccount(...): empty account ID!'); } $adapter = $this->getAdapter(); $adapter_class = get_class($adapter); if (!strlen($adapter->getAdapterType())) { throw new Exception( "AuthAdapter (of class '{$adapter_class}') has an invalid ". "implementation: no adapter type."); } if (!strlen($adapter->getAdapterDomain())) { throw new Exception( "AuthAdapter (of class '{$adapter_class}') has an invalid ". "implementation: no adapter domain."); } $account = id(new PhabricatorExternalAccount())->loadOneWhere( 'accountType = %s AND accountDomain = %s AND accountID = %s', $adapter->getAdapterType(), $adapter->getAdapterDomain(), $account_id); if (!$account) { $account = id(new PhabricatorExternalAccount()) ->setAccountType($adapter->getAdapterType()) ->setAccountDomain($adapter->getAdapterDomain()) ->setAccountID($account_id); } $account->setUsername($adapter->getAccountName()); $account->setRealName($adapter->getAccountRealName()); $account->setEmail($adapter->getAccountEmail()); $account->setAccountURI($adapter->getAccountURI()); $account->setProfileImagePHID(null); $image_uri = $adapter->getAccountImageURI(); if ($image_uri) { try { $name = PhabricatorSlug::normalize($this->getProviderName()); $name = $name.'-profile.jpg'; // TODO: If the image has not changed, we do not need to make a new // file entry for it, but there's no convenient way to do this with // PhabricatorFile right now. The storage will get shared, so the impact // here is negligible. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $image_file = PhabricatorFile::newFromFileDownload( $image_uri, array( 'name' => $name, - 'canCDN' => true + 'canCDN' => true, )); unset($unguarded); if ($image_file) { $account->setProfileImagePHID($image_file->getPHID()); } } catch (Exception $ex) { // Log this but proceed, it's not especially important that we // be able to pull profile images. phlog($ex); } } $this->willSaveAccount($account); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $account->save(); unset($unguarded); return $account; } public function getLoginURI() { $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication'); return $app->getApplicationURI('/login/'.$this->getProviderKey().'/'); } public function getSettingsURI() { return '/settings/panel/external/'; } public function getStartURI() { $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication'); $uri = $app->getApplicationURI('/start/'); return $uri; } public function isDefaultRegistrationProvider() { return false; } public function shouldRequireRegistrationPassword() { return false; } public function getDefaultExternalAccount() { throw new PhutilMethodNotImplementedException(); } public function getLoginOrder() { return '500-'.$this->getProviderName(); } protected function getLoginIcon() { return 'Generic'; } public function isLoginFormAButton() { return false; } public function renderConfigPropertyTransactionTitle( PhabricatorAuthProviderConfigTransaction $xaction) { return null; } public function readFormValuesFromProvider() { return array(); } public function readFormValuesFromRequest(AphrontRequest $request) { return array(); } public function processEditForm( AphrontRequest $request, array $values) { $errors = array(); $issues = array(); return array($errors, $issues, $values); } public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { return; } public function willRenderLinkedAccount( PhabricatorUser $viewer, PHUIObjectItemView $item, PhabricatorExternalAccount $account) { $account_view = id(new PhabricatorAuthAccountView()) ->setExternalAccount($account) ->setAuthProvider($this); $item->appendChild( phutil_tag( 'div', array( 'class' => 'mmr mml mst mmb', ), $account_view)); } /** * Return true to use a two-step configuration (setup, configure) instead of * the default single-step configuration. In practice, this means that * creating a new provider instance will redirect back to the edit page * instead of the provider list. * * @return bool True if this provider uses two-step configuration. */ public function hasSetupStep() { return false; } /** * Render a standard login/register button element. * * The `$attributes` parameter takes these keys: * * - `uri`: URI the button should take the user to when clicked. * - `method`: Optional HTTP method the button should use, defaults to GET. * * @param AphrontRequest HTTP request. * @param string Request mode string. * @param map Additional parameters, see above. * @return wild Login button. */ protected function renderStandardLoginButton( AphrontRequest $request, $mode, array $attributes = array()) { PhutilTypeSpec::checkMap( $attributes, array( 'method' => 'optional string', 'uri' => 'string', 'sigil' => 'optional string', )); $viewer = $request->getUser(); $adapter = $this->getAdapter(); if ($mode == 'link') { $button_text = pht('Link External Account'); } else if ($mode == 'refresh') { $button_text = pht('Refresh Account Link'); } else if ($this->shouldAllowRegistration()) { $button_text = pht('Login or Register'); } else { $button_text = pht('Login'); } $icon = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) ->setSpriteIcon($this->getLoginIcon()); $button = id(new PHUIButtonView()) ->setSize(PHUIButtonView::BIG) ->setColor(PHUIButtonView::GREY) ->setIcon($icon) ->setText($button_text) ->setSubtext($this->getProviderName()); $uri = $attributes['uri']; $uri = new PhutilURI($uri); $params = $uri->getQueryParams(); $uri->setQueryParams(array()); $content = array($button); foreach ($params as $key => $value) { $content[] = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value, )); } return phabricator_form( $viewer, array( 'method' => idx($attributes, 'method', 'GET'), 'action' => (string)$uri, 'sigil' => idx($attributes, 'sigil'), ), $content); } public function renderConfigurationFooter() { return null; } protected function getAuthCSRFCode(AphrontRequest $request) { $phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID); if (!strlen($phcid)) { throw new Exception( pht( 'Your browser did not submit a "%s" cookie with client state '. 'information in the request. Check that cookies are enabled. '. 'If this problem persists, you may need to clear your cookies.', PhabricatorCookies::COOKIE_CLIENTID)); } return PhabricatorHash::digest($phcid); } protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) { $expect = $this->getAuthCSRFCode($request); if (!strlen($actual)) { throw new Exception( pht( 'The authentication provider did not return a client state '. 'parameter in its response, but one was expected. If this '. 'problem persists, you may need to clear your cookies.')); } if ($actual !== $expect) { throw new Exception( pht( 'The authentication provider did not return the correct client '. 'state parameter in its response. If this problem persists, you may '. 'need to clear your cookies.')); } } } diff --git a/src/applications/calendar/__tests__/CalendarTimeUtilTestCase.php b/src/applications/calendar/__tests__/CalendarTimeUtilTestCase.php index 1eec4be1f0..f5946ea32b 100644 --- a/src/applications/calendar/__tests__/CalendarTimeUtilTestCase.php +++ b/src/applications/calendar/__tests__/CalendarTimeUtilTestCase.php @@ -1,61 +1,62 @@ <?php final class CalendarTimeUtilTestCase extends PhabricatorTestCase { public function testTimestampsAtMidnight() { $u = new PhabricatorUser(); $u->setTimezoneIdentifier('America/Los_Angeles'); $days = $this->getAllDays(); foreach ($days as $day) { $data = CalendarTimeUtil::getCalendarWidgetTimestamps( $u, $day); $this->assertEqual( '000000', $data['epoch_stamps'][0]->format('His')); } } public function testTimestampsStartDay() { $u = new PhabricatorUser(); $u->setTimezoneIdentifier('America/Los_Angeles'); $days = $this->getAllDays(); foreach ($days as $day) { $data = CalendarTimeUtil::getTimestamps( $u, $day, 1); $this->assertEqual( $day, $data['epoch_stamps'][0]->format('l')); } $t = 1370202281; // 2013-06-02 12:44:41 -0700 -- a Sunday $time = PhabricatorTime::pushTime($t, 'America/Los_Angeles'); foreach ($days as $day) { $data = CalendarTimeUtil::getTimestamps( $u, $day, 1); $this->assertEqual( $day, $data['epoch_stamps'][0]->format('l')); } unset($time); } private function getAllDays() { return array( 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', - 'Saturday'); + 'Saturday', + ); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarViewController.php b/src/applications/calendar/controller/PhabricatorCalendarViewController.php index 9131e3c2bd..f7a0bdd0ff 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarViewController.php @@ -1,113 +1,114 @@ <?php final class PhabricatorCalendarViewController extends PhabricatorCalendarController { public function shouldAllowPublic() { return true; } public function processRequest() { $user = $this->getRequest()->getUser(); $now = time(); $request = $this->getRequest(); $year_d = phabricator_format_local_time($now, $user, 'Y'); $year = $request->getInt('year', $year_d); $month_d = phabricator_format_local_time($now, $user, 'm'); $month = $request->getInt('month', $month_d); $day = phabricator_format_local_time($now, $user, 'j'); $holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere( 'day BETWEEN %s AND %s', "{$year}-{$month}-01", "{$year}-{$month}-31"); $statuses = id(new PhabricatorCalendarEventQuery()) ->setViewer($user) ->withInvitedPHIDs(array($user->getPHID())) ->withDateRange( strtotime("{$year}-{$month}-01"), strtotime("{$year}-{$month}-01 next month")) ->execute(); if ($month == $month_d && $year == $year_d) { $month_view = new PHUICalendarMonthView($month, $year, $day); } else { $month_view = new PHUICalendarMonthView($month, $year); } $month_view->setBrowseURI($request->getRequestURI()); $month_view->setUser($user); $month_view->setHolidays($holidays); $phids = mpull($statuses, 'getUserPHID'); $handles = $this->loadViewerHandles($phids); foreach ($statuses as $status) { $event = new AphrontCalendarEventView(); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $event->setUserPHID($status->getUserPHID()); $event->setName($status->getHumanStatus()); $event->setDescription($status->getDescription()); $event->setEventID($status->getID()); $month_view->addEvent($event); } $date = new DateTime("{$year}-{$month}-01"); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('My Events')); $crumbs->addTextCrumb($date->format('F Y')); $nav = $this->buildSideNavView(); $nav->selectFilter('/'); $nav->appendChild( array( $crumbs, $this->getNoticeView(), $month_view, )); return $this->buildApplicationPage( $nav, array( 'title' => pht('Calendar'), )); } private function getNoticeView() { $request = $this->getRequest(); $view = null; if ($request->getExists('created')) { $view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Successfully created your status.')); } else if ($request->getExists('updated')) { $view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Successfully updated your status.')); } else if ($request->getExists('deleted')) { $view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Successfully deleted your status.')); } else if (!$request->getUser()->isLoggedIn()) { $login_uri = id(new PhutilURI('/auth/start/')) ->setQueryParam('next', '/calendar/'); $view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle( pht( 'You are not logged in. %s to see your calendar events.', phutil_tag( 'a', array( - 'href' => $login_uri), + 'href' => $login_uri, + ), pht('Log in')))); } return $view; } } diff --git a/src/applications/calendar/util/CalendarTimeUtil.php b/src/applications/calendar/util/CalendarTimeUtil.php index 8669cde033..71edb69465 100644 --- a/src/applications/calendar/util/CalendarTimeUtil.php +++ b/src/applications/calendar/util/CalendarTimeUtil.php @@ -1,86 +1,89 @@ <?php /** * This class is useful for generating various time objects, relative to the * user and their timezone. * * For now, the class exposes two sets of static methods for the two main * calendar views - one for the conpherence calendar widget and one for the * user profile calendar view. These have slight differences such as * conpherence showing both a three day "today 'til 2 days from now" *and* * a Sunday -> Saturday list, whilest the profile view shows a more simple * seven day rolling list of events. */ final class CalendarTimeUtil { public static function getCalendarEventEpochs( PhabricatorUser $user, $start_day_str = 'Sunday', $days = 9) { $objects = self::getStartDateTimeObjects($user, $start_day_str); $start_day = $objects['start_day']; $end_day = clone $start_day; $end_day->modify('+'.$days.' days'); return array( 'start_epoch' => $start_day->format('U'), - 'end_epoch' => $end_day->format('U')); + 'end_epoch' => $end_day->format('U'), + ); } public static function getCalendarWeekTimestamps( PhabricatorUser $user) { return self::getTimestamps($user, 'Today', 7); } public static function getCalendarWidgetTimestamps( PhabricatorUser $user) { return self::getTimestamps($user, 'Sunday', 9); } /** * Public for testing purposes only. You should probably use one of the * functions above. */ public static function getTimestamps( PhabricatorUser $user, $start_day_str, $days) { $objects = self::getStartDateTimeObjects($user, $start_day_str); $start_day = $objects['start_day']; $timestamps = array(); for ($day = 0; $day < $days; $day++) { $timestamp = clone $start_day; $timestamp->modify(sprintf('+%d days', $day)); $timestamps[] = $timestamp; } return array( 'today' => $objects['today'], - 'epoch_stamps' => $timestamps); + 'epoch_stamps' => $timestamps, + ); } private static function getStartDateTimeObjects( PhabricatorUser $user, $start_day_str) { $timezone = new DateTimeZone($user->getTimezoneIdentifier()); $today_epoch = PhabricatorTime::parseLocalTime('today', $user); $today = new DateTime('@'.$today_epoch); $today->setTimeZone($timezone); if (strtolower($start_day_str) == 'today' || $today->format('l') == $start_day_str) { $start_day = clone $today; } else { $start_epoch = PhabricatorTime::parseLocalTime( 'last '.$start_day_str, $user); $start_day = new DateTime('@'.$start_epoch); $start_day->setTimeZone($timezone); } return array( 'today' => $today, - 'start_day' => $start_day); + 'start_day' => $start_day, + ); } } diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php index 58f9af658e..eff8637f12 100644 --- a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php +++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php @@ -1,331 +1,333 @@ <?php final class PhabricatorChatLogChannelLogController extends PhabricatorChatLogController { private $channelID; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->channelID = $data['channelID']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $uri = clone $request->getRequestURI(); $uri->setQueryParams(array()); $pager = new AphrontCursorPagerView(); $pager->setURI($uri); $pager->setPageSize(250); $query = id(new PhabricatorChatLogQuery()) ->setViewer($user) ->withChannelIDs(array($this->channelID)); $channel = id(new PhabricatorChatLogChannelQuery()) ->setViewer($user) ->withIDs(array($this->channelID)) ->executeOne(); if (!$channel) { return new Aphront404Response(); } list($after, $before, $map) = $this->getPagingParameters($request, $query); $pager->setAfterID($after); $pager->setBeforeID($before); $logs = $query->executeWithCursorPager($pager); // Show chat logs oldest-first. $logs = array_reverse($logs); // Divide all the logs into blocks, where a block is the same author saying // several things in a row. A block ends when another user speaks, or when // two minutes pass without the author speaking. $blocks = array(); $block = null; $last_author = null; $last_epoch = null; foreach ($logs as $log) { $this_author = $log->getAuthor(); $this_epoch = $log->getEpoch(); // Decide whether we should start a new block or not. $new_block = ($this_author !== $last_author) || ($this_epoch - (60 * 2) > $last_epoch); if ($new_block) { if ($block) { $blocks[] = $block; } $block = array( 'id' => $log->getID(), 'epoch' => $this_epoch, 'author' => $this_author, 'logs' => array($log), ); } else { $block['logs'][] = $log; } $last_author = $this_author; $last_epoch = $this_epoch; } if ($block) { $blocks[] = $block; } // Figure out CSS classes for the blocks. We alternate colors between // lines, and highlight the entire block which contains the target ID or // date, if applicable. foreach ($blocks as $key => $block) { $classes = array(); if ($key % 2) { $classes[] = 'alternate'; } $ids = mpull($block['logs'], 'getID', 'getID'); if (array_intersect_key($ids, $map)) { $classes[] = 'highlight'; } $blocks[$key]['class'] = $classes ? implode(' ', $classes) : null; } require_celerity_resource('phabricator-chatlog-css'); $out = array(); foreach ($blocks as $block) { $author = $block['author']; $author = id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(18) ->truncateString($author); $author = phutil_tag('td', array('class' => 'author'), $author); $href = $uri->alter('at', $block['id']); $timestamp = $block['epoch']; $timestamp = phabricator_datetime($timestamp, $user); $timestamp = phutil_tag( 'a', array( 'href' => $href, - 'class' => 'timestamp' + 'class' => 'timestamp', ), $timestamp); $message = mpull($block['logs'], 'getMessage'); $message = implode("\n", $message); $message = phutil_tag( 'td', array( - 'class' => 'message' + 'class' => 'message', ), array( $timestamp, - $message)); + $message, + )); $out[] = phutil_tag( 'tr', array( 'class' => $block['class'], ), array( $author, - $message)); + $message, + )); } $links = array(); $first_uri = $pager->getFirstPageURI(); if ($first_uri) { $links[] = phutil_tag( 'a', array( 'href' => $first_uri, ), "\xC2\xAB ".pht('Newest')); } $prev_uri = $pager->getPrevPageURI(); if ($prev_uri) { $links[] = phutil_tag( 'a', array( 'href' => $prev_uri, ), "\xE2\x80\xB9 ".pht('Newer')); } $next_uri = $pager->getNextPageURI(); if ($next_uri) { $links[] = phutil_tag( 'a', array( 'href' => $next_uri, ), pht('Older')." \xE2\x80\xBA"); } $pager_top = phutil_tag( 'div', array('class' => 'phabricator-chat-log-pager-top'), $links); $pager_bottom = phutil_tag( 'div', array('class' => 'phabricator-chat-log-pager-bottom'), $links); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb($channel->getChannelName(), $uri); $form = id(new AphrontFormView()) ->setUser($user) ->setMethod('GET') ->setAction($uri) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Date')) ->setName('date') ->setValue($request->getStr('date'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Jump'))); $filter = new AphrontListFilterView(); $filter->appendChild($form); $table = phutil_tag( 'table', array( - 'class' => 'phabricator-chat-log' + 'class' => 'phabricator-chat-log', ), $out); $log = phutil_tag( 'div', array( - 'class' => 'phabricator-chat-log-panel' + 'class' => 'phabricator-chat-log-panel', ), $table); $jump_link = phutil_tag( 'a', array( - 'href' => '#latest' + 'href' => '#latest', ), pht('Jump to Bottom')." \xE2\x96\xBE"); $jump = phutil_tag( 'div', array( - 'class' => 'phabricator-chat-log-jump' + 'class' => 'phabricator-chat-log-jump', ), $jump_link); $jump_target = phutil_tag( 'div', array( - 'id' => 'latest' + 'id' => 'latest', )); $content = phutil_tag( 'div', array( - 'class' => 'phabricator-chat-log-wrap' + 'class' => 'phabricator-chat-log-wrap', ), array( $jump, $pager_top, $log, $jump_target, $pager_bottom, )); return $this->buildApplicationPage( array( $crumbs, $filter, $content, ), array( 'title' => pht('Channel Log'), )); } /** * From request parameters, figure out where we should jump to in the log. * We jump to either a date or log ID, but load a few lines of context before * it so the user can see the nearby conversation. */ private function getPagingParameters( AphrontRequest $request, PhabricatorChatLogQuery $query) { $user = $request->getUser(); $at_id = $request->getInt('at'); $at_date = $request->getStr('date'); $context_log = null; $map = array(); $query = clone $query; $query->setLimit(8); if ($at_id) { // Jump to the log in question, and load a few lines of context before // it. $context_logs = $query ->setAfterID($at_id) ->execute(); $context_log = last($context_logs); $map = array( $at_id => true, ); } else if ($at_date) { $timestamp = PhabricatorTime::parseLocalTime($at_date, $user); if ($timestamp) { $context_logs = $query ->withMaximumEpoch($timestamp) ->execute(); $context_log = last($context_logs); $target_log = head($context_logs); if ($target_log) { $map = array( $target_log->getID() => true, ); } } } if ($context_log) { $after = null; $before = $context_log->getID() - 1; } else { $after = $request->getInt('after'); $before = $request->getInt('before'); } return array($after, $before, $map); } } diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index bf1c95fe3f..1a6080b0fe 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -1,475 +1,475 @@ <?php final class PhabricatorConduitAPIController extends PhabricatorConduitController { public function shouldRequireLogin() { return false; } private $method; public function willProcessRequest(array $data) { $this->method = $data['method']; return $this; } public function processRequest() { $time_start = microtime(true); $request = $this->getRequest(); $method = $this->method; $api_request = null; $log = new PhabricatorConduitMethodCallLog(); $log->setMethod($method); $metadata = array(); try { $params = $this->decodeConduitParams($request, $method); $metadata = idx($params, '__conduit__', array()); unset($params['__conduit__']); $call = new ConduitCall( $method, $params, idx($metadata, 'isProxied', false)); $result = null; // TODO: Straighten out the auth pathway here. We shouldn't be creating // a ConduitAPIRequest at this level, but some of the auth code expects // it. Landing a halfway version of this to unblock T945. $api_request = new ConduitAPIRequest($params); $allow_unguarded_writes = false; $auth_error = null; $conduit_username = '-'; if ($call->shouldRequireAuthentication()) { $metadata['scope'] = $call->getRequiredScope(); $auth_error = $this->authenticateUser($api_request, $metadata); // If we've explicitly authenticated the user here and either done // CSRF validation or are using a non-web authentication mechanism. $allow_unguarded_writes = true; if (isset($metadata['actAsUser'])) { $this->actAsUser($api_request, $metadata['actAsUser']); } if ($auth_error === null) { $conduit_user = $api_request->getUser(); if ($conduit_user && $conduit_user->getPHID()) { $conduit_username = $conduit_user->getUsername(); } $call->setUser($api_request->getUser()); } } $access_log = PhabricatorAccessLog::getLog(); if ($access_log) { $access_log->setData( array( 'u' => $conduit_username, 'm' => $method, )); } if ($call->shouldAllowUnguardedWrites()) { $allow_unguarded_writes = true; } if ($auth_error === null) { if ($allow_unguarded_writes) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); } try { $result = $call->execute(); $error_code = null; $error_info = null; } catch (ConduitException $ex) { $result = null; $error_code = $ex->getMessage(); if ($ex->getErrorDescription()) { $error_info = $ex->getErrorDescription(); } else { $error_info = $call->getErrorDescription($error_code); } } if ($allow_unguarded_writes) { unset($unguarded); } } else { list($error_code, $error_info) = $auth_error; } } catch (Exception $ex) { if (!($ex instanceof ConduitMethodNotFoundException)) { phlog($ex); } $result = null; $error_code = ($ex instanceof ConduitException ? 'ERR-CONDUIT-CALL' : 'ERR-CONDUIT-CORE'); $error_info = $ex->getMessage(); } $time_end = microtime(true); $connection_id = null; if (idx($metadata, 'connectionID')) { $connection_id = $metadata['connectionID']; } else if (($method == 'conduit.connect') && $result) { $connection_id = idx($result, 'connectionID'); } $log ->setCallerPHID( isset($conduit_user) ? $conduit_user->getPHID() : null) ->setConnectionID($connection_id) ->setError((string)$error_code) ->setDuration(1000000 * ($time_end - $time_start)); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $log->save(); unset($unguarded); $response = id(new ConduitAPIResponse()) ->setResult($result) ->setErrorCode($error_code) ->setErrorInfo($error_info); switch ($request->getStr('output')) { case 'human': return $this->buildHumanReadableResponse( $method, $api_request, $response->toDictionary()); case 'json': default: return id(new AphrontJSONResponse()) ->setAddJSONShield(false) ->setContent($response->toDictionary()); } } /** * Change the api request user to the user that we want to act as. * Only admins can use actAsUser * * @param ConduitAPIRequest Request being executed. * @param string The username of the user we want to act as */ private function actAsUser( ConduitAPIRequest $api_request, $user_name) { $config_key = 'security.allow-conduit-act-as-user'; if (!PhabricatorEnv::getEnvConfig($config_key)) { throw new Exception('security.allow-conduit-act-as-user is disabled'); } if (!$api_request->getUser()->getIsAdmin()) { throw new Exception('Only administrators can use actAsUser'); } $user = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $user_name); if (!$user) { throw new Exception( "The actAsUser username '{$user_name}' is not a valid user." ); } $api_request->setUser($user); } /** * Authenticate the client making the request to a Phabricator user account. * * @param ConduitAPIRequest Request being executed. * @param dict Request metadata. * @return null|pair Null to indicate successful authentication, or * an error code and error message pair. */ private function authenticateUser( ConduitAPIRequest $api_request, array $metadata) { $request = $this->getRequest(); if ($request->getUser()->getPHID()) { $request->validateCSRF(); return $this->validateAuthenticatedUser( $api_request, $request->getUser()); } // handle oauth $access_token = $request->getStr('access_token'); $method_scope = $metadata['scope']; if ($access_token && $method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) { $token = id(new PhabricatorOAuthServerAccessToken()) ->loadOneWhere('token = %s', $access_token); if (!$token) { return array( 'ERR-INVALID-AUTH', 'Access token does not exist.', ); } $oauth_server = new PhabricatorOAuthServer(); $valid = $oauth_server->validateAccessToken($token, $method_scope); if (!$valid) { return array( 'ERR-INVALID-AUTH', 'Access token is invalid.', ); } // valid token, so let's log in the user! $user_phid = $token->getUserPHID(); $user = id(new PhabricatorUser()) ->loadOneWhere('phid = %s', $user_phid); if (!$user) { return array( 'ERR-INVALID-AUTH', 'Access token is for invalid user.', ); } return $this->validateAuthenticatedUser( $api_request, $user); } // Handle sessionless auth. TOOD: This is super messy. if (isset($metadata['authUser'])) { $user = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $metadata['authUser']); if (!$user) { return array( 'ERR-INVALID-AUTH', 'Authentication is invalid.', ); } $token = idx($metadata, 'authToken'); $signature = idx($metadata, 'authSignature'); $certificate = $user->getConduitCertificate(); if (sha1($token.$certificate) !== $signature) { return array( 'ERR-INVALID-AUTH', 'Authentication is invalid.', ); } return $this->validateAuthenticatedUser( $api_request, $user); } $session_key = idx($metadata, 'sessionKey'); if (!$session_key) { return array( 'ERR-INVALID-SESSION', - 'Session key is not present.' + 'Session key is not present.', ); } $user = id(new PhabricatorAuthSessionEngine()) ->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key); if (!$user) { return array( 'ERR-INVALID-SESSION', 'Session key is invalid.', ); } return $this->validateAuthenticatedUser( $api_request, $user); } private function validateAuthenticatedUser( ConduitAPIRequest $request, PhabricatorUser $user) { if (!$user->isUserActivated()) { return array( 'ERR-USER-DISABLED', pht('User account is not activated.'), ); } $request->setUser($user); return null; } private function buildHumanReadableResponse( $method, ConduitAPIRequest $request = null, $result = null) { $param_rows = array(); $param_rows[] = array('Method', $this->renderAPIValue($method)); if ($request) { foreach ($request->getAllParameters() as $key => $value) { $param_rows[] = array( $key, $this->renderAPIValue($value), ); } } $param_table = new AphrontTableView($param_rows); $param_table->setDeviceReadyTable(true); $param_table->setColumnClasses( array( 'header', 'wide', )); $result_rows = array(); foreach ($result as $key => $value) { $result_rows[] = array( $key, $this->renderAPIValue($value), ); } $result_table = new AphrontTableView($result_rows); $result_table->setDeviceReadyTable(true); $result_table->setColumnClasses( array( 'header', 'wide', )); $param_panel = new AphrontPanelView(); $param_panel->setHeader('Method Parameters'); $param_panel->appendChild($param_table); $result_panel = new AphrontPanelView(); $result_panel->setHeader('Method Result'); $result_panel->appendChild($result_table); $param_head = id(new PHUIHeaderView()) ->setHeader(pht('Method Parameters')); $result_head = id(new PHUIHeaderView()) ->setHeader(pht('Method Result')); $method_uri = $this->getApplicationURI('method/'.$method.'/'); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb($method, $method_uri) ->addTextCrumb(pht('Call')); return $this->buildApplicationPage( array( $crumbs, $param_head, $param_table, $result_head, $result_table, ), array( 'title' => 'Method Call Result', )); } private function renderAPIValue($value) { $json = new PhutilJSON(); if (is_array($value)) { $value = $json->encodeFormatted($value); } $value = phutil_tag( 'pre', array('style' => 'white-space: pre-wrap;'), $value); return $value; } private function decodeConduitParams( AphrontRequest $request, $method) { // Look for parameters from the Conduit API Console, which are encoded // as HTTP POST parameters in an array, e.g.: // // params[name]=value¶ms[name2]=value2 // // The fields are individually JSON encoded, since we require users to // enter JSON so that we avoid type ambiguity. $params = $request->getArr('params', null); if ($params !== null) { foreach ($params as $key => $value) { if ($value == '') { // Interpret empty string null (e.g., the user didn't type anything // into the box). $value = 'null'; } $decoded_value = json_decode($value, true); if ($decoded_value === null && strtolower($value) != 'null') { // When json_decode() fails, it returns null. This almost certainly // indicates that a user was using the web UI and didn't put quotes // around a string value. We can either do what we think they meant // (treat it as a string) or fail. For now, err on the side of // caution and fail. In the future, if we make the Conduit API // actually do type checking, it might be reasonable to treat it as // a string if the parameter type is string. throw new Exception( "The value for parameter '{$key}' is not valid JSON. All ". "parameters must be encoded as JSON values, including strings ". "(which means you need to surround them in double quotes). ". "Check your syntax. Value was: {$value}"); } $params[$key] = $decoded_value; } return $params; } // Otherwise, look for a single parameter called 'params' which has the // entire param dictionary JSON encoded. This is the usual case for remote // requests. $params_json = $request->getStr('params'); if (!strlen($params_json)) { if ($request->getBool('allowEmptyParams')) { // TODO: This is a bit messy, but otherwise you can't call // "conduit.ping" from the web console. $params = array(); } else { throw new Exception( "Request has no 'params' key. This may mean that an extension like ". "Suhosin has dropped data from the request. Check the PHP ". "configuration on your server. If you are developing a Conduit ". "client, you MUST provide a 'params' parameter when making a ". "Conduit request, even if the value is empty (e.g., provide '{}')."); } } else { $params = json_decode($params_json, true); if (!is_array($params)) { throw new Exception( "Invalid parameter information was passed to method ". "'{$method}', could not decode JSON serialization. Data: ". $params_json); } } return $params; } } diff --git a/src/applications/config/check/PhabricatorSetupCheckDaemons.php b/src/applications/config/check/PhabricatorSetupCheckDaemons.php index 29a36e7a7c..71dfc4598f 100644 --- a/src/applications/config/check/PhabricatorSetupCheckDaemons.php +++ b/src/applications/config/check/PhabricatorSetupCheckDaemons.php @@ -1,129 +1,129 @@ <?php final class PhabricatorSetupCheckDaemons extends PhabricatorSetupCheck { protected function executeChecks() { $task_daemon = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->withDaemonClasses(array('PhabricatorTaskmasterDaemon')) ->setLimit(1) ->execute(); if (!$task_daemon) { $doc_href = PhabricatorEnv::getDocLink( 'Managing Daemons with phd'); $summary = pht( 'You must start the Phabricator daemons to send email, rebuild '. 'search indexes, and do other background processing.'); $message = pht( 'The Phabricator daemons are not running, so Phabricator will not '. 'be able to perform background processing (including sending email, '. 'rebuilding search indexes, importing commits, cleaning up old data, '. 'and running builds).'. "\n\n". 'Use %s to start daemons. See %s for more information.', phutil_tag('tt', array(), 'bin/phd start'), phutil_tag( 'a', array( 'href' => $doc_href, - 'target' => '_blank' + 'target' => '_blank', ), pht('Managing Daemons with phd'))); $this->newIssue('daemons.not-running') ->setShortName(pht('Daemons Not Running')) ->setName(pht('Phabricator Daemons Are Not Running')) ->setSummary($summary) ->setMessage($message) ->addCommand('phabricator/ $ ./bin/phd start'); } $environment_hash = PhabricatorEnv::calculateEnvironmentHash(); $all_daemons = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->execute(); foreach ($all_daemons as $daemon) { if ($daemon->getEnvHash() != $environment_hash) { $doc_href = PhabricatorEnv::getDocLink( 'Managing Daemons with phd'); $summary = pht( 'At least one daemon is currently running with different '. 'configuration than the Phabricator web application.'); $message = pht( 'At least one daemon is currently running with a different '. 'configuration (config checksum %s) than the web application '. '(config checksum %s).'. "\n\n". 'This usually means that you have just made a configuration change '. 'from the web UI, but have not yet restarted the daemons. You '. 'need to restart the daemons after making configuration changes '. 'so they will pick up the new values: until you do, they will '. 'continue operating with the old settings.'. "\n\n". '(If you plan to make more changes, you can restart the daemons '. 'once after you finish making all of your changes.)'. "\n\n". 'Use %s to restart daemons. You can find a list of running daemons '. 'in the %s, which will also help you identify which daemon (or '. 'daemons) have divergent configuration. For more information about '. 'managing the daemons, see %s in the documentation.'. "\n\n". 'This can also happen if you use the %s environmental variable to '. 'choose a configuration file, but the daemons run with a different '. 'value than the web application. If restarting the daemons does '. 'not resolve this issue and you use %s to select configuration, '. 'check that it is set consistently.'. "\n\n". 'A third possible cause is that you run several machines, and '. 'the %s configuration file differs between them. This file is '. 'updated when you edit configuration from the CLI with %s. If '. 'restarting the daemons does not resolve this issue and you '. 'run multiple machines, check that all machines have identical '. '%s configuration files.'. "\n\n". 'This issue is not severe, but usually indicates that something '. 'is not configured the way you expect, and may cause the daemons '. 'to exhibit different behavior than the web application does.', phutil_tag('tt', array(), substr($daemon->getEnvHash(), 0, 12)), phutil_tag('tt', array(), substr($environment_hash, 0, 12)), phutil_tag('tt', array(), 'bin/phd restart'), phutil_tag( 'a', array( 'href' => '/daemon/', - 'target' => '_blank' + 'target' => '_blank', ), pht('Daemon Console')), phutil_tag( 'a', array( 'href' => $doc_href, - 'target' => '_blank' + 'target' => '_blank', ), pht('Managing Daemons with phd')), phutil_tag('tt', array(), 'PHABRICATOR_ENV'), phutil_tag('tt', array(), 'PHABRICATOR_ENV'), phutil_tag('tt', array(), 'phabricator/conf/local/local.json'), phutil_tag('tt', array(), 'bin/config'), phutil_tag('tt', array(), 'phabricator/conf/local/local.json')); $this->newIssue('daemons.need-restarting') ->setName(pht('Daemons and Web Have Different Config')) ->setSummary($summary) ->setMessage($message) ->addCommand('phabricator/ $ ./bin/phd restart'); break; } } } } diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php index bac31c5033..acaf061a19 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseIssueController.php @@ -1,170 +1,174 @@ <?php final class PhabricatorConfigDatabaseIssueController extends PhabricatorConfigDatabaseController { public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $query = $this->buildSchemaQuery(); $actual = $query->loadActualSchema(); $expect = $query->loadExpectedSchema(); $comp = $query->buildComparisonSchema($expect, $actual); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Database Issues')); // Collect all open issues. $issues = array(); foreach ($comp->getDatabases() as $database_name => $database) { foreach ($database->getLocalIssues() as $issue) { $issues[] = array( $database_name, null, null, null, - $issue); + $issue, + ); } foreach ($database->getTables() as $table_name => $table) { foreach ($table->getLocalIssues() as $issue) { $issues[] = array( $database_name, $table_name, null, null, - $issue); + $issue, + ); } foreach ($table->getColumns() as $column_name => $column) { foreach ($column->getLocalIssues() as $issue) { $issues[] = array( $database_name, $table_name, 'column', $column_name, - $issue); + $issue, + ); } } foreach ($table->getKeys() as $key_name => $key) { foreach ($key->getLocalIssues() as $issue) { $issues[] = array( $database_name, $table_name, 'key', $key_name, - $issue); + $issue, + ); } } } } // Sort all open issues so that the most severe issues appear first. $order = array(); $counts = array(); foreach ($issues as $key => $issue) { $const = $issue[4]; $status = PhabricatorConfigStorageSchema::getIssueStatus($const); $severity = PhabricatorConfigStorageSchema::getStatusSeverity($status); $order[$key] = sprintf( '~%d~%s%s%s', 9 - $severity, $issue[0], $issue[1], $issue[3]); if (empty($counts[$status])) { $counts[$status] = 0; } $counts[$status]++; } asort($order); $issues = array_select_keys($issues, array_keys($order)); // Render the issues. $rows = array(); foreach ($issues as $issue) { $const = $issue[4]; $database_link = phutil_tag( 'a', array( 'href' => $this->getApplicationURI('/database/'.$issue[0].'/'), ), $issue[0]); $rows[] = array( $this->renderIcon( PhabricatorConfigStorageSchema::getIssueStatus($const)), $database_link, $issue[1], $issue[2], $issue[3], PhabricatorConfigStorageSchema::getIssueDescription($const), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Database'), pht('Table'), pht('Type'), pht('Column/Key'), pht('Issue'), )) ->setColumnClasses( array( null, null, null, null, null, 'wide', )); $errors = array(); $errors[] = pht( 'IMPORTANT: This feature is in development and the information below '. 'is not accurate! Ignore it for now. See T1191.'); if (isset($counts[PhabricatorConfigStorageSchema::STATUS_FAIL])) { $errors[] = pht( 'Detected %s serious issue(s) with the schemata.', new PhutilNumber($counts[PhabricatorConfigStorageSchema::STATUS_FAIL])); } if (isset($counts[PhabricatorConfigStorageSchema::STATUS_WARN])) { $errors[] = pht( 'Detected %s warning(s) with the schemata.', new PhutilNumber($counts[PhabricatorConfigStorageSchema::STATUS_WARN])); } $title = pht('Database Issues'); $table_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->appendChild($table); $nav = $this->buildSideNavView(); $nav->selectFilter('dbissue/'); $nav->appendChild( array( $crumbs, $table_box, )); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } } diff --git a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php index 4d7ba636ff..1ca31e02e0 100644 --- a/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php +++ b/src/applications/config/controller/PhabricatorConfigDatabaseStatusController.php @@ -1,756 +1,756 @@ <?php final class PhabricatorConfigDatabaseStatusController extends PhabricatorConfigDatabaseController { private $database; private $table; private $column; private $key; public function willProcessRequest(array $data) { $this->database = idx($data, 'database'); $this->table = idx($data, 'table'); $this->column = idx($data, 'column'); $this->key = idx($data, 'key'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $query = $this->buildSchemaQuery(); $actual = $query->loadActualSchema(); $expect = $query->loadExpectedSchema(); $comp = $query->buildComparisonSchema($expect, $actual); if ($this->column) { return $this->renderColumn( $comp, $expect, $actual, $this->database, $this->table, $this->column); } else if ($this->key) { return $this->renderKey( $comp, $expect, $actual, $this->database, $this->table, $this->key); } else if ($this->table) { return $this->renderTable( $comp, $expect, $actual, $this->database, $this->table); } else if ($this->database) { return $this->renderDatabase( $comp, $expect, $actual, $this->database); } else { return $this->renderServer( $comp, $expect, $actual); } } private function buildResponse($title, $body) { $nav = $this->buildSideNavView(); $nav->selectFilter('database/'); $crumbs = $this->buildApplicationCrumbs(); if ($this->database) { $crumbs->addTextCrumb( pht('Database Status'), $this->getApplicationURI('database/')); if ($this->table) { $crumbs->addTextCrumb( $this->database, $this->getApplicationURI('database/'.$this->database.'/')); if ($this->column || $this->key) { $crumbs->addTextCrumb( $this->table, $this->getApplicationURI( 'database/'.$this->database.'/'.$this->table.'/')); if ($this->column) { $crumbs->addTextCrumb($this->column); } else { $crumbs->addTextCrumb($this->key); } } else { $crumbs->addTextCrumb($this->table); } } else { $crumbs->addTextCrumb($this->database); } } else { $crumbs->addTextCrumb(pht('Database Status')); } $nav->setCrumbs($crumbs); $nav->appendChild($body); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } private function renderServer( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual) { $charset_issue = PhabricatorConfigStorageSchema::ISSUE_CHARSET; $collation_issue = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $rows = array(); foreach ($comp->getDatabases() as $database_name => $database) { $actual_database = $actual->getDatabase($database_name); if ($actual_database) { $charset = $actual_database->getCharacterSet(); $collation = $actual_database->getCollation(); } else { $charset = null; $collation = null; } $status = $database->getStatus(); $issues = $database->getIssues(); $rows[] = array( $this->renderIcon($status), phutil_tag( 'a', array( 'href' => $this->getApplicationURI( '/database/'.$database_name.'/'), ), $database_name), $this->renderAttr($charset, $database->hasIssue($charset_issue)), $this->renderAttr($collation, $database->hasIssue($collation_issue)), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Database'), pht('Charset'), pht('Collation'), )) ->setColumnClasses( array( null, 'wide pri', null, null, )); $title = pht('Database Status'); $properties = $this->buildProperties( array( ), $comp->getIssues()); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->addPropertyList($properties) ->appendChild($table); return $this->buildResponse($title, $box); } private function renderDatabase( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name) { $collation_issue = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $rows = array(); foreach ($database->getTables() as $table_name => $table) { $status = $table->getStatus(); $rows[] = array( $this->renderIcon($status), phutil_tag( 'a', array( 'href' => $this->getApplicationURI( '/database/'.$database_name.'/'.$table_name.'/'), ), $table_name), $this->renderAttr( $table->getCollation(), $table->hasIssue($collation_issue)), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Table'), pht('Collation'), )) ->setColumnClasses( array( null, 'wide pri', null, )); $title = pht('Database Status: %s', $database_name); $actual_database = $actual->getDatabase($database_name); if ($actual_database) { $actual_charset = $actual_database->getCharacterSet(); $actual_collation = $actual_database->getCollation(); } else { $actual_charset = null; $actual_collation = null; } $expect_database = $expect->getDatabase($database_name); if ($expect_database) { $expect_charset = $expect_database->getCharacterSet(); $expect_collation = $expect_database->getCollation(); } else { $expect_charset = null; $expect_collation = null; } $properties = $this->buildProperties( array( array( pht('Character Set'), $actual_charset, ), array( pht('Expected Character Set'), $expect_charset, ), array( pht('Collation'), $actual_collation, ), array( pht('Expected Collation'), $expect_collation, ), ), $database->getIssues()); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->addPropertyList($properties) ->appendChild($table); return $this->buildResponse($title, $box); } private function renderTable( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name, $table_name) { $type_issue = PhabricatorConfigStorageSchema::ISSUE_COLUMNTYPE; $charset_issue = PhabricatorConfigStorageSchema::ISSUE_CHARSET; $collation_issue = PhabricatorConfigStorageSchema::ISSUE_COLLATION; $nullable_issue = PhabricatorConfigStorageSchema::ISSUE_NULLABLE; $unique_issue = PhabricatorConfigStorageSchema::ISSUE_UNIQUE; $columns_issue = PhabricatorConfigStorageSchema::ISSUE_KEYCOLUMNS; $longkey_issue = PhabricatorConfigStorageSchema::ISSUE_LONGKEY; $auto_issue = PhabricatorConfigStorageSchema::ISSUE_AUTOINCREMENT; $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $table = $database->getTable($table_name); if (!$table) { return new Aphront404Response(); } $actual_database = $actual->getDatabase($database_name); $actual_table = null; if ($actual_database) { $actual_table = $actual_database->getTable($table_name); } $expect_database = $expect->getDatabase($database_name); $expect_table = null; if ($expect_database) { $expect_table = $expect_database->getTable($table_name); } $rows = array(); foreach ($table->getColumns() as $column_name => $column) { $expect_column = null; if ($expect_table) { $expect_column = $expect_table->getColumn($column_name); } $status = $column->getStatus(); $data_type = null; if ($expect_column) { $data_type = $expect_column->getDataType(); } $rows[] = array( $this->renderIcon($status), phutil_tag( 'a', array( 'href' => $this->getApplicationURI( 'database/'. $database_name.'/'. $table_name.'/'. 'col/'. $column_name.'/'), ), $column_name), $data_type, $this->renderAttr( $column->getColumnType(), $column->hasIssue($type_issue)), $this->renderAttr( $this->renderBoolean($column->getNullable()), $column->hasIssue($nullable_issue)), $this->renderAttr( $this->renderBoolean($column->getAutoIncrement()), $column->hasIssue($auto_issue)), $this->renderAttr( $column->getCharacterSet(), $column->hasIssue($charset_issue)), $this->renderAttr( $column->getCollation(), $column->hasIssue($collation_issue)), ); } $table_view = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Column'), pht('Data Type'), pht('Column Type'), pht('Nullable'), pht('Autoincrement'), pht('Character Set'), pht('Collation'), )) ->setColumnClasses( array( null, 'wide pri', null, null, null, null, - null + null, )); $key_rows = array(); foreach ($table->getKeys() as $key_name => $key) { $expect_key = null; if ($expect_table) { $expect_key = $expect_table->getKey($key_name); } $status = $key->getStatus(); $size = 0; foreach ($key->getColumnNames() as $column_spec) { list($column_name, $prefix) = $key->getKeyColumnAndPrefix($column_spec); $column = $table->getColumn($column_name); if (!$column) { $size = 0; break; } $size += $column->getKeyByteLength($prefix); } $size_formatted = null; if ($size) { $size_formatted = $this->renderAttr( $size, $key->hasIssue($longkey_issue)); } $key_rows[] = array( $this->renderIcon($status), phutil_tag( 'a', array( 'href' => $this->getApplicationURI( 'database/'. $database_name.'/'. $table_name.'/'. 'key/'. $key_name.'/'), ), $key_name), $this->renderAttr( implode(', ', $key->getColumnNames()), $key->hasIssue($columns_issue)), $this->renderAttr( $this->renderBoolean($key->getUnique()), $key->hasIssue($unique_issue)), $size_formatted, ); } $keys_view = id(new AphrontTableView($key_rows)) ->setHeaders( array( null, pht('Key'), pht('Columns'), pht('Unique'), pht('Size'), )) ->setColumnClasses( array( null, 'wide pri', null, null, null, )); $title = pht('Database Status: %s.%s', $database_name, $table_name); if ($actual_table) { $actual_collation = $actual_table->getCollation(); } else { $actual_collation = null; } if ($expect_table) { $expect_collation = $expect_table->getCollation(); } else { $expect_collation = null; } $properties = $this->buildProperties( array( array( pht('Collation'), $actual_collation, ), array( pht('Expected Collation'), $expect_collation, ), ), $table->getIssues()); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->addPropertyList($properties) ->appendChild($table_view) ->appendChild($keys_view); return $this->buildResponse($title, $box); } private function renderColumn( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name, $table_name, $column_name) { $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $table = $database->getTable($table_name); if (!$table) { return new Aphront404Response(); } $column = $table->getColumn($column_name); if (!$column) { return new Aphront404Response(); } $actual_database = $actual->getDatabase($database_name); $actual_table = null; $actual_column = null; if ($actual_database) { $actual_table = $actual_database->getTable($table_name); if ($actual_table) { $actual_column = $actual_table->getColumn($column_name); } } $expect_database = $expect->getDatabase($database_name); $expect_table = null; $expect_column = null; if ($expect_database) { $expect_table = $expect_database->getTable($table_name); if ($expect_table) { $expect_column = $expect_table->getColumn($column_name); } } if ($actual_column) { $actual_coltype = $actual_column->getColumnType(); $actual_charset = $actual_column->getCharacterSet(); $actual_collation = $actual_column->getCollation(); $actual_nullable = $actual_column->getNullable(); $actual_auto = $actual_column->getAutoIncrement(); } else { $actual_coltype = null; $actual_charset = null; $actual_collation = null; $actual_nullable = null; $actual_auto = null; } if ($expect_column) { $data_type = $expect_column->getDataType(); $expect_coltype = $expect_column->getColumnType(); $expect_charset = $expect_column->getCharacterSet(); $expect_collation = $expect_column->getCollation(); $expect_nullable = $expect_column->getNullable(); $expect_auto = $expect_column->getAutoIncrement(); } else { $data_type = null; $expect_coltype = null; $expect_charset = null; $expect_collation = null; $expect_nullable = null; $expect_auto = null; } $title = pht( 'Database Status: %s.%s.%s', $database_name, $table_name, $column_name); $properties = $this->buildProperties( array( array( pht('Data Type'), $data_type, ), array( pht('Column Type'), $actual_coltype, ), array( pht('Expected Column Type'), $expect_coltype, ), array( pht('Character Set'), $actual_charset, ), array( pht('Expected Character Set'), $expect_charset, ), array( pht('Collation'), $actual_collation, ), array( pht('Expected Collation'), $expect_collation, ), array( pht('Nullable'), $this->renderBoolean($actual_nullable), ), array( pht('Expected Nullable'), $this->renderBoolean($expect_nullable), ), array( pht('Autoincrement'), $this->renderBoolean($actual_auto), ), array( pht('Expected Autoincrement'), $this->renderBoolean($expect_auto), ), ), $column->getIssues()); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->addPropertyList($properties); return $this->buildResponse($title, $box); } private function renderKey( PhabricatorConfigServerSchema $comp, PhabricatorConfigServerSchema $expect, PhabricatorConfigServerSchema $actual, $database_name, $table_name, $key_name) { $database = $comp->getDatabase($database_name); if (!$database) { return new Aphront404Response(); } $table = $database->getTable($table_name); if (!$table) { return new Aphront404Response(); } $key = $table->getKey($key_name); if (!$key) { return new Aphront404Response(); } $actual_database = $actual->getDatabase($database_name); $actual_table = null; $actual_key = null; if ($actual_database) { $actual_table = $actual_database->getTable($table_name); if ($actual_table) { $actual_key = $actual_table->getKey($key_name); } } $expect_database = $expect->getDatabase($database_name); $expect_table = null; $expect_key = null; if ($expect_database) { $expect_table = $expect_database->getTable($table_name); if ($expect_table) { $expect_key = $expect_table->getKey($key_name); } } if ($actual_key) { $actual_columns = $actual_key->getColumnNames(); $actual_unique = $actual_key->getUnique(); } else { $actual_columns = array(); $actual_unique = null; } if ($expect_key) { $expect_columns = $expect_key->getColumnNames(); $expect_unique = $expect_key->getUnique(); } else { $expect_columns = array(); $expect_unique = null; } $title = pht( 'Database Status: %s.%s (%s)', $database_name, $table_name, $key_name); $properties = $this->buildProperties( array( array( pht('Unique'), $this->renderBoolean($actual_unique), ), array( pht('Expected Unique'), $this->renderBoolean($expect_unique), ), array( pht('Columns'), implode(', ', $actual_columns), ), array( pht('Expected Columns'), implode(', ', $expect_columns), ), ), $key->getIssues()); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->addPropertyList($properties); return $this->buildResponse($title, $box); } private function buildProperties(array $properties, array $issues) { $view = id(new PHUIPropertyListView()) ->setUser($this->getRequest()->getUser()); foreach ($properties as $property) { list($key, $value) = $property; $view->addProperty($key, $value); } $status_view = new PHUIStatusListView(); if (!$issues) { $status_view->addItem( id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_ACCEPT, 'green') ->setTarget(pht('No Schema Issues'))); } else { foreach ($issues as $issue) { $note = PhabricatorConfigStorageSchema::getIssueDescription($issue); $status = PhabricatorConfigStorageSchema::getIssueStatus($issue); switch ($status) { case PhabricatorConfigStorageSchema::STATUS_WARN: $icon = PHUIStatusItemView::ICON_WARNING; $color = 'yellow'; break; case PhabricatorConfigStorageSchema::STATUS_FAIL: default: $icon = PHUIStatusItemView::ICON_REJECT; $color = 'red'; break; } $item = id(new PHUIStatusItemView()) ->setTarget(PhabricatorConfigStorageSchema::getIssueName($issue)) ->setIcon($icon, $color) ->setNote($note); $status_view->addItem($item); } } $view->addProperty(pht('Schema Status'), $status_view); return $view; } } diff --git a/src/applications/config/controller/PhabricatorConfigListController.php b/src/applications/config/controller/PhabricatorConfigListController.php index 69f062fa91..8ad2e73354 100644 --- a/src/applications/config/controller/PhabricatorConfigListController.php +++ b/src/applications/config/controller/PhabricatorConfigListController.php @@ -1,57 +1,57 @@ <?php final class PhabricatorConfigListController extends PhabricatorConfigController { public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $nav = $this->buildSideNavView(); $nav->selectFilter('/'); $groups = PhabricatorApplicationConfigOptions::loadAll(); $list = $this->buildConfigOptionsList($groups); $title = pht('Phabricator Configuration'); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->appendChild($list); $nav->appendChild( array( - $box + $box, )); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb(pht('Config'), $this->getApplicationURI()); $nav->setCrumbs($crumbs); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } private function buildConfigOptionsList(array $groups) { assert_instances_of($groups, 'PhabricatorApplicationConfigOptions'); $list = new PHUIObjectItemListView(); $list->setStackable(true); $groups = msort($groups, 'getName'); foreach ($groups as $group) { $item = id(new PHUIObjectItemView()) ->setHeader($group->getName()) ->setHref('/config/group/'.$group->getKey().'/') ->addAttribute($group->getDescription()); $list->addItem($item); } return $list; } } diff --git a/src/applications/config/editor/PhabricatorConfigEditor.php b/src/applications/config/editor/PhabricatorConfigEditor.php index 8735ab7319..5b0febb902 100644 --- a/src/applications/config/editor/PhabricatorConfigEditor.php +++ b/src/applications/config/editor/PhabricatorConfigEditor.php @@ -1,136 +1,136 @@ <?php final class PhabricatorConfigEditor extends PhabricatorApplicationTransactionEditor { public function getEditorApplicationClass() { return 'PhabricatorConfigApplication'; } public function getEditorObjectsDescription() { return pht('Phabricator Configuration'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorConfigTransaction::TYPE_EDIT; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorConfigTransaction::TYPE_EDIT: return array( 'deleted' => (int)$object->getIsDeleted(), 'value' => $object->getValue(), ); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorConfigTransaction::TYPE_EDIT: return $xaction->getNewValue(); } } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorConfigTransaction::TYPE_EDIT: $v = $xaction->getNewValue(); // If this is a defined configuration option (vs a straggler from an // old version of Phabricator or a configuration file misspelling) // submit it to the validation gauntlet. $key = $object->getConfigKey(); $all_options = PhabricatorApplicationConfigOptions::loadAllOptions(); $option = idx($all_options, $key); if ($option) { $option->getGroup()->validateOption( $option, $v['value']); } $object->setIsDeleted((int)$v['deleted']); $object->setValue($v['value']); break; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { return; } protected function mergeTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { $type = $u->getTransactionType(); switch ($type) { case PhabricatorConfigTransaction::TYPE_EDIT: return $v; } return parent::mergeTransactions($u, $v); } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorConfigTransaction::TYPE_EDIT: // If an edit deletes an already-deleted entry, no-op it. if (idx($old, 'deleted') && idx($new, 'deleted')) { return false; } break; } return parent::transactionHasEffect($object, $xaction); } protected function didApplyTransactions(array $xactions) { // Force all the setup checks to run on the next page load. PhabricatorSetupCheck::deleteSetupCheckCache(); } public static function storeNewValue( PhabricatorConfigEntry $config_entry, $value, AphrontRequest $request) { $xaction = id(new PhabricatorConfigTransaction()) ->setTransactionType(PhabricatorConfigTransaction::TYPE_EDIT) ->setNewValue( array( 'deleted' => false, - 'value' => $value + 'value' => $value, )); $editor = id(new PhabricatorConfigEditor()) ->setActor($request->getUser()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $editor->applyTransactions($config_entry, array($xaction)); } } diff --git a/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php b/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php index d002ca4179..0c4c321e60 100644 --- a/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php +++ b/src/applications/config/option/PhabricatorAuthenticationConfigOptions.php @@ -1,102 +1,102 @@ <?php final class PhabricatorAuthenticationConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Authentication'); } public function getDescription() { return pht('Options relating to authentication.'); } public function getOptions() { return array( $this->newOption('auth.require-email-verification', 'bool', false) ->setBoolOptions( array( pht('Require email verification'), - pht("Don't require email verification") + pht("Don't require email verification"), )) ->setSummary( pht('Require email verification before a user can log in.')) ->setDescription( pht( 'If true, email addresses must be verified (by clicking a link '. 'in an email) before a user can login. By default, verification '. 'is optional unless {{auth.email-domains}} is nonempty.')), $this->newOption('auth.require-approval', 'bool', true) ->setBoolOptions( array( pht('Require Administrators to Approve Accounts'), pht("Don't Require Manual Approval"), )) ->setSummary( pht('Require administrators to approve new accounts.')) ->setDescription( pht( "Newly registered Phabricator accounts can either be placed ". "into a manual approval queue for administrative review, or ". "automatically activated immediately. The approval queue is ". "enabled by default because it gives you greater control over ". "who can register an account and access Phabricator.\n\n". "If your install is completely public, or on a VPN, or users can ". "only register with a trusted provider like LDAP, or you've ". "otherwise configured Phabricator to prevent unauthorized ". "registration, you can disable the queue to reduce administrative ". "overhead.\n\n". "NOTE: Before you disable the queue, make sure ". "{{auth.email-domains}} is configured correctly for your ". "install!")), $this->newOption('auth.email-domains', 'list<string>', array()) ->setSummary(pht('Only allow registration from particular domains.')) ->setDescription( pht( "You can restrict allowed email addresses to certain domains ". "(like `yourcompany.com`) by setting a list of allowed domains ". "here.\n\nUsers will only be allowed to register using email ". "addresses at one of the domains, and will only be able to add ". "new email addresses for these domains. If you configure this, ". "it implies {{auth.require-email-verification}}.\n\n". "You should omit the `@` from domains. Note that the domain must ". "match exactly. If you allow `yourcompany.com`, that permits ". "`joe@yourcompany.com` but rejects `joe@mail.yourcompany.com`.")) ->addExample( "yourcompany.com\nmail.yourcompany.com", pht('Valid Setting')), $this->newOption('auth.login-message', 'string', null) ->setLocked(true) ->setSummary(pht('A block of HTML displayed on the login screen.')) ->setDescription( pht( "You can provide an arbitrary block of HTML here, which will ". "appear on the login screen. Normally, you'd use this to provide ". "login or registration instructions to users.")), $this->newOption('account.editable', 'bool', true) ->setBoolOptions( array( pht('Allow editing'), - pht('Prevent editing') + pht('Prevent editing'), )) ->setSummary( pht( 'Determines whether or not basic account information is '. 'editable.')) ->setDescription( pht( 'Is basic account information (email, real name, profile '. 'picture) editable? If you set up Phabricator to automatically '. 'synchronize account information from some other authoritative '. 'system, you can disable this to ensure information remains '. 'consistent across both systems.')), $this->newOption('account.minimum-password-length', 'int', 8) ->setSummary(pht('Minimum password length.')) ->setDescription( pht( 'When users set or reset a password, it must have at least this '. 'many characters.')), ); } } diff --git a/src/applications/config/option/PhabricatorCoreConfigOptions.php b/src/applications/config/option/PhabricatorCoreConfigOptions.php index e1ffbcbddb..d846155a99 100644 --- a/src/applications/config/option/PhabricatorCoreConfigOptions.php +++ b/src/applications/config/option/PhabricatorCoreConfigOptions.php @@ -1,276 +1,276 @@ <?php final class PhabricatorCoreConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Core'); } public function getDescription() { return pht('Configure core options, including URIs.'); } public function getOptions() { if (phutil_is_windows()) { $paths = array(); } else { $paths = array( '/bin', '/usr/bin', '/usr/local/bin', ); } $path = getenv('PATH'); $proto_doc_href = PhabricatorEnv::getDoclink( 'User Guide: Prototype Applications'); $proto_doc_name = pht('User Guide: Prototype Applications'); return array( $this->newOption('phabricator.base-uri', 'string', null) ->setLocked(true) ->setSummary(pht('URI where Phabricator is installed.')) ->setDescription( pht( 'Set the URI where Phabricator is installed. Setting this '. 'improves security by preventing cookies from being set on other '. 'domains, and allows daemons to send emails with links that have '. 'the correct domain.')) ->addExample('http://phabricator.example.com/', pht('Valid Setting')), $this->newOption('phabricator.production-uri', 'string', null) ->setSummary( pht('Primary install URI, for multi-environment installs.')) ->setDescription( pht( 'If you have multiple Phabricator environments (like a '. 'development/staging environment for working on testing '. 'Phabricator, and a production environment for deploying it), '. 'set the production environment URI here so that emails and other '. 'durable URIs will always generate with links pointing at the '. 'production environment. If unset, defaults to '. '{{phabricator.base-uri}}. Most installs do not need to set '. 'this option.')) ->addExample('http://phabricator.example.com/', pht('Valid Setting')), $this->newOption('phabricator.allowed-uris', 'list<string>', array()) ->setLocked(true) ->setSummary(pht('Alternative URIs that can access Phabricator.')) ->setDescription( pht( "These alternative URIs will be able to access 'normal' pages ". "on your Phabricator install. Other features such as OAuth ". "won't work. The major use case for this is moving installs ". "across domains.")) ->addExample( "http://phabricator2.example.com/\n". "http://phabricator3.example.com/", pht('Valid Setting')), $this->newOption('phabricator.timezone', 'string', null) ->setSummary( pht('The timezone Phabricator should use.')) ->setDescription( pht( "PHP requires that you set a timezone in your php.ini before ". "using date functions, or it will emit a warning. If this isn't ". "possible (for instance, because you are using HPHP) you can set ". "some valid constant for date_default_timezone_set() here and ". "Phabricator will set it on your behalf, silencing the warning.")) ->addExample('America/New_York', pht('US East (EDT)')) ->addExample('America/Chicago', pht('US Central (CDT)')) ->addExample('America/Boise', pht('US Mountain (MDT)')) ->addExample('America/Los_Angeles', pht('US West (PDT)')), $this->newOption('phabricator.cookie-prefix', 'string', null) ->setSummary( pht('Set a string Phabricator should use to prefix '. 'cookie names')) ->setDescription( pht( 'Cookies set for x.com are also sent for y.x.com. Assuming '. 'Phabricator instances are running on both domains, this will '. 'create a collision preventing you from logging in.')) ->addExample('dev', pht('Prefix cookie with "dev"')), $this->newOption('phabricator.show-prototypes', 'bool', false) ->setBoolOptions( array( pht('Enable Prototypes'), - pht('Disable Prototypes') + pht('Disable Prototypes'), )) ->setSummary( pht( 'Install applications which are still under development.')) ->setDescription( pht( "IMPORTANT: The upstream does not provide support for prototype ". "applications.". "\n\n". "Phabricator includes prototype applications which are in an ". "**early stage of development**. By default, prototype ". "applications are not installed, because they are often not yet ". "developed enough to be generally usable. You can enable ". "this option to install them if you're developing Phabricator ". "or are interested in previewing upcoming features.". "\n\n". "To learn more about prototypes, see [[ %s | %s ]].". "\n\n". "After enabling prototypes, you can selectively uninstall them ". "(like normal applications).", $proto_doc_href, $proto_doc_name)), $this->newOption('phabricator.serious-business', 'bool', false) ->setBoolOptions( array( pht('Serious business'), pht('Shenanigans'), // That should be interesting to translate. :P )) ->setSummary( pht('Allows you to remove levity and jokes from the UI.')) ->setDescription( pht( 'By default, Phabricator includes some flavor text in the UI, '. 'like a prompt to "Weigh In" rather than "Add Comment" in '. 'Maniphest. If you\'d prefer more traditional UI strings like '. '"Add Comment", you can set this flag to disable most of the '. 'extra flavor.')), $this->newOption('remarkup.ignored-object-names', 'string', '/^(Q|V)\d$/') ->setSummary( pht('Text values that match this regex and are also object names '. 'will not be linked.')) ->setDescription( pht( 'By default, Phabricator links object names in Remarkup fields '. 'to the corresponding object. This regex can be used to modify '. 'this behavior; object names that match this regex will not be '. 'linked.')), $this->newOption('environment.append-paths', 'list<string>', $paths) ->setSummary( pht('These paths get appended to your \$PATH envrionment variable.')) ->setDescription( pht( "Phabricator occasionally shells out to other binaries on the ". "server. An example of this is the `pygmentize` command, used ". "to syntax-highlight code written in languages other than PHP. ". "By default, it is assumed that these binaries are in the \$PATH ". "of the user running Phabricator (normally 'apache', 'httpd', or ". "'nobody'). Here you can add extra directories to the \$PATH ". "environment variable, for when these binaries are in ". "non-standard locations.\n\n". "Note that you can also put binaries in ". "`phabricator/support/bin/` (for example, by symlinking them).\n\n". "The current value of PATH after configuration is applied is:\n\n". " lang=text\n". " %s", $path)) ->setLocked(true) ->addExample('/usr/local/bin', pht('Add One Path')) ->addExample("/usr/bin\n/usr/local/bin", pht('Add Multiple Paths')), $this->newOption('config.lock', 'set', array()) ->setLocked(true) ->setDescription(pht('Additional configuration options to lock.')), $this->newOption('config.hide', 'set', array()) ->setLocked(true) ->setDescription(pht('Additional configuration options to hide.')), $this->newOption('config.mask', 'set', array()) ->setLocked(true) ->setDescription(pht('Additional configuration options to mask.')), $this->newOption('config.ignore-issues', 'set', array()) ->setLocked(true) ->setDescription(pht('Setup issues to ignore.')), $this->newOption('phabricator.env', 'string', null) ->setLocked(true) ->setDescription(pht('Internal.')), $this->newOption('test.value', 'wild', null) ->setLocked(true) ->setDescription(pht('Unit test value.')), $this->newOption('phabricator.uninstalled-applications', 'set', array()) ->setLocked(true) ->setDescription( pht('Array containing list of Uninstalled applications.')), $this->newOption('phabricator.application-settings', 'wild', array()) ->setLocked(true) ->setDescription( pht('Customized settings for Phabricator applications.')), $this->newOption('welcome.html', 'string', null) ->setLocked(true) ->setDescription( pht('Custom HTML to show on the main Phabricator dashboard.')), $this->newOption('phabricator.cache-namespace', 'string', null) ->setLocked(true) ->setDescription(pht('Cache namespace.')), $this->newOption('phabricator.allow-email-users', 'bool', false) ->setBoolOptions( array( pht('Allow'), pht('Disallow'), ))->setDescription( pht( 'Allow non-members to interact with tasks over email.')), ); } protected function didValidateOption( PhabricatorConfigOption $option, $value) { $key = $option->getKey(); if ($key == 'phabricator.base-uri' || $key == 'phabricator.production-uri') { $uri = new PhutilURI($value); $protocol = $uri->getProtocol(); if ($protocol !== 'http' && $protocol !== 'https') { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must start with ". "'http://' or 'https://'.", $key)); } $domain = $uri->getDomain(); if (strpos($domain, '.') === false) { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must contain a dot ('.'), ". "like 'http://example.com/', not just a bare name like ". "'http://example/'. Some web browsers will not set cookies on ". "domains with no TLD.", $key)); } $path = $uri->getPath(); if ($path !== '' && $path !== '/') { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The URI must NOT have a path, ". "e.g. 'http://phabricator.example.com/' is OK, but ". "'http://example.com/phabricator/' is not. Phabricator must be ". "installed on an entire domain; it can not be installed on a ". "path.", $key)); } } if ($key === 'phabricator.timezone') { $old = date_default_timezone_get(); $ok = @date_default_timezone_set($value); @date_default_timezone_set($old); if (!$ok) { throw new PhabricatorConfigValidationException( pht( "Config option '%s' is invalid. The timezone identifier must ". "be a valid timezone identifier recognized by PHP, like ". "'America/Los_Angeles'. You can find a list of valid identifiers ". "here: %s", $key, 'http://php.net/manual/timezones.php')); } } } } diff --git a/src/applications/config/option/PhabricatorGarbageCollectorConfigOptions.php b/src/applications/config/option/PhabricatorGarbageCollectorConfigOptions.php index 24fd1a4014..5660c4a253 100644 --- a/src/applications/config/option/PhabricatorGarbageCollectorConfigOptions.php +++ b/src/applications/config/option/PhabricatorGarbageCollectorConfigOptions.php @@ -1,55 +1,62 @@ <?php final class PhabricatorGarbageCollectorConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Garbage Collector'); } public function getDescription() { return pht('Configure the GC for old logs, caches, etc.'); } public function getOptions() { $options = array( 'gcdaemon.ttl.herald-transcripts' => array( 30, - pht('Number of seconds to retain Herald transcripts for.')), + pht('Number of seconds to retain Herald transcripts for.'), + ), 'gcdaemon.ttl.daemon-logs' => array( 7, - pht('Number of seconds to retain Daemon logs for.')), + pht('Number of seconds to retain Daemon logs for.'), + ), 'gcdaemon.ttl.differential-parse-cache' => array( 14, - pht('Number of seconds to retain Differential parse caches for.')), + pht('Number of seconds to retain Differential parse caches for.'), + ), 'gcdaemon.ttl.markup-cache' => array( 30, - pht('Number of seconds to retain Markup cache entries for.')), + pht('Number of seconds to retain Markup cache entries for.'), + ), 'gcdaemon.ttl.task-archive' => array( 14, - pht('Number of seconds to retain archived background tasks for.')), + pht('Number of seconds to retain archived background tasks for.'), + ), 'gcdaemon.ttl.general-cache' => array( 30, - pht('Number of seconds to retain general cache entries for.')), + pht('Number of seconds to retain general cache entries for.'), + ), 'gcdaemon.ttl.conduit-logs' => array( 180, - pht('Number of seconds to retain Conduit call logs for.')) + pht('Number of seconds to retain Conduit call logs for.'), + ), ); $result = array(); foreach ($options as $key => $spec) { list($default_days, $description) = $spec; $result[] = $this ->newOption($key, 'int', $default_days * (24 * 60 * 60)) ->setDescription($description) ->addExample((7 * 24 * 60 * 60), pht('Retain for 1 week')) ->addExample((14 * 24 * 60 * 60), pht('Retain for 2 weeks')) ->addExample((30 * 24 * 60 * 60), pht('Retain for 30 days')) ->addExample((60 * 24 * 60 * 60), pht('Retain for 60 days')) ->addExample(0, pht('Retain indefinitely')); } return $result; } } diff --git a/src/applications/config/option/PhabricatorSMSConfigOptions.php b/src/applications/config/option/PhabricatorSMSConfigOptions.php index 402ac63d1d..2e8743950b 100644 --- a/src/applications/config/option/PhabricatorSMSConfigOptions.php +++ b/src/applications/config/option/PhabricatorSMSConfigOptions.php @@ -1,55 +1,55 @@ <?php final class PhabricatorSMSConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('SMS'); } public function getDescription() { return pht('Configure SMS.'); } public function getOptions() { $adapter_description = $this->deformat(pht(<<<EODOC Adapter class to use to transmit SMS to an external provider. A given external provider will most likely need more configuration which will most likely require registration and payment for the service. EODOC )); return array( $this->newOption( 'sms.default-sender', 'string', null) ->setDescription(pht('Default "from" number.')) ->addExample('8675309', 'Jenny still has this number') ->addExample('18005555555', 'Maybe not a real number'), $this->newOption( 'sms.default-adapter', 'class', null) ->setBaseClass('PhabricatorSMSImplementationAdapter') ->setSummary(pht('Control how sms is sent.')) ->setDescription($adapter_description), $this->newOption( 'twilio.account-sid', 'string', null) ->setDescription(pht('Account ID on Twilio service.')) ->setLocked(true) ->addExample('gf5kzccfn2sfknpnadvz7kokv6nz5v', pht('30 characters')), $this->newOption( 'twilio.auth-token', 'string', null) ->setDescription(pht('Authorization token from Twilio service.')) ->setLocked(true) ->setHidden(true) - ->addExample('f3jsi4i67wiwt6w54hf2zwvy3fjf5h', pht('30 characters')) + ->addExample('f3jsi4i67wiwt6w54hf2zwvy3fjf5h', pht('30 characters')), ); } } diff --git a/src/applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php b/src/applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php index 757d71bf85..b62a61113c 100644 --- a/src/applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php +++ b/src/applications/conpherence/conduit/ConpherenceCreateThreadConduitAPIMethod.php @@ -1,67 +1,67 @@ <?php final class ConpherenceCreateThreadConduitAPIMethod extends ConpherenceConduitAPIMethod { public function getAPIMethodName() { return 'conpherence.createthread'; } public function getMethodDescription() { return pht('Create a new conpherence thread.'); } public function defineParamTypes() { return array( 'title' => 'optional string', 'message' => 'required string', - 'participantPHIDs' => 'required list<phids>' + 'participantPHIDs' => 'required list<phids>', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR_EMPTY_PARTICIPANT_PHIDS' => pht( 'You must specify participant phids.'), 'ERR_EMPTY_MESSAGE' => pht( - 'You must specify a message.') + 'You must specify a message.'), ); } protected function execute(ConduitAPIRequest $request) { $participant_phids = $request->getValue('participantPHIDs', array()); $message = $request->getValue('message'); $title = $request->getValue('title'); list($errors, $conpherence) = ConpherenceEditor::createConpherence( $request->getUser(), $participant_phids, $title, $message, PhabricatorContentSource::newFromConduitRequest($request)); if ($errors) { foreach ($errors as $error_code) { switch ($error_code) { case ConpherenceEditor::ERROR_EMPTY_MESSAGE: throw new ConduitException('ERR_EMPTY_MESSAGE'); break; case ConpherenceEditor::ERROR_EMPTY_PARTICIPANTS: throw new ConduitException('ERR_EMPTY_PARTICIPANT_PHIDS'); break; } } } return array( 'conpherenceID' => $conpherence->getID(), 'conpherencePHID' => $conpherence->getPHID(), 'conpherenceURI' => $this->getConpherenceURI($conpherence), ); } } diff --git a/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php b/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php index 1bffc8d043..2fa3dc6f27 100644 --- a/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php +++ b/src/applications/conpherence/conduit/ConpherenceQueryThreadConduitAPIMethod.php @@ -1,86 +1,87 @@ <?php final class ConpherenceQueryThreadConduitAPIMethod extends ConpherenceConduitAPIMethod { public function getAPIMethodName() { return 'conpherence.querythread'; } public function getMethodDescription() { return pht( 'Query for conpherence threads for the logged in user. '. 'You can query by ids or phids for specific conpherence threads. '. 'Otherwise, specify limit and offset to query the most recently '. 'updated conpherences for the logged in user.'); } public function defineParamTypes() { return array( 'ids' => 'optional array<int>', 'phids' => 'optional array<phids>', 'limit' => 'optional int', - 'offset' => 'optional int' + 'offset' => 'optional int', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array(); } protected function execute(ConduitAPIRequest $request) { $user = $request->getUser(); $ids = $request->getValue('ids', array()); $phids = $request->getValue('phids', array()); $limit = $request->getValue('limit'); $offset = $request->getValue('offset'); $query = id(new ConpherenceThreadQuery()) ->setViewer($user) ->needParticipantCache(true) ->needFilePHIDs(true); if ($ids) { $conpherences = $query ->withIDs($ids) ->setLimit($limit) ->setOffset($offset) ->execute(); } else if ($phids) { $conpherences = $query ->withPHIDs($phids) ->setLimit($limit) ->setOffset($offset) ->execute(); } else { $participation = id(new ConpherenceParticipantQuery()) ->withParticipantPHIDs(array($user->getPHID())) ->setLimit($limit) ->setOffset($offset) ->execute(); $conpherence_phids = array_keys($participation); $query->withPHIDs($conpherence_phids); $conpherences = $query->execute(); $conpherences = array_select_keys($conpherences, $conpherence_phids); } $data = array(); foreach ($conpherences as $conpherence) { $id = $conpherence->getID(); $data[$id] = array( 'conpherenceID' => $id, 'conpherencePHID' => $conpherence->getPHID(), 'conpherenceTitle' => $conpherence->getTitle(), 'messageCount' => $conpherence->getMessageCount(), 'recentParticipantPHIDs' => $conpherence->getRecentParticipantPHIDs(), 'filePHIDs' => $conpherence->getFilePHIDs(), - 'conpherenceURI' => $this->getConpherenceURI($conpherence)); + 'conpherenceURI' => $this->getConpherenceURI($conpherence), + ); } return $data; } } diff --git a/src/applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php b/src/applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php index 5045ced57c..a7d30c5a45 100644 --- a/src/applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php +++ b/src/applications/conpherence/conduit/ConpherenceQueryTransactionConduitAPIMethod.php @@ -1,96 +1,97 @@ <?php final class ConpherenceQueryTransactionConduitAPIMethod extends ConpherenceConduitAPIMethod { public function getAPIMethodName() { return 'conpherence.querytransaction'; } public function getMethodDescription() { return pht( 'Query for transactions for the logged in user within a specific '. 'conpherence thread. You can specify the thread by id or phid. '. 'Otherwise, specify limit and offset to query the most recent '. 'transactions within the conpherence for the logged in user.'); } public function defineParamTypes() { return array( 'threadID' => 'optional int', 'threadPHID' => 'optional phid', 'limit' => 'optional int', - 'offset' => 'optional int' + 'offset' => 'optional int', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR_USAGE_NO_THREAD_ID' => pht( 'You must specify a thread id or thread phid to query transactions '. - 'from.') + 'from.'), ); } protected function execute(ConduitAPIRequest $request) { $user = $request->getUser(); $thread_id = $request->getValue('threadID'); $thread_phid = $request->getValue('threadPHID'); $limit = $request->getValue('limit'); $offset = $request->getValue('offset'); $query = id(new ConpherenceThreadQuery()) ->setViewer($user); if ($thread_id) { $query->withIDs(array($thread_id)); } else if ($thread_phid) { $query->withPHIDs(array($thread_phid)); } else { throw new ConduitException('ERR_USAGE_NO_THREAD_ID'); } $conpherence = $query->executeOne(); $query = id(new ConpherenceTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($conpherence->getPHID())) ->setLimit($limit) ->setOffset($offset); $transactions = $query->execute(); $data = array(); foreach ($transactions as $transaction) { $comment = null; $comment_obj = $transaction->getComment(); if ($comment_obj) { $comment = $comment_obj->getContent(); } $title = null; $title_obj = $transaction->getTitle(); if ($title_obj) { $title = $title_obj->getHTMLContent(); } $id = $transaction->getID(); $data[$id] = array( 'transactionID' => $id, 'transactionType' => $transaction->getTransactionType(), 'transactionTitle' => $title, 'transactionComment' => $comment, 'transactionOldValue' => $transaction->getOldValue(), 'transactionNewValue' => $transaction->getNewValue(), 'transactionMetadata' => $transaction->getMetadata(), 'authorPHID' => $transaction->getAuthorPHID(), 'dateCreated' => $transaction->getDateCreated(), 'conpherenceID' => $conpherence->getID(), - 'conpherencePHID' => $conpherence->getPHID()); + 'conpherencePHID' => $conpherence->getPHID(), + ); } return $data; } } diff --git a/src/applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php b/src/applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php index dec949a18d..1e98011b83 100644 --- a/src/applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php +++ b/src/applications/conpherence/conduit/ConpherenceUpdateThreadConduitAPIMethod.php @@ -1,109 +1,109 @@ <?php final class ConpherenceUpdateThreadConduitAPIMethod extends ConpherenceConduitAPIMethod { public function getAPIMethodName() { return 'conpherence.updatethread'; } public function getMethodDescription() { return pht('Update an existing conpherence thread.'); } public function defineParamTypes() { return array( 'id' => 'optional int', 'phid' => 'optional phid', 'title' => 'optional string', 'message' => 'optional string', 'addParticipantPHIDs' => 'optional list<phids>', - 'removeParticipantPHID' => 'optional phid' + 'removeParticipantPHID' => 'optional phid', ); } public function defineReturnType() { return 'bool'; } public function defineErrorTypes() { return array( 'ERR_USAGE_NO_THREAD_ID' => pht( 'You must specify a thread id or thread phid to query transactions '. 'from.'), 'ERR_USAGE_THREAD_NOT_FOUND' => pht( 'Thread does not exist or logged in user can not see it.'), 'ERR_USAGE_ONLY_SELF_REMOVE' => pht( 'Only a user can remove themselves from a thread.'), 'ERR_USAGE_NO_UPDATES' => pht( - 'You must specify data that actually updates the conpherence.') + 'You must specify data that actually updates the conpherence.'), ); } protected function execute(ConduitAPIRequest $request) { $user = $request->getUser(); $id = $request->getValue('id'); $phid = $request->getValue('phid'); $query = id(new ConpherenceThreadQuery()) ->setViewer($user) ->needFilePHIDs(true); if ($id) { $query->withIDs(array($id)); } else if ($phid) { $query->withPHIDs(array($phid)); } else { throw new ConduitException('ERR_USAGE_NO_THREAD_ID'); } $conpherence = $query->executeOne(); if (!$conpherence) { throw new ConduitException('ERR_USAGE_THREAD_NOT_FOUND'); } $source = PhabricatorContentSource::newFromConduitRequest($request); $editor = id(new ConpherenceEditor()) ->setContentSource($source) ->setActor($user); $xactions = array(); $add_participant_phids = $request->getValue('addParticipantPHIDs', array()); $remove_participant_phid = $request->getValue('removeParticipantPHID'); $message = $request->getValue('message'); $title = $request->getValue('title'); if ($add_participant_phids) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( ConpherenceTransactionType::TYPE_PARTICIPANTS) ->setNewValue(array('+' => $add_participant_phids)); } if ($remove_participant_phid) { if ($remove_participant_phid != $user->getPHID()) { throw new ConduitException('ERR_USAGE_ONLY_SELF_REMOVE'); } $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( ConpherenceTransactionType::TYPE_PARTICIPANTS) ->setNewValue(array('-' => array($remove_participant_phid))); } if ($title) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceTransactionType::TYPE_TITLE) ->setNewValue($title); } if ($message) { $xactions = array_merge( $xactions, $editor->generateTransactionsFromText( $user, $conpherence, $message)); } try { $xactions = $editor->applyTransactions($conpherence, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { throw new ConduitException('ERR_USAGE_NO_UPDATES'); } return true; } } diff --git a/src/applications/conpherence/controller/ConpherenceController.php b/src/applications/conpherence/controller/ConpherenceController.php index 3347d4eddb..f1997f96c8 100644 --- a/src/applications/conpherence/controller/ConpherenceController.php +++ b/src/applications/conpherence/controller/ConpherenceController.php @@ -1,154 +1,155 @@ <?php abstract class ConpherenceController extends PhabricatorController { private $conpherences; public function buildApplicationMenu() { $nav = new PHUIListView(); $nav->newLink( pht('New Message'), $this->getApplicationURI('new/')); $nav->addMenuItem( id(new PHUIListItemView()) ->setName(pht('Add Participants')) ->setType(PHUIListItemView::TYPE_LINK) ->setHref('#') ->addSigil('conpherence-widget-adder') ->setMetadata(array('widget' => 'widgets-people'))); $nav->addMenuItem( id(new PHUIListItemView()) ->setName(pht('New Calendar Item')) ->setType(PHUIListItemView::TYPE_LINK) ->setHref('/calendar/event/create/') ->addSigil('conpherence-widget-adder') ->setMetadata(array('widget' => 'widgets-calendar'))); return $nav; } public function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $crumbs ->addAction( id(new PHUIListItemView()) ->setName(pht('New Message')) ->setHref($this->getApplicationURI('new/')) ->setIcon('fa-plus-square') ->setWorkflow(true)) ->addAction( id(new PHUIListItemView()) ->setName(pht('Thread')) ->setHref('#') ->setIcon('fa-bars') ->setStyle('display: none;') ->addClass('device-widgets-selector') ->addSigil('device-widgets-selector')); return $crumbs; } protected function buildHeaderPaneContent(ConpherenceThread $conpherence) { $crumbs = $this->buildApplicationCrumbs(); if ($conpherence->getTitle()) { $title = $conpherence->getTitle(); } else { $title = pht('[No Title]'); } $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($title) ->setHref($this->getApplicationURI('update/'.$conpherence->getID().'/')) ->setWorkflow(true)); return hsprintf( '%s', array( phutil_tag( 'div', array( - 'class' => 'header-loading-mask' + 'class' => 'header-loading-mask', ), ''), - $crumbs)); + $crumbs, + )); } protected function renderConpherenceTransactions( ConpherenceThread $conpherence) { $user = $this->getRequest()->getUser(); $transactions = $conpherence->getTransactions(); $oldest_transaction_id = 0; $too_many = ConpherenceThreadQuery::TRANSACTION_LIMIT + 1; if (count($transactions) == $too_many) { $last_transaction = end($transactions); unset($transactions[$last_transaction->getID()]); $oldest_transaction = end($transactions); $oldest_transaction_id = $oldest_transaction->getID(); } $transactions = array_reverse($transactions); $handles = $conpherence->getHandles(); $rendered_transactions = array(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); foreach ($transactions as $key => $transaction) { if ($transaction->shouldHide()) { unset($transactions[$key]); continue; } if ($transaction->getComment()) { $engine->addObject( $transaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); // we're going to insert a dummy date marker transaction for breaks // between days. some setup required! $previous_transaction = null; $date_marker_transaction = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceTransactionType::TYPE_DATE_MARKER) ->makeEphemeral(); $date_marker_transaction_view = id(new ConpherenceTransactionView()) ->setUser($user) ->setConpherenceTransaction($date_marker_transaction) ->setHandles($handles) ->setMarkupEngine($engine); foreach ($transactions as $transaction) { if ($previous_transaction) { $previous_day = phabricator_format_local_time( $previous_transaction->getDateCreated(), $user, 'Ymd'); $current_day = phabricator_format_local_time( $transaction->getDateCreated(), $user, 'Ymd'); // date marker transaction time! if ($previous_day != $current_day) { $date_marker_transaction->setDateCreated( $transaction->getDateCreated()); $rendered_transactions[] = $date_marker_transaction_view->render(); } } $rendered_transactions[] = id(new ConpherenceTransactionView()) ->setUser($user) ->setConpherenceTransaction($transaction) ->setHandles($handles) ->setMarkupEngine($engine) ->render(); $previous_transaction = $transaction; } $latest_transaction_id = $transaction->getID(); return array( 'transactions' => $rendered_transactions, 'latest_transaction_id' => $latest_transaction_id, 'oldest_transaction_id' => $oldest_transaction_id, ); } } diff --git a/src/applications/conpherence/controller/ConpherenceListController.php b/src/applications/conpherence/controller/ConpherenceListController.php index 0cf678ff67..f253de28a2 100644 --- a/src/applications/conpherence/controller/ConpherenceListController.php +++ b/src/applications/conpherence/controller/ConpherenceListController.php @@ -1,253 +1,254 @@ <?php final class ConpherenceListController extends ConpherenceController { const SELECTED_MODE = 'selected'; const UNSELECTED_MODE = 'unselected'; const PAGING_MODE = 'paging'; private $conpherenceID; public function setConpherenceID($conpherence_id) { $this->conpherenceID = $conpherence_id; return $this; } public function getConpherenceID() { return $this->conpherenceID; } public function willProcessRequest(array $data) { $this->setConpherenceID(idx($data, 'id')); } /** * Three main modes of operation... * * 1 - /conpherence/ - UNSELECTED_MODE * 2 - /conpherence/<id>/ - SELECTED_MODE * 3 - /conpherence/?direction='up'&... - PAGING_MODE * * UNSELECTED_MODE is not an Ajax request while the other two are Ajax * requests. */ private function determineMode() { $request = $this->getRequest(); $mode = self::UNSELECTED_MODE; if ($request->isAjax()) { if ($request->getStr('direction')) { $mode = self::PAGING_MODE; } else { $mode = self::SELECTED_MODE; } } return $mode; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $title = pht('Conpherence'); $conpherence = null; $scroll_up_participant = $this->getEmptyParticipant(); $scroll_down_participant = $this->getEmptyParticipant(); $too_many = ConpherenceParticipantQuery::LIMIT + 1; $all_participation = array(); $mode = $this->determineMode(); switch ($mode) { case self::SELECTED_MODE: $conpherence_id = $this->getConpherenceID(); $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) ->executeOne(); if (!$conpherence) { return new Aphront404Response(); } if ($conpherence->getTitle()) { $title = $conpherence->getTitle(); } $cursor = $conpherence->getParticipant($user->getPHID()); $data = $this->loadParticipationWithMidCursor($cursor); $all_participation = $data['participation']; $scroll_up_participant = $data['scroll_up_participant']; $scroll_down_participant = $data['scroll_down_participant']; break; case self::PAGING_MODE: $direction = $request->getStr('direction'); $id = $request->getInt('participant_id'); $date_touched = $request->getInt('date_touched'); $conpherence_phid = $request->getStr('conpherence_phid'); if ($direction == 'up') { $order = ConpherenceParticipantQuery::ORDER_NEWER; } else { $order = ConpherenceParticipantQuery::ORDER_OLDER; } $scroller_participant = id(new ConpherenceParticipant()) ->makeEphemeral() ->setID($id) ->setDateTouched($date_touched) ->setConpherencePHID($conpherence_phid); $participation = id(new ConpherenceParticipantQuery()) ->withParticipantPHIDs(array($user->getPHID())) ->withParticipantCursor($scroller_participant) ->setOrder($order) ->setLimit($too_many) ->execute(); if (count($participation) == $too_many) { if ($direction == 'up') { $node = $scroll_up_participant = reset($participation); } else { $node = $scroll_down_participant = end($participation); } unset($participation[$node->getConpherencePHID()]); } $all_participation = $participation; break; case self::UNSELECTED_MODE: default: $too_many = ConpherenceParticipantQuery::LIMIT + 1; $all_participation = id(new ConpherenceParticipantQuery()) ->withParticipantPHIDs(array($user->getPHID())) ->setLimit($too_many) ->execute(); if (count($all_participation) == $too_many) { $node = end($participation); unset($all_participation[$node->getConpherencePHID()]); $scroll_down_participant = $node; } break; } $threads = $this->loadConpherenceThreadData( $all_participation); $thread_view = id(new ConpherenceThreadListView()) ->setUser($user) ->setBaseURI($this->getApplicationURI()) ->setThreads($threads) ->setScrollUpParticipant($scroll_up_participant) ->setScrollDownParticipant($scroll_down_participant); switch ($mode) { case self::SELECTED_MODE: $response = id(new AphrontAjaxResponse())->setContent($thread_view); break; case self::PAGING_MODE: $thread_html = $thread_view->renderThreadsHTML(); $phids = array_keys($participation); $content = array( 'html' => $thread_html, - 'phids' => $phids); + 'phids' => $phids, + ); $response = id(new AphrontAjaxResponse())->setContent($content); break; case self::UNSELECTED_MODE: default: $layout = id(new ConpherenceLayoutView()) ->setBaseURI($this->getApplicationURI()) ->setThreadView($thread_view) ->setRole('list'); if ($conpherence) { $layout->setHeader($this->buildHeaderPaneContent($conpherence)); $layout->setThread($conpherence); } else { $layout->setHeader( $this->buildHeaderPaneContent( id(new ConpherenceThread()) ->makeEphemeral())); } $response = $this->buildApplicationPage( $layout, array( 'title' => $title, )); break; } return $response; } /** * Handles the curious case when we are visiting a conpherence directly * by issuing two separate queries. Otherwise, additional conpherences * are fetched asynchronously. Note these can be earlier or later * (up or down), depending on what conpherence was selected on initial * load. */ private function loadParticipationWithMidCursor( ConpherenceParticipant $cursor) { $user = $this->getRequest()->getUser(); $scroll_up_participant = $this->getEmptyParticipant(); $scroll_down_participant = $this->getEmptyParticipant(); // Note this is a bit dodgy since there may be less than this // amount in either the up or down direction, thus having us fail // to fetch LIMIT in total. Whatevs for now and re-visit if we're // fine-tuning this loading process. $too_many = ceil(ConpherenceParticipantQuery::LIMIT / 2) + 1; $participant_query = id(new ConpherenceParticipantQuery()) ->withParticipantPHIDs(array($user->getPHID())) ->setLimit($too_many); $current_selection_epoch = $cursor->getDateTouched(); $set_one = $participant_query ->withParticipantCursor($cursor) ->setOrder(ConpherenceParticipantQuery::ORDER_NEWER) ->execute(); if (count($set_one) == $too_many) { $node = reset($set_one); unset($set_one[$node->getConpherencePHID()]); $scroll_up_participant = $node; } $set_two = $participant_query ->withParticipantCursor($cursor) ->setOrder(ConpherenceParticipantQuery::ORDER_OLDER) ->execute(); if (count($set_two) == $too_many) { $node = end($set_two); unset($set_two[$node->getConpherencePHID()]); $scroll_down_participant = $node; } $participation = array_merge( $set_one, $set_two); return array( 'scroll_up_participant' => $scroll_up_participant, 'scroll_down_participant' => $scroll_down_participant, 'participation' => $participation, ); } private function loadConpherenceThreadData($participation) { $user = $this->getRequest()->getUser(); $conpherence_phids = array_keys($participation); $conpherences = array(); if ($conpherence_phids) { $conpherences = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withPHIDs($conpherence_phids) ->needParticipantCache(true) ->execute(); // this will re-sort by participation data $conpherences = array_select_keys($conpherences, $conpherence_phids); } return $conpherences; } private function getEmptyParticipant() { return id(new ConpherenceParticipant()) ->makeEphemeral(); } } diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 800dad1fa3..ded971ddf5 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -1,171 +1,171 @@ <?php final class ConpherenceViewController extends ConpherenceController { private $conpherenceID; private $conpherence; public function setConpherence(ConpherenceThread $conpherence) { $this->conpherence = $conpherence; return $this; } public function getConpherence() { return $this->conpherence; } public function setConpherenceID($conpherence_id) { $this->conpherenceID = $conpherence_id; return $this; } public function getConpherenceID() { return $this->conpherenceID; } public function willProcessRequest(array $data) { $this->setConpherenceID(idx($data, 'id')); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $conpherence_id = $this->getConpherenceID(); if (!$conpherence_id) { return new Aphront404Response(); } $query = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) ->needParticipantCache(true) ->needTransactions(true) ->setTransactionLimit(ConpherenceThreadQuery::TRANSACTION_LIMIT); $before_transaction_id = $request->getInt('oldest_transaction_id'); if ($before_transaction_id) { $query ->setBeforeTransactionID($before_transaction_id); } $conpherence = $query->executeOne(); if (!$conpherence) { return new Aphront404Response(); } $this->setConpherence($conpherence); $participant = $conpherence->getParticipant($user->getPHID()); $transactions = $conpherence->getTransactions(); $latest_transaction = end($transactions); $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites(); $participant->markUpToDate($conpherence, $latest_transaction); unset($write_guard); $data = $this->renderConpherenceTransactions($conpherence); $messages = $this->renderMessagePaneContent( $data['transactions'], $data['oldest_transaction_id']); if ($before_transaction_id) { $header = null; $form = null; $content = array('messages' => $messages); } else { $header = $this->buildHeaderPaneContent($conpherence); $form = $this->renderFormContent($data['latest_transaction_id']); $content = array( 'header' => $header, 'messages' => $messages, - 'form' => $form + 'form' => $form, ); } if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($content); } $layout = id(new ConpherenceLayoutView()) ->setBaseURI($this->getApplicationURI()) ->setThread($conpherence) ->setHeader($header) ->setMessages($messages) ->setReplyForm($form) ->setRole('thread'); $title = $conpherence->getTitle(); if (!$title) { $title = pht('[No Title]'); } return $this->buildApplicationPage( $layout, array( 'title' => $title, 'pageObjects' => array($conpherence->getPHID()), )); } private function renderMessagePaneContent( array $transactions, $oldest_transaction_id) { $scrollbutton = ''; if ($oldest_transaction_id) { $scrollbutton = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'show-older-messages', 'class' => 'conpherence-show-older-messages', 'meta' => array( - 'oldest_transaction_id' => $oldest_transaction_id - ) + 'oldest_transaction_id' => $oldest_transaction_id, + ), ), pht('Show Older Messages')); } return hsprintf('%s%s', $scrollbutton, $transactions); } private function renderFormContent($latest_transaction_id) { $conpherence = $this->getConpherence(); $user = $this->getRequest()->getUser(); $draft = PhabricatorDraft::newFromUserAndKey( $user, $conpherence->getPHID()); $update_uri = $this->getApplicationURI('update/'.$conpherence->getID().'/'); $this->initBehavior('conpherence-pontificate'); $form = id(new AphrontFormView()) ->setAction($update_uri) ->addSigil('conpherence-pontificate') ->setWorkflow(true) ->setUser($user) ->addHiddenInput('action', 'message') ->appendChild( id(new PhabricatorRemarkupControl()) ->setUser($user) ->setName('text') ->setValue($draft->getDraft())) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Send Message'))) ->appendChild( javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'latest_transaction_id', 'value' => $latest_transaction_id, 'sigil' => 'latest-transaction-id', 'meta' => array( 'threadPHID' => $conpherence->getPHID(), 'threadID' => $conpherence->getID(), ), ), '')) ->render(); return $form; } } diff --git a/src/applications/conpherence/controller/ConpherenceWidgetController.php b/src/applications/conpherence/controller/ConpherenceWidgetController.php index 10c53b73cd..b508861765 100644 --- a/src/applications/conpherence/controller/ConpherenceWidgetController.php +++ b/src/applications/conpherence/controller/ConpherenceWidgetController.php @@ -1,384 +1,386 @@ <?php final class ConpherenceWidgetController extends ConpherenceController { private $conpherenceID; private $conpherence; private $userPreferences; public function setUserPreferences(PhabricatorUserPreferences $pref) { $this->userPreferences = $pref; return $this; } public function getUserPreferences() { return $this->userPreferences; } public function setConpherence(ConpherenceThread $conpherence) { $this->conpherence = $conpherence; return $this; } public function getConpherence() { return $this->conpherence; } public function setConpherenceID($conpherence_id) { $this->conpherenceID = $conpherence_id; return $this; } public function getConpherenceID() { return $this->conpherenceID; } public function willProcessRequest(array $data) { $this->setConpherenceID(idx($data, 'id')); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $conpherence_id = $this->getConpherenceID(); if (!$conpherence_id) { return new Aphront404Response(); } $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) ->needWidgetData(true) ->executeOne(); $this->setConpherence($conpherence); $this->setUserPreferences($user->loadPreferences()); $widgets = $this->renderWidgetPaneContent(); $content = $widgets; return id(new AphrontAjaxResponse())->setContent($content); } private function renderWidgetPaneContent() { require_celerity_resource('sprite-conpherence-css'); $conpherence = $this->getConpherence(); $widgets = array(); $new_icon = id(new PHUIIconView()) ->setIconFont('fa-plus') ->setHref($this->getWidgetURI()) ->setMetadata(array('widget' => null)) ->addSigil('conpherence-widget-adder'); $widgets[] = phutil_tag( 'div', array( 'class' => 'widgets-header', ), id(new PHUIActionHeaderView()) ->setHeaderColor(PHUIActionHeaderView::HEADER_GREY) ->setHeaderTitle(pht('Participants')) ->setHeaderHref('#') ->setDropdown(true) ->addAction($new_icon) ->addHeaderSigil('widgets-selector')); $user = $this->getRequest()->getUser(); // now the widget bodies $widgets[] = javelin_tag( 'div', array( 'class' => 'widgets-body', 'id' => 'widgets-people', 'sigil' => 'widgets-people', ), id(new ConpherencePeopleWidgetView()) ->setUser($user) ->setConpherence($conpherence) ->setUpdateURI($this->getWidgetURI())); $widgets[] = javelin_tag( 'div', array( 'class' => 'widgets-body', 'id' => 'widgets-files', 'sigil' => 'widgets-files', - 'style' => 'display: none;' + 'style' => 'display: none;', ), id(new ConpherenceFileWidgetView()) ->setUser($user) ->setConpherence($conpherence) ->setUpdateURI($this->getWidgetURI())); $widgets[] = phutil_tag( 'div', array( 'class' => 'widgets-body', 'id' => 'widgets-calendar', - 'style' => 'display: none;' + 'style' => 'display: none;', ), $this->renderCalendarWidgetPaneContent()); $widgets[] = phutil_tag( 'div', array( 'class' => 'widgets-body', 'id' => 'widgets-settings', - 'style' => 'display: none' + 'style' => 'display: none', ), $this->renderSettingsWidgetPaneContent()); // without this implosion we get "," between each element in our widgets // array return array('widgets' => phutil_implode_html('', $widgets)); } private function renderSettingsWidgetPaneContent() { $user = $this->getRequest()->getUser(); $conpherence = $this->getConpherence(); $participants = $conpherence->getParticipants(); $participant = $participants[$user->getPHID()]; $default = ConpherenceSettings::EMAIL_ALWAYS; $preference = $this->getUserPreferences(); if ($preference) { $default = $preference->getPreference( PhabricatorUserPreferences::PREFERENCE_CONPH_NOTIFICATIONS, ConpherenceSettings::EMAIL_ALWAYS); } $settings = $participant->getSettings(); $notifications = idx( $settings, 'notifications', $default); $options = id(new AphrontFormRadioButtonControl()) ->addButton( ConpherenceSettings::EMAIL_ALWAYS, ConpherenceSettings::getHumanString( ConpherenceSettings::EMAIL_ALWAYS), '') ->addButton( ConpherenceSettings::NOTIFICATIONS_ONLY, ConpherenceSettings::getHumanString( ConpherenceSettings::NOTIFICATIONS_ONLY), '') ->setName('notifications') ->setValue($notifications); $layout = array( $options, phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'action', - 'value' => 'notifications' + 'value' => 'notifications', )), phutil_tag( 'button', array( 'type' => 'submit', 'class' => 'notifications-update', ), - pht('Save')) + pht('Save')), ); return phabricator_form( $user, array( 'method' => 'POST', 'action' => $this->getWidgetURI(), 'sigil' => 'notifications-update', ), $layout); } private function renderCalendarWidgetPaneContent() { $user = $this->getRequest()->getUser(); $conpherence = $this->getConpherence(); $participants = $conpherence->getParticipants(); $widget_data = $conpherence->getWidgetData(); $statuses = $widget_data['statuses']; $handles = $conpherence->getHandles(); $content = array(); $layout = id(new AphrontMultiColumnView()) ->setFluidLayout(true); $timestamps = CalendarTimeUtil::getCalendarWidgetTimestamps($user); $today = $timestamps['today']; $epoch_stamps = $timestamps['epoch_stamps']; $one_day = 24 * 60 * 60; $is_today = false; $calendar_columns = 0; $list_days = 0; foreach ($epoch_stamps as $day) { // build a header for the new day if ($day->format('Ymd') == $today->format('Ymd')) { $active_class = 'today'; $is_today = true; } else { $active_class = ''; $is_today = false; } $should_draw_list = $list_days < 7; $list_days++; if ($should_draw_list) { $content[] = phutil_tag( 'div', array( - 'class' => 'day-header '.$active_class + 'class' => 'day-header '.$active_class, ), array( phutil_tag( 'div', array( - 'class' => 'day-name' + 'class' => 'day-name', ), $day->format('l')), phutil_tag( 'div', array( - 'class' => 'day-date' + 'class' => 'day-date', ), - $day->format('m/d/y')))); + $day->format('m/d/y')), + )); } $week_day_number = $day->format('w'); $epoch_start = $day->format('U'); $next_day = clone $day; $next_day->modify('+1 day'); $epoch_end = $next_day->format('U'); $first_status_of_the_day = true; $statuses_of_the_day = array(); // keep looking through statuses where we last left off foreach ($statuses as $status) { if ($status->getDateFrom() >= $epoch_end) { // This list is sorted, so we can stop looking. break; } if ($status->getDateFrom() < $epoch_end && $status->getDateTo() > $epoch_start) { $statuses_of_the_day[$status->getUserPHID()] = $status; if ($should_draw_list) { $top_border = ''; if (!$first_status_of_the_day) { $top_border = ' top-border'; } $timespan = $status->getDateTo() - $status->getDateFrom(); if ($timespan > $one_day) { $time_str = 'm/d'; } else { $time_str = 'h:i A'; } $epoch_range = phabricator_format_local_time( $status->getDateFrom(), $user, $time_str). ' - '. phabricator_format_local_time( $status->getDateTo(), $user, $time_str); $secondary_info = pht('%s, %s', $handles[$status->getUserPHID()]->getName(), $epoch_range); $content[] = phutil_tag( 'div', array( 'class' => 'user-status '.$status->getTextStatus().$top_border, ), array( phutil_tag( 'div', array( 'class' => 'icon', ), ''), phutil_tag( 'div', array( - 'class' => 'description' + 'class' => 'description', ), array( $status->getTerseSummary($user), phutil_tag( 'div', array( - 'class' => 'participant' + 'class' => 'participant', ), - $secondary_info))))); + $secondary_info), + )), + )); } $first_status_of_the_day = false; } } // we didn't get a status on this day so add a spacer if ($first_status_of_the_day && $should_draw_list) { $content[] = phutil_tag( 'div', array('class' => 'no-events pm'), pht('No Events Scheduled.')); } if ($is_today || ($calendar_columns && $calendar_columns < 3)) { $active_class = ''; if ($is_today) { $active_class = '-active'; } $inner_layout = array(); foreach ($participants as $phid => $participant) { $status = idx($statuses_of_the_day, $phid, false); if ($status) { $inner_layout[] = phutil_tag( 'div', array( - 'class' => $status->getTextStatus() + 'class' => $status->getTextStatus(), ), ''); } else { $inner_layout[] = phutil_tag( 'div', array( - 'class' => 'present' + 'class' => 'present', ), ''); } } $layout->addColumn( phutil_tag( 'div', array( - 'class' => 'day-column'.$active_class + 'class' => 'day-column'.$active_class, ), array( phutil_tag( 'div', array( - 'class' => 'day-name' + 'class' => 'day-name', ), $day->format('D')), phutil_tag( 'div', array( 'class' => 'day-number', ), $day->format('j')), - $inner_layout + $inner_layout, ))); $calendar_columns++; } } - return - array( - $layout, - $content - ); + return array( + $layout, + $content, + ); } private function getWidgetURI() { $conpherence = $this->getConpherence(); return $this->getApplicationURI('update/'.$conpherence->getID().'/'); } } diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php index dae980a7c2..5ce2ff42ca 100644 --- a/src/applications/conpherence/query/ConpherenceThreadQuery.php +++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php @@ -1,294 +1,294 @@ <?php final class ConpherenceThreadQuery extends PhabricatorCursorPagedPolicyAwareQuery { const TRANSACTION_LIMIT = 100; private $phids; private $ids; private $needWidgetData; private $needTransactions; private $needParticipantCache; private $needFilePHIDs; private $afterTransactionID; private $beforeTransactionID; private $transactionLimit; public function needFilePHIDs($need_file_phids) { $this->needFilePHIDs = $need_file_phids; return $this; } public function needParticipantCache($participant_cache) { $this->needParticipantCache = $participant_cache; return $this; } public function needWidgetData($need_widget_data) { $this->needWidgetData = $need_widget_data; return $this; } public function needTransactions($need_transactions) { $this->needTransactions = $need_transactions; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function setAfterTransactionID($id) { $this->afterTransactionID = $id; return $this; } public function setBeforeTransactionID($id) { $this->beforeTransactionID = $id; return $this; } public function setTransactionLimit($transaction_limit) { $this->transactionLimit = $transaction_limit; return $this; } public function getTransactionLimit() { return $this->transactionLimit; } protected function loadPage() { $table = new ConpherenceThread(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT conpherence_thread.* FROM %T conpherence_thread %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $conpherences = $table->loadAllFromArray($data); if ($conpherences) { $conpherences = mpull($conpherences, null, 'getPHID'); $this->loadParticipantsAndInitHandles($conpherences); if ($this->needParticipantCache) { $this->loadCoreHandles($conpherences, 'getRecentParticipantPHIDs'); } else if ($this->needWidgetData) { $this->loadCoreHandles($conpherences, 'getParticipantPHIDs'); } if ($this->needTransactions) { $this->loadTransactionsAndHandles($conpherences); } if ($this->needFilePHIDs || $this->needWidgetData) { $this->loadFilePHIDs($conpherences); } if ($this->needWidgetData) { $this->loadWidgetData($conpherences); } } return $conpherences; } protected function buildWhereClause($conn_r) { $where = array(); $where[] = $this->buildPagingClause($conn_r); if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } return $this->formatWhereClause($where); } private function loadParticipantsAndInitHandles(array $conpherences) { $participants = id(new ConpherenceParticipant()) ->loadAllWhere('conpherencePHID IN (%Ls)', array_keys($conpherences)); $map = mgroup($participants, 'getConpherencePHID'); foreach ($conpherences as $current_conpherence) { $conpherence_phid = $current_conpherence->getPHID(); $conpherence_participants = idx( $map, $conpherence_phid, array()); $conpherence_participants = mpull( $conpherence_participants, null, 'getParticipantPHID'); $current_conpherence->attachParticipants($conpherence_participants); $current_conpherence->attachHandles(array()); } return $this; } private function loadCoreHandles( array $conpherences, $method) { $handle_phids = array(); foreach ($conpherences as $conpherence) { $handle_phids[$conpherence->getPHID()] = $conpherence->$method(); } $flat_phids = array_mergev($handle_phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getViewer()) ->withPHIDs($flat_phids) ->execute(); foreach ($handle_phids as $conpherence_phid => $phids) { $conpherence = $conpherences[$conpherence_phid]; $conpherence->attachHandles(array_select_keys($handles, $phids)); } return $this; } private function loadTransactionsAndHandles(array $conpherences) { $query = id(new ConpherenceTransactionQuery()) ->setViewer($this->getViewer()) ->withObjectPHIDs(array_keys($conpherences)) ->needHandles(true); // We have to flip these for the underyling query class. The semantics of // paging are tricky business. if ($this->afterTransactionID) { $query->setBeforeID($this->afterTransactionID); } else if ($this->beforeTransactionID) { $query->setAfterID($this->beforeTransactionID); } if ($this->getTransactionLimit()) { // fetch an extra for "show older" scenarios $query->setLimit($this->getTransactionLimit() + 1); } $transactions = $query->execute(); $transactions = mgroup($transactions, 'getObjectPHID'); foreach ($conpherences as $phid => $conpherence) { $current_transactions = idx($transactions, $phid, array()); $handles = array(); foreach ($current_transactions as $transaction) { $handles += $transaction->getHandles(); } $conpherence->attachHandles($conpherence->getHandles() + $handles); $conpherence->attachTransactions($current_transactions); } return $this; } private function loadFilePHIDs(array $conpherences) { $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE; $file_edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array_keys($conpherences)) ->withEdgeTypes(array($edge_type)) ->execute(); foreach ($file_edges as $conpherence_phid => $data) { $conpherence = $conpherences[$conpherence_phid]; $conpherence->attachFilePHIDs(array_keys($data[$edge_type])); } return $this; } private function loadWidgetData(array $conpherences) { $participant_phids = array(); $file_phids = array(); foreach ($conpherences as $conpherence) { $participant_phids[] = array_keys($conpherence->getParticipants()); $file_phids[] = $conpherence->getFilePHIDs(); } $participant_phids = array_mergev($participant_phids); $file_phids = array_mergev($file_phids); $epochs = CalendarTimeUtil::getCalendarEventEpochs( $this->getViewer()); $start_epoch = $epochs['start_epoch']; $end_epoch = $epochs['end_epoch']; $statuses = id(new PhabricatorCalendarEventQuery()) ->setViewer($this->getViewer()) ->withInvitedPHIDs($participant_phids) ->withDateRange($start_epoch, $end_epoch) ->execute(); $statuses = mgroup($statuses, 'getUserPHID'); // attached files $files = array(); $file_author_phids = array(); $authors = array(); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); $file_author_phids = mpull($files, 'getAuthorPHID', 'getPHID'); $authors = id(new PhabricatorHandleQuery()) ->setViewer($this->getViewer()) ->withPHIDs($file_author_phids) ->execute(); $authors = mpull($authors, null, 'getPHID'); } foreach ($conpherences as $phid => $conpherence) { $participant_phids = array_keys($conpherence->getParticipants()); $statuses = array_select_keys($statuses, $participant_phids); $statuses = array_mergev($statuses); $statuses = msort($statuses, 'getDateFrom'); $conpherence_files = array(); $files_authors = array(); foreach ($conpherence->getFilePHIDs() as $curr_phid) { $curr_file = idx($files, $curr_phid); if (!$curr_file) { // this file was deleted or user doesn't have permission to see it // this is generally weird continue; } $conpherence_files[$curr_phid] = $curr_file; // some files don't have authors so be careful $current_author = null; $current_author_phid = idx($file_author_phids, $curr_phid); if ($current_author_phid) { $current_author = $authors[$current_author_phid]; } $files_authors[$curr_phid] = $current_author; } $widget_data = array( 'statuses' => $statuses, 'files' => $conpherence_files, - 'files_authors' => $files_authors + 'files_authors' => $files_authors, ); $conpherence->attachWidgetData($widget_data); } return $this; } public function getQueryApplicationClass() { return 'PhabricatorConpherenceApplication'; } } diff --git a/src/applications/conpherence/view/ConpherenceFileWidgetView.php b/src/applications/conpherence/view/ConpherenceFileWidgetView.php index 277202065c..b3321c479a 100644 --- a/src/applications/conpherence/view/ConpherenceFileWidgetView.php +++ b/src/applications/conpherence/view/ConpherenceFileWidgetView.php @@ -1,76 +1,77 @@ <?php final class ConpherenceFileWidgetView extends ConpherenceWidgetView { public function render() { require_celerity_resource('sprite-docs-css'); $conpherence = $this->getConpherence(); $widget_data = $conpherence->getWidgetData(); $files = $widget_data['files']; $files_authors = $widget_data['files_authors']; $files_html = array(); foreach ($files as $file) { $icon_class = $file->getDisplayIconForMimeType(); $icon_view = phutil_tag( 'div', array( - 'class' => 'file-icon sprite-docs '.$icon_class + 'class' => 'file-icon sprite-docs '.$icon_class, ), ''); $file_view = id(new PhabricatorFileLinkView()) ->setFilePHID($file->getPHID()) ->setFileName(id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(28) ->truncateString($file->getName())) ->setFileViewable($file->isViewableImage()) ->setFileViewURI($file->getBestURI()) ->setCustomClass('file-title'); $who_done_it_text = ''; // system generated files don't have authors if ($file->getAuthorPHID()) { $who_done_it_text = pht( 'By %s ', $files_authors[$file->getPHID()]->renderLink()); } $date_text = phabricator_relative_date( $file->getDateCreated(), $this->getUser()); $who_done_it = phutil_tag( 'div', array( - 'class' => 'file-uploaded-by' + 'class' => 'file-uploaded-by', ), pht('%s%s.', $who_done_it_text, $date_text)); $files_html[] = phutil_tag( 'div', array( - 'class' => 'file-entry' + 'class' => 'file-entry', ), array( $icon_view, $file_view, - $who_done_it + $who_done_it, )); } if (empty($files)) { $files_html[] = javelin_tag( 'div', array( 'class' => 'no-files', - 'sigil' => 'no-files'), + 'sigil' => 'no-files', + ), pht('No files.')); } return phutil_tag( 'div', array('class' => 'file-list'), $files_html); } } diff --git a/src/applications/conpherence/view/ConpherenceLayoutView.php b/src/applications/conpherence/view/ConpherenceLayoutView.php index 5036304f73..6458f37867 100644 --- a/src/applications/conpherence/view/ConpherenceLayoutView.php +++ b/src/applications/conpherence/view/ConpherenceLayoutView.php @@ -1,239 +1,241 @@ <?php final class ConpherenceLayoutView extends AphrontView { private $thread; private $baseURI; private $threadView; private $role; private $header; private $messages; private $replyForm; public function setMessages($messages) { $this->messages = $messages; return $this; } public function setReplyForm($reply_form) { $this->replyForm = $reply_form; return $this; } public function setHeader($header) { $this->header = $header; return $this; } public function setRole($role) { $this->role = $role; return $this; } public function getThreadView() { return $this->threadView; } public function setBaseURI($base_uri) { $this->baseURI = $base_uri; return $this; } public function setThread(ConpherenceThread $thread) { $this->thread = $thread; return $this; } public function setThreadView(ConpherenceThreadListView $thead_view) { $this->threadView = $thead_view; return $this; } public function render() { require_celerity_resource('conpherence-menu-css'); require_celerity_resource('conpherence-message-pane-css'); require_celerity_resource('conpherence-widget-pane-css'); $layout_id = celerity_generate_unique_node_id(); $selected_id = null; $selected_thread_id = null; if ($this->thread) { $selected_id = $this->thread->getPHID().'-nav-item'; $selected_thread_id = $this->thread->getID(); } $this->initBehavior('conpherence-menu', array( 'baseURI' => $this->baseURI, 'layoutID' => $layout_id, 'selectedID' => $selected_id, 'selectedThreadID' => $selected_thread_id, 'role' => $this->role, 'hasThreadList' => (bool)$this->threadView, 'hasThread' => (bool)$this->messages, 'hasWidgets' => false, )); $this->initBehavior( 'conpherence-widget-pane', array( 'widgetBaseUpdateURI' => $this->baseURI.'update/', 'widgetRegistry' => array( 'conpherence-message-pane' => array( 'name' => pht('Thread'), 'icon' => 'fa-comment', 'deviceOnly' => true, - 'hasCreate' => false + 'hasCreate' => false, ), 'widgets-people' => array( 'name' => pht('Participants'), 'icon' => 'fa-users', 'deviceOnly' => false, 'hasCreate' => true, 'createData' => array( 'refreshFromResponse' => true, 'action' => ConpherenceUpdateActions::ADD_PERSON, - 'customHref' => null - ) + 'customHref' => null, + ), ), 'widgets-files' => array( 'name' => pht('Files'), 'icon' => 'fa-files-o', 'deviceOnly' => false, - 'hasCreate' => false + 'hasCreate' => false, ), 'widgets-calendar' => array( 'name' => pht('Calendar'), 'icon' => 'fa-calendar', 'deviceOnly' => false, 'hasCreate' => true, 'createData' => array( 'refreshFromResponse' => false, 'action' => ConpherenceUpdateActions::ADD_STATUS, - 'customHref' => '/calendar/event/create/' - ) + 'customHref' => '/calendar/event/create/', + ), ), 'widgets-settings' => array( 'name' => pht('Settings'), 'icon' => 'fa-wrench', 'deviceOnly' => false, - 'hasCreate' => false + 'hasCreate' => false, ), - ))); + ), + )); return javelin_tag( 'div', array( 'id' => $layout_id, 'sigil' => 'conpherence-layout', 'class' => 'conpherence-layout conpherence-role-'.$this->role, ), array( javelin_tag( 'div', array( 'class' => 'phabricator-nav-column-background', 'sigil' => 'phabricator-nav-column-background', ), ''), javelin_tag( 'div', array( 'id' => 'conpherence-menu-pane', 'class' => 'conpherence-menu-pane phabricator-side-menu', 'sigil' => 'conpherence-menu-pane', ), $this->threadView), javelin_tag( 'div', array( 'class' => 'conpherence-content-pane', ), array( javelin_tag( 'div', array( 'class' => 'conpherence-header-pane', 'id' => 'conpherence-header-pane', 'sigil' => 'conpherence-header-pane', ), nonempty($this->header, '')), javelin_tag( 'div', array( 'class' => 'conpherence-no-threads', 'sigil' => 'conpherence-no-threads', 'style' => 'display: none;', ), array( phutil_tag( 'div', array( - 'class' => 'text' + 'class' => 'text', ), pht('You do not have any messages yet.')), javelin_tag( 'a', array( 'href' => '/conpherence/new/', 'class' => 'button grey', 'sigil' => 'workflow', ), - pht('Send a Message')) + pht('Send a Message')), )), javelin_tag( 'div', array( 'class' => 'conpherence-widget-pane', 'id' => 'conpherence-widget-pane', 'sigil' => 'conpherence-widget-pane', ), array( phutil_tag( 'div', array( - 'class' => 'widgets-loading-mask' + 'class' => 'widgets-loading-mask', ), ''), javelin_tag( 'div', array( - 'sigil' => 'conpherence-widgets-holder' + 'sigil' => 'conpherence-widgets-holder', ), - ''))), + ''), + )), javelin_tag( 'div', array( 'class' => 'conpherence-message-pane', 'id' => 'conpherence-message-pane', - 'sigil' => 'conpherence-message-pane' + 'sigil' => 'conpherence-message-pane', ), array( javelin_tag( 'div', array( 'class' => 'conpherence-messages', 'id' => 'conpherence-messages', 'sigil' => 'conpherence-messages', ), nonempty($this->messages, '')), phutil_tag( 'div', array( 'class' => 'messages-loading-mask', ), ''), javelin_tag( 'div', array( 'id' => 'conpherence-form', - 'sigil' => 'conpherence-form' + 'sigil' => 'conpherence-form', ), - nonempty($this->replyForm, '')) + nonempty($this->replyForm, '')), )), )), )); } } diff --git a/src/applications/conpherence/view/ConpherenceMenuItemView.php b/src/applications/conpherence/view/ConpherenceMenuItemView.php index 88794e1546..2b5fbcdc20 100644 --- a/src/applications/conpherence/view/ConpherenceMenuItemView.php +++ b/src/applications/conpherence/view/ConpherenceMenuItemView.php @@ -1,120 +1,120 @@ <?php final class ConpherenceMenuItemView extends AphrontTagView { private $title; private $subtitle; private $imageURI; private $href; private $epoch; private $unreadCount; public function setUnreadCount($unread_count) { $this->unreadCount = $unread_count; return $this; } public function getUnreadCount() { return $this->unreadCount; } public function setEpoch($epoch) { $this->epoch = $epoch; return $this; } public function getEpoch() { return $this->epoch; } public function setHref($href) { $this->href = $href; return $this; } public function getHref() { return $this->href; } public function setImageURI($image_uri) { $this->imageURI = $image_uri; return $this; } public function getImageURI() { return $this->imageURI; } public function setSubtitle($subtitle) { $this->subtitle = $subtitle; return $this; } public function getSubtitle() { return $this->subtitle; } public function setTitle($title) { $this->title = $title; return $this; } public function getTitle() { return $this->title; } protected function getTagName() { return 'a'; } protected function getTagAttributes() { $classes = array('conpherence-menu-item-view'); return array( 'class' => $classes, 'href' => $this->href, ); } protected function getTagContent() { $image = null; if ($this->imageURI) { $image = phutil_tag( 'span', array( 'class' => 'conpherence-menu-item-image', - 'style' => 'background-image: url('.$this->imageURI.');' + 'style' => 'background-image: url('.$this->imageURI.');', ), ''); } $title = null; if ($this->title) { $title = phutil_tag( 'span', array( 'class' => 'conpherence-menu-item-title', ), $this->title); } $subtitle = null; if ($this->subtitle) { $subtitle = phutil_tag( 'span', array( 'class' => 'conpherence-menu-item-subtitle', ), $this->subtitle); } $unread_count = null; if ($this->unreadCount) { $unread_count = phutil_tag( 'span', array( - 'class' => 'conpherence-menu-item-unread-count' + 'class' => 'conpherence-menu-item-unread-count', ), (int)$this->unreadCount); } return array( $image, $title, $subtitle, $unread_count, ); } } diff --git a/src/applications/conpherence/view/ConpherencePeopleWidgetView.php b/src/applications/conpherence/view/ConpherencePeopleWidgetView.php index 87660a767c..d0d8c454d2 100644 --- a/src/applications/conpherence/view/ConpherencePeopleWidgetView.php +++ b/src/applications/conpherence/view/ConpherencePeopleWidgetView.php @@ -1,56 +1,57 @@ <?php final class ConpherencePeopleWidgetView extends ConpherenceWidgetView { public function render() { $conpherence = $this->getConpherence(); $widget_data = $conpherence->getWidgetData(); $user = $this->getUser(); $conpherence = $this->getConpherence(); $participants = $conpherence->getParticipants(); $handles = $conpherence->getHandles(); $body = array(); // future proof by using participants to iterate through handles; // we may have non-people handles sooner or later foreach ($participants as $user_phid => $participant) { $handle = $handles[$user_phid]; $remove_html = ''; if ($user_phid == $user->getPHID()) { $remove_html = javelin_tag( 'a', array( 'class' => 'remove', 'sigil' => 'remove-person', 'meta' => array( 'remove_person' => $handle->getPHID(), 'action' => 'remove_person', - ) + ), ), hsprintf('<span class="close-icon">×</span>')); } $body[] = phutil_tag( 'div', array( - 'class' => 'person-entry grouped' + 'class' => 'person-entry grouped', ), array( phutil_tag( 'a', array( 'class' => 'pic', ), phutil_tag( 'img', array( - 'src' => $handle->getImageURI() + 'src' => $handle->getImageURI(), ), '')), $handle->renderLink(), - $remove_html)); + $remove_html, + )); } return $body; } } diff --git a/src/applications/conpherence/view/ConpherenceThreadListView.php b/src/applications/conpherence/view/ConpherenceThreadListView.php index 7b357f0cab..d670c447fa 100644 --- a/src/applications/conpherence/view/ConpherenceThreadListView.php +++ b/src/applications/conpherence/view/ConpherenceThreadListView.php @@ -1,168 +1,169 @@ <?php final class ConpherenceThreadListView extends AphrontView { private $baseURI; private $threads; private $scrollUpParticipant; private $scrollDownParticipant; public function setThreads(array $threads) { assert_instances_of($threads, 'ConpherenceThread'); $this->threads = $threads; return $this; } public function setScrollUpParticipant( ConpherenceParticipant $participant) { $this->scrollUpParticipant = $participant; return $this; } public function setScrollDownParticipant( ConpherenceParticipant $participant) { $this->scrollDownParticipant = $participant; return $this; } public function setBaseURI($base_uri) { $this->baseURI = $base_uri; return $this; } public function render() { require_celerity_resource('conpherence-menu-css'); $menu = id(new PHUIListView()) ->addClass('conpherence-menu') ->setID('conpherence-menu'); $this->addThreadsToMenu($menu, $this->threads); return $menu; } public function renderSingleThread(ConpherenceThread $thread) { return $this->renderThread($thread); } public function renderThreadsHTML() { $thread_html = array(); if ($this->scrollUpParticipant->getID()) { $thread_html[] = $this->getScrollMenuItem( $this->scrollUpParticipant, 'up'); } foreach ($this->threads as $thread) { $thread_html[] = $this->renderSingleThread($thread); } if ($this->scrollDownParticipant->getID()) { $thread_html[] = $this->getScrollMenuItem( $this->scrollDownParticipant, 'down'); } return phutil_implode_html('', $thread_html); } private function renderThreadItem(ConpherenceThread $thread) { return id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_CUSTOM) ->setName($this->renderThread($thread)); } private function renderThread(ConpherenceThread $thread) { $user = $this->getUser(); $uri = $this->baseURI.$thread->getID().'/'; $data = $thread->getDisplayData($user); $title = $data['title']; $subtitle = $data['subtitle']; $unread_count = $data['unread_count']; $epoch = $data['epoch']; $image = $data['image']; $dom_id = $thread->getPHID().'-nav-item'; return id(new ConpherenceMenuItemView()) ->setUser($user) ->setTitle($title) ->setSubtitle($subtitle) ->setHref($uri) ->setEpoch($epoch) ->setImageURI($image) ->setUnreadCount($unread_count) ->setID($thread->getPHID().'-nav-item') ->addSigil('conpherence-menu-click') ->setMetadata( array( 'title' => $data['js_title'], 'id' => $dom_id, 'threadID' => $thread->getID(), )); } private function addThreadsToMenu( PHUIListView $menu, array $conpherences) { if ($this->scrollUpParticipant->getID()) { $item = $this->getScrollMenuItem($this->scrollUpParticipant, 'up'); $menu->addMenuItem($item); } foreach ($conpherences as $conpherence) { $item = $this->renderThreadItem($conpherence); $menu->addMenuItem($item); } if (empty($conpherences)) { $menu->addMenuItem($this->getNoConpherencesMenuItem()); } if ($this->scrollDownParticipant->getID()) { $item = $this->getScrollMenuItem($this->scrollDownParticipant, 'down'); $menu->addMenuItem($item); } return $menu; } public function getScrollMenuItem( ConpherenceParticipant $participant, $direction) { if ($direction == 'up') { $name = pht('Load Newer Threads'); } else { $name = pht('Load Older Threads'); } $item = id(new PHUIListItemView()) ->addSigil('conpherence-menu-scroller') ->setName($name) ->setHref($this->baseURI) ->setType(PHUIListItemView::TYPE_BUTTON) ->setMetadata(array( 'participant_id' => $participant->getID(), 'conpherence_phid' => $participant->getConpherencePHID(), 'date_touched' => $participant->getDateTouched(), - 'direction' => $direction)); + 'direction' => $direction, + )); return $item; } private function getNoConpherencesMenuItem() { $message = phutil_tag( 'div', array( - 'class' => 'no-conpherences-menu-item' + 'class' => 'no-conpherences-menu-item', ), pht('No conpherences.')); return id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_CUSTOM) ->setName($message); } } diff --git a/src/applications/conpherence/view/ConpherenceTransactionView.php b/src/applications/conpherence/view/ConpherenceTransactionView.php index bffe5da9fd..1ea53b1c8c 100644 --- a/src/applications/conpherence/view/ConpherenceTransactionView.php +++ b/src/applications/conpherence/view/ConpherenceTransactionView.php @@ -1,102 +1,103 @@ <?php final class ConpherenceTransactionView extends AphrontView { private $conpherenceTransaction; private $handles; private $markupEngine; public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) { $this->markupEngine = $markup_engine; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function getHandles() { return $this->handles; } public function setConpherenceTransaction(ConpherenceTransaction $tx) { $this->conpherenceTransaction = $tx; return $this; } private function getConpherenceTransaction() { return $this->conpherenceTransaction; } public function render() { $user = $this->getUser(); $transaction = $this->getConpherenceTransaction(); switch ($transaction->getTransactionType()) { case ConpherenceTransactionType::TYPE_DATE_MARKER: return phutil_tag( 'div', array( - 'class' => 'date-marker' + 'class' => 'date-marker', ), array( phutil_tag( 'span', array( 'class' => 'date', ), phabricator_format_local_time( $transaction->getDateCreated(), $user, - 'M jS, Y')))); + 'M jS, Y')), + )); break; } $handles = $this->getHandles(); $transaction->setHandles($handles); $author = $handles[$transaction->getAuthorPHID()]; $transaction_view = id(new PhabricatorTransactionView()) ->setUser($user) ->setEpoch($transaction->getDateCreated()) ->setContentSource($transaction->getContentSource()); $content = null; $content_class = null; $content = null; switch ($transaction->getTransactionType()) { case ConpherenceTransactionType::TYPE_TITLE: $content = $transaction->getTitle(); $transaction_view->addClass('conpherence-edited'); break; case ConpherenceTransactionType::TYPE_FILES: $content = $transaction->getTitle(); break; case ConpherenceTransactionType::TYPE_PARTICIPANTS: $content = $transaction->getTitle(); $transaction_view->addClass('conpherence-edited'); break; case PhabricatorTransactions::TYPE_COMMENT: $comment = $transaction->getComment(); $content = $this->markupEngine->getOutput( $comment, PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); $content_class = 'conpherence-message phabricator-remarkup'; $transaction_view ->setImageURI($author->getImageURI()) ->setActions(array($author->renderLink())); break; } $transaction_view->appendChild( phutil_tag( 'div', array( - 'class' => $content_class + 'class' => $content_class, ), $content)); return $transaction_view->render(); } } diff --git a/src/applications/countdown/application/PhabricatorCountdownApplication.php b/src/applications/countdown/application/PhabricatorCountdownApplication.php index 9f54829a8c..e590c4e4bc 100644 --- a/src/applications/countdown/application/PhabricatorCountdownApplication.php +++ b/src/applications/countdown/application/PhabricatorCountdownApplication.php @@ -1,59 +1,59 @@ <?php final class PhabricatorCountdownApplication extends PhabricatorApplication { public function getBaseURI() { return '/countdown/'; } public function getIconName() { return 'countdown'; } public function getName() { return pht('Countdown'); } public function getShortDescription() { return pht('Countdown to Events'); } public function getTitleGlyph() { return "\xE2\x9A\xB2"; } public function getFlavorText() { return pht('Utilize the full capabilities of your ALU.'); } public function getApplicationGroup() { return self::GROUP_UTILITIES; } public function getRemarkupRules() { return array( new PhabricatorCountdownRemarkupRule(), ); } public function getRoutes() { return array( '/countdown/' => array( '(?:query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorCountdownListController', '(?P<id>[1-9]\d*)/' => 'PhabricatorCountdownViewController', 'edit/(?:(?P<id>[1-9]\d*)/)?' => 'PhabricatorCountdownEditController', - 'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorCountdownDeleteController' + 'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorCountdownDeleteController', ), ); } public function getCustomCapabilities() { return array( PhabricatorCountdownDefaultViewCapability::CAPABILITY => array( 'caption' => pht('Default view policy for new countdowns.'), ), ); } } diff --git a/src/applications/dashboard/controller/PhabricatorDashboardInstallController.php b/src/applications/dashboard/controller/PhabricatorDashboardInstallController.php index e9632d8949..fde192b7b4 100644 --- a/src/applications/dashboard/controller/PhabricatorDashboardInstallController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardInstallController.php @@ -1,156 +1,157 @@ <?php final class PhabricatorDashboardInstallController extends PhabricatorDashboardController { private $id; public function willProcessRequest(array $data) { $this->id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $dashboard = id(new PhabricatorDashboardQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$dashboard) { return new Aphront404Response(); } $dashboard_phid = $dashboard->getPHID(); $object_phid = $request->getStr('objectPHID', $viewer->getPHID()); switch ($object_phid) { case PhabricatorHomeApplication::DASHBOARD_DEFAULT: if (!$viewer->getIsAdmin()) { return new Aphront404Response(); } break; default: $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withPHIDs(array($object_phid)) ->executeOne(); if (!$object) { return new Aphront404Response(); } break; } $installer_phid = $viewer->getPHID(); $application_class = $request->getStr( 'applicationClass', 'PhabricatorHomeApplication'); $handles = $this->loadHandles(array( $object_phid, - $installer_phid)); + $installer_phid, + )); if ($request->isFormPost()) { $dashboard_install = id(new PhabricatorDashboardInstall()) ->loadOneWhere( 'objectPHID = %s AND applicationClass = %s', $object_phid, $application_class); if (!$dashboard_install) { $dashboard_install = id(new PhabricatorDashboardInstall()) ->setObjectPHID($object_phid) ->setApplicationClass($application_class); } $dashboard_install ->setInstallerPHID($installer_phid) ->setDashboardPHID($dashboard_phid) ->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getRedirectURI($application_class, $object_phid)); } $dialog = $this->newDialog() ->setTitle(pht('Install Dashboard')) ->addHiddenInput('objectPHID', $object_phid) ->addCancelButton($this->getCancelURI($application_class, $object_phid)) ->addSubmitButton(pht('Install Dashboard')); switch ($application_class) { case 'PhabricatorHomeApplication': if ($viewer->getPHID() == $object_phid) { if ($viewer->getIsAdmin()) { $dialog->setWidth(AphrontDialogView::WIDTH_FORM); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions( pht('Choose where to install this dashboard.')) ->appendChild( id(new AphrontFormRadioButtonControl()) ->setName('objectPHID') ->setValue(PhabricatorHomeApplication::DASHBOARD_DEFAULT) ->addButton( PhabricatorHomeApplication::DASHBOARD_DEFAULT, pht('Default Dashboard for All Users'), pht( 'Install this dashboard as the global default dashboard '. 'for all users. Users can install a personal dashboard '. 'to replace it. All users who have not configured '. 'a personal dashboard will be affected by this change.')) ->addButton( $viewer->getPHID(), pht('Personal Home Page Dashboard'), pht( 'Install this dashboard as your personal home page '. 'dashboard. Only you will be affected by this change.'))); $dialog->appendChild($form->buildLayoutView()); } else { $dialog->appendParagraph( pht('Install this dashboard on your home page?')); } } else { $dialog->appendParagraph( pht( 'Install this dashboard as the home page dashboard for %s?', phutil_tag( 'strong', array(), $this->getHandle($object_phid)->getName()))); } break; default: throw new Exception( pht( 'Unknown dashboard application class "%s"!', $application_class)); } return $dialog; } private function getCancelURI($application_class, $object_phid) { $uri = null; switch ($application_class) { case 'PhabricatorHomeApplication': $uri = '/dashboard/view/'.$this->id.'/'; break; } return $uri; } private function getRedirectURI($application_class, $object_phid) { $uri = null; switch ($application_class) { case 'PhabricatorHomeApplication': $uri = '/'; break; } return $uri; } } diff --git a/src/applications/dashboard/customfield/PhabricatorDashboardPanelSearchQueryCustomField.php b/src/applications/dashboard/customfield/PhabricatorDashboardPanelSearchQueryCustomField.php index 7e31054269..9b0ed47bf9 100644 --- a/src/applications/dashboard/customfield/PhabricatorDashboardPanelSearchQueryCustomField.php +++ b/src/applications/dashboard/customfield/PhabricatorDashboardPanelSearchQueryCustomField.php @@ -1,68 +1,68 @@ <?php final class PhabricatorDashboardPanelSearchQueryCustomField extends PhabricatorStandardCustomField { public function getFieldType() { return 'search.query'; } public function shouldAppearInApplicationSearch() { return false; } public function renderEditControl(array $handles) { $engines = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorApplicationSearchEngine') ->loadObjects(); $value = $this->getFieldValue(); $queries = array(); $seen = false; foreach ($engines as $engine_class => $engine) { $engine->setViewer($this->getViewer()); $engine_queries = $engine->loadEnabledNamedQueries(); $query_map = mpull($engine_queries, 'getQueryName', 'getQueryKey'); asort($query_map); foreach ($query_map as $key => $name) { $queries[$engine_class][] = array('key' => $key, 'name' => $name); if ($key == $value) { $seen = true; } } } if (strlen($value) && !$seen) { $name = pht('Custom Query ("%s")', $value); } else { $name = pht('(None)'); } $options = array($value => $name); $app_control_key = $this->getFieldConfigValue('control.application'); Javelin::initBehavior( 'dashboard-query-panel-select', array( 'applicationID' => $this->getFieldControlID($app_control_key), 'queryID' => $this->getFieldControlID(), 'options' => $queries, 'value' => array( 'key' => strlen($value) ? $value : null, - 'name' => $name - ) + 'name' => $name, + ), )); return id(new AphrontFormSelectControl()) ->setID($this->getFieldControlID()) ->setLabel($this->getFieldName()) ->setCaption($this->getCaption()) ->setName($this->getFieldKey()) ->setValue($this->getFieldValue()) ->setOptions($options); } } diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php index b94887ccff..3834ca9e23 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -1,295 +1,298 @@ <?php final class PhabricatorDashboardPanelRenderingEngine extends Phobject { const HEADER_MODE_NORMAL = 'normal'; const HEADER_MODE_NONE = 'none'; const HEADER_MODE_EDIT = 'edit'; private $panel; private $viewer; private $enableAsyncRendering; private $parentPanelPHIDs; private $headerMode = self::HEADER_MODE_NORMAL; private $dashboardID; public function setDashboardID($id) { $this->dashboardID = $id; return $this; } public function getDashboardID() { return $this->dashboardID; } public function setHeaderMode($header_mode) { $this->headerMode = $header_mode; return $this; } public function getHeaderMode() { return $this->headerMode; } /** * Allow the engine to render the panel via Ajax. */ public function setEnableAsyncRendering($enable) { $this->enableAsyncRendering = $enable; return $this; } public function setParentPanelPHIDs(array $parents) { $this->parentPanelPHIDs = $parents; return $this; } public function getParentPanelPHIDs() { return $this->parentPanelPHIDs; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setPanel(PhabricatorDashboardPanel $panel) { $this->panel = $panel; return $this; } public function getPanel() { return $this->panel; } public function renderPanel() { $panel = $this->getPanel(); $viewer = $this->getViewer(); if (!$panel) { return $this->renderErrorPanel( pht('Missing Panel'), pht('This panel does not exist.')); } $panel_type = $panel->getImplementation(); if (!$panel_type) { return $this->renderErrorPanel( $panel->getName(), pht( 'This panel has type "%s", but that panel type is not known to '. 'Phabricator.', $panel->getPanelType())); } try { $this->detectRenderingCycle($panel); if ($this->enableAsyncRendering) { if ($panel_type->shouldRenderAsync()) { return $this->renderAsyncPanel(); } } return $this->renderNormalPanel($viewer, $panel, $this); } catch (Exception $ex) { return $this->renderErrorPanel( $panel->getName(), pht( '%s: %s', phutil_tag('strong', array(), get_class($ex)), $ex->getMessage())); } } private function renderNormalPanel() { $panel = $this->getPanel(); $panel_type = $panel->getImplementation(); $content = $panel_type->renderPanelContent( $this->getViewer(), $panel, $this); $header = $this->renderPanelHeader(); return $this->renderPanelDiv( $content, $header); } private function renderAsyncPanel() { $panel = $this->getPanel(); $panel_id = celerity_generate_unique_node_id(); $dashboard_id = $this->getDashboardID(); Javelin::initBehavior( 'dashboard-async-panel', array( 'panelID' => $panel_id, 'parentPanelPHIDs' => $this->getParentPanelPHIDs(), 'headerMode' => $this->getHeaderMode(), 'dashboardID' => $dashboard_id, 'uri' => '/dashboard/panel/render/'.$panel->getID().'/', )); $header = $this->renderPanelHeader(); $content = id(new PHUIPropertyListView()) ->addTextContent(pht('Loading...')); return $this->renderPanelDiv( $content, $header, $panel_id); } private function renderErrorPanel($title, $body) { switch ($this->getHeaderMode()) { case self::HEADER_MODE_NONE: $header = null; break; case self::HEADER_MODE_EDIT: $header = id(new PHUIActionHeaderView()) ->setHeaderTitle($title) ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE); $header = $this->addPanelHeaderActions($header); break; case self::HEADER_MODE_NORMAL: default: $header = id(new PHUIActionHeaderView()) ->setHeaderTitle($title) ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE); break; } $icon = id(new PHUIIconView()) ->setIconFont('fa-warning red msr'); $content = id(new PHUIBoxView()) ->addClass('dashboard-box') ->appendChild($icon) ->appendChild($body); return $this->renderPanelDiv( $content, $header); } private function renderPanelDiv( $content, $header = null, $id = null) { require_celerity_resource('phabricator-dashboard-css'); $panel = $this->getPanel(); if (!$id) { $id = celerity_generate_unique_node_id(); } return javelin_tag( 'div', array( 'id' => $id, 'sigil' => 'dashboard-panel', 'meta' => array( - 'objectPHID' => $panel->getPHID()), - 'class' => 'dashboard-panel'), + 'objectPHID' => $panel->getPHID(), + ), + 'class' => 'dashboard-panel', + ), array( $header, - $content)); + $content, + )); } private function renderPanelHeader() { $panel = $this->getPanel(); switch ($this->getHeaderMode()) { case self::HEADER_MODE_NONE: $header = null; break; case self::HEADER_MODE_EDIT: $header = id(new PHUIActionHeaderView()) ->setHeaderTitle($panel->getName()) ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE); $header = $this->addPanelHeaderActions($header); break; case self::HEADER_MODE_NORMAL: default: $header = id(new PHUIActionHeaderView()) ->setHeaderTitle($panel->getName()) ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE); break; } return $header; } private function addPanelHeaderActions( PHUIActionHeaderView $header) { $panel = $this->getPanel(); $dashboard_id = $this->getDashboardID(); $edit_uri = id(new PhutilURI( '/dashboard/panel/edit/'.$panel->getID().'/')); if ($dashboard_id) { $edit_uri->setQueryParam('dashboardID', $dashboard_id); } $action_edit = id(new PHUIIconView()) ->setIconFont('fa-pencil') ->setWorkflow(true) ->setHref((string) $edit_uri); $header->addAction($action_edit); if ($dashboard_id) { $uri = id(new PhutilURI( '/dashboard/removepanel/'.$dashboard_id.'/')) ->setQueryParam('panelPHID', $panel->getPHID()); $action_remove = id(new PHUIIconView()) ->setIconFont('fa-trash-o') ->setHref((string) $uri) ->setWorkflow(true); $header->addAction($action_remove); } return $header; } /** * Detect graph cycles in panels, and deeply nested panels. * * This method throws if the current rendering stack is too deep or contains * a cycle. This can happen if you embed layout panels inside each other, * build a big stack of panels, or embed a panel in remarkup inside another * panel. Generally, all of this stuff is ridiculous and we just want to * shut it down. * * @param PhabricatorDashboardPanel Panel being rendered. * @return void */ private function detectRenderingCycle(PhabricatorDashboardPanel $panel) { if ($this->parentPanelPHIDs === null) { throw new Exception( pht( 'You must call setParentPanelPHIDs() before rendering panels.')); } $max_depth = 4; if (count($this->parentPanelPHIDs) >= $max_depth) { throw new Exception( pht( 'To render more than %s levels of panels nested inside other '. 'panels, purchase a subscription to Phabricator Gold.', new PhutilNumber($max_depth))); } if (in_array($panel->getPHID(), $this->parentPanelPHIDs)) { throw new Exception( pht( 'You awake in a twisting maze of mirrors, all alike. '. 'You are likely to be eaten by a graph cycle. '. 'Should you escape alive, you resolve to be more careful about '. 'putting dashboard panels inside themselves.')); } } } diff --git a/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php b/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php index a8cf788d8b..af4a947c8e 100644 --- a/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php +++ b/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php @@ -1,163 +1,164 @@ <?php final class PhabricatorDashboardLayoutConfig { const MODE_FULL = 'layout-mode-full'; const MODE_HALF_AND_HALF = 'layout-mode-half-and-half'; const MODE_THIRD_AND_THIRDS = 'layout-mode-third-and-thirds'; const MODE_THIRDS_AND_THIRD = 'layout-mode-thirds-and-third'; private $layoutMode = self::MODE_FULL; private $panelLocations = array(); public function setLayoutMode($mode) { $this->layoutMode = $mode; return $this; } public function getLayoutMode() { return $this->layoutMode; } public function setPanelLocation($which_column, $panel_phid) { $this->panelLocations[$which_column][] = $panel_phid; return $this; } public function setPanelLocations(array $locations) { $this->panelLocations = $locations; return $this; } public function getPanelLocations() { return $this->panelLocations; } public function replacePanel($old_phid, $new_phid) { $locations = $this->getPanelLocations(); foreach ($locations as $column => $panel_phids) { foreach ($panel_phids as $key => $panel_phid) { if ($panel_phid == $old_phid) { $locations[$column][$key] = $new_phid; } } } return $this->setPanelLocations($locations); } public function removePanel($panel_phid) { $panel_location_grid = $this->getPanelLocations(); foreach ($panel_location_grid as $column => $panel_columns) { $found_old_column = array_search($panel_phid, $panel_columns); if ($found_old_column !== false) { $new_panel_columns = $panel_columns; array_splice( $new_panel_columns, $found_old_column, 1, array()); $panel_location_grid[$column] = $new_panel_columns; break; } } $this->setPanelLocations($panel_location_grid); } public function getDefaultPanelLocations() { switch ($this->getLayoutMode()) { case self::MODE_HALF_AND_HALF: case self::MODE_THIRD_AND_THIRDS: case self::MODE_THIRDS_AND_THIRD: $locations = array(array(), array()); break; case self::MODE_FULL: default: $locations = array(array()); break; } return $locations; } public function getColumnClass($column_index, $grippable = false) { switch ($this->getLayoutMode()) { case self::MODE_HALF_AND_HALF: $class = 'half'; break; case self::MODE_THIRD_AND_THIRDS: if ($column_index) { $class = 'thirds'; } else { $class = 'third'; } break; case self::MODE_THIRDS_AND_THIRD: if ($column_index) { $class = 'third'; } else { $class = 'thirds'; } break; case self::MODE_FULL: default: $class = null; break; } if ($grippable) { $class .= ' grippable'; } return $class; } public function isMultiColumnLayout() { return $this->getLayoutMode() != self::MODE_FULL; } public function getColumnSelectOptions() { $options = array(); switch ($this->getLayoutMode()) { case self::MODE_HALF_AND_HALF: case self::MODE_THIRD_AND_THIRDS: case self::MODE_THIRDS_AND_THIRD: return array( 0 => pht('Left'), - 1 => pht('Right')); + 1 => pht('Right'), + ); break; case self::MODE_FULL: throw new Exception('There is only one column in mode full.'); break; default: throw new Exception('Unknown layout mode!'); break; } return $options; } public static function getLayoutModeSelectOptions() { return array( self::MODE_FULL => pht('One full-width column'), self::MODE_HALF_AND_HALF => pht('Two columns, 1/2 and 1/2'), self::MODE_THIRD_AND_THIRDS => pht('Two columns, 1/3 and 2/3'), self::MODE_THIRDS_AND_THIRD => pht('Two columns, 2/3 and 1/3'), ); } public static function newFromDictionary(array $dict) { $layout_config = id(new PhabricatorDashboardLayoutConfig()) ->setLayoutMode(idx($dict, 'layoutMode', self::MODE_FULL)); $layout_config->setPanelLocations(idx( $dict, 'panelLocations', $layout_config->getDefaultPanelLocations())); return $layout_config; } public function toDictionary() { return array( 'layoutMode' => $this->getLayoutMode(), - 'panelLocations' => $this->getPanelLocations() + 'panelLocations' => $this->getPanelLocations(), ); } } diff --git a/src/applications/dashboard/query/PhabricatorDashboardSearchEngine.php b/src/applications/dashboard/query/PhabricatorDashboardSearchEngine.php index 1fdc7b9016..7263e93d00 100644 --- a/src/applications/dashboard/query/PhabricatorDashboardSearchEngine.php +++ b/src/applications/dashboard/query/PhabricatorDashboardSearchEngine.php @@ -1,106 +1,110 @@ <?php final class PhabricatorDashboardSearchEngine extends PhabricatorApplicationSearchEngine { public function getResultTypeDescription() { return pht('Dashboards'); } public function getApplicationClassName() { return 'PhabricatorDashboardApplication'; } public function buildSavedQueryFromRequest(AphrontRequest $request) { return new PhabricatorSavedQuery(); } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { return new PhabricatorDashboardQuery(); } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved_query) { return; } protected function getURI($path) { return '/dashboard/'.$path; } public function getBuiltinQueryNames() { return array( 'all' => pht('All Dashboards'), ); } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $dashboards, PhabricatorSavedQuery $query, array $handles) { $dashboards = mpull($dashboards, null, 'getPHID'); $viewer = $this->requireViewer(); if ($dashboards) { $installs = id(new PhabricatorDashboardInstall()) ->loadAllWhere( 'objectPHID IN (%Ls) AND dashboardPHID IN (%Ls)', - array(PhabricatorHomeApplication::DASHBOARD_DEFAULT, - $viewer->getPHID()), + array( + PhabricatorHomeApplication::DASHBOARD_DEFAULT, + $viewer->getPHID(), + ), array_keys($dashboards)); $installs = mpull($installs, null, 'getDashboardPHID'); } else { $installs = array(); } $list = new PHUIObjectItemListView(); $list->setUser($viewer); $list->initBehavior('phabricator-tooltips', array()); $list->requireResource('aphront-tooltip-css'); foreach ($dashboards as $dashboard_phid => $dashboard) { $id = $dashboard->getID(); $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Dashboard %d', $id)) ->setHeader($dashboard->getName()) ->setHref($this->getApplicationURI("view/{$id}/")) ->setObject($dashboard); if (isset($installs[$dashboard_phid])) { $install = $installs[$dashboard_phid]; if ($install->getObjectPHID() == $viewer->getPHID()) { $attrs = array( 'tip' => pht( - 'This dashboard is installed to your personal homepage.')); + 'This dashboard is installed to your personal homepage.'), + ); $item->addIcon('fa-user', pht('Installed'), $attrs); } else { $attrs = array( 'tip' => pht( - 'This dashboard is the default homepage for all users.')); + 'This dashboard is the default homepage for all users.'), + ); $item->addIcon('fa-globe', pht('Installed'), $attrs); } } $list->addItem($item); } return $list; } } diff --git a/src/applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php index 67a6f01eb3..67e4c68a60 100644 --- a/src/applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialCreateInlineConduitAPIMethod.php @@ -1,109 +1,109 @@ <?php final class DifferentialCreateInlineConduitAPIMethod extends DifferentialConduitAPIMethod { public function getAPIMethodName() { return 'differential.createinline'; } public function getMethodDescription() { return 'Add an inline comment to a Differential revision.'; } public function defineParamTypes() { return array( 'revisionID' => 'optional revisionid', 'diffID' => 'optional diffid', 'filePath' => 'required string', 'isNewFile' => 'required bool', 'lineNumber' => 'required int', 'lineLength' => 'optional int', 'content' => 'required string', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR-BAD-REVISION' => 'Bad revision ID.', 'ERR-BAD-DIFF' => 'Bad diff ID, or diff does not belong to revision.', 'ERR-NEED-DIFF' => 'Neither revision ID nor diff ID was provided.', 'ERR-NEED-FILE' => 'A file path was not provided.', - 'ERR-BAD-FILE' => "Requested file doesn't exist in this revision." + 'ERR-BAD-FILE' => "Requested file doesn't exist in this revision.", ); } protected function execute(ConduitAPIRequest $request) { $rid = $request->getValue('revisionID'); $did = $request->getValue('diffID'); if ($rid) { // Given both a revision and a diff, check that they match. // Given only a revision, find the active diff. $revision = id(new DifferentialRevisionQuery()) ->setViewer($request->getUser()) ->withIDs(array($rid)) ->executeOne(); if (!$revision) { throw new ConduitException('ERR-BAD-REVISION'); } if (!$did) { // did not! $diff = $revision->loadActiveDiff(); $did = $diff->getID(); } else { // did too! $diff = id(new DifferentialDiff())->load($did); if (!$diff || $diff->getRevisionID() != $rid) { throw new ConduitException('ERR-BAD-DIFF'); } } } else if ($did) { // Given only a diff, find the parent revision. $diff = id(new DifferentialDiff())->load($did); if (!$diff) { throw new ConduitException('ERR-BAD-DIFF'); } $rid = $diff->getRevisionID(); } else { // Given neither, bail. throw new ConduitException('ERR-NEED-DIFF'); } $file = $request->getValue('filePath'); if (!$file) { throw new ConduitException('ERR-NEED-FILE'); } $changes = id(new DifferentialChangeset())->loadAllWhere( 'diffID = %d', $did); $cid = null; foreach ($changes as $id => $change) { if ($file == $change->getFilename()) { $cid = $id; } } if ($cid == null) { throw new ConduitException('ERR-BAD-FILE'); } $inline = id(new DifferentialInlineComment()) ->setRevisionID($rid) ->setChangesetID($cid) ->setAuthorPHID($request->getUser()->getPHID()) ->setContent($request->getValue('content')) ->setIsNewFile($request->getValue('isNewFile')) ->setLineNumber($request->getValue('lineNumber')) ->setLineLength($request->getValue('lineLength', 0)) ->save(); // Load everything again, just to be safe. $changeset = id(new DifferentialChangeset()) ->load($inline->getChangesetID()); return $this->buildInlineInfoDictionary($inline, $changeset); } } diff --git a/src/applications/differential/conduit/DifferentialFinishPostponedLintersConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialFinishPostponedLintersConduitAPIMethod.php index c834958dc9..3e183d6bd5 100644 --- a/src/applications/differential/conduit/DifferentialFinishPostponedLintersConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialFinishPostponedLintersConduitAPIMethod.php @@ -1,120 +1,122 @@ <?php final class DifferentialFinishPostponedLintersConduitAPIMethod extends DifferentialConduitAPIMethod { public function getAPIMethodName() { return 'differential.finishpostponedlinters'; } public function getMethodDescription() { return 'Update diff with new lint messages and mark postponed '. 'linters as finished.'; } public function defineParamTypes() { return array( 'diffID' => 'required diffID', 'linters' => 'required dict', ); } public function defineReturnType() { return 'void'; } public function defineErrorTypes() { return array( 'ERR-BAD-DIFF' => 'Bad diff ID.', 'ERR-BAD-LINTER' => 'No postponed linter by the given name', 'ERR-NO-LINT' => 'No postponed lint field available in diff', ); } protected function execute(ConduitAPIRequest $request) { $diff_id = $request->getValue('diffID'); $linter_map = $request->getValue('linters'); $diff = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withIDs(array($diff_id)) ->executeOne(); if (!$diff) { throw new ConduitException('ERR-BAD-DIFF'); } // Extract the finished linters and messages from the linter map. $finished_linters = array_keys($linter_map); $new_messages = array(); foreach ($linter_map as $linter => $messages) { $new_messages = array_merge($new_messages, $messages); } // Load the postponed linters attached to this diff. $postponed_linters_property = id( new DifferentialDiffProperty())->loadOneWhere( 'diffID = %d AND name = %s', $diff_id, 'arc:lint-postponed'); if ($postponed_linters_property) { $postponed_linters = $postponed_linters_property->getData(); } else { $postponed_linters = array(); } foreach ($finished_linters as $linter) { if (!in_array($linter, $postponed_linters)) { throw new ConduitException('ERR-BAD-LINTER'); } } foreach ($postponed_linters as $idx => $linter) { if (in_array($linter, $finished_linters)) { unset($postponed_linters[$idx]); } } // Load the lint messages currenty attached to the diff. If this // diff property doesn't exist, create it. $messages_property = id(new DifferentialDiffProperty())->loadOneWhere( 'diffID = %d AND name = %s', $diff_id, 'arc:lint'); if ($messages_property) { $messages = $messages_property->getData(); } else { $messages = array(); } // Add new lint messages, removing duplicates. foreach ($new_messages as $new_message) { if (!in_array($new_message, $messages)) { $messages[] = $new_message; } } // Use setdiffproperty to update the postponed linters and messages, // as these will also update the lint status correctly. $call = new ConduitCall( 'differential.setdiffproperty', array( 'diff_id' => $diff_id, 'name' => 'arc:lint', - 'data' => json_encode($messages))); + 'data' => json_encode($messages), + )); $call->setForceLocal(true); $call->setUser($request->getUser()); $call->execute(); $call = new ConduitCall( 'differential.setdiffproperty', array( 'diff_id' => $diff_id, 'name' => 'arc:lint-postponed', - 'data' => json_encode($postponed_linters))); + 'data' => json_encode($postponed_linters), + )); $call->setForceLocal(true); $call->setUser($request->getUser()); $call->execute(); } } diff --git a/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php index 31ec13a2a4..df0dfa5c9d 100644 --- a/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialGetRevisionConduitAPIMethod.php @@ -1,103 +1,103 @@ <?php final class DifferentialGetRevisionConduitAPIMethod extends DifferentialConduitAPIMethod { public function getAPIMethodName() { return 'differential.getrevision'; } public function getMethodStatus() { return self::METHOD_STATUS_DEPRECATED; } public function getMethodStatusDescription() { return "Replaced by 'differential.query'."; } public function getMethodDescription() { return 'Load the content of a revision from Differential.'; } public function defineParamTypes() { return array( 'revision_id' => 'required id', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR_BAD_REVISION' => 'No such revision exists.', ); } protected function execute(ConduitAPIRequest $request) { $diff = null; $revision_id = $request->getValue('revision_id'); $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer($request->getUser()) ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); if (!$revision) { throw new ConduitException('ERR_BAD_REVISION'); } $reviewer_phids = array_values($revision->getReviewers()); $diffs = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withRevisionIDs(array($revision_id)) ->needChangesets(true) ->needArcanistProjects(true) ->execute(); $diff_dicts = mpull($diffs, 'getDiffDict'); $commit_dicts = array(); $commit_phids = $revision->loadCommitPHIDs(); $handles = id(new PhabricatorHandleQuery()) ->setViewer($request->getUser()) ->withPHIDs($commit_phids) ->execute(); foreach ($commit_phids as $commit_phid) { $commit_dicts[] = array( 'fullname' => $handles[$commit_phid]->getFullName(), 'dateCommitted' => $handles[$commit_phid]->getTimestamp(), ); } $field_data = $this->loadCustomFieldsForRevisions( $request->getUser(), array($revision)); $dict = array( 'id' => $revision->getID(), 'phid' => $revision->getPHID(), 'authorPHID' => $revision->getAuthorPHID(), 'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()), 'title' => $revision->getTitle(), 'status' => $revision->getStatus(), 'statusName' => ArcanistDifferentialRevisionStatus::getNameForRevisionStatus( $revision->getStatus()), 'summary' => $revision->getSummary(), 'testPlan' => $revision->getTestPlan(), 'lineCount' => $revision->getLineCount(), 'reviewerPHIDs' => $reviewer_phids, 'diffs' => $diff_dicts, 'commits' => $commit_dicts, - 'auxiliary' => idx($field_data, $revision->getPHID(), array()) + 'auxiliary' => idx($field_data, $revision->getPHID(), array()), ); return $dict; } } diff --git a/src/applications/differential/conduit/DifferentialUpdateUnitResultsConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialUpdateUnitResultsConduitAPIMethod.php index cfa2497f77..90dc59761a 100644 --- a/src/applications/differential/conduit/DifferentialUpdateUnitResultsConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialUpdateUnitResultsConduitAPIMethod.php @@ -1,153 +1,155 @@ <?php final class DifferentialUpdateUnitResultsConduitAPIMethod extends DifferentialConduitAPIMethod { public function getAPIMethodName() { return 'differential.updateunitresults'; } public function getMethodDescription() { return 'Update arc unit results for a postponed test.'; } public function defineParamTypes() { return array( 'diff_id' => 'required diff_id', 'file' => 'required string', 'name' => 'required string', 'link' => 'optional string', 'result' => 'required string', 'message' => 'required string', 'coverage' => 'optional map<string, string>', ); } public function defineReturnType() { return 'void'; } public function defineErrorTypes() { return array( 'ERR_BAD_DIFF' => 'Bad diff ID.', 'ERR_NO_RESULTS' => 'Could not find the postponed test', ); } protected function execute(ConduitAPIRequest $request) { $diff_id = $request->getValue('diff_id'); if (!$diff_id) { throw new ConduitException('ERR_BAD_DIFF'); } $file = $request->getValue('file'); $name = $request->getValue('name'); $link = $request->getValue('link'); $message = $request->getValue('message'); $result = $request->getValue('result'); $coverage = $request->getValue('coverage', array()); $diff_property = id(new DifferentialDiffProperty())->loadOneWhere( 'diffID = %d AND name = %s', $diff_id, 'arc:unit'); if (!$diff_property) { throw new ConduitException('ERR_NO_RESULTS'); } $diff = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withIDs(array($diff_id)) ->executeOne(); $unit_results = $diff_property->getData(); $postponed_count = 0; $unit_status = null; // If the test result already exists, then update it with // the new info. foreach ($unit_results as &$unit_result) { if ($unit_result['name'] === $name || $unit_result['name'] === $file || $unit_result['name'] === $diff->getSourcePath().$file) { $unit_result['name'] = $name; $unit_result['link'] = $link; $unit_result['file'] = $file; $unit_result['result'] = $result; $unit_result['userdata'] = $message; $unit_result['coverage'] = $coverage; $unit_status = $result; break; } } unset($unit_result); // If the test result doesn't exist, just add it. if (!$unit_status) { $unit_result = array(); $unit_result['file'] = $file; $unit_result['name'] = $name; $unit_result['link'] = $link; $unit_result['result'] = $result; $unit_result['userdata'] = $message; $unit_result['coverage'] = $coverage; $unit_status = $result; $unit_results[] = $unit_result; } unset($unit_result); $diff_property->setData($unit_results); $diff_property->save(); // Map external unit test status to internal overall diff status $status_codes = array( DifferentialUnitTestResult::RESULT_PASS => DifferentialUnitStatus::UNIT_OKAY, DifferentialUnitTestResult::RESULT_UNSOUND => DifferentialUnitStatus::UNIT_WARN, DifferentialUnitTestResult::RESULT_FAIL => DifferentialUnitStatus::UNIT_FAIL, DifferentialUnitTestResult::RESULT_BROKEN => DifferentialUnitStatus::UNIT_FAIL, DifferentialUnitTestResult::RESULT_SKIP => DifferentialUnitStatus::UNIT_OKAY, DifferentialUnitTestResult::RESULT_POSTPONED => - DifferentialUnitStatus::UNIT_POSTPONED); + DifferentialUnitStatus::UNIT_POSTPONED, + ); // These are the relative priorities for the unit test results $status_codes_priority = array( DifferentialUnitStatus::UNIT_OKAY => 1, DifferentialUnitStatus::UNIT_WARN => 2, DifferentialUnitStatus::UNIT_POSTPONED => 3, - DifferentialUnitStatus::UNIT_FAIL => 4); + DifferentialUnitStatus::UNIT_FAIL => 4, + ); // Walk the now-current list of status codes to find the overall diff // status $final_diff_status = DifferentialUnitStatus::UNIT_NONE; foreach ($unit_results as $unit_result) { // Convert the text result into a diff unit status value $status_code = idx($status_codes, $unit_result['result'], DifferentialUnitStatus::UNIT_NONE); // Convert the unit status into a relative value $diff_status_priority = idx($status_codes_priority, $status_code, 0); // If the relative value of this result is "more bad" than previous // results, use it as the new final diff status if ($diff_status_priority > idx($status_codes_priority, $final_diff_status, 0)) { $final_diff_status = $status_code; } } // Update our unit test result status with the final value $diff->setUnitStatus($final_diff_status); $diff->save(); } } diff --git a/src/applications/differential/controller/DifferentialRevisionLandController.php b/src/applications/differential/controller/DifferentialRevisionLandController.php index 0e1f64aeb4..36ebf5fb90 100644 --- a/src/applications/differential/controller/DifferentialRevisionLandController.php +++ b/src/applications/differential/controller/DifferentialRevisionLandController.php @@ -1,163 +1,163 @@ <?php final class DifferentialRevisionLandController extends DifferentialController { private $revisionID; private $strategyClass; private $pushStrategy; public function willProcessRequest(array $data) { $this->revisionID = $data['id']; $this->strategyClass = $data['strategy']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $revision_id = $this->revisionID; $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer($viewer) ->executeOne(); if (!$revision) { return new Aphront404Response(); } if (is_subclass_of($this->strategyClass, 'DifferentialLandingStrategy')) { $this->pushStrategy = newv($this->strategyClass, array()); } else { throw new Exception( "Strategy type must be a valid class name and must subclass ". "DifferentialLandingStrategy. ". "'{$this->strategyClass}' is not a subclass of ". "DifferentialLandingStrategy."); } if ($request->isDialogFormPost()) { $response = null; $text = ''; try { $response = $this->attemptLand($revision, $request); $title = pht('Success!'); $text = pht('Revision was successfully landed.'); } catch (Exception $ex) { $title = pht('Failed to land revision'); if ($ex instanceof PhutilProxyException) { $text = hsprintf( '%s:<br><pre>%s</pre>', $ex->getMessage(), $ex->getPreviousException()->getMessage()); } else { $text = phutil_tag('pre', array(), $ex->getMessage()); } $text = id(new AphrontErrorView()) ->appendChild($text); } if ($response instanceof AphrontDialogView) { $dialog = $response; } else { $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle($title) ->appendChild(phutil_tag('p', array(), $text)) ->addCancelButton('/D'.$revision_id, pht('Done')); } return id(new AphrontDialogResponse())->setDialog($dialog); } $is_disabled = $this->pushStrategy->isActionDisabled( $viewer, $revision, $revision->getRepository()); if ($is_disabled) { if (is_string($is_disabled)) { $explain = $is_disabled; } else { $explain = pht('This action is not currently enabled.'); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht("Can't land revision")) ->appendChild($explain) ->addCancelButton('/D'.$revision_id); return id(new AphrontDialogResponse())->setDialog($dialog); } $prompt = hsprintf('%s<br><br>%s', pht( 'This will squash and rebase revision %s, and push it to '. 'the default / master branch.', $revision_id), pht('It is an experimental feature and may not work.')); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Land Revision %s?', $revision_id)) ->appendChild($prompt) ->setSubmitURI($request->getRequestURI()) ->addSubmitButton(pht('Land it!')) ->addCancelButton('/D'.$revision_id); return id(new AphrontDialogResponse())->setDialog($dialog); } private function attemptLand($revision, $request) { $status = $revision->getStatus(); if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) { throw new Exception('Only Accepted revisions can be landed.'); } $repository = $revision->getRepository(); if ($repository === null) { throw new Exception('revision is not attached to a repository.'); } $can_push = PhabricatorPolicyFilter::hasCapability( $request->getUser(), $repository, DiffusionPushCapability::CAPABILITY); if (!$can_push) { throw new Exception( pht('You do not have permission to push to this repository.')); } $lock = $this->lockRepository($repository); try { $response = $this->pushStrategy->processLandRequest( $request, $revision, $repository); } catch (Exception $e) { $lock->unlock(); throw $e; } $lock->unlock(); $looksoon = new ConduitCall( 'diffusion.looksoon', array( - 'callsigns' => array($repository->getCallsign()) - )); + 'callsigns' => array($repository->getCallsign()), + )); $looksoon->setUser($request->getUser()); $looksoon->execute(); return $response; } private function lockRepository($repository) { $lock_name = __CLASS__.':'.($repository->getCallsign()); $lock = PhabricatorGlobalLock::newLock($lock_name); $lock->lock(); return $lock; } } diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php index c80ec46677..ae94b74026 100644 --- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php @@ -1,466 +1,466 @@ <?php abstract class DifferentialChangesetHTMLRenderer extends DifferentialChangesetRenderer { protected function renderChangeTypeHeader($force) { $changeset = $this->getChangeset(); $change = $changeset->getChangeType(); $file = $changeset->getFileType(); $messages = array(); $none = hsprintf(''); switch ($change) { case DifferentialChangeType::TYPE_ADD: switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht( 'This file was <strong>added</strong>.', $none); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht( 'This image was <strong>added</strong>.', $none); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht( 'This directory was <strong>added</strong>.', $none); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht( 'This binary file was <strong>added</strong>.', $none); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht( 'This symlink was <strong>added</strong>.', $none); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht( 'This submodule was <strong>added</strong>.', $none); break; } break; case DifferentialChangeType::TYPE_DELETE: switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht( 'This file was <strong>deleted</strong>.', $none); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht( 'This image was <strong>deleted</strong>.', $none); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht( 'This directory was <strong>deleted</strong>.', $none); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht( 'This binary file was <strong>deleted</strong>.', $none); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht( 'This symlink was <strong>deleted</strong>.', $none); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht( 'This submodule was <strong>deleted</strong>.', $none); break; } break; case DifferentialChangeType::TYPE_MOVE_HERE: $from = phutil_tag('strong', array(), $changeset->getOldFile()); switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht('This file was moved from %s.', $from); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht('This image was moved from %s.', $from); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht('This directory was moved from %s.', $from); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht('This binary file was moved from %s.', $from); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht('This symlink was moved from %s.', $from); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht('This submodule was moved from %s.', $from); break; } break; case DifferentialChangeType::TYPE_COPY_HERE: $from = phutil_tag('strong', array(), $changeset->getOldFile()); switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht('This file was copied from %s.', $from); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht('This image was copied from %s.', $from); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht('This directory was copied from %s.', $from); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht('This binary file was copied from %s.', $from); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht('This symlink was copied from %s.', $from); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht('This submodule was copied from %s.', $from); break; } break; case DifferentialChangeType::TYPE_MOVE_AWAY: $paths = phutil_tag( 'strong', array(), implode(', ', $changeset->getAwayPaths())); switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht('This file was moved to %s.', $paths); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht('This image was moved to %s.', $paths); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht('This directory was moved to %s.', $paths); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht('This binary file was moved to %s.', $paths); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht('This symlink was moved to %s.', $paths); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht('This submodule was moved to %s.', $paths); break; } break; case DifferentialChangeType::TYPE_COPY_AWAY: $paths = phutil_tag( 'strong', array(), implode(', ', $changeset->getAwayPaths())); switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht('This file was copied to %s.', $paths); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht('This image was copied to %s.', $paths); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht('This directory was copied to %s.', $paths); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht('This binary file was copied to %s.', $paths); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht('This symlink was copied to %s.', $paths); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht('This submodule was copied to %s.', $paths); break; } break; case DifferentialChangeType::TYPE_MULTICOPY: $paths = phutil_tag( 'strong', array(), implode(', ', $changeset->getAwayPaths())); switch ($file) { case DifferentialChangeType::FILE_TEXT: $messages[] = pht( 'This file was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht( 'This image was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht( 'This directory was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht( 'This binary file was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht( 'This symlink was deleted after being copied to %s.', $paths); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht( 'This submodule was deleted after being copied to %s.', $paths); break; } break; default: switch ($file) { case DifferentialChangeType::FILE_TEXT: // This is the default case, so we only render this header if // forced to since it's not very useful. if ($force) { $messages[] = pht('This file was not modified.'); } break; case DifferentialChangeType::FILE_IMAGE: $messages[] = pht('This is an image.'); break; case DifferentialChangeType::FILE_DIRECTORY: $messages[] = pht('This is a directory.'); break; case DifferentialChangeType::FILE_BINARY: $messages[] = pht('This is a binary file.'); break; case DifferentialChangeType::FILE_SYMLINK: $messages[] = pht('This is a symlink.'); break; case DifferentialChangeType::FILE_SUBMODULE: $messages[] = pht('This is a submodule.'); break; } break; } // If this is a text file with at least one hunk, we may have converted // the text encoding. In this case, show a note. $show_encoding = ($file == DifferentialChangeType::FILE_TEXT) && ($changeset->getHunks()); if ($show_encoding) { $encoding = $this->getOriginalCharacterEncoding(); if ($encoding != 'utf8') { if ($encoding) { $messages[] = pht( 'This file was converted from %s for display.', phutil_tag('strong', array(), $encoding)); } else { $messages[] = pht( 'This file uses an unknown character encoding.'); } } } if (!$messages) { return null; } foreach ($messages as $key => $message) { $messages[$key] = phutil_tag('li', array(), $message); } return phutil_tag( 'ul', array( 'class' => 'differential-meta-notice', ), $messages); } protected function renderPropertyChangeHeader() { $changeset = $this->getChangeset(); list($old, $new) = $this->getChangesetProperties($changeset); // If we don't have any property changes, don't render this table. if ($old === $new) { return null; } $keys = array_keys($old + $new); sort($keys); $key_map = array( 'unix:filemode' => pht('File Mode'), 'file:dimensions' => pht('Image Dimensions'), 'file:mimetype' => pht('MIME Type'), 'file:size' => pht('File Size'), ); $rows = array(); foreach ($keys as $key) { $oval = idx($old, $key); $nval = idx($new, $key); if ($oval !== $nval) { if ($oval === null) { $oval = phutil_tag('em', array(), 'null'); } else { $oval = phutil_escape_html_newlines($oval); } if ($nval === null) { $nval = phutil_tag('em', array(), 'null'); } else { $nval = phutil_escape_html_newlines($nval); } $readable_key = idx($key_map, $key, $key); $row = array( $readable_key, $oval, - $nval + $nval, ); $rows[] = $row; } } $classes = array('', 'oval', 'nval'); $headers = array( pht('Property'), pht('Old Value'), pht('New Value'), ); $table = id(new AphrontTableView($rows)) ->setHeaders($headers) ->setColumnClasses($classes); return phutil_tag( 'div', array( 'class' => 'differential-property-table', ), $table); } public function renderShield($message, $force = 'default') { $end = count($this->getOldLines()); $reference = $this->getRenderingReference(); if ($force !== 'text' && $force !== 'whitespace' && $force !== 'none' && $force !== 'default') { throw new Exception("Invalid 'force' parameter '{$force}'!"); } $range = "0-{$end}"; if ($force == 'text') { // If we're forcing text, force the whole file to be rendered. $range = "{$range}/0-{$end}"; } $meta = array( 'ref' => $reference, 'range' => $range, ); if ($force == 'whitespace') { $meta['whitespace'] = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; } $content = array(); $content[] = $message; if ($force !== 'none') { $content[] = ' '; $content[] = javelin_tag( 'a', array( 'mustcapture' => true, 'sigil' => 'show-more', 'class' => 'complete', 'href' => '#', 'meta' => $meta, ), pht('Show File Contents')); } return $this->wrapChangeInTable( javelin_tag( 'tr', array( 'sigil' => 'context-target', ), phutil_tag( 'td', array( 'class' => 'differential-shield', 'colspan' => 6, ), $content))); } private function renderColgroup() { return phutil_tag('colgroup', array(), array( phutil_tag('col', array('class' => 'num')), phutil_tag('col', array('class' => 'left')), phutil_tag('col', array('class' => 'num')), phutil_tag('col', array('class' => 'copy')), phutil_tag('col', array('class' => 'right')), phutil_tag('col', array('class' => 'cov')), )); } protected function wrapChangeInTable($content) { if (!$content) { return null; } return javelin_tag( 'table', array( 'class' => 'differential-diff remarkup-code PhabricatorMonospaced', 'sigil' => 'differential-diff', ), array( $this->renderColgroup(), $content, )); } protected function renderInlineComment( PhabricatorInlineCommentInterface $comment, $on_right = false) { return $this->buildInlineComment($comment, $on_right)->render(); } protected function buildInlineComment( PhabricatorInlineCommentInterface $comment, $on_right = false) { $user = $this->getUser(); $edit = $user && ($comment->getAuthorPHID() == $user->getPHID()) && ($comment->isDraft()); $allow_reply = (bool)$user; return id(new DifferentialInlineCommentView()) ->setInlineComment($comment) ->setOnRight($on_right) ->setHandles($this->getHandles()) ->setMarkupEngine($this->getMarkupEngine()) ->setEditable($edit) ->setAllowReply($allow_reply); } } diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index b4d36723ff..4608c2453d 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -1,452 +1,452 @@ <?php final class DifferentialChangesetTwoUpRenderer extends DifferentialChangesetHTMLRenderer { public function isOneUpRenderer() { return false; } public function renderTextChange( $range_start, $range_len, $rows) { $hunk_starts = $this->getHunkStartLines(); $context_not_available = null; if ($hunk_starts) { $context_not_available = javelin_tag( 'tr', array( 'sigil' => 'context-target', ), phutil_tag( 'td', array( 'colspan' => 6, - 'class' => 'show-more' + 'class' => 'show-more', ), pht('Context not available.'))); } $html = array(); $old_lines = $this->getOldLines(); $new_lines = $this->getNewLines(); $gaps = $this->getGaps(); $reference = $this->getRenderingReference(); $left_id = $this->getOldChangesetID(); $right_id = $this->getNewChangesetID(); // "N" stands for 'new' and means the comment should attach to the new file // when stored, i.e. DifferentialInlineComment->setIsNewFile(). // "O" stands for 'old' and means the comment should attach to the old file. $left_char = $this->getOldAttachesToNewFile() ? 'N' : 'O'; $right_char = $this->getNewAttachesToNewFile() ? 'N' : 'O'; $changeset = $this->getChangeset(); $copy_lines = idx($changeset->getMetadata(), 'copy:lines', array()); $highlight_old = $this->getHighlightOld(); $highlight_new = $this->getHighlightNew(); $old_render = $this->getOldRender(); $new_render = $this->getNewRender(); $original_left = $this->getOriginalOld(); $original_right = $this->getOriginalNew(); $depths = $this->getDepths(); $mask = $this->getMask(); for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) { if (empty($mask[$ii])) { // If we aren't going to show this line, we've just entered a gap. // Pop information about the next gap off the $gaps stack and render // an appropriate "Show more context" element. This branch eventually // increments $ii by the entire size of the gap and then continues // the loop. $gap = array_pop($gaps); $top = $gap[0]; $len = $gap[1]; $end = $top + $len - 20; $contents = array(); if ($len > 40) { $is_first_block = false; if ($ii == 0) { $is_first_block = true; } $contents[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'show-more', 'meta' => array( 'ref' => $reference, 'range' => "{$top}-{$len}/{$top}-20", ), ), $is_first_block ? pht('Show First 20 Lines') : pht("\xE2\x96\xB2 Show 20 Lines")); } $contents[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'show-more', 'meta' => array( 'type' => 'all', 'ref' => $reference, 'range' => "{$top}-{$len}/{$top}-{$len}", ), ), pht('Show All %d Lines', $len)); $is_last_block = false; if ($ii + $len >= $rows) { $is_last_block = true; } if ($len > 40) { $contents[] = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'sigil' => 'show-more', 'meta' => array( 'ref' => $reference, 'range' => "{$top}-{$len}/{$end}-20", ), ), $is_last_block ? pht('Show Last 20 Lines') : pht("\xE2\x96\xBC Show 20 Lines")); } $context = null; $context_line = null; if (!$is_last_block && $depths[$ii + $len]) { for ($l = $ii + $len - 1; $l >= $ii; $l--) { $line = $new_lines[$l]['text']; if ($depths[$l] < $depths[$ii + $len] && trim($line) != '') { $context = $new_render[$l]; $context_line = $new_lines[$l]['line']; break; } } } $container = javelin_tag( 'tr', array( 'sigil' => 'context-target', ), array( phutil_tag( 'td', array( 'colspan' => 2, 'class' => 'show-more', ), phutil_implode_html( " \xE2\x80\xA2 ", // Bullet $contents)), phutil_tag( 'th', array( 'class' => 'show-context-line', ), $context_line ? (int)$context_line : null), phutil_tag( 'td', array( 'colspan' => 3, 'class' => 'show-context', ), // TODO: [HTML] Escaping model here isn't ideal. phutil_safe_html($context)), )); $html[] = $container; $ii += ($len - 1); continue; } $o_num = null; $o_classes = ''; $o_text = null; if (isset($old_lines[$ii])) { $o_num = $old_lines[$ii]['line']; $o_text = isset($old_render[$ii]) ? $old_render[$ii] : null; if ($old_lines[$ii]['type']) { if ($old_lines[$ii]['type'] == '\\') { $o_text = $old_lines[$ii]['text']; $o_class = 'comment'; } else if ($original_left && !isset($highlight_old[$o_num])) { $o_class = 'old-rebase'; } else if (empty($new_lines[$ii])) { $o_class = 'old old-full'; } else { $o_class = 'old'; } $o_classes = $o_class; } } $n_copy = hsprintf('<td class="copy" />'); $n_cov = null; $n_colspan = 2; $n_classes = ''; $n_num = null; $n_text = null; if (isset($new_lines[$ii])) { $n_num = $new_lines[$ii]['line']; $n_text = isset($new_render[$ii]) ? $new_render[$ii] : null; $coverage = $this->getCodeCoverage(); if ($coverage !== null) { if (empty($coverage[$n_num - 1])) { $cov_class = 'N'; } else { $cov_class = $coverage[$n_num - 1]; } $cov_class = 'cov-'.$cov_class; $n_cov = phutil_tag('td', array('class' => "cov {$cov_class}")); $n_colspan--; } if ($new_lines[$ii]['type']) { if ($new_lines[$ii]['type'] == '\\') { $n_text = $new_lines[$ii]['text']; $n_class = 'comment'; } else if ($original_right && !isset($highlight_new[$n_num])) { $n_class = 'new-rebase'; } else if (empty($old_lines[$ii])) { $n_class = 'new new-full'; } else { $n_class = 'new'; } $n_classes = $n_class; if ($new_lines[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) { $n_copy = phutil_tag('td', array('class' => "copy {$n_class}")); } else { list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num]; $title = ($orig_type == '-' ? 'Moved' : 'Copied').' from '; if ($orig_file == '') { $title .= "line {$orig_line}"; } else { $title .= basename($orig_file). ":{$orig_line} in dir ". dirname('/'.$orig_file); } $class = ($orig_type == '-' ? 'new-move' : 'new-copy'); $n_copy = javelin_tag( 'td', array( 'meta' => array( 'msg' => $title, ), 'class' => 'copy '.$class, ), ''); } } } if (isset($hunk_starts[$o_num])) { $html[] = $context_not_available; } if ($o_num && $left_id) { $o_id = 'C'.$left_id.$left_char.'L'.$o_num; } else { $o_id = null; } if ($n_num && $right_id) { $n_id = 'C'.$right_id.$right_char.'L'.$n_num; } else { $n_id = null; } // NOTE: This is a unicode zero-width space, which we use as a hint when // intercepting 'copy' events to make sure sensible text ends up on the // clipboard. See the 'phabricator-oncopy' behavior. $zero_space = "\xE2\x80\x8B"; // NOTE: The Javascript is sensitive to whitespace changes in this // block! $html[] = phutil_tag('tr', array(), array( phutil_tag('th', array('id' => $o_id), $o_num), phutil_tag('td', array('class' => $o_classes), $o_text), phutil_tag('th', array('id' => $n_id), $n_num), $n_copy, phutil_tag( 'td', array('class' => $n_classes, 'colspan' => $n_colspan), array( phutil_tag('span', array('class' => 'zwsp'), $zero_space), - $n_text + $n_text, )), $n_cov, )); if ($context_not_available && ($ii == $rows - 1)) { $html[] = $context_not_available; } $old_comments = $this->getOldComments(); $new_comments = $this->getNewComments(); if ($o_num && isset($old_comments[$o_num])) { foreach ($old_comments[$o_num] as $comment) { $comment_html = $this->renderInlineComment($comment, $on_right = false); $new = ''; if ($n_num && isset($new_comments[$n_num])) { foreach ($new_comments[$n_num] as $key => $new_comment) { if ($comment->isCompatible($new_comment)) { $new = $this->renderInlineComment($new_comment, $on_right = true); unset($new_comments[$n_num][$key]); } } } $html[] = phutil_tag('tr', array('class' => 'inline'), array( phutil_tag('th', array()), phutil_tag('td', array(), $comment_html), phutil_tag('th', array()), phutil_tag('td', array('colspan' => 3), $new), )); } } if ($n_num && isset($new_comments[$n_num])) { foreach ($new_comments[$n_num] as $comment) { $comment_html = $this->renderInlineComment($comment, $on_right = true); $html[] = phutil_tag('tr', array('class' => 'inline'), array( phutil_tag('th', array()), phutil_tag('td', array()), phutil_tag('th', array()), phutil_tag( 'td', array('colspan' => 3), $comment_html), )); } } } return $this->wrapChangeInTable(phutil_implode_html('', $html)); } public function renderFileChange($old_file = null, $new_file = null, $id = 0, $vs = 0) { $old = null; if ($old_file) { $old = phutil_tag( 'div', array( - 'class' => 'differential-image-stage' + 'class' => 'differential-image-stage', ), phutil_tag( 'img', array( 'src' => $old_file->getBestURI(), ))); } $new = null; if ($new_file) { $new = phutil_tag( 'div', array( - 'class' => 'differential-image-stage' + 'class' => 'differential-image-stage', ), phutil_tag( 'img', array( 'src' => $new_file->getBestURI(), ))); } $html_old = array(); $html_new = array(); foreach ($this->getOldComments() as $on_line => $comment_group) { foreach ($comment_group as $comment) { $comment_html = $this->renderInlineComment($comment, $on_right = false); $html_old[] = phutil_tag('tr', array('class' => 'inline'), array( phutil_tag('th', array()), phutil_tag('td', array(), $comment_html), phutil_tag('th', array()), phutil_tag('td', array('colspan' => 3)), )); } } foreach ($this->getNewComments() as $lin_line => $comment_group) { foreach ($comment_group as $comment) { $comment_html = $this->renderInlineComment($comment, $on_right = true); $html_new[] = phutil_tag('tr', array('class' => 'inline'), array( phutil_tag('th', array()), phutil_tag('td', array()), phutil_tag('th', array()), phutil_tag( 'td', array('colspan' => 3), $comment_html), )); } } if (!$old) { $th_old = phutil_tag('th', array()); } else { $th_old = phutil_tag('th', array('id' => "C{$vs}OL1"), 1); } if (!$new) { $th_new = phutil_tag('th', array()); } else { $th_new = phutil_tag('th', array('id' => "C{$id}OL1"), 1); } $output = hsprintf( '<tr class="differential-image-diff">'. '%s'. '<td class="differential-old-image">%s</td>'. '%s'. '<td class="differential-new-image" colspan="3">%s</td>'. '</tr>'. '%s'. '%s', $th_old, $old, $th_new, $new, phutil_implode_html('', $html_old), phutil_implode_html('', $html_new)); $output = $this->wrapChangeInTable($output); return $this->renderChangesetTable($output); } } diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index 7e74ad0929..d52df5c2ec 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -1,446 +1,446 @@ <?php final class DifferentialDiff extends DifferentialDAO implements PhabricatorPolicyInterface, HarbormasterBuildableInterface, PhabricatorApplicationTransactionInterface, PhabricatorDestructibleInterface { protected $revisionID; protected $authorPHID; protected $repositoryPHID; protected $sourceMachine; protected $sourcePath; protected $sourceControlSystem; protected $sourceControlBaseRevision; protected $sourceControlPath; protected $lintStatus; protected $unitStatus; protected $lineCount; protected $branch; protected $bookmark; protected $arcanistProjectPHID; protected $creationMethod; protected $repositoryUUID; protected $description; private $unsavedChangesets = array(); private $changesets = self::ATTACHABLE; private $arcanistProject = self::ATTACHABLE; private $revision = self::ATTACHABLE; private $properties = array(); public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'revisionID' => 'id?', 'authorPHID' => 'phid?', 'repositoryPHID' => 'phid?', 'sourceMachine' => 'text255?', 'sourcePath' => 'text255?', 'sourceControlSystem' => 'text64?', 'sourceControlBaseRevision' => 'text255?', 'sourceControlPath' => 'text255?', 'lintStatus' => 'uint32', 'unitStatus' => 'uint32', 'lineCount' => 'uint32', 'branch' => 'text255?', 'bookmark' => 'text255?', 'arcanistProjectPHID' => 'phid?', 'repositoryUUID' => 'text64?', // T6203/NULLABILITY // These should be non-null; all diffs should have a creation method // and the description should just be empty. 'creationMethod' => 'text255?', 'description' => 'text255?', ), self::CONFIG_KEY_SCHEMA => array( 'revisionID' => array( 'columns' => array('revisionID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DifferentialDiffPHIDType::TYPECONST); } public function addUnsavedChangeset(DifferentialChangeset $changeset) { if ($this->changesets === null) { $this->changesets = array(); } $this->unsavedChangesets[] = $changeset; $this->changesets[] = $changeset; return $this; } public function attachChangesets(array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $this->changesets = $changesets; return $this; } public function getChangesets() { return $this->assertAttached($this->changesets); } public function loadChangesets() { if (!$this->getID()) { return array(); } return id(new DifferentialChangeset())->loadAllWhere( 'diffID = %d', $this->getID()); } public function attachArcanistProject( PhabricatorRepositoryArcanistProject $project = null) { $this->arcanistProject = $project; return $this; } public function getArcanistProject() { return $this->assertAttached($this->arcanistProject); } public function getArcanistProjectName() { $name = ''; if ($this->arcanistProject) { $project = $this->getArcanistProject(); $name = $project->getName(); } return $name; } public function save() { $this->openTransaction(); $ret = parent::save(); foreach ($this->unsavedChangesets as $changeset) { $changeset->setDiffID($this->getID()); $changeset->save(); } $this->saveTransaction(); return $ret; } public static function newFromRawChanges(array $changes) { assert_instances_of($changes, 'ArcanistDiffChange'); $diff = new DifferentialDiff(); // There may not be any changes; initialize the changesets list so that // we don't throw later when accessing it. $diff->attachChangesets(array()); $lines = 0; foreach ($changes as $change) { if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) { // If a user pastes a diff into Differential which includes a commit // message (e.g., they ran `git show` to generate it), discard that // change when constructing a DifferentialDiff. continue; } $changeset = new DifferentialChangeset(); $add_lines = 0; $del_lines = 0; $first_line = PHP_INT_MAX; $hunks = $change->getHunks(); if ($hunks) { foreach ($hunks as $hunk) { $dhunk = new DifferentialHunkModern(); $dhunk->setOldOffset($hunk->getOldOffset()); $dhunk->setOldLen($hunk->getOldLength()); $dhunk->setNewOffset($hunk->getNewOffset()); $dhunk->setNewLen($hunk->getNewLength()); $dhunk->setChanges($hunk->getCorpus()); $changeset->addUnsavedHunk($dhunk); $add_lines += $hunk->getAddLines(); $del_lines += $hunk->getDelLines(); $added_lines = $hunk->getChangedLines('new'); if ($added_lines) { $first_line = min($first_line, head_key($added_lines)); } } $lines += $add_lines + $del_lines; } else { // This happens when you add empty files. $changeset->attachHunks(array()); } $metadata = $change->getAllMetadata(); if ($first_line != PHP_INT_MAX) { $metadata['line:first'] = $first_line; } $changeset->setOldFile($change->getOldPath()); $changeset->setFilename($change->getCurrentPath()); $changeset->setChangeType($change->getType()); $changeset->setFileType($change->getFileType()); $changeset->setMetadata($metadata); $changeset->setOldProperties($change->getOldProperties()); $changeset->setNewProperties($change->getNewProperties()); $changeset->setAwayPaths($change->getAwayPaths()); $changeset->setAddLines($add_lines); $changeset->setDelLines($del_lines); $diff->addUnsavedChangeset($changeset); } $diff->setLineCount($lines); $parser = new DifferentialChangesetParser(); $changesets = $parser->detectCopiedCode( $diff->getChangesets(), $min_width = 30, $min_lines = 3); $diff->attachChangesets($changesets); return $diff; } public function getDiffDict() { $dict = array( 'id' => $this->getID(), 'revisionID' => $this->getRevisionID(), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), 'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(), 'sourceControlPath' => $this->getSourceControlPath(), 'sourceControlSystem' => $this->getSourceControlSystem(), 'branch' => $this->getBranch(), 'bookmark' => $this->getBookmark(), 'creationMethod' => $this->getCreationMethod(), 'description' => $this->getDescription(), 'unitStatus' => $this->getUnitStatus(), 'lintStatus' => $this->getLintStatus(), 'changes' => array(), 'properties' => array(), - 'projectName' => $this->getArcanistProjectName() + 'projectName' => $this->getArcanistProjectName(), ); $dict['changes'] = $this->buildChangesList(); $properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $this->getID()); foreach ($properties as $property) { $dict['properties'][$property->getName()] = $property->getData(); if ($property->getName() == 'local:commits') { foreach ($property->getData() as $commit) { $dict['authorName'] = $commit['author']; $dict['authorEmail'] = idx($commit, 'authorEmail'); break; } } } return $dict; } public function buildChangesList() { $changes = array(); foreach ($this->getChangesets() as $changeset) { $hunks = array(); foreach ($changeset->getHunks() as $hunk) { $hunks[] = array( 'oldOffset' => $hunk->getOldOffset(), 'newOffset' => $hunk->getNewOffset(), 'oldLength' => $hunk->getOldLen(), 'newLength' => $hunk->getNewLen(), 'addLines' => null, 'delLines' => null, 'isMissingOldNewline' => null, 'isMissingNewNewline' => null, 'corpus' => $hunk->getChanges(), ); } $change = array( 'id' => $changeset->getID(), 'metadata' => $changeset->getMetadata(), 'oldPath' => $changeset->getOldFile(), 'currentPath' => $changeset->getFilename(), 'awayPaths' => $changeset->getAwayPaths(), 'oldProperties' => $changeset->getOldProperties(), 'newProperties' => $changeset->getNewProperties(), 'type' => $changeset->getChangeType(), 'fileType' => $changeset->getFileType(), 'commitHash' => null, 'addLines' => $changeset->getAddLines(), 'delLines' => $changeset->getDelLines(), 'hunks' => $hunks, ); $changes[] = $change; } return $changes; } public function getRevision() { return $this->assertAttached($this->revision); } public function attachRevision(DifferentialRevision $revision = null) { $this->revision = $revision; return $this; } public function attachProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key) { return $this->assertAttachedKey($this->properties, $key); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { if ($this->getRevision()) { return $this->getRevision()->getPolicy($capability); } return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->getRevision()) { return $this->getRevision()->hasAutomaticCapability($capability, $viewer); } return false; } public function describeAutomaticCapability($capability) { if ($this->getRevision()) { return pht( 'This diff is attached to a revision, and inherits its policies.'); } return null; } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildablePHID() { return $this->getPHID(); } public function getHarbormasterContainerPHID() { if ($this->getRevisionID()) { $revision = id(new DifferentialRevision())->load($this->getRevisionID()); if ($revision) { return $revision->getPHID(); } } return null; } public function getBuildVariables() { $results = array(); $results['buildable.diff'] = $this->getID(); $revision = $this->getRevision(); $results['buildable.revision'] = $revision->getID(); $repo = $revision->getRepository(); if ($repo) { $results['repository.callsign'] = $repo->getCallsign(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); } return $results; } public function getAvailableBuildVariables() { return array( 'buildable.diff' => pht('The differential diff ID, if applicable.'), 'buildable.revision' => pht('The differential revision ID, if applicable.'), 'repository.callsign' => pht('The callsign of the repository in Phabricator.'), 'repository.vcs' => pht('The version control system, either "svn", "hg" or "git".'), 'repository.uri' => pht('The URI to clone or checkout the repository from.'), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { if (!$this->getRevisionID()) { return null; } return $this->getRevision()->getApplicationTransactionEditor(); } public function getApplicationTransactionObject() { if (!$this->getRevisionID()) { return null; } return $this->getRevision(); } public function getApplicationTransactionTemplate() { if (!$this->getRevisionID()) { return null; } return $this->getRevision()->getApplicationTransactionTemplate(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); foreach ($this->loadChangesets() as $changeset) { $changeset->delete(); } $properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $this->getID()); foreach ($properties as $prop) { $prop->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/differential/view/DifferentialChangesetDetailView.php b/src/applications/differential/view/DifferentialChangesetDetailView.php index 79c278f0e2..6392360cdf 100644 --- a/src/applications/differential/view/DifferentialChangesetDetailView.php +++ b/src/applications/differential/view/DifferentialChangesetDetailView.php @@ -1,233 +1,234 @@ <?php final class DifferentialChangesetDetailView extends AphrontView { private $changeset; private $buttons = array(); private $editable; private $symbolIndex; private $id; private $vsChangesetID; private $renderURI; private $whitespace; private $renderingRef; private $autoload; public function setAutoload($autoload) { $this->autoload = $autoload; return $this; } public function getAutoload() { return $this->autoload; } public function setRenderingRef($rendering_ref) { $this->renderingRef = $rendering_ref; return $this; } public function getRenderingRef() { return $this->renderingRef; } public function setWhitespace($whitespace) { $this->whitespace = $whitespace; return $this; } public function getWhitespace() { return $this->whitespace; } public function setRenderURI($render_uri) { $this->renderURI = $render_uri; return $this; } public function getRenderURI() { return $this->renderURI; } public function setChangeset($changeset) { $this->changeset = $changeset; return $this; } public function addButton($button) { $this->buttons[] = $button; return $this; } public function setEditable($editable) { $this->editable = $editable; return $this; } public function setSymbolIndex($symbol_index) { $this->symbolIndex = $symbol_index; return $this; } public function getID() { if (!$this->id) { $this->id = celerity_generate_unique_node_id(); } return $this->id; } public function setID($id) { $this->id = $id; return $this; } public function setVsChangesetID($vs_changeset_id) { $this->vsChangesetID = $vs_changeset_id; return $this; } public function getVsChangesetID() { return $this->vsChangesetID; } public function getFileIcon($filename) { $path_info = pathinfo($filename); $extension = idx($path_info, 'extension'); switch ($extension) { case 'psd': case 'ai': $icon = 'fa-eye'; break; case 'conf': $icon = 'fa-wrench'; break; case 'wav': case 'mp3': case 'aiff': $icon = 'fa-file-sound-o'; break; case 'm4v': case 'mov': $icon = 'fa-file-movie-o'; break; case 'sql': case 'db': $icon = 'fa-database'; break; case 'xls': case 'csv': $icon = 'fa-file-excel-o'; break; case 'ics': $icon = 'fa-calendar'; break; case 'zip': case 'tar': case 'bz': case 'tgz': case 'gz': $icon = 'fa-file-archive-o'; break; case 'png': case 'jpg': case 'bmp': case 'gif': $icon = 'fa-file-picture-o'; break; case 'txt': $icon = 'fa-file-text-o'; break; case 'doc': case 'docx': $icon = 'fa-file-word-o'; break; case 'pdf': $icon = 'fa-file-pdf-o'; break; default: $icon = 'fa-file-code-o'; break; } return $icon; } public function render() { $this->requireResource('differential-changeset-view-css'); $this->requireResource('syntax-highlighting-css'); Javelin::initBehavior('phabricator-oncopy', array()); $changeset = $this->changeset; $class = 'differential-changeset'; if (!$this->editable) { $class .= ' differential-changeset-immutable'; } $buttons = null; if ($this->buttons) { $buttons = phutil_tag( 'div', array( 'class' => 'differential-changeset-buttons', ), $this->buttons); } $id = $this->getID(); if ($this->symbolIndex) { Javelin::initBehavior( 'repository-crossreference', array( 'container' => $id, ) + $this->symbolIndex); } $display_filename = $changeset->getDisplayFilename(); $display_icon = $this->getFileIcon($display_filename); $icon = id(new PHUIIconView()) ->setIconFont($display_icon); return javelin_tag( 'div', array( 'sigil' => 'differential-changeset', 'meta' => array( 'left' => nonempty( $this->getVsChangesetID(), $this->changeset->getID()), 'right' => $this->changeset->getID(), 'renderURI' => $this->getRenderURI(), 'whitespace' => $this->getWhitespace(), 'highlight' => null, 'renderer' => null, 'ref' => $this->getRenderingRef(), 'autoload' => $this->getAutoload(), ), 'class' => $class, 'id' => $id, ), array( id(new PhabricatorAnchorView()) ->setAnchorName($changeset->getAnchorName()) ->setNavigationMarker(true) ->render(), $buttons, phutil_tag('h1', array( - 'class' => 'differential-file-icon-header'), + 'class' => 'differential-file-icon-header', + ), array( $icon, $display_filename, )), javelin_tag( 'div', array( 'class' => 'changeset-view-content', 'sigil' => 'changeset-view-content', ), $this->renderChildren()), )); } } diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php index d16a2c1bd8..afdc75c451 100644 --- a/src/applications/differential/view/DifferentialChangesetListView.php +++ b/src/applications/differential/view/DifferentialChangesetListView.php @@ -1,359 +1,360 @@ <?php final class DifferentialChangesetListView extends AphrontView { private $changesets = array(); private $visibleChangesets = array(); private $references = array(); private $inlineURI; private $renderURI = '/differential/changeset/'; private $whitespace; private $standaloneURI; private $leftRawFileURI; private $rightRawFileURI; private $symbolIndexes = array(); private $repository; private $branch; private $diff; private $vsMap = array(); private $title; public function setTitle($title) { $this->title = $title; return $this; } private function getTitle() { return $this->title; } public function setBranch($branch) { $this->branch = $branch; return $this; } private function getBranch() { return $this->branch; } public function setChangesets($changesets) { $this->changesets = $changesets; return $this; } public function setVisibleChangesets($visible_changesets) { $this->visibleChangesets = $visible_changesets; return $this; } public function setInlineCommentControllerURI($uri) { $this->inlineURI = $uri; return $this; } public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function setDiff(DifferentialDiff $diff) { $this->diff = $diff; return $this; } public function setRenderingReferences(array $references) { $this->references = $references; return $this; } public function setSymbolIndexes(array $indexes) { $this->symbolIndexes = $indexes; return $this; } public function setRenderURI($render_uri) { $this->renderURI = $render_uri; return $this; } public function setWhitespace($whitespace) { $this->whitespace = $whitespace; return $this; } public function setVsMap(array $vs_map) { $this->vsMap = $vs_map; return $this; } public function getVsMap() { return $this->vsMap; } public function setStandaloneURI($uri) { $this->standaloneURI = $uri; return $this; } public function setRawFileURIs($l, $r) { $this->leftRawFileURI = $l; $this->rightRawFileURI = $r; return $this; } public function render() { $this->requireResource('differential-changeset-view-css'); $changesets = $this->changesets; Javelin::initBehavior('differential-toggle-files', array( 'pht' => array( 'undo' => pht('Undo'), - 'collapsed' => pht('This file content has been collapsed.')) - )); + 'collapsed' => pht('This file content has been collapsed.'), + ), + )); Javelin::initBehavior( 'differential-dropdown-menus', array( 'pht' => array( 'Open in Editor' => pht('Open in Editor'), 'Show Entire File' => pht('Show Entire File'), 'Entire File Shown' => pht('Entire File Shown'), "Can't Toggle Unloaded File" => pht("Can't Toggle Unloaded File"), 'Expand File' => pht('Expand File'), 'Collapse File' => pht('Collapse File'), 'Browse in Diffusion' => pht('Browse in Diffusion'), 'View Standalone' => pht('View Standalone'), 'Show Raw File (Left)' => pht('Show Raw File (Left)'), 'Show Raw File (Right)' => pht('Show Raw File (Right)'), 'Configure Editor' => pht('Configure Editor'), 'Load Changes' => pht('Load Changes'), 'View Side-by-Side' => pht('View Side-by-Side'), 'View Unified' => pht('View Unified (Barely Works!)'), 'Change Text Encoding...' => pht('Change Text Encoding...'), 'Highlight As...' => pht('Highlight As...'), ), )); $output = array(); $ids = array(); foreach ($changesets as $key => $changeset) { $file = $changeset->getFilename(); $class = 'differential-changeset'; if (!$this->inlineURI) { $class .= ' differential-changeset-noneditable'; } $ref = $this->references[$key]; $detail = new DifferentialChangesetDetailView(); $uniq_id = 'diff-'.$changeset->getAnchorName(); $detail->setID($uniq_id); $view_options = $this->renderViewOptionsDropdown( $detail, $ref, $changeset); $detail->setChangeset($changeset); $detail->addButton($view_options); $detail->setSymbolIndex(idx($this->symbolIndexes, $key)); $detail->setVsChangesetID(idx($this->vsMap, $changeset->getID())); $detail->setEditable(true); $detail->setRenderingRef($ref); $detail->setAutoload(isset($this->visibleChangesets[$key])); $detail->setRenderURI($this->renderURI); $detail->setWhitespace($this->whitespace); if (isset($this->visibleChangesets[$key])) { $load = 'Loading...'; } else { $load = javelin_tag( 'a', array( 'href' => '#'.$uniq_id, 'sigil' => 'differential-load', 'meta' => array( 'id' => $detail->getID(), 'kill' => true, ), 'mustcapture' => true, ), pht('Load')); } $detail->appendChild( phutil_tag( 'div', array( 'id' => $uniq_id, ), phutil_tag('div', array('class' => 'differential-loading'), $load))); $output[] = $detail->render(); $ids[] = $detail->getID(); } $this->requireResource('aphront-tooltip-css'); $this->initBehavior('differential-populate', array( 'changesetViewIDs' => $ids, )); $this->initBehavior('differential-show-more', array( 'uri' => $this->renderURI, 'whitespace' => $this->whitespace, )); $this->initBehavior('differential-comment-jump', array()); if ($this->inlineURI) { $undo_templates = $this->renderUndoTemplates(); Javelin::initBehavior('differential-edit-inline-comments', array( 'uri' => $this->inlineURI, 'undo_templates' => $undo_templates, 'stage' => 'differential-review-stage', )); } $header = id(new PHUIHeaderView()) ->setHeader($this->getTitle()); $content = phutil_tag( 'div', array( 'class' => 'differential-review-stage', 'id' => 'differential-review-stage', ), $output); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($content); return $object_box; } /** * Render the "Undo" markup for the inline comment undo feature. */ private function renderUndoTemplates() { $link = javelin_tag( 'a', array( 'href' => '#', 'sigil' => 'differential-inline-comment-undo', ), pht('Undo')); $div = phutil_tag( 'div', array( 'class' => 'differential-inline-undo', ), array('Changes discarded. ', $link)); return array( 'l' => phutil_tag('table', array(), phutil_tag('tr', array(), array( phutil_tag('th', array()), phutil_tag('td', array(), $div), phutil_tag('th', array()), phutil_tag('td', array('colspan' => 3)), ))), 'r' => phutil_tag('table', array(), phutil_tag('tr', array(), array( phutil_tag('th', array()), phutil_tag('td', array()), phutil_tag('th', array()), phutil_tag('td', array('colspan' => 3), $div), ))), ); } private function renderViewOptionsDropdown( DifferentialChangesetDetailView $detail, $ref, DifferentialChangeset $changeset) { $meta = array(); $qparams = array( 'ref' => $ref, 'whitespace' => $this->whitespace, ); if ($this->standaloneURI) { $uri = new PhutilURI($this->standaloneURI); $uri->setQueryParams($uri->getQueryParams() + $qparams); $meta['standaloneURI'] = (string)$uri; } $repository = $this->repository; if ($repository) { try { $meta['diffusionURI'] = (string)$repository->getDiffusionBrowseURIForPath( $this->user, $changeset->getAbsoluteRepositoryPath($repository, $this->diff), idx($changeset->getMetadata(), 'line:first'), $this->getBranch()); } catch (DiffusionSetupException $e) { // Ignore } } $change = $changeset->getChangeType(); if ($this->leftRawFileURI) { if ($change != DifferentialChangeType::TYPE_ADD) { $uri = new PhutilURI($this->leftRawFileURI); $uri->setQueryParams($uri->getQueryParams() + $qparams); $meta['leftURI'] = (string)$uri; } } if ($this->rightRawFileURI) { if ($change != DifferentialChangeType::TYPE_DELETE && $change != DifferentialChangeType::TYPE_MULTICOPY) { $uri = new PhutilURI($this->rightRawFileURI); $uri->setQueryParams($uri->getQueryParams() + $qparams); $meta['rightURI'] = (string)$uri; } } $user = $this->user; if ($user && $repository) { $path = ltrim( $changeset->getAbsoluteRepositoryPath($repository, $this->diff), '/'); $line = idx($changeset->getMetadata(), 'line:first', 1); $callsign = $repository->getCallsign(); $editor_link = $user->loadEditorLink($path, $line, $callsign); if ($editor_link) { $meta['editor'] = $editor_link; } else { $meta['editorConfigure'] = '/settings/panel/display/'; } } $meta['containerID'] = $detail->getID(); $caret = phutil_tag('span', array('class' => 'caret'), ''); return javelin_tag( 'a', array( 'class' => 'button grey small dropdown', 'meta' => $meta, 'href' => idx($meta, 'detailURI', '#'), 'target' => '_blank', 'sigil' => 'differential-view-options', ), array(pht('View Options'), $caret)); } } diff --git a/src/applications/differential/view/DifferentialDiffTableOfContentsView.php b/src/applications/differential/view/DifferentialDiffTableOfContentsView.php index 90056969f5..94b9907ecb 100644 --- a/src/applications/differential/view/DifferentialDiffTableOfContentsView.php +++ b/src/applications/differential/view/DifferentialDiffTableOfContentsView.php @@ -1,325 +1,325 @@ <?php final class DifferentialDiffTableOfContentsView extends AphrontView { private $changesets = array(); private $visibleChangesets = array(); private $references = array(); private $repository; private $diff; private $renderURI = '/differential/changeset/'; private $revisionID; private $whitespace; private $unitTestData; public function setChangesets($changesets) { $this->changesets = $changesets; return $this; } public function setVisibleChangesets($visible_changesets) { $this->visibleChangesets = $visible_changesets; return $this; } public function setRenderingReferences(array $references) { $this->references = $references; return $this; } public function setRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function setDiff(DifferentialDiff $diff) { $this->diff = $diff; return $this; } public function setUnitTestData($unit_test_data) { $this->unitTestData = $unit_test_data; return $this; } public function setRevisionID($revision_id) { $this->revisionID = $revision_id; return $this; } public function setWhitespace($whitespace) { $this->whitespace = $whitespace; return $this; } public function render() { $this->requireResource('differential-core-view-css'); $this->requireResource('differential-table-of-contents-css'); $rows = array(); $coverage = array(); if ($this->unitTestData) { $coverage_by_file = array(); foreach ($this->unitTestData as $result) { $test_coverage = idx($result, 'coverage'); if (!$test_coverage) { continue; } foreach ($test_coverage as $file => $results) { $coverage_by_file[$file][] = $results; } } foreach ($coverage_by_file as $file => $coverages) { $coverage[$file] = ArcanistUnitTestResult::mergeCoverage($coverages); } } $changesets = $this->changesets; $paths = array(); foreach ($changesets as $id => $changeset) { $type = $changeset->getChangeType(); $ftype = $changeset->getFileType(); $ref = idx($this->references, $id); $display_file = $changeset->getDisplayFilename(); $meta = null; if (DifferentialChangeType::isOldLocationChangeType($type)) { $away = $changeset->getAwayPaths(); if (count($away) > 1) { $meta = array(); if ($type == DifferentialChangeType::TYPE_MULTICOPY) { $meta[] = pht('Deleted after being copied to multiple locations:'); } else { $meta[] = pht('Copied to multiple locations:'); } foreach ($away as $path) { $meta[] = $path; } $meta = phutil_implode_html(phutil_tag('br'), $meta); } else { if ($type == DifferentialChangeType::TYPE_MOVE_AWAY) { $display_file = $this->renderRename( $display_file, reset($away), "\xE2\x86\x92"); } else { $meta = pht('Copied to %s', reset($away)); } } } else if ($type == DifferentialChangeType::TYPE_MOVE_HERE) { $old_file = $changeset->getOldFile(); $display_file = $this->renderRename( $display_file, $old_file, "\xE2\x86\x90"); } else if ($type == DifferentialChangeType::TYPE_COPY_HERE) { $meta = pht('Copied from %s', $changeset->getOldFile()); } $link = $this->renderChangesetLink($changeset, $ref, $display_file); $line_count = $changeset->getAffectedLineCount(); if ($line_count == 0) { $lines = ''; } else { $lines = ' '.pht('(%d line(s))', $line_count); } $char = DifferentialChangeType::getSummaryCharacterForChangeType($type); $chartitle = DifferentialChangeType::getFullNameForChangeType($type); $desc = DifferentialChangeType::getShortNameForFileType($ftype); if ($desc) { $desc = '('.$desc.')'; } $pchar = ($changeset->getOldProperties() === $changeset->getNewProperties()) ? '' : phutil_tag( 'span', array('title' => pht('Properties Changed')), 'M'); $fname = $changeset->getFilename(); $cov = $this->renderCoverage($coverage, $fname); if ($cov === null) { $mcov = $cov = phutil_tag('em', array(), '-'); } else { $mcov = phutil_tag( 'div', array( 'id' => 'differential-mcoverage-'.md5($fname), 'class' => 'differential-mcoverage-loading', ), (isset($this->visibleChangesets[$id]) ? pht('Loading...') : pht('?'))); } if ($meta) { $meta = phutil_tag( 'div', array( - 'class' => 'differential-toc-meta' + 'class' => 'differential-toc-meta', ), $meta); } if ($this->diff && $this->repository) { $paths[] = $changeset->getAbsoluteRepositoryPath($this->repository, $this->diff); } $rows[] = array( $char, $pchar, $desc, array($link, $lines, $meta), $cov, - $mcov + $mcov, ); } $editor_link = null; if ($paths && $this->user) { $editor_link = $this->user->loadEditorLink( $paths, 1, // line number $this->repository->getCallsign()); if ($editor_link) { $editor_link = phutil_tag( 'a', array( 'href' => $editor_link, 'class' => 'button differential-toc-edit-all', ), pht('Open All in Editor')); } } $reveal_link = javelin_tag( 'a', array( 'sigil' => 'differential-reveal-all', 'mustcapture' => true, 'class' => 'button differential-toc-reveal-all', ), pht('Show All Context')); $buttons = phutil_tag( 'div', array( - 'class' => 'differential-toc-buttons grouped' + 'class' => 'differential-toc-buttons grouped', ), array( $editor_link, - $reveal_link + $reveal_link, )); $table = id(new AphrontTableView($rows)); $table->setHeaders( array( '', '', '', pht('Path'), pht('Coverage (All)'), pht('Coverage (Touched)'), )); $table->setColumnClasses( array( 'differential-toc-char center', 'differential-toc-prop center', 'differential-toc-ftype center', 'differential-toc-file wide', 'differential-toc-cov', 'differential-toc-cov', )); $table->setDeviceVisibility( array( true, true, true, true, false, false, )); $anchor = id(new PhabricatorAnchorView()) ->setAnchorName('toc') ->setNavigationMarker(true); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Table of Contents')) ->appendChild($anchor) ->appendChild($table) ->appendChild($buttons); } private function renderRename($display_file, $other_file, $arrow) { $old = explode('/', $display_file); $new = explode('/', $other_file); $start = count($old); foreach ($old as $index => $part) { if (!isset($new[$index]) || $part != $new[$index]) { $start = $index; break; } } $end = count($old); foreach (array_reverse($old) as $from_end => $part) { $index = count($new) - $from_end - 1; if (!isset($new[$index]) || $part != $new[$index]) { $end = $from_end; break; } } $rename = '{'. implode('/', array_slice($old, $start, count($old) - $end - $start)). ' '.$arrow.' '. implode('/', array_slice($new, $start, count($new) - $end - $start)). '}'; array_splice($new, $start, count($new) - $end - $start, $rename); return implode('/', $new); } private function renderCoverage(array $coverage, $file) { $info = idx($coverage, $file); if (!$info) { return null; } $not_covered = substr_count($info, 'U'); $covered = substr_count($info, 'C'); if (!$not_covered && !$covered) { return null; } return sprintf('%d%%', 100 * ($covered / ($covered + $not_covered))); } private function renderChangesetLink( DifferentialChangeset $changeset, $ref, $display_file) { return javelin_tag( 'a', array( 'href' => '#'.$changeset->getAnchorName(), 'sigil' => 'differential-load', 'meta' => array( 'id' => 'diff-'.$changeset->getAnchorName(), ), ), $display_file); } } diff --git a/src/applications/differential/view/DifferentialLocalCommitsView.php b/src/applications/differential/view/DifferentialLocalCommitsView.php index 8082d05ab4..489674b0ab 100644 --- a/src/applications/differential/view/DifferentialLocalCommitsView.php +++ b/src/applications/differential/view/DifferentialLocalCommitsView.php @@ -1,155 +1,156 @@ <?php final class DifferentialLocalCommitsView extends AphrontView { private $localCommits; private $commitsForLinks = array(); public function setLocalCommits($local_commits) { $this->localCommits = $local_commits; return $this; } public function setCommitsForLinks(array $commits) { assert_instances_of($commits, 'PhabricatorRepositoryCommit'); $this->commitsForLinks = $commits; return $this; } public function render() { $user = $this->user; if (!$user) { throw new Exception('Call setUser() before render()-ing this view.'); } $local = $this->localCommits; if (!$local) { return null; } $has_tree = false; $has_local = false; foreach ($local as $commit) { if (idx($commit, 'tree')) { $has_tree = true; } if (idx($commit, 'local')) { $has_local = true; } } $rows = array(); foreach ($local as $commit) { $row = array(); if (idx($commit, 'commit')) { $commit_link = $this->buildCommitLink($commit['commit']); } else if (isset($commit['rev'])) { $commit_link = $this->buildCommitLink($commit['rev']); } else { $commit_link = null; } $row[] = $commit_link; if ($has_tree) { $row[] = $this->buildCommitLink($commit['tree']); } if ($has_local) { $row[] = $this->buildCommitLink($commit['local']); } $parents = idx($commit, 'parents', array()); foreach ($parents as $k => $parent) { if (is_array($parent)) { $parent = idx($parent, 'rev'); } $parents[$k] = $this->buildCommitLink($parent); } $parents = phutil_implode_html(phutil_tag('br'), $parents); $row[] = $parents; $author = nonempty( idx($commit, 'user'), idx($commit, 'author')); $row[] = $author; $message = idx($commit, 'message'); $summary = idx($commit, 'summary'); $summary = id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(80) ->truncateString($summary); $view = new AphrontMoreView(); $view->setSome($summary); if ($message && (trim($summary) != trim($message))) { $view->setMore(phutil_escape_html_newlines($message)); } $row[] = $view->render(); $date = nonempty( idx($commit, 'date'), idx($commit, 'time')); if ($date) { $date = phabricator_datetime($date, $user); } $row[] = $date; $rows[] = $row; } $column_classes = array(''); if ($has_tree) { $column_classes[] = ''; } if ($has_local) { $column_classes[] = ''; } $column_classes[] = ''; $column_classes[] = ''; $column_classes[] = 'wide'; $column_classes[] = 'date'; $table = id(new AphrontTableView($rows)) ->setColumnClasses($column_classes); $headers = array(); $headers[] = pht('Commit'); if ($has_tree) { $headers[] = pht('Tree'); } if ($has_local) { $headers[] = pht('Local'); } $headers[] = pht('Parents'); $headers[] = pht('Author'); $headers[] = pht('Summary'); $headers[] = pht('Date'); $table->setHeaders($headers); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Local Commits')) ->appendChild($table); } private static function formatCommit($commit) { return substr($commit, 0, 12); } private function buildCommitLink($hash) { $commit_for_link = idx($this->commitsForLinks, $hash); $commit_hash = self::formatCommit($hash); if ($commit_for_link) { $link = phutil_tag( 'a', array( - 'href' => $commit_for_link->getURI()), + 'href' => $commit_for_link->getURI(), + ), $commit_hash); } else { $link = $commit_hash; } return $link; } } diff --git a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php index 463f6c1752..e37e952d8f 100644 --- a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php +++ b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php @@ -1,429 +1,429 @@ <?php final class DifferentialRevisionUpdateHistoryView extends AphrontView { private $diffs = array(); private $selectedVersusDiffID; private $selectedDiffID; private $selectedWhitespace; private $commitsForLinks = array(); public function setDiffs(array $diffs) { assert_instances_of($diffs, 'DifferentialDiff'); $this->diffs = $diffs; return $this; } public function setSelectedVersusDiffID($id) { $this->selectedVersusDiffID = $id; return $this; } public function setSelectedDiffID($id) { $this->selectedDiffID = $id; return $this; } public function setSelectedWhitespace($whitespace) { $this->selectedWhitespace = $whitespace; return $this; } public function setCommitsForLinks(array $commits) { assert_instances_of($commits, 'PhabricatorRepositoryCommit'); $this->commitsForLinks = $commits; return $this; } public function render() { $this->requireResource('differential-core-view-css'); $this->requireResource('differential-revision-history-css'); $data = array( array( 'name' => 'Base', 'id' => null, 'desc' => 'Base', 'age' => null, 'obj' => null, ), ); $seq = 0; foreach ($this->diffs as $diff) { $data[] = array( 'name' => 'Diff '.(++$seq), 'id' => $diff->getID(), 'desc' => $diff->getDescription(), 'age' => $diff->getDateCreated(), 'obj' => $diff, ); } $max_id = $diff->getID(); $idx = 0; $rows = array(); $disable = false; $radios = array(); $last_base = null; $rowc = array(); foreach ($data as $row) { $diff = $row['obj']; $name = $row['name']; $id = $row['id']; $old_class = false; $new_class = false; if ($id) { $new_checked = ($this->selectedDiffID == $id); $new = javelin_tag( 'input', array( 'type' => 'radio', 'name' => 'id', 'value' => $id, 'checked' => $new_checked ? 'checked' : null, 'sigil' => 'differential-new-radio', )); if ($new_checked) { $new_class = true; $disable = true; } $new = phutil_tag( 'div', array( 'class' => 'differential-update-history-radio', ), $new); } else { $new = null; } if ($max_id != $id) { $uniq = celerity_generate_unique_node_id(); $old_checked = ($this->selectedVersusDiffID == $id); $old = phutil_tag( 'input', array( 'type' => 'radio', 'name' => 'vs', 'value' => $id, 'id' => $uniq, 'checked' => $old_checked ? 'checked' : null, 'disabled' => $disable ? 'disabled' : null, )); $radios[] = $uniq; if ($old_checked) { $old_class = true; } $old = phutil_tag( 'div', array( 'class' => 'differential-update-history-radio', ), $old); } else { $old = null; } $desc = $row['desc']; if ($row['age']) { $age = phabricator_datetime($row['age'], $this->getUser()); } else { $age = null; } if ($diff) { $lint = self::renderDiffLintStar($row['obj']); $lint = phutil_tag( 'div', array( 'class' => 'lintunit-star', 'title' => self::getDiffLintMessage($diff), ), $lint); $unit = self::renderDiffUnitStar($row['obj']); $unit = phutil_tag( 'div', array( 'class' => 'lintunit-star', 'title' => self::getDiffUnitMessage($diff), ), $unit); $base = $this->renderBaseRevision($diff); } else { $lint = null; $unit = null; $base = null; } if ($last_base !== null && $base !== $last_base) { // TODO: Render some kind of notice about rebases. } $last_base = $base; $id_link = phutil_tag( 'a', array( 'href' => '/differential/diff/'.$id.'/', ), $id); $rows[] = array( $name, $id_link, $base, $desc, $age, $lint, $unit, $old, $new, ); $classes = array(); if ($old_class) { $classes[] = 'differential-update-history-old-now'; } if ($new_class) { $classes[] = 'differential-update-history-new-now'; } $rowc[] = nonempty(implode(' ', $classes), null); } Javelin::initBehavior( 'differential-diff-radios', array( 'radios' => $radios, )); $options = array( DifferentialChangesetParser::WHITESPACE_IGNORE_FORCE => 'Ignore All', DifferentialChangesetParser::WHITESPACE_IGNORE_ALL => 'Ignore Most', DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING => 'Ignore Trailing', DifferentialChangesetParser::WHITESPACE_SHOW_ALL => 'Show All', ); foreach ($options as $value => $label) { $options[$value] = phutil_tag( 'option', array( 'value' => $value, 'selected' => ($value == $this->selectedWhitespace) ? 'selected' : null, ), $label); } $select = phutil_tag('select', array('name' => 'whitespace'), $options); $table = id(new AphrontTableView($rows)); $table->setHeaders( array( pht('Diff'), pht('ID'), pht('Base'), pht('Description'), pht('Created'), pht('Lint'), pht('Unit'), '', '', )); $table->setColumnClasses( array( 'pri', '', '', 'wide', 'date', 'center', 'center', 'center differential-update-history-old', 'center differential-update-history-new', )); $table->setRowClasses($rowc); $table->setDeviceVisibility( array( true, true, false, true, false, false, false, true, true, )); $show_diff = phutil_tag( 'div', array( 'class' => 'differential-update-history-footer', ), array( phutil_tag( 'label', array(), array( pht('Whitespace Changes:'), $select, )), phutil_tag( 'button', array(), pht('Show Diff')), )); $content = phabricator_form( $this->getUser(), array( 'action' => '#toc', ), array( $table, $show_diff, )); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Revision Update History')) ->setFlush(true) ->appendChild($content); } const STAR_NONE = 'none'; const STAR_OKAY = 'okay'; const STAR_WARN = 'warn'; const STAR_FAIL = 'fail'; const STAR_SKIP = 'skip'; public static function renderDiffLintStar(DifferentialDiff $diff) { static $map = array( DifferentialLintStatus::LINT_NONE => self::STAR_NONE, DifferentialLintStatus::LINT_OKAY => self::STAR_OKAY, DifferentialLintStatus::LINT_WARN => self::STAR_WARN, DifferentialLintStatus::LINT_FAIL => self::STAR_FAIL, DifferentialLintStatus::LINT_SKIP => self::STAR_SKIP, DifferentialLintStatus::LINT_AUTO_SKIP => self::STAR_SKIP, - DifferentialLintStatus::LINT_POSTPONED => self::STAR_SKIP + DifferentialLintStatus::LINT_POSTPONED => self::STAR_SKIP, ); $star = idx($map, $diff->getLintStatus(), self::STAR_FAIL); return self::renderDiffStar($star); } public static function renderDiffUnitStar(DifferentialDiff $diff) { static $map = array( DifferentialUnitStatus::UNIT_NONE => self::STAR_NONE, DifferentialUnitStatus::UNIT_OKAY => self::STAR_OKAY, DifferentialUnitStatus::UNIT_WARN => self::STAR_WARN, DifferentialUnitStatus::UNIT_FAIL => self::STAR_FAIL, DifferentialUnitStatus::UNIT_SKIP => self::STAR_SKIP, DifferentialUnitStatus::UNIT_AUTO_SKIP => self::STAR_SKIP, DifferentialUnitStatus::UNIT_POSTPONED => self::STAR_SKIP, ); $star = idx($map, $diff->getUnitStatus(), self::STAR_FAIL); return self::renderDiffStar($star); } public static function getDiffLintMessage(DifferentialDiff $diff) { switch ($diff->getLintStatus()) { case DifferentialLintStatus::LINT_NONE: return pht('No Linters Available'); case DifferentialLintStatus::LINT_OKAY: return pht('Lint OK'); case DifferentialLintStatus::LINT_WARN: return pht('Lint Warnings'); case DifferentialLintStatus::LINT_FAIL: return pht('Lint Errors'); case DifferentialLintStatus::LINT_SKIP: return pht('Lint Skipped'); case DifferentialLintStatus::LINT_AUTO_SKIP: return pht('Automatic diff as part of commit; lint not applicable.'); case DifferentialLintStatus::LINT_POSTPONED: return pht('Lint Postponed'); } return '???'; } public static function getDiffUnitMessage(DifferentialDiff $diff) { switch ($diff->getUnitStatus()) { case DifferentialUnitStatus::UNIT_NONE: return pht('No Unit Test Coverage'); case DifferentialUnitStatus::UNIT_OKAY: return pht('Unit Tests OK'); case DifferentialUnitStatus::UNIT_WARN: return pht('Unit Test Warnings'); case DifferentialUnitStatus::UNIT_FAIL: return pht('Unit Test Errors'); case DifferentialUnitStatus::UNIT_SKIP: return pht('Unit Tests Skipped'); case DifferentialUnitStatus::UNIT_AUTO_SKIP: return pht( 'Automatic diff as part of commit; unit tests not applicable.'); case DifferentialUnitStatus::UNIT_POSTPONED: return pht('Unit Tests Postponed'); } return '???'; } private static function renderDiffStar($star) { $class = 'diff-star-'.$star; return phutil_tag( 'span', array('class' => $class), "\xE2\x98\x85"); } private function renderBaseRevision(DifferentialDiff $diff) { switch ($diff->getSourceControlSystem()) { case 'git': $base = $diff->getSourceControlBaseRevision(); if (strpos($base, '@') === false) { $label = substr($base, 0, 7); } else { // The diff is from git-svn $base = explode('@', $base); $base = last($base); $label = $base; } break; case 'svn': $base = $diff->getSourceControlBaseRevision(); $base = explode('@', $base); $base = last($base); $label = $base; break; default: $label = null; break; } $link = null; if ($label) { $commit_for_link = idx( $this->commitsForLinks, $diff->getSourceControlBaseRevision()); if ($commit_for_link) { $link = phutil_tag( 'a', array('href' => $commit_for_link->getURI()), $label); } else { $link = $label; } } return $link; } } diff --git a/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php index d107b995ad..c0b311c8eb 100644 --- a/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionDiffQueryConduitAPIMethod.php @@ -1,236 +1,239 @@ <?php final class DiffusionDiffQueryConduitAPIMethod extends DiffusionQueryConduitAPIMethod { private $effectiveCommit; public function getAPIMethodName() { return 'diffusion.diffquery'; } public function getMethodDescription() { return 'Get diff information from a repository for a specific path at an '. '(optional) commit.'; } public function defineReturnType() { return 'array'; } protected function defineCustomParamTypes() { return array( 'path' => 'required string', 'commit' => 'optional string', ); } protected function getResult(ConduitAPIRequest $request) { $result = parent::getResult($request); return array( 'changes' => mpull($result, 'toDictionary'), 'effectiveCommit' => $this->getEffectiveCommit($request), ); } protected function getGitResult(ConduitAPIRequest $request) { return $this->getGitOrMercurialResult($request); } protected function getMercurialResult(ConduitAPIRequest $request) { return $this->getGitOrMercurialResult($request); } /** * NOTE: We have to work particularly hard for SVN as compared to other VCS. * That's okay but means this shares little code with the other VCS. */ protected function getSVNResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $effective_commit = $this->getEffectiveCommit($request); if (!$effective_commit) { return $this->getEmptyResult(); } $drequest = clone $drequest; $drequest->updateSymbolicCommit($effective_commit); $path_change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $drequest); $path_changes = $path_change_query->loadChanges(); $path = null; foreach ($path_changes as $change) { if ($change->getPath() == $drequest->getPath()) { $path = $change; } } if (!$path) { return $this->getEmptyResult(); } $change_type = $path->getChangeType(); switch ($change_type) { case DifferentialChangeType::TYPE_MULTICOPY: case DifferentialChangeType::TYPE_DELETE: if ($path->getTargetPath()) { $old = array( $path->getTargetPath(), - $path->getTargetCommitIdentifier()); + $path->getTargetCommitIdentifier(), + ); } else { $old = array($path->getPath(), $path->getCommitIdentifier() - 1); } $old_name = $path->getPath(); $new_name = ''; $new = null; break; case DifferentialChangeType::TYPE_ADD: $old = null; $new = array($path->getPath(), $path->getCommitIdentifier()); $old_name = ''; $new_name = $path->getPath(); break; case DifferentialChangeType::TYPE_MOVE_HERE: case DifferentialChangeType::TYPE_COPY_HERE: $old = array( $path->getTargetPath(), - $path->getTargetCommitIdentifier()); + $path->getTargetCommitIdentifier(), + ); $new = array($path->getPath(), $path->getCommitIdentifier()); $old_name = $path->getTargetPath(); $new_name = $path->getPath(); break; case DifferentialChangeType::TYPE_MOVE_AWAY: $old = array( $path->getPath(), - $path->getCommitIdentifier() - 1); + $path->getCommitIdentifier() - 1, + ); $old_name = $path->getPath(); $new_name = null; $new = null; break; default: $old = array($path->getPath(), $path->getCommitIdentifier() - 1); $new = array($path->getPath(), $path->getCommitIdentifier()); $old_name = $path->getPath(); $new_name = $path->getPath(); break; } $futures = array( 'old' => $this->buildSVNContentFuture($old), 'new' => $this->buildSVNContentFuture($new), ); $futures = array_filter($futures); foreach (Futures($futures) as $key => $future) { $stdout = ''; try { list($stdout) = $future->resolvex(); } catch (CommandException $e) { if ($path->getFileType() != DifferentialChangeType::FILE_DIRECTORY) { throw $e; } } $futures[$key] = $stdout; } $old_data = idx($futures, 'old', ''); $new_data = idx($futures, 'new', ''); $engine = new PhabricatorDifferenceEngine(); $engine->setOldName($old_name); $engine->setNewName($new_name); $raw_diff = $engine->generateRawDiffFromFileContent($old_data, $new_data); $arcanist_changes = DiffusionPathChange::convertToArcanistChanges( $path_changes); $parser = $this->getDefaultParser(); $parser->setChanges($arcanist_changes); $parser->forcePath($path->getPath()); $changes = $parser->parseDiff($raw_diff); $change = $changes[$path->getPath()]; return array($change); } private function getEffectiveCommit(ConduitAPIRequest $request) { if ($this->effectiveCommit === null) { $drequest = $this->getDiffusionRequest(); $path = $drequest->getPath(); $result = DiffusionQuery::callConduitWithDiffusionRequest( $request->getUser(), $drequest, 'diffusion.lastmodifiedquery', array( 'paths' => array($path => $drequest->getStableCommit()), )); $this->effectiveCommit = idx($result, $path); } return $this->effectiveCommit; } private function buildSVNContentFuture($spec) { if (!$spec) { return null; } $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); list($ref, $rev) = $spec; return $repository->getRemoteCommandFuture( 'cat %s', $repository->getSubversionPathURI($ref, $rev)); } private function getGitOrMercurialResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $effective_commit = $this->getEffectiveCommit($request); if (!$effective_commit) { return $this->getEmptyResult(1); } $raw_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest) ->setAnchorCommit($effective_commit); $raw_diff = $raw_query->loadRawDiff(); if (!$raw_diff) { return $this->getEmptyResult(2); } $parser = $this->getDefaultParser(); $changes = $parser->parseDiff($raw_diff); return $changes; } private function getDefaultParser() { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $parser = new ArcanistDiffParser(); $try_encoding = $repository->getDetail('encoding'); if ($try_encoding) { $parser->setTryEncoding($try_encoding); } $parser->setDetectBinaryFiles(true); return $parser; } private function getEmptyResult() { return array(); } } diff --git a/src/applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php index e349b78fbf..9728c2f1f5 100644 --- a/src/applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionExistsQueryConduitAPIMethod.php @@ -1,57 +1,57 @@ <?php final class DiffusionExistsQueryConduitAPIMethod extends DiffusionQueryConduitAPIMethod { public function getAPIMethodName() { return 'diffusion.existsquery'; } public function getMethodDescription() { return 'Determine if code exists in a version control system.'; } public function defineReturnType() { return 'bool'; } protected function defineCustomParamTypes() { return array( - 'commit' => 'required string' + 'commit' => 'required string', ); } protected function getGitResult(ConduitAPIRequest $request) { $repository = $this->getDiffusionRequest()->getRepository(); $commit = $request->getValue('commit'); list($err, $merge_base) = $repository->execLocalCommand( 'cat-file -t %s', $commit); return !$err; } protected function getSVNResult(ConduitAPIRequest $request) { $repository = $this->getDiffusionRequest()->getRepository(); $commit = $request->getValue('commit'); list($info) = $repository->execxRemoteCommand( 'info %s', $repository->getRemoteURI()); $exists = false; $matches = null; if (preg_match('/^Revision: (\d+)$/m', $info, $matches)) { $base_revision = $matches[1]; $exists = $base_revision >= $commit; } return $exists; } protected function getMercurialResult(ConduitAPIRequest $request) { $repository = $this->getDiffusionRequest()->getRepository(); $commit = $request->getValue('commit'); list($err, $stdout) = $repository->execLocalCommand( 'id --rev %s', $commit); return !$err; } } diff --git a/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php index 2de2a0ccd8..4ef28ddbad 100644 --- a/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionGetRecentCommitsByPathConduitAPIMethod.php @@ -1,69 +1,70 @@ <?php final class DiffusionGetRecentCommitsByPathConduitAPIMethod extends DiffusionConduitAPIMethod { const DEFAULT_LIMIT = 10; public function getAPIMethodName() { return 'diffusion.getrecentcommitsbypath'; } public function getMethodDescription() { return 'Get commit identifiers for recent commits affecting a given path.'; } public function defineParamTypes() { return array( 'callsign' => 'required string', 'path' => 'required string', 'branch' => 'optional string', 'limit' => 'optional int', ); } public function defineReturnType() { return 'nonempty list<string>'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $request->getUser(), 'callsign' => $request->getValue('callsign'), 'path' => $request->getValue('path'), 'branch' => $request->getValue('branch'), )); $limit = nonempty( $request->getValue('limit'), self::DEFAULT_LIMIT); $history_result = DiffusionQuery::callConduitWithDiffusionRequest( $request->getUser(), $drequest, 'diffusion.historyquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'offset' => 0, 'limit' => $limit, 'needDirectChanges' => true, - 'needChildChanges' => true)); + 'needChildChanges' => true, + )); $history = DiffusionPathChange::newFromConduit( $history_result['pathChanges']); $raw_commit_identifiers = mpull($history, 'getCommitIdentifier'); $result = array(); foreach ($raw_commit_identifiers as $id) { $result[] = 'r'.$request->getValue('callsign').$id; } return $result; } } diff --git a/src/applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php index 4340b87bbc..490ce03985 100644 --- a/src/applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php @@ -1,141 +1,142 @@ <?php final class DiffusionQueryCommitsConduitAPIMethod extends DiffusionConduitAPIMethod { public function getAPIMethodName() { return 'diffusion.querycommits'; } public function getMethodDescription() { return pht('Retrieve information about commits.'); } public function defineReturnType() { return 'map<string, dict>'; } public function defineParamTypes() { return array( 'ids' => 'optional list<int>', 'phids' => 'optional list<phid>', 'names' => 'optional list<string>', 'repositoryPHID' => 'optional phid', 'needMessages' => 'optional bool', 'bypassCache' => 'optional bool', ) + $this->getPagerParamTypes(); } public function defineErrorTypes() { return array(); } protected function execute(ConduitAPIRequest $request) { $need_messages = $request->getValue('needMessages'); $bypass_cache = $request->getValue('bypassCache'); $query = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->needCommitData(true); $repository_phid = $request->getValue('repositoryPHID'); if ($repository_phid) { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($request->getUser()) ->withPHIDs(array($repository_phid)) ->executeOne(); if ($repository) { $query->withRepository($repository); } } $names = $request->getValue('names'); if ($names) { $query->withIdentifiers($names); } $ids = $request->getValue('ids'); if ($ids) { $query->withIDs($ids); } $phids = $request->getValue('phids'); if ($phids) { $query->withPHIDs($phids); } $pager = $this->newPager($request); $commits = $query->executeWithCursorPager($pager); $map = $query->getIdentifierMap(); $map = mpull($map, 'getPHID'); $data = array(); foreach ($commits as $commit) { $commit_data = $commit->getCommitData(); $callsign = $commit->getRepository()->getCallsign(); $identifier = $commit->getCommitIdentifier(); $uri = '/r'.$callsign.$identifier; $uri = PhabricatorEnv::getProductionURI($uri); $dict = array( 'id' => $commit->getID(), 'phid' => $commit->getPHID(), 'repositoryPHID' => $commit->getRepository()->getPHID(), 'identifier' => $identifier, 'epoch' => $commit->getEpoch(), 'uri' => $uri, 'isImporting' => !$commit->isImported(), 'summary' => $commit->getSummary(), 'authorPHID' => $commit->getAuthorPHID(), 'committerPHID' => $commit_data->getCommitDetail('committerPHID'), 'author' => $commit_data->getAuthorName(), 'authorName' => $commit_data->getCommitDetail('authorName'), 'authorEmail' => $commit_data->getCommitDetail('authorEmail'), 'committer' => $commit_data->getCommitDetail('committer'), 'committerName' => $commit_data->getCommitDetail('committerName'), 'committerEmail' => $commit_data->getCommitDetail('committerEmail'), 'hashes' => array(), ); if ($bypass_cache) { $lowlevel_commitref = id(new DiffusionLowLevelCommitQuery()) ->setRepository($commit->getRepository()) ->withIdentifier($commit->getCommitIdentifier()) ->execute(); $dict['author'] = $lowlevel_commitref->getAuthor(); $dict['authorName'] = $lowlevel_commitref->getAuthorName(); $dict['authorEmail'] = $lowlevel_commitref->getAuthorEmail(); $dict['committer'] = $lowlevel_commitref->getCommitter(); $dict['committerName'] = $lowlevel_commitref->getCommitterName(); $dict['committerEmail'] = $lowlevel_commitref->getCommitterEmail(); if ($need_messages) { $dict['message'] = $lowlevel_commitref->getMessage(); } foreach ($lowlevel_commitref->getHashes() as $hash) { $dict['hashes'][] = array( 'type' => $hash->getHashType(), - 'value' => $hash->getHashValue()); + 'value' => $hash->getHashValue(), + ); } } if ($need_messages && !$bypass_cache) { $dict['message'] = $commit_data->getCommitMessage(); } $data[$commit->getPHID()] = $dict; } $result = array( 'data' => $data, 'identifierMap' => nonempty($map, (object)array()), ); return $this->addPagerResults($result, $pager); } } diff --git a/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php index ec8e2e4535..21f1f4e705 100644 --- a/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionQueryConduitAPIMethod.php @@ -1,132 +1,133 @@ <?php abstract class DiffusionQueryConduitAPIMethod extends DiffusionConduitAPIMethod { public function shouldAllowPublic() { return true; } public function getMethodStatus() { return self::METHOD_STATUS_UNSTABLE; } public function getMethodStatusDescription() { return pht( 'See T2784 - migrating diffusion working copy calls to conduit methods. '. 'Until that task is completed (and possibly after) these methods are '. 'unstable.'); } private $diffusionRequest; private $repository; protected function setDiffusionRequest(DiffusionRequest $request) { $this->diffusionRequest = $request; return $this; } protected function getDiffusionRequest() { return $this->diffusionRequest; } protected function getRepository(ConduitAPIRequest $request) { return $this->getDiffusionRequest()->getRepository(); } final public function defineErrorTypes() { return $this->defineCustomErrorTypes() + array( 'ERR-UNKNOWN-REPOSITORY' => pht('There is no repository with that callsign.'), 'ERR-UNKNOWN-VCS-TYPE' => pht('Unknown repository VCS type.'), 'ERR-UNSUPPORTED-VCS' => - pht('VCS is not supported for this method.')); + pht('VCS is not supported for this method.'), + ); } /** * Subclasses should override this to specify custom error types. */ protected function defineCustomErrorTypes() { return array(); } final public function defineParamTypes() { return $this->defineCustomParamTypes() + array( 'callsign' => 'required string', 'branch' => 'optional string', ); } /** * Subclasses should override this to specify custom param types. */ protected function defineCustomParamTypes() { return array(); } /** * Subclasses should override these methods with the proper result for the * pertinent version control system, e.g. getGitResult for Git. * * If the result is not supported for that VCS, do not implement it. e.g. * Subversion (SVN) does not support branches. */ protected function getGitResult(ConduitAPIRequest $request) { throw new ConduitException('ERR-UNSUPPORTED-VCS'); } protected function getSVNResult(ConduitAPIRequest $request) { throw new ConduitException('ERR-UNSUPPORTED-VCS'); } protected function getMercurialResult(ConduitAPIRequest $request) { throw new ConduitException('ERR-UNSUPPORTED-VCS'); } /** * This method is final because most queries will need to construct a * @{class:DiffusionRequest} and use it. Consolidating this codepath and * enforcing @{method:getDiffusionRequest} works when we need it is good. * * @{method:getResult} should be overridden by subclasses as necessary, e.g. * there is a common operation across all version control systems that * should occur after @{method:getResult}, like formatting a timestamp. */ final protected function execute(ConduitAPIRequest $request) { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $request->getUser(), 'callsign' => $request->getValue('callsign'), 'branch' => $request->getValue('branch'), 'path' => $request->getValue('path'), 'commit' => $request->getValue('commit'), 'initFromConduit' => false, )); $this->setDiffusionRequest($drequest); return $this->getResult($request); } protected function getResult(ConduitAPIRequest $request) { $repository = $this->getRepository($request); $result = null; switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = $this->getGitResult($request); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $result = $this->getMercurialResult($request); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = $this->getSVNResult($request); break; default: throw new ConduitException('ERR-UNKNOWN-VCS-TYPE'); break; } return $result; } } diff --git a/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php index 2f9dd83c68..38a44de6b5 100644 --- a/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php @@ -1,58 +1,59 @@ <?php final class DiffusionRefsQueryConduitAPIMethod extends DiffusionQueryConduitAPIMethod { public function getAPIMethodName() { return 'diffusion.refsquery'; } public function getMethodDescription() { return 'Query a git repository for ref information at a specific commit.'; } public function defineReturnType() { return 'array'; } protected function defineCustomParamTypes() { return array( 'commit' => 'required string', ); } protected function getGitResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $commit = $request->getValue('commit'); list($stdout) = $repository->execxLocalCommand( 'log --format=%s -n 1 %s --', '%d', $commit); // %d, gives a weird output format // similar to (remote/one, remote/two, remote/three) $refs = trim($stdout, "() \n"); if (!$refs) { return array(); } $refs = explode(',', $refs); $refs = array_map('trim', $refs); $ref_links = array(); foreach ($refs as $ref) { $ref_links[] = array( 'ref' => $ref, 'href' => $drequest->generateURI( array( 'action' => 'browse', 'branch' => $ref, - ))); + )), + ); } return $ref_links; } } diff --git a/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php index 434e6b78e5..db73f6b79d 100644 --- a/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php @@ -1,117 +1,118 @@ <?php final class DiffusionSearchQueryConduitAPIMethod extends DiffusionQueryConduitAPIMethod { public function getAPIMethodName() { return 'diffusion.searchquery'; } public function getMethodDescription() { return 'Search (grep) a repository at a specific path and commit.'; } public function defineReturnType() { return 'array'; } protected function defineCustomParamTypes() { return array( 'path' => 'required string', 'commit' => 'optional string', 'grep' => 'required string', 'limit' => 'optional int', 'offset' => 'optional int', ); } protected function defineCustomErrorTypes() { return array( - 'ERR-GREP-COMMAND' => 'Grep command failed.'); + 'ERR-GREP-COMMAND' => 'Grep command failed.', + ); } protected function getResult(ConduitAPIRequest $request) { try { $results = parent::getResult($request); } catch (CommandException $ex) { throw id(new ConduitException('ERR-GREP-COMMAND')) ->setErrorDescription($ex->getStderr()); } $offset = $request->getValue('offset'); $results = array_slice($results, $offset); return $results; } protected function getGitResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $path = $drequest->getPath(); $grep = $request->getValue('grep'); $repository = $drequest->getRepository(); $limit = $request->getValue('limit'); $offset = $request->getValue('offset'); $results = array(); $future = $repository->getLocalCommandFuture( // NOTE: --perl-regexp is available only with libpcre compiled in. 'grep --extended-regexp --null -n --no-color -e %s %s -- %s', $grep, $drequest->getStableCommit(), $path); $binary_pattern = '/Binary file [^:]*:(.+) matches/'; $lines = new LinesOfALargeExecFuture($future); foreach ($lines as $line) { $result = null; if (preg_match('/[^:]*:(.+)\0(.+)\0(.*)/', $line, $result)) { $results[] = array_slice($result, 1); } else if (preg_match($binary_pattern, $line, $result)) { list(, $path) = $result; $results[] = array($path, null, pht('Binary file')); } else { $results[] = array(null, null, $line); } if (count($results) >= $offset + $limit) { break; } } unset($lines); return $results; } protected function getMercurialResult(ConduitAPIRequest $request) { $drequest = $this->getDiffusionRequest(); $path = $drequest->getPath(); $grep = $request->getValue('grep'); $repository = $drequest->getRepository(); $limit = $request->getValue('limit'); $offset = $request->getValue('offset'); $results = array(); $future = $repository->getLocalCommandFuture( 'grep --rev %s --print0 --line-number %s %s', hgsprintf('ancestors(%s)', $drequest->getStableCommit()), $grep, $path); $lines = id(new LinesOfALargeExecFuture($future))->setDelimiter("\0"); $parts = array(); foreach ($lines as $line) { $parts[] = $line; if (count($parts) == 4) { list($path, $char_offset, $line, $string) = $parts; $results[] = array($path, $line, $string); if (count($results) >= $offset + $limit) { break; } $parts = array(); } } unset($lines); return $results; } } diff --git a/src/applications/diffusion/controller/DiffusionBranchTableController.php b/src/applications/diffusion/controller/DiffusionBranchTableController.php index e4c1311c91..aa36662093 100644 --- a/src/applications/diffusion/controller/DiffusionBranchTableController.php +++ b/src/applications/diffusion/controller/DiffusionBranchTableController.php @@ -1,76 +1,76 @@ <?php final class DiffusionBranchTableController extends DiffusionController { public function shouldAllowPublic() { return true; } public function processRequest() { $drequest = $this->getDiffusionRequest(); $request = $this->getRequest(); $viewer = $request->getUser(); $repository = $drequest->getRepository(); $pager = new AphrontPagerView(); $pager->setURI($request->getRequestURI(), 'offset'); $pager->setOffset($request->getInt('offset')); // TODO: Add support for branches that contain commit $branches = $this->callConduitWithDiffusionRequest( 'diffusion.branchquery', array( 'offset' => $pager->getOffset(), - 'limit' => $pager->getPageSize() + 1 + 'limit' => $pager->getPageSize() + 1, )); $branches = $pager->sliceResults($branches); $branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches); $content = null; if (!$branches) { $content = $this->renderStatusMessage( pht('No Branches'), pht('This repository has no branches.')); } else { $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers(mpull($branches, 'getCommitIdentifier')) ->withRepository($repository) ->execute(); $view = id(new DiffusionBranchTableView()) ->setUser($viewer) ->setBranches($branches) ->setCommits($commits) ->setDiffusionRequest($drequest); $panel = id(new AphrontPanelView()) ->setNoBackground(true) ->appendChild($view) ->appendChild($pager); $content = $panel; } $crumbs = $this->buildCrumbs( array( 'branches' => true, )); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => array( pht('Branches'), 'r'.$repository->getCallsign(), ), 'device' => false, )); } } diff --git a/src/applications/diffusion/controller/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/DiffusionBrowseFileController.php index a8d11431aa..6e04da9b18 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseFileController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseFileController.php @@ -1,1093 +1,1094 @@ <?php final class DiffusionBrowseFileController extends DiffusionBrowseController { private $lintCommit; private $lintMessages; private $coverage; public function processRequest() { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); $viewer = $request->getUser(); $before = $request->getStr('before'); if ($before) { return $this->buildBeforeResponse($before); } $path = $drequest->getPath(); $preferences = $viewer->loadPreferences(); $show_blame = $request->getBool( 'blame', $preferences->getPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME, false)); $show_color = $request->getBool( 'color', $preferences->getPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR, true)); $view = $request->getStr('view'); if ($request->isFormPost() && $view != 'raw' && $viewer->isLoggedIn()) { $preferences->setPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_BLAME, $show_blame); $preferences->setPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_COLOR, $show_color); $preferences->save(); $uri = $request->getRequestURI() ->alter('blame', null) ->alter('color', null); return id(new AphrontRedirectResponse())->setURI($uri); } // We need the blame information if blame is on and we're building plain // text, or blame is on and this is an Ajax request. If blame is on and // this is a colorized request, we don't show blame at first (we ajax it // in afterward) so we don't need to query for it. $needs_blame = ($show_blame && !$show_color) || ($show_blame && $request->isAjax()); $file_content = DiffusionFileContent::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.filecontentquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'needsBlame' => $needs_blame, ))); $data = $file_content->getCorpus(); if ($view === 'raw') { return $this->buildRawResponse($path, $data); } $this->loadLintMessages(); $this->coverage = $drequest->loadCoverage(); $binary_uri = null; if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) { $file = $this->loadFileForData($path, $data); $file_uri = $file->getBestURI(); if ($file->isViewableImage()) { $corpus = $this->buildImageCorpus($file_uri); } else { $corpus = $this->buildBinaryCorpus($file_uri, $data); $binary_uri = $file_uri; } } else { // Build the content of the file. $corpus = $this->buildCorpus( $show_blame, $show_color, $file_content, $needs_blame, $drequest, $path, $data); } if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($corpus); } require_celerity_resource('diffusion-source-css'); // Render the page. $view = $this->buildActionView($drequest); $action_list = $this->enrichActionView( $view, $drequest, $show_blame, $show_color); $properties = $this->buildPropertyView($drequest, $action_list); $object_box = id(new PHUIObjectBoxView()) ->setHeader($this->buildHeaderView($drequest)) ->addPropertyList($properties); $content = array(); $content[] = $object_box; $follow = $request->getStr('follow'); if ($follow) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_WARNING); $notice->setTitle(pht('Unable to Continue')); switch ($follow) { case 'first': $notice->appendChild( pht('Unable to continue tracing the history of this file because '. 'this commit is the first commit in the repository.')); break; case 'created': $notice->appendChild( pht('Unable to continue tracing the history of this file because '. 'this commit created the file.')); break; } $content[] = $notice; } $renamed = $request->getStr('renamed'); if ($renamed) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('File Renamed')); $notice->appendChild( pht("File history passes through a rename from '%s' to '%s'.", $drequest->getPath(), $renamed)); $content[] = $notice; } $content[] = $corpus; $content[] = $this->buildOpenRevisions(); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $basename = basename($this->getDiffusionRequest()->getPath()); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => $basename, 'device' => false, )); } private function loadLintMessages() { $drequest = $this->getDiffusionRequest(); $branch = $drequest->loadBranch(); if (!$branch || !$branch->getLintCommit()) { return; } $this->lintCommit = $branch->getLintCommit(); $conn = id(new PhabricatorRepository())->establishConnection('r'); $where = ''; if ($drequest->getLint()) { $where = qsprintf( $conn, 'AND code = %s', $drequest->getLint()); } $this->lintMessages = queryfx_all( $conn, 'SELECT * FROM %T WHERE branchID = %d %Q AND path = %s', PhabricatorRepository::TABLE_LINTMESSAGE, $branch->getID(), $where, '/'.$drequest->getPath()); } private function buildCorpus( $show_blame, $show_color, DiffusionFileContent $file_content, $needs_blame, DiffusionRequest $drequest, $path, $data) { if (!$show_color) { $style = 'border: none; width: 100%; height: 80em; font-family: monospace'; if (!$show_blame) { $corpus = phutil_tag( 'textarea', array( 'style' => $style, ), $file_content->getCorpus()); } else { $text_list = $file_content->getTextList(); $rev_list = $file_content->getRevList(); $blame_dict = $file_content->getBlameDict(); $rows = array(); foreach ($text_list as $k => $line) { $rev = $rev_list[$k]; $author = $blame_dict[$rev]['author']; $rows[] = sprintf('%-10s %-20s %s', substr($rev, 0, 7), $author, $line); } $corpus = phutil_tag( 'textarea', array( 'style' => $style, ), implode("\n", $rows)); } } else { require_celerity_resource('syntax-highlighting-css'); $text_list = $file_content->getTextList(); $rev_list = $file_content->getRevList(); $blame_dict = $file_content->getBlameDict(); $text_list = implode("\n", $text_list); $text_list = PhabricatorSyntaxHighlighter::highlightWithFilename( $path, $text_list); $text_list = explode("\n", $text_list); $rows = $this->buildDisplayRows($text_list, $rev_list, $blame_dict, $needs_blame, $drequest, $show_blame, $show_color); $corpus_table = javelin_tag( 'table', array( 'class' => 'diffusion-source remarkup-code PhabricatorMonospaced', 'sigil' => 'phabricator-source', ), $rows); if ($this->getRequest()->isAjax()) { return $corpus_table; } $id = celerity_generate_unique_node_id(); $projects = $drequest->loadArcanistProjects(); $langs = array(); foreach ($projects as $project) { $ls = $project->getSymbolIndexLanguages(); if (!$ls) { continue; } $dep_projects = $project->getSymbolIndexProjects(); $dep_projects[] = $project->getPHID(); foreach ($ls as $lang) { if (!isset($langs[$lang])) { $langs[$lang] = array(); } $langs[$lang] += $dep_projects + array($project); } } $lang = last(explode('.', $drequest->getPath())); if (isset($langs[$lang])) { Javelin::initBehavior( 'repository-crossreference', array( 'container' => $id, 'lang' => $lang, 'projects' => $langs[$lang], )); } $corpus = phutil_tag( 'div', array( 'id' => $id, ), $corpus_table); Javelin::initBehavior('load-blame', array('id' => $id)); } $edit = $this->renderEditButton(); $file = $this->renderFileButton(); $header = id(new PHUIHeaderView()) ->setHeader(pht('File Contents')) ->addActionLink($edit) ->addActionLink($file); $corpus = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($corpus); return $corpus; } private function enrichActionView( PhabricatorActionListView $view, DiffusionRequest $drequest, $show_blame, $show_color) { $viewer = $this->getRequest()->getUser(); $base_uri = $this->getRequest()->getRequestURI(); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Show Last Change')) ->setHref( $drequest->generateURI( array( 'action' => 'change', ))) ->setIcon('fa-backward')); if ($show_blame) { $blame_text = pht('Disable Blame'); $blame_icon = 'fa-exclamation-circle lightgreytext'; $blame_value = 0; } else { $blame_text = pht('Enable Blame'); $blame_icon = 'fa-exclamation-circle'; $blame_value = 1; } $view->addAction( id(new PhabricatorActionView()) ->setName($blame_text) ->setHref($base_uri->alter('blame', $blame_value)) ->setIcon($blame_icon) ->setUser($viewer) ->setRenderAsForm($viewer->isLoggedIn())); if ($show_color) { $highlight_text = pht('Disable Highlighting'); $highlight_icon = 'fa-star-o grey'; $highlight_value = 0; } else { $highlight_text = pht('Enable Highlighting'); $highlight_icon = 'fa-star'; $highlight_value = 1; } $view->addAction( id(new PhabricatorActionView()) ->setName($highlight_text) ->setHref($base_uri->alter('color', $highlight_value)) ->setIcon($highlight_icon) ->setUser($viewer) ->setRenderAsForm($viewer->isLoggedIn())); $href = null; if ($this->getRequest()->getStr('lint') !== null) { $lint_text = pht('Hide %d Lint Message(s)', count($this->lintMessages)); $href = $base_uri->alter('lint', null); } else if ($this->lintCommit === null) { $lint_text = pht('Lint not Available'); } else { $lint_text = pht( 'Show %d Lint Message(s)', count($this->lintMessages)); $href = $this->getDiffusionRequest()->generateURI(array( 'action' => 'browse', 'commit' => $this->lintCommit, ))->alter('lint', ''); } $view->addAction( id(new PhabricatorActionView()) ->setName($lint_text) ->setHref($href) ->setIcon('fa-exclamation-triangle') ->setDisabled(!$href)); return $view; } private function renderEditButton() { $request = $this->getRequest(); $user = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $line = nonempty((int)$drequest->getLine(), 1); $callsign = $repository->getCallsign(); $editor_link = $user->loadEditorLink($path, $line, $callsign); $template = $user->loadEditorLink($path, '%l', $callsign); $icon_edit = id(new PHUIIconView()) ->setIconFont('fa-pencil'); $button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Open in Editor')) ->setHref($editor_link) ->setIcon($icon_edit) ->setID('editor_link') ->setMetadata(array('link_template' => $template)) ->setDisabled(!$editor_link); return $button; } private function renderFileButton($file_uri = null) { $base_uri = $this->getRequest()->getRequestURI(); if ($file_uri) { $text = pht('Download Raw File'); $href = $file_uri; $icon = 'fa-download'; } else { $text = pht('View Raw File'); $href = $base_uri->alter('view', 'raw'); $icon = 'fa-file-text'; } $iconview = id(new PHUIIconView()) ->setIconFont($icon); $button = id(new PHUIButtonView()) ->setTag('a') ->setText($text) ->setHref($href) ->setIcon($iconview); return $button; } private function buildDisplayRows( array $text_list, array $rev_list, array $blame_dict, $needs_blame, DiffusionRequest $drequest, $show_blame, $show_color) { $handles = array(); if ($blame_dict) { $epoch_list = ipull(ifilter($blame_dict, 'epoch'), 'epoch'); $epoch_min = min($epoch_list); $epoch_max = max($epoch_list); $epoch_range = ($epoch_max - $epoch_min) + 1; $author_phids = ipull(ifilter($blame_dict, 'authorPHID'), 'authorPHID'); $handles = $this->loadViewerHandles($author_phids); } $line_arr = array(); $line_str = $drequest->getLine(); $ranges = explode(',', $line_str); foreach ($ranges as $range) { if (strpos($range, '-') !== false) { list($min, $max) = explode('-', $range, 2); $line_arr[] = array( 'min' => min($min, $max), 'max' => max($min, $max), ); } else if (strlen($range)) { $line_arr[] = array( 'min' => $range, 'max' => $range, ); } } $display = array(); $line_number = 1; $last_rev = null; $color = null; foreach ($text_list as $k => $line) { $display_line = array( 'epoch' => null, 'commit' => null, 'author' => null, 'target' => null, 'highlighted' => null, 'line' => $line_number, 'data' => $line, ); if ($show_blame) { // If the line's rev is same as the line above, show empty content // with same color; otherwise generate blame info. The newer a change // is, the more saturated the color. $rev = idx($rev_list, $k, $last_rev); if ($last_rev == $rev) { $display_line['color'] = $color; } else { $blame = $blame_dict[$rev]; if (!isset($blame['epoch'])) { $color = '#ffd'; // Render as warning. } else { $color_ratio = ($blame['epoch'] - $epoch_min) / $epoch_range; $color_value = 0xE6 * (1.0 - $color_ratio); $color = sprintf( '#%02x%02x%02x', $color_value, 0xF6, $color_value); } $display_line['epoch'] = idx($blame, 'epoch'); $display_line['color'] = $color; $display_line['commit'] = $rev; $author_phid = idx($blame, 'authorPHID'); if ($author_phid && $handles[$author_phid]) { $author_link = $handles[$author_phid]->renderLink(); } else { $author_link = $blame['author']; } $display_line['author'] = $author_link; $last_rev = $rev; } } if ($line_arr) { if ($line_number == $line_arr[0]['min']) { $display_line['target'] = true; } foreach ($line_arr as $range) { if ($line_number >= $range['min'] && $line_number <= $range['max']) { $display_line['highlighted'] = true; } } } $display[] = $display_line; ++$line_number; } $request = $this->getRequest(); $viewer = $request->getUser(); $commits = array_filter(ipull($display, 'commit')); if ($commits) { $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($drequest->getRepository()) ->withIdentifiers($commits) ->execute(); $commits = mpull($commits, null, 'getCommitIdentifier'); } $revision_ids = id(new DifferentialRevision()) ->loadIDsByCommitPHIDs(mpull($commits, 'getPHID')); $revisions = array(); if ($revision_ids) { $revisions = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs($revision_ids) ->execute(); } $phids = array(); foreach ($commits as $commit) { if ($commit->getAuthorPHID()) { $phids[] = $commit->getAuthorPHID(); } } foreach ($revisions as $revision) { if ($revision->getAuthorPHID()) { $phids[] = $revision->getAuthorPHID(); } } $handles = $this->loadViewerHandles($phids); Javelin::initBehavior('phabricator-oncopy', array()); $engine = null; $inlines = array(); if ($this->getRequest()->getStr('lint') !== null && $this->lintMessages) { $engine = new PhabricatorMarkupEngine(); $engine->setViewer($viewer); foreach ($this->lintMessages as $message) { $inline = id(new PhabricatorAuditInlineComment()) ->setSyntheticAuthor( ArcanistLintSeverity::getStringForSeverity($message['severity']). ' '.$message['code'].' ('.$message['name'].')') ->setLineNumber($message['line']) ->setContent($message['description']); $inlines[$message['line']][] = $inline; $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); require_celerity_resource('differential-changeset-view-css'); } $rows = $this->renderInlines( idx($inlines, 0, array()), $show_blame, (bool)$this->coverage, $engine); foreach ($display as $line) { $line_href = $drequest->generateURI( array( 'action' => 'browse', 'line' => $line['line'], 'stable' => true, )); $blame = array(); $style = null; if (array_key_exists('color', $line)) { if ($line['color']) { $style = 'background: '.$line['color'].';'; } $before_link = null; $commit_link = null; $revision_link = null; if (idx($line, 'commit')) { $commit = $line['commit']; if (idx($commits, $commit)) { $tooltip = $this->renderCommitTooltip( $commits[$commit], $handles, $line['author']); } else { $tooltip = null; } Javelin::initBehavior('phabricator-tooltips', array()); require_celerity_resource('aphront-tooltip-css'); $commit_link = javelin_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $line['commit'], )), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(9) ->setTerminator('') ->truncateString($line['commit'])); $revision_id = null; if (idx($commits, $commit)) { $revision_id = idx($revision_ids, $commits[$commit]->getPHID()); } if ($revision_id) { $revision = idx($revisions, $revision_id); if ($revision) { $tooltip = $this->renderRevisionTooltip($revision, $handles); $revision_link = javelin_tag( 'a', array( 'href' => '/D'.$revision->getID(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), 'D'.$revision->getID()); } } $uri = $line_href->alter('before', $commit); $before_link = javelin_tag( 'a', array( 'href' => $uri->setQueryParam('view', 'blame'), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Skip Past This Commit'), 'align' => 'E', 'size' => 300, ), ), "\xC2\xAB"); } $blame[] = phutil_tag( 'th', array( 'class' => 'diffusion-blame-link', ), $before_link); $object_links = array(); $object_links[] = $commit_link; if ($revision_link) { $object_links[] = phutil_tag('span', array(), '/'); $object_links[] = $revision_link; } $blame[] = phutil_tag( 'th', array( 'class' => 'diffusion-rev-link', ), $object_links); } $line_link = phutil_tag( 'a', array( 'href' => $line_href, 'style' => $style, ), $line['line']); $blame[] = javelin_tag( 'th', array( 'class' => 'diffusion-line-link', 'sigil' => 'phabricator-source-line', 'style' => $style, ), $line_link); Javelin::initBehavior('phabricator-line-linker'); if ($line['target']) { Javelin::initBehavior( 'diffusion-jump-to', array( 'target' => 'scroll_target', )); $anchor_text = phutil_tag( 'a', array( 'id' => 'scroll_target', ), ''); } else { $anchor_text = null; } $blame[] = phutil_tag( 'td', array( ), array( $anchor_text, // NOTE: See phabricator-oncopy behavior. "\xE2\x80\x8B", // TODO: [HTML] Not ideal. phutil_safe_html(str_replace("\t", ' ', $line['data'])), )); if ($this->coverage) { require_celerity_resource('differential-changeset-view-css'); $cov_index = $line['line'] - 1; if (isset($this->coverage[$cov_index])) { $cov_class = $this->coverage[$cov_index]; } else { $cov_class = 'N'; } $blame[] = phutil_tag( 'td', array( 'class' => 'cov cov-'.$cov_class, ), ''); } $rows[] = phutil_tag( 'tr', array( 'class' => ($line['highlighted'] ? 'phabricator-source-highlight' : null), ), $blame); $cur_inlines = $this->renderInlines( idx($inlines, $line['line'], array()), $show_blame, $this->coverage, $engine); foreach ($cur_inlines as $cur_inline) { $rows[] = $cur_inline; } } return $rows; } private function renderInlines( array $inlines, $needs_blame, $has_coverage, $engine) { $rows = array(); foreach ($inlines as $inline) { $inline_view = id(new DifferentialInlineCommentView()) ->setMarkupEngine($engine) ->setInlineComment($inline) ->render(); $row = array_fill(0, ($needs_blame ? 3 : 1), phutil_tag('th')); $row[] = phutil_tag('td', array(), $inline_view); if ($has_coverage) { $row[] = phutil_tag( 'td', array( 'class' => 'cov cov-I', )); } $rows[] = phutil_tag('tr', array('class' => 'inline'), $row); } return $rows; } private function loadFileForData($path, $data) { $file = PhabricatorFile::buildFromFileDataOrHash( $data, array( 'name' => basename($path), 'ttl' => time() + 60 * 60 * 24, 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, )); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $file->attachToObject( $this->getDiffusionRequest()->getRepository()->getPHID()); unset($unguarded); return $file; } private function buildRawResponse($path, $data) { $file = $this->loadFileForData($path, $data); return $file->getRedirectResponse(); } private function buildImageCorpus($file_uri) { $properties = new PHUIPropertyListView(); $properties->addImageContent( phutil_tag( 'img', array( 'src' => $file_uri, ))); $file = $this->renderFileButton($file_uri); $header = id(new PHUIHeaderView()) ->setHeader(pht('Image')) ->addActionLink($file); return id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); } private function buildBinaryCorpus($file_uri, $data) { $size = new PhutilNumber(strlen($data)); $text = pht('This is a binary file. It is %s byte(s) in length.', $size); $text = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->appendChild($text); $file = $this->renderFileButton($file_uri); $header = id(new PHUIHeaderView()) ->setHeader(pht('Details')) ->addActionLink($file); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($text); return $box; } private function buildBeforeResponse($before) { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); // NOTE: We need to get the grandparent so we can capture filename changes // in the parent. $parent = $this->loadParentCommitOf($before); $old_filename = null; $was_created = false; if ($parent) { $grandparent = $this->loadParentCommitOf($parent); if ($grandparent) { $rename_query = new DiffusionRenameHistoryQuery(); $rename_query->setRequest($drequest); $rename_query->setOldCommit($grandparent); $rename_query->setViewer($request->getUser()); $old_filename = $rename_query->loadOldFilename(); $was_created = $rename_query->getWasCreated(); } } $follow = null; if ($was_created) { // If the file was created in history, that means older commits won't // have it. Since we know it existed at 'before', it must have been // created then; jump there. $target_commit = $before; $follow = 'created'; } else if ($parent) { // If we found a parent, jump to it. This is the normal case. $target_commit = $parent; } else { // If there's no parent, this was probably created in the initial commit? // And the "was_created" check will fail because we can't identify the // grandparent. Keep the user at 'before'. $target_commit = $before; $follow = 'first'; } $path = $drequest->getPath(); $renamed = null; if ($old_filename !== null && $old_filename !== '/'.$path) { $renamed = $path; $path = $old_filename; } $line = null; // If there's a follow error, drop the line so the user sees the message. if (!$follow) { $line = $this->getBeforeLineNumber($target_commit); } $before_uri = $drequest->generateURI( array( 'action' => 'browse', 'commit' => $target_commit, 'line' => $line, 'path' => $path, )); $before_uri->setQueryParams($request->getRequestURI()->getQueryParams()); $before_uri = $before_uri->alter('before', null); $before_uri = $before_uri->alter('renamed', $renamed); $before_uri = $before_uri->alter('follow', $follow); return id(new AphrontRedirectResponse())->setURI($before_uri); } private function getBeforeLineNumber($target_commit) { $drequest = $this->getDiffusionRequest(); $line = $drequest->getLine(); if (!$line) { return null; } $raw_diff = $this->callConduitWithDiffusionRequest( 'diffusion.rawdiffquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), - 'againstCommit' => $target_commit)); + 'againstCommit' => $target_commit, + )); $old_line = 0; $new_line = 0; foreach (explode("\n", $raw_diff) as $text) { if ($text[0] == '-' || $text[0] == ' ') { $old_line++; } if ($text[0] == '+' || $text[0] == ' ') { $new_line++; } if ($new_line == $line) { return $old_line; } } // We didn't find the target line. return $line; } private function loadParentCommitOf($commit) { $drequest = $this->getDiffusionRequest(); $user = $this->getRequest()->getUser(); $before_req = DiffusionRequest::newFromDictionary( array( 'user' => $user, 'repository' => $drequest->getRepository(), 'commit' => $commit, )); $parents = DiffusionQuery::callConduitWithDiffusionRequest( $user, $before_req, 'diffusion.commitparentsquery', array( 'commit' => $commit, )); return head($parents); } private function renderRevisionTooltip( DifferentialRevision $revision, array $handles) { $viewer = $this->getRequest()->getUser(); $date = phabricator_date($revision->getDateModified(), $viewer); $id = $revision->getID(); $title = $revision->getTitle(); $header = "D{$id} {$title}"; $author = $handles[$revision->getAuthorPHID()]->getName(); return "{$header}\n{$date} \xC2\xB7 {$author}"; } private function renderCommitTooltip( PhabricatorRepositoryCommit $commit, array $handles, $author) { $viewer = $this->getRequest()->getUser(); $date = phabricator_date($commit->getEpoch(), $viewer); $summary = trim($commit->getSummary()); if ($commit->getAuthorPHID()) { $author = $handles[$commit->getAuthorPHID()]->getName(); } return "{$summary}\n{$date} \xC2\xB7 {$author}"; } } diff --git a/src/applications/diffusion/controller/DiffusionChangeController.php b/src/applications/diffusion/controller/DiffusionChangeController.php index 846b116eb6..37e1dfb02d 100644 --- a/src/applications/diffusion/controller/DiffusionChangeController.php +++ b/src/applications/diffusion/controller/DiffusionChangeController.php @@ -1,163 +1,163 @@ <?php final class DiffusionChangeController extends DiffusionController { public function shouldAllowPublic() { return true; } public function processRequest() { $drequest = $this->diffusionRequest; $viewer = $this->getRequest()->getUser(); $content = array(); $data = $this->callConduitWithDiffusionRequest( 'diffusion.diffquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), )); $drequest->updateSymbolicCommit($data['effectiveCommit']); $raw_changes = ArcanistDiffChange::newFromConduit($data['changes']); $diff = DifferentialDiff::newFromRawChanges($raw_changes); $changesets = $diff->getChangesets(); $changeset = reset($changesets); if (!$changeset) { // TODO: Refine this. return new Aphront404Response(); } $repository = $drequest->getRepository(); $callsign = $repository->getCallsign(); $changesets = array( 0 => $changeset, ); $changeset_view = new DifferentialChangesetListView(); $changeset_view->setTitle(pht('Change')); $changeset_view->setChangesets($changesets); $changeset_view->setVisibleChangesets($changesets); $changeset_view->setRenderingReferences( array( - 0 => $drequest->generateURI(array('action' => 'rendering-ref')) + 0 => $drequest->generateURI(array('action' => 'rendering-ref')), )); $raw_params = array( 'action' => 'browse', 'params' => array( 'view' => 'raw', ), ); $right_uri = $drequest->generateURI($raw_params); $raw_params['params']['before'] = $drequest->getStableCommit(); $left_uri = $drequest->generateURI($raw_params); $changeset_view->setRawFileURIs($left_uri, $right_uri); $changeset_view->setRenderURI('/diffusion/'.$callsign.'/diff/'); $changeset_view->setWhitespace( DifferentialChangesetParser::WHITESPACE_SHOW_ALL); $changeset_view->setUser($this->getRequest()->getUser()); // TODO: This is pretty awkward, unify the CSS between Diffusion and // Differential better. require_celerity_resource('differential-core-view-css'); $content[] = $changeset_view->render(); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'change', )); $links = $this->renderPathLinks($drequest, $mode = 'browse'); $header = id(new PHUIHeaderView()) ->setHeader($links) ->setUser($viewer) ->setPolicyObject($drequest->getRepository()); $actions = $this->buildActionView($drequest); $properties = $this->buildPropertyView($drequest, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $content, ), array( 'title' => pht('Change'), 'device' => false, )); } private function buildActionView(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); $view = id(new PhabricatorActionListView()) ->setUser($viewer); $history_uri = $drequest->generateURI( array( 'action' => 'history', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setHref($history_uri) ->setIcon('fa-clock-o')); $browse_uri = $drequest->generateURI( array( 'action' => 'browse', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Browse Content')) ->setHref($browse_uri) ->setIcon('fa-files-o')); return $view; } protected function buildPropertyView( DiffusionRequest $drequest, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setActionList($actions); $stable_commit = $drequest->getStableCommit(); $callsign = $drequest->getRepository()->getCallsign(); $view->addProperty( pht('Commit'), phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $stable_commit, )), ), $drequest->getRepository()->formatCommitName($stable_commit))); return $view; } } diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index 101d1e1fce..b225a35e99 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -1,1152 +1,1154 @@ <?php final class DiffusionCommitController extends DiffusionController { const CHANGES_LIMIT = 100; private $auditAuthorityPHIDs; private $highlightedAudits; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { // This controller doesn't use blob/path stuff, just pass the dictionary // in directly instead of using the AphrontRequest parsing mechanism. $data['user'] = $this->getRequest()->getUser(); $drequest = DiffusionRequest::newFromDictionary($data); $this->diffusionRequest = $drequest; } public function processRequest() { $drequest = $this->getDiffusionRequest(); $request = $this->getRequest(); $user = $request->getUser(); if ($request->getStr('diff')) { return $this->buildRawDiffResponse($drequest); } $repository = $drequest->getRepository(); $callsign = $repository->getCallsign(); $content = array(); $commit = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->withRepository($repository) ->withIdentifiers(array($drequest->getCommit())) ->needCommitData(true) ->needAuditRequests(true) ->executeOne(); $crumbs = $this->buildCrumbs(array( 'commit' => true, )); if (!$commit) { $exists = $this->callConduitWithDiffusionRequest( 'diffusion.existsquery', array('commit' => $drequest->getCommit())); if (!$exists) { return new Aphront404Response(); } $error = id(new AphrontErrorView()) ->setTitle(pht('Commit Still Parsing')) ->appendChild( pht( 'Failed to load the commit because the commit has not been '. 'parsed yet.')); return $this->buildApplicationPage( array( $crumbs, $error, ), array( 'title' => pht('Commit Still Parsing'), 'device' => false, )); } $top_anchor = id(new PhabricatorAnchorView()) ->setAnchorName('top') ->setNavigationMarker(true); $audit_requests = $commit->getAudits(); $this->auditAuthorityPHIDs = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user); $commit_data = $commit->getCommitData(); $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub'); $changesets = null; if ($is_foreign) { $subpath = $commit_data->getCommitDetail('svn-subpath'); $error_panel = new AphrontErrorView(); $error_panel->setTitle(pht('Commit Not Tracked')); $error_panel->setSeverity(AphrontErrorView::SEVERITY_WARNING); $error_panel->appendChild( pht("This Diffusion repository is configured to track only one ". "subdirectory of the entire Subversion repository, and this commit ". "didn't affect the tracked subdirectory ('%s'), so no ". "information is available.", $subpath)); $content[] = $error_panel; $content[] = $top_anchor; } else { $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $user); require_celerity_resource('phabricator-remarkup-css'); $parents = $this->callConduitWithDiffusionRequest( 'diffusion.commitparentsquery', array('commit' => $drequest->getCommit())); if ($parents) { $parents = id(new DiffusionCommitQuery()) ->setViewer($user) ->withRepository($repository) ->withIdentifiers($parents) ->execute(); } $headsup_view = id(new PHUIHeaderView()) ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))); $headsup_actions = $this->renderHeadsupActionList($commit, $repository); $commit_properties = $this->loadCommitProperties( $commit, $commit_data, $parents, $audit_requests); $property_list = id(new PHUIPropertyListView()) ->setHasKeyboardShortcuts(true) ->setUser($user) ->setObject($commit); foreach ($commit_properties as $key => $value) { $property_list->addProperty($key, $value); } $message = $commit_data->getCommitMessage(); $revision = $commit->getCommitIdentifier(); $message = $this->linkBugtraq($message); $message = $engine->markupText($message); $property_list->invokeWillRenderEvent(); $property_list->setActionList($headsup_actions); $detail_list = new PHUIPropertyListView(); $detail_list->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $detail_list->addTextContent( phutil_tag( 'div', array( 'class' => 'diffusion-commit-message phabricator-remarkup', ), $message)); $content[] = $top_anchor; $object_box = id(new PHUIObjectBoxView()) ->setHeader($headsup_view) ->addPropertyList($property_list) ->addPropertyList($detail_list); $content[] = $object_box; } $content[] = $this->buildComments($commit); $hard_limit = 1000; if ($commit->isImported()) { $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $drequest); $change_query->setLimit($hard_limit + 1); $changes = $change_query->loadChanges(); } else { $changes = array(); } $was_limited = (count($changes) > $hard_limit); if ($was_limited) { $changes = array_slice($changes, 0, $hard_limit); } $content[] = $this->buildMergesTable($commit); $highlighted_audits = $commit->getAuthorityAudits( $user, $this->auditAuthorityPHIDs); $owners_paths = array(); if ($highlighted_audits) { $packages = id(new PhabricatorOwnersPackage())->loadAllWhere( 'phid IN (%Ls)', mpull($highlighted_audits, 'getAuditorPHID')); if ($packages) { $owners_paths = id(new PhabricatorOwnersPath())->loadAllWhere( 'repositoryPHID = %s AND packageID IN (%Ld)', $repository->getPHID(), mpull($packages, 'getID')); } } $change_table = new DiffusionCommitChangeTableView(); $change_table->setDiffusionRequest($drequest); $change_table->setPathChanges($changes); $change_table->setOwnersPaths($owners_paths); $count = count($changes); $bad_commit = null; if ($count == 0) { $bad_commit = queryfx_one( id(new PhabricatorRepository())->establishConnection('r'), 'SELECT * FROM %T WHERE fullCommitName = %s', PhabricatorRepository::TABLE_BADCOMMIT, 'r'.$callsign.$commit->getCommitIdentifier()); } if ($bad_commit) { $content[] = $this->renderStatusMessage( pht('Bad Commit'), $bad_commit['description']); } else if ($is_foreign) { // Don't render anything else. } else if (!$commit->isImported()) { $content[] = $this->renderStatusMessage( pht('Still Importing...'), pht( 'This commit is still importing. Changes will be visible once '. 'the import finishes.')); } else if (!count($changes)) { $content[] = $this->renderStatusMessage( pht('Empty Commit'), pht( 'This commit is empty and does not affect any paths.')); } else if ($was_limited) { $content[] = $this->renderStatusMessage( pht('Enormous Commit'), pht( 'This commit is enormous, and affects more than %d files. '. 'Changes are not shown.', $hard_limit)); } else { // The user has clicked "Show All Changes", and we should show all the // changes inline even if there are more than the soft limit. $show_all_details = $request->getBool('show_all'); $change_panel = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); $header->setHeader('Changes ('.number_format($count).')'); $change_panel->setID('toc'); if ($count > self::CHANGES_LIMIT && !$show_all_details) { $icon = id(new PHUIIconView()) ->setIconFont('fa-files-o'); $button = id(new PHUIButtonView()) ->setText(pht('Show All Changes')) ->setHref('?show_all=true') ->setTag('a') ->setIcon($icon); $warning_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ->setTitle('Very Large Commit') ->appendChild( pht('This commit is very large. Load each file individually.')); $change_panel->setErrorView($warning_view); $header->addActionLink($button); } $change_panel->appendChild($change_table); $change_panel->setHeader($header); $content[] = $change_panel; $changesets = DiffusionPathChange::convertToDifferentialChangesets( $changes); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $vcs_supports_directory_changes = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $vcs_supports_directory_changes = false; break; default: throw new Exception('Unknown VCS.'); } $references = array(); foreach ($changesets as $key => $changeset) { $file_type = $changeset->getFileType(); if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { if (!$vcs_supports_directory_changes) { unset($changesets[$key]); continue; } } $references[$key] = $drequest->generateURI( array( 'action' => 'rendering-ref', 'path' => $changeset->getFilename(), )); } // TODO: Some parts of the views still rely on properties of the // DifferentialChangeset. Make the objects ephemeral to make sure we don't // accidentally save them, and then set their ID to the appropriate ID for // this application (the path IDs). $path_ids = array_flip(mpull($changes, 'getPath')); foreach ($changesets as $changeset) { $changeset->makeEphemeral(); $changeset->setID($path_ids[$changeset->getFilename()]); } if ($count <= self::CHANGES_LIMIT || $show_all_details) { $visible_changesets = $changesets; } else { $visible_changesets = array(); $inlines = PhabricatorAuditInlineComment::loadDraftAndPublishedComments( $user, $commit->getPHID()); $path_ids = mpull($inlines, null, 'getPathID'); foreach ($changesets as $key => $changeset) { if (array_key_exists($changeset->getID(), $path_ids)) { $visible_changesets[$key] = $changeset; } } } $change_list_title = DiffusionView::nameCommit( $repository, $commit->getCommitIdentifier()); $change_list = new DifferentialChangesetListView(); $change_list->setTitle($change_list_title); $change_list->setChangesets($changesets); $change_list->setVisibleChangesets($visible_changesets); $change_list->setRenderingReferences($references); $change_list->setRenderURI('/diffusion/'.$callsign.'/diff/'); $change_list->setRepository($repository); $change_list->setUser($user); // TODO: Try to setBranch() to something reasonable here? $change_list->setStandaloneURI( '/diffusion/'.$callsign.'/diff/'); $change_list->setRawFileURIs( // TODO: Implement this, somewhat tricky if there's an octopus merge // or whatever? null, '/diffusion/'.$callsign.'/diff/?view=r'); $change_list->setInlineCommentControllerURI( '/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/'); $change_references = array(); foreach ($changesets as $key => $changeset) { $change_references[$changeset->getID()] = $references[$key]; } $change_table->setRenderingReferences($change_references); $content[] = $change_list->render(); } $content[] = $this->renderAddCommentPanel($commit, $audit_requests); $commit_id = 'r'.$callsign.$commit->getCommitIdentifier(); $short_name = DiffusionView::nameCommit( $repository, $commit->getCommitIdentifier()); $prefs = $user->loadPreferences(); $pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE; $pref_collapse = PhabricatorUserPreferences::PREFERENCE_NAV_COLLAPSED; $show_filetree = $prefs->getPreference($pref_filetree); $collapsed = $prefs->getPreference($pref_collapse); if ($changesets && $show_filetree) { $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setAnchorName('top') ->setTitle($short_name) ->setBaseURI(new PhutilURI('/'.$commit_id)) ->build($changesets) ->setCrumbs($crumbs) ->setCollapsed((bool)$collapsed) ->appendChild($content); $content = $nav; } else { $content = array($crumbs, $content); } return $this->buildApplicationPage( $content, array( 'title' => $commit_id, 'pageObjects' => array($commit->getPHID()), 'device' => false, )); } private function loadCommitProperties( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data, array $parents, array $audit_requests) { assert_instances_of($parents, 'PhabricatorRepositoryCommit'); $viewer = $this->getRequest()->getUser(); $commit_phid = $commit->getPHID(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($commit_phid)) ->withEdgeTypes(array( DiffusionCommitHasTaskEdgeType::EDGECONST, PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT, PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV, )); $edges = $edge_query->execute(); $task_phids = array_keys( $edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]); $proj_phids = array_keys( $edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT]); $revision_phid = key( $edges[$commit_phid][PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV]); $phids = $edge_query->getDestinationPHIDs(array($commit_phid)); if ($data->getCommitDetail('authorPHID')) { $phids[] = $data->getCommitDetail('authorPHID'); } if ($data->getCommitDetail('reviewerPHID')) { $phids[] = $data->getCommitDetail('reviewerPHID'); } if ($data->getCommitDetail('committerPHID')) { $phids[] = $data->getCommitDetail('committerPHID'); } if ($parents) { foreach ($parents as $parent) { $phids[] = $parent->getPHID(); } } // NOTE: We should never normally have more than a single push log, but // it can occur naturally if a commit is pushed, then the branch it was // on is deleted, then the commit is pushed again (or through other similar // chains of events). This should be rare, but does not indicate a bug // or data issue. // NOTE: We never query push logs in SVN because the commiter is always // the pusher and the commit time is always the push time; the push log // is redundant and we save a query by skipping it. $push_logs = array(); if ($repository->isHosted() && !$repository->isSVN()) { $push_logs = id(new PhabricatorRepositoryPushLogQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withNewRefs(array($commit->getCommitIdentifier())) ->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)) ->execute(); foreach ($push_logs as $log) { $phids[] = $log->getPusherPHID(); } } $handles = array(); if ($phids) { $handles = $this->loadViewerHandles($phids); } $props = array(); if ($commit->getAuditStatus()) { $status = PhabricatorAuditCommitStatusConstants::getStatusName( $commit->getAuditStatus()); $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE) ->setName($status); switch ($commit->getAuditStatus()) { case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT: $tag->setBackgroundColor(PHUITagView::COLOR_ORANGE); break; case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED: $tag->setBackgroundColor(PHUITagView::COLOR_RED); break; case PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED: $tag->setBackgroundColor(PHUITagView::COLOR_BLUE); break; case PhabricatorAuditCommitStatusConstants::FULLY_AUDITED: $tag->setBackgroundColor(PHUITagView::COLOR_GREEN); break; } $props['Status'] = $tag; } if ($audit_requests) { $user_requests = array(); $other_requests = array(); foreach ($audit_requests as $audit_request) { if ($audit_request->isUser()) { $user_requests[] = $audit_request; } else { $other_requests[] = $audit_request; } } if ($user_requests) { $props['Auditors'] = $this->renderAuditStatusView( $user_requests); } if ($other_requests) { $props['Project/Package Auditors'] = $this->renderAuditStatusView( $other_requests); } } $author_phid = $data->getCommitDetail('authorPHID'); $author_name = $data->getAuthorName(); if (!$repository->isSVN()) { $authored_info = id(new PHUIStatusItemView()); // TODO: In Git, a distinct authorship date is available. When present, // we should show it here. if ($author_phid) { $authored_info->setTarget($handles[$author_phid]->renderLink()); } else if (strlen($author_name)) { $authored_info->setTarget($author_name); } $props['Authored'] = id(new PHUIStatusListView()) ->addItem($authored_info); } $committed_info = id(new PHUIStatusItemView()) ->setNote(phabricator_datetime($commit->getEpoch(), $viewer)); $committer_phid = $data->getCommitDetail('committerPHID'); $committer_name = $data->getCommitDetail('committer'); if ($committer_phid) { $committed_info->setTarget($handles[$committer_phid]->renderLink()); } else if (strlen($committer_name)) { $committed_info->setTarget($committer_name); } else if ($author_phid) { $committed_info->setTarget($handles[$author_phid]->renderLink()); } else if (strlen($author_name)) { $committed_info->setTarget($author_name); } $props['Committed'] = id(new PHUIStatusListView()) ->addItem($committed_info); if ($push_logs) { $pushed_list = new PHUIStatusListView(); foreach ($push_logs as $push_log) { $pushed_item = id(new PHUIStatusItemView()) ->setTarget($handles[$push_log->getPusherPHID()]->renderLink()) ->setNote(phabricator_datetime($push_log->getEpoch(), $viewer)); $pushed_list->addItem($pushed_item); } $props['Pushed'] = $pushed_list; } $reviewer_phid = $data->getCommitDetail('reviewerPHID'); if ($reviewer_phid) { $props['Reviewer'] = $handles[$reviewer_phid]->renderLink(); } if ($revision_phid) { $props['Differential Revision'] = $handles[$revision_phid]->renderLink(); } if ($parents) { $parent_links = array(); foreach ($parents as $parent) { $parent_links[] = $handles[$parent->getPHID()]->renderLink(); } $props['Parents'] = phutil_implode_html(" \xC2\xB7 ", $parent_links); } $props['Branches'] = phutil_tag( 'span', array( 'id' => 'commit-branches', ), pht('Unknown')); $props['Tags'] = phutil_tag( 'span', array( 'id' => 'commit-tags', ), pht('Unknown')); $callsign = $repository->getCallsign(); $root = '/diffusion/'.$callsign.'/commit/'.$commit->getCommitIdentifier(); Javelin::initBehavior( 'diffusion-commit-branches', array( $root.'/branches/' => 'commit-branches', $root.'/tags/' => 'commit-tags', )); $refs = $this->buildRefs($drequest); if ($refs) { $props['References'] = $refs; } if ($task_phids) { $task_list = array(); foreach ($task_phids as $phid) { $task_list[] = $handles[$phid]->renderLink(); } $task_list = phutil_implode_html(phutil_tag('br'), $task_list); $props['Tasks'] = $task_list; } if ($proj_phids) { $proj_list = array(); foreach ($proj_phids as $phid) { $proj_list[] = $handles[$phid]->renderLink(); } $proj_list = phutil_implode_html(phutil_tag('br'), $proj_list); $props['Projects'] = $proj_list; } return $props; } private function buildComments(PhabricatorRepositoryCommit $commit) { $viewer = $this->getRequest()->getUser(); $xactions = id(new PhabricatorAuditTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($commit->getPHID())) ->needComments(true) ->execute(); $path_ids = array(); foreach ($xactions as $xaction) { if ($xaction->hasComment()) { $path_id = $xaction->getComment()->getPathID(); if ($path_id) { $path_ids[] = $path_id; } } } $path_map = array(); if ($path_ids) { $path_map = id(new DiffusionPathQuery()) ->withPathIDs($path_ids) ->execute(); $path_map = ipull($path_map, 'path', 'id'); } return id(new PhabricatorAuditTransactionView()) ->setUser($viewer) ->setObjectPHID($commit->getPHID()) ->setPathMap($path_map) ->setTransactions($xactions); } private function renderAddCommentPanel( PhabricatorRepositoryCommit $commit, array $audit_requests) { assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); $request = $this->getRequest(); $user = $request->getUser(); if (!$user->isLoggedIn()) { return id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setRequestURI($request->getRequestURI()); } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $pane_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'differential-keyboard-navigation', array( 'haunt' => $pane_id, )); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), 'diffusion-audit-'.$commit->getID()); if ($draft) { $draft = $draft->getDraft(); } else { $draft = null; } $actions = $this->getAuditActions($commit, $audit_requests); $form = id(new AphrontFormView()) ->setUser($user) ->setAction('/audit/addcomment/') ->addHiddenInput('commit', $commit->getPHID()) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Action')) ->setName('action') ->setID('audit-action') ->setOptions($actions)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Add Auditors')) ->setName('auditors') ->setControlID('add-auditors') ->setControlStyle('display: none') ->setID('add-auditors-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Add CCs')) ->setName('ccs') ->setControlID('add-ccs') ->setControlStyle('display: none') ->setID('add-ccs-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Comments')) ->setName('content') ->setValue($draft) ->setID('audit-content') ->setUser($user)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Submit'))); $header = new PHUIHeaderView(); $header->setHeader( $is_serious ? pht('Audit Commit') : pht('Creative Accounting')); require_celerity_resource('phabricator-transaction-view-css'); $mailable_source = new PhabricatorMetaMTAMailableDatasource(); $auditor_source = new DiffusionAuditorDatasource(); Javelin::initBehavior( 'differential-add-reviewers-and-ccs', array( 'dynamic' => array( 'add-auditors-tokenizer' => array( 'actions' => array('add_auditors' => 1), 'src' => $auditor_source->getDatasourceURI(), 'row' => 'add-auditors', 'placeholder' => $auditor_source->getPlaceholderText(), ), 'add-ccs-tokenizer' => array( 'actions' => array('add_ccs' => 1), 'src' => $mailable_source->getDatasourceURI(), 'row' => 'add-ccs', 'placeholder' => $mailable_source->getPlaceholderText(), ), ), 'select' => 'audit-action', )); Javelin::initBehavior('differential-feedback-preview', array( 'uri' => '/audit/preview/'.$commit->getID().'/', 'preview' => 'audit-preview', 'content' => 'audit-content', 'action' => 'audit-action', 'previewTokenizers' => array( 'auditors' => 'add-auditors-tokenizer', 'ccs' => 'add-ccs-tokenizer', ), 'inline' => 'inline-comment-preview', 'inlineuri' => '/diffusion/inline/preview/'.$commit->getPHID().'/', )); $loading = phutil_tag_div( 'aphront-panel-preview-loading-text', pht('Loading preview...')); $preview_panel = phutil_tag_div( 'aphront-panel-preview aphront-panel-flush', array( phutil_tag('div', array('id' => 'audit-preview'), $loading), - phutil_tag('div', array('id' => 'inline-comment-preview')) + phutil_tag('div', array('id' => 'inline-comment-preview')), )); // TODO: This is pretty awkward, unify the CSS between Diffusion and // Differential better. require_celerity_resource('differential-core-view-css'); $anchor = id(new PhabricatorAnchorView()) ->setAnchorName('comment') ->setNavigationMarker(true) ->render(); $comment_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($form); return phutil_tag( 'div', array( 'id' => $pane_id, ), phutil_tag_div( 'differential-add-comment-panel', array($anchor, $comment_box, $preview_panel))); } /** * Return a map of available audit actions for rendering into a <select />. * This shows the user valid actions, and does not show nonsense/invalid * actions (like closing an already-closed commit, or resigning from a commit * you have no association with). */ private function getAuditActions( PhabricatorRepositoryCommit $commit, array $audit_requests) { assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); $user = $this->getRequest()->getUser(); $user_is_author = ($commit->getAuthorPHID() == $user->getPHID()); $user_request = null; foreach ($audit_requests as $audit_request) { if ($audit_request->getAuditorPHID() == $user->getPHID()) { $user_request = $audit_request; break; } } $actions = array(); $actions[PhabricatorAuditActionConstants::COMMENT] = true; $actions[PhabricatorAuditActionConstants::ADD_CCS] = true; $actions[PhabricatorAuditActionConstants::ADD_AUDITORS] = true; // We allow you to accept your own commits. A use case here is that you // notice an issue with your own commit and "Raise Concern" as an indicator // to other auditors that you're on top of the issue, then later resolve it // and "Accept". You can not accept on behalf of projects or packages, // however. $actions[PhabricatorAuditActionConstants::ACCEPT] = true; $actions[PhabricatorAuditActionConstants::CONCERN] = true; // To resign, a user must have authority on some request and not be the // commit's author. if (!$user_is_author) { $may_resign = false; $authority_map = array_fill_keys($this->auditAuthorityPHIDs, true); foreach ($audit_requests as $request) { if (empty($authority_map[$request->getAuditorPHID()])) { continue; } $may_resign = true; break; } // If the user has already resigned, don't show "Resign...". $status_resigned = PhabricatorAuditStatusConstants::RESIGNED; if ($user_request) { if ($user_request->getAuditStatus() == $status_resigned) { $may_resign = false; } } if ($may_resign) { $actions[PhabricatorAuditActionConstants::RESIGN] = true; } } $status_concern = PhabricatorAuditCommitStatusConstants::CONCERN_RAISED; $concern_raised = ($commit->getAuditStatus() == $status_concern); $can_close_option = PhabricatorEnv::getEnvConfig( 'audit.can-author-close-audit'); if ($can_close_option && $user_is_author && $concern_raised) { $actions[PhabricatorAuditActionConstants::CLOSE] = true; } foreach ($actions as $constant => $ignored) { $actions[$constant] = PhabricatorAuditActionConstants::getActionName($constant); } return $actions; } private function buildMergesTable(PhabricatorRepositoryCommit $commit) { $drequest = $this->getDiffusionRequest(); $limit = 50; $merges = array(); try { $merges = $this->callConduitWithDiffusionRequest( 'diffusion.mergedcommitsquery', array( 'commit' => $drequest->getCommit(), - 'limit' => $limit + 1)); + 'limit' => $limit + 1, + )); } catch (ConduitException $ex) { if ($ex->getMessage() != 'ERR-UNSUPPORTED-VCS') { throw $ex; } } if (!$merges) { return null; } $caption = null; if (count($merges) > $limit) { $merges = array_slice($merges, 0, $limit); $caption = "This commit merges more than {$limit} changes. Only the first ". "{$limit} are shown."; } $history_table = new DiffusionHistoryTableView(); $history_table->setUser($this->getRequest()->getUser()); $history_table->setDiffusionRequest($drequest); $history_table->setHistory($merges); $history_table->loadRevisions(); $phids = $history_table->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $history_table->setHandles($handles); $panel = new AphrontPanelView(); $panel->setHeader(pht('Merged Changes')); $panel->setCaption($caption); $panel->appendChild($history_table); $panel->setNoBackground(); return $panel; } private function renderHeadsupActionList( PhabricatorRepositoryCommit $commit, PhabricatorRepository $repository) { $request = $this->getRequest(); $user = $request->getUser(); $actions = id(new PhabricatorActionListView()) ->setUser($user) ->setObject($commit) ->setObjectURI($request->getRequestURI()); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $commit, PhabricatorPolicyCapability::CAN_EDIT); $uri = '/diffusion/'.$repository->getCallsign().'/commit/'. $commit->getCommitIdentifier().'/edit/'; $action = id(new PhabricatorActionView()) ->setName(pht('Edit Commit')) ->setHref($uri) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit); $actions->addAction($action); require_celerity_resource('phabricator-object-selector-css'); require_celerity_resource('javelin-behavior-phabricator-object-selector'); $maniphest = 'PhabricatorManiphestApplication'; if (PhabricatorApplication::isClassInstalled($maniphest)) { $action = id(new PhabricatorActionView()) ->setName(pht('Edit Maniphest Tasks')) ->setIcon('fa-anchor') ->setHref('/search/attach/'.$commit->getPHID().'/TASK/edge/') ->setWorkflow(true) ->setDisabled(!$can_edit); $actions->addAction($action); } $action = id(new PhabricatorActionView()) ->setName(pht('Download Raw Diff')) ->setHref($request->getRequestURI()->alter('diff', true)) ->setIcon('fa-download'); $actions->addAction($action); return $actions; } private function buildRefs(DiffusionRequest $request) { // this is git-only, so save a conduit round trip and just get out of // here if the repository isn't git $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; $repository = $request->getRepository(); if ($repository->getVersionControlSystem() != $type_git) { return null; } $results = $this->callConduitWithDiffusionRequest( 'diffusion.refsquery', array('commit' => $request->getCommit())); $ref_links = array(); foreach ($results as $ref_data) { $ref_links[] = phutil_tag('a', array('href' => $ref_data['href']), $ref_data['ref']); } return phutil_implode_html(', ', $ref_links); } private function buildRawDiffResponse(DiffusionRequest $drequest) { $raw_diff = $this->callConduitWithDiffusionRequest( 'diffusion.rawdiffquery', array( 'commit' => $drequest->getCommit(), - 'path' => $drequest->getPath())); + 'path' => $drequest->getPath(), + )); $file = PhabricatorFile::buildFromFileDataOrHash( $raw_diff, array( 'name' => $drequest->getCommit().'.diff', 'ttl' => (60 * 60 * 24), 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, )); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $file->attachToObject($drequest->getRepository()->getPHID()); unset($unguarded); return $file->getRedirectResponse(); } private function renderAuditStatusView(array $audit_requests) { assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); $phids = mpull($audit_requests, 'getAuditorPHID'); $this->loadHandles($phids); $authority_map = array_fill_keys($this->auditAuthorityPHIDs, true); $view = new PHUIStatusListView(); foreach ($audit_requests as $request) { $item = new PHUIStatusItemView(); switch ($request->getAuditStatus()) { case PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED: $item->setIcon( PHUIStatusItemView::ICON_OPEN, 'blue', pht('Commented')); break; case PhabricatorAuditStatusConstants::AUDIT_REQUIRED: $item->setIcon( PHUIStatusItemView::ICON_WARNING, 'blue', pht('Audit Required')); break; case PhabricatorAuditStatusConstants::CONCERNED: $item->setIcon( PHUIStatusItemView::ICON_REJECT, 'red', pht('Concern Raised')); break; case PhabricatorAuditStatusConstants::ACCEPTED: $item->setIcon( PHUIStatusItemView::ICON_ACCEPT, 'green', pht('Accepted')); break; case PhabricatorAuditStatusConstants::AUDIT_REQUESTED: $item->setIcon( PHUIStatusItemView::ICON_WARNING, 'dark', pht('Audit Requested')); break; case PhabricatorAuditStatusConstants::RESIGNED: $item->setIcon( PHUIStatusItemView::ICON_OPEN, 'dark', pht('Resigned')); break; case PhabricatorAuditStatusConstants::CLOSED: $item->setIcon( PHUIStatusItemView::ICON_ACCEPT, 'blue', pht('Closed')); break; default: $item->setIcon( PHUIStatusItemView::ICON_QUESTION, 'dark', pht('%s?', $request->getAuditStatus())); break; } $note = array(); foreach ($request->getAuditReasons() as $reason) { $note[] = phutil_tag('div', array(), $reason); } $item->setNote($note); $auditor_phid = $request->getAuditorPHID(); $target = $this->getHandle($auditor_phid)->renderLink(); $item->setTarget($target); if (isset($authority_map[$auditor_phid])) { $item->setHighlighted(true); } $view->addItem($item); } return $view; } private function linkBugtraq($corpus) { $url = PhabricatorEnv::getEnvConfig('bugtraq.url'); if (!strlen($url)) { return $corpus; } $regexes = PhabricatorEnv::getEnvConfig('bugtraq.logregex'); if (!$regexes) { return $corpus; } $parser = id(new PhutilBugtraqParser()) ->setBugtraqPattern("[[ {$url} | %BUGID% ]]") ->setBugtraqCaptureExpression(array_shift($regexes)); $select = array_shift($regexes); if ($select) { $parser->setBugtraqSelectExpression($select); } return $parser->processCorpus($corpus); } } diff --git a/src/applications/diffusion/controller/DiffusionCommitTagsController.php b/src/applications/diffusion/controller/DiffusionCommitTagsController.php index 986390c79e..6ea235052a 100644 --- a/src/applications/diffusion/controller/DiffusionCommitTagsController.php +++ b/src/applications/diffusion/controller/DiffusionCommitTagsController.php @@ -1,64 +1,65 @@ <?php final class DiffusionCommitTagsController extends DiffusionController { public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $data['user'] = $this->getRequest()->getUser(); $this->diffusionRequest = DiffusionRequest::newFromDictionary($data); } public function processRequest() { $request = $this->getDiffusionRequest(); $tag_limit = 10; $tags = array(); try { $tags = DiffusionRepositoryTag::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.tagsquery', array( 'commit' => $request->getCommit(), - 'limit' => $tag_limit + 1))); + 'limit' => $tag_limit + 1, + ))); } catch (ConduitException $ex) { if ($ex->getMessage() != 'ERR-UNSUPPORTED-VCS') { throw $ex; } } $has_more_tags = (count($tags) > $tag_limit); $tags = array_slice($tags, 0, $tag_limit); $tag_links = array(); foreach ($tags as $tag) { $tag_links[] = phutil_tag( 'a', array( 'href' => $request->generateURI( array( 'action' => 'browse', 'commit' => $tag->getName(), )), ), $tag->getName()); } if ($has_more_tags) { $tag_links[] = phutil_tag( 'a', array( 'href' => $request->generateURI( array( 'action' => 'tags', )), ), pht("More Tags\xE2\x80\xA6")); } return id(new AphrontAjaxResponse()) ->setContent($tag_links ? implode(', ', $tag_links) : pht('None')); } } diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php index a8c42eab15..a50627dee0 100644 --- a/src/applications/diffusion/controller/DiffusionController.php +++ b/src/applications/diffusion/controller/DiffusionController.php @@ -1,238 +1,239 @@ <?php abstract class DiffusionController extends PhabricatorController { protected $diffusionRequest; public function setDiffusionRequest(DiffusionRequest $request) { $this->diffusionRequest = $request; return $this; } protected function getDiffusionRequest() { if (!$this->diffusionRequest) { throw new Exception('No Diffusion request object!'); } return $this->diffusionRequest; } public function willBeginExecution() { $request = $this->getRequest(); // Check if this is a VCS request, e.g. from "git clone", "hg clone", or // "svn checkout". If it is, we jump off into repository serving code to // process the request. if (DiffusionServeController::isVCSRequest($request)) { $serve_controller = id(new DiffusionServeController($request)) ->setCurrentApplication($this->getCurrentApplication()); return $this->delegateToController($serve_controller); } return parent::willBeginExecution(); } public function willProcessRequest(array $data) { if (isset($data['callsign'])) { $drequest = DiffusionRequest::newFromAphrontRequestDictionary( $data, $this->getRequest()); $this->setDiffusionRequest($drequest); } } public function buildCrumbs(array $spec = array()) { $crumbs = $this->buildApplicationCrumbs(); $crumb_list = $this->buildCrumbList($spec); foreach ($crumb_list as $crumb) { $crumbs->addCrumb($crumb); } return $crumbs; } private function buildCrumbList(array $spec = array()) { $spec = $spec + array( 'commit' => null, 'tags' => null, 'branches' => null, 'view' => null, ); $crumb_list = array(); // On the home page, we don't have a DiffusionRequest. if ($this->diffusionRequest) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); } else { $drequest = null; $repository = null; } if (!$repository) { return $crumb_list; } $callsign = $repository->getCallsign(); $repository_name = $repository->getName(); if (!$spec['commit'] && !$spec['tags'] && !$spec['branches']) { $branch_name = $drequest->getBranch(); if ($branch_name) { $repository_name .= ' ('.$branch_name.')'; } } $crumb = id(new PhabricatorCrumbView()) ->setName($repository_name); if (!$spec['view'] && !$spec['commit'] && !$spec['tags'] && !$spec['branches']) { $crumb_list[] = $crumb; return $crumb_list; } $crumb->setHref( $drequest->generateURI( array( 'action' => 'branch', 'path' => '/', ))); $crumb_list[] = $crumb; $stable_commit = $drequest->getStableCommit(); if ($spec['tags']) { $crumb = new PhabricatorCrumbView(); if ($spec['commit']) { $crumb->setName( pht('Tags for %s', 'r'.$callsign.$stable_commit)); $crumb->setHref($drequest->generateURI( array( 'action' => 'commit', 'commit' => $drequest->getStableCommit(), ))); } else { $crumb->setName(pht('Tags')); } $crumb_list[] = $crumb; return $crumb_list; } if ($spec['branches']) { $crumb = id(new PhabricatorCrumbView()) ->setName(pht('Branches')); $crumb_list[] = $crumb; return $crumb_list; } if ($spec['commit']) { $crumb = id(new PhabricatorCrumbView()) ->setName("r{$callsign}{$stable_commit}") ->setHref("r{$callsign}{$stable_commit}"); $crumb_list[] = $crumb; return $crumb_list; } $crumb = new PhabricatorCrumbView(); $view = $spec['view']; switch ($view) { case 'history': $view_name = pht('History'); break; case 'browse': $view_name = pht('Browse'); break; case 'lint': $view_name = pht('Lint'); break; case 'change': $view_name = pht('Change'); break; } $crumb = id(new PhabricatorCrumbView()) ->setName($view_name); $crumb_list[] = $crumb; return $crumb_list; } protected function callConduitWithDiffusionRequest( $method, array $params = array()) { $user = $this->getRequest()->getUser(); $drequest = $this->getDiffusionRequest(); return DiffusionQuery::callConduitWithDiffusionRequest( $user, $drequest, $method, $params); } protected function getRepositoryControllerURI( PhabricatorRepository $repository, $path) { return $this->getApplicationURI($repository->getCallsign().'/'.$path); } protected function renderPathLinks(DiffusionRequest $drequest, $action) { $path = $drequest->getPath(); $path_parts = array_filter(explode('/', trim($path, '/'))); $divider = phutil_tag( 'span', array( - 'class' => 'phui-header-divider'), + 'class' => 'phui-header-divider', + ), '/'); $links = array(); if ($path_parts) { $links[] = phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => $action, 'path' => '', )), ), 'r'.$drequest->getRepository()->getCallsign()); $links[] = $divider; $accum = ''; $last_key = last_key($path_parts); foreach ($path_parts as $key => $part) { $accum .= '/'.$part; if ($key === $last_key) { $links[] = $part; } else { $links[] = phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => $action, 'path' => $accum.'/', )), ), $part); $links[] = $divider; } } } else { $links[] = 'r'.$drequest->getRepository()->getCallsign(); $links[] = $divider; } return $links; } protected function renderStatusMessage($title, $body) { return id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ->setTitle($title) ->appendChild($body); } } diff --git a/src/applications/diffusion/controller/DiffusionDiffController.php b/src/applications/diffusion/controller/DiffusionDiffController.php index 6740eb0bc5..92b7b64cd4 100644 --- a/src/applications/diffusion/controller/DiffusionDiffController.php +++ b/src/applications/diffusion/controller/DiffusionDiffController.php @@ -1,125 +1,127 @@ <?php final class DiffusionDiffController extends DiffusionController { public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $data = $data + array( 'dblob' => $this->getRequest()->getStr('ref'), ); $drequest = DiffusionRequest::newFromAphrontRequestDictionary( $data, $this->getRequest()); $this->diffusionRequest = $drequest; } public function processRequest() { $drequest = $this->getDiffusionRequest(); $request = $this->getRequest(); $user = $request->getUser(); if (!$request->isAjax()) { // This request came out of the dropdown menu, either "View Standalone" // or "View Raw File". $view = $request->getStr('view'); if ($view == 'r') { $uri = $drequest->generateURI( array( 'action' => 'browse', 'params' => array( 'view' => 'raw', ), )); } else { $uri = $drequest->generateURI( array( 'action' => 'change', )); } return id(new AphrontRedirectResponse())->setURI($uri); } $data = $this->callConduitWithDiffusionRequest( 'diffusion.diffquery', array( 'commit' => $drequest->getCommit(), - 'path' => $drequest->getPath())); + 'path' => $drequest->getPath(), + )); $drequest->updateSymbolicCommit($data['effectiveCommit']); $raw_changes = ArcanistDiffChange::newFromConduit($data['changes']); $diff = DifferentialDiff::newFromRawChanges($raw_changes); $changesets = $diff->getChangesets(); $changeset = reset($changesets); if (!$changeset) { return new Aphront404Response(); } $parser = new DifferentialChangesetParser(); $parser->setUser($user); $parser->setChangeset($changeset); $parser->setRenderingReference($drequest->generateURI( array( - 'action' => 'rendering-ref'))); + 'action' => 'rendering-ref', + ))); $parser->setCharacterEncoding($request->getStr('encoding')); $parser->setHighlightAs($request->getStr('highlight')); $coverage = $drequest->loadCoverage(); if ($coverage) { $parser->setCoverage($coverage); } $pquery = new DiffusionPathIDQuery(array($changeset->getFilename())); $ids = $pquery->loadPathIDs(); $path_id = $ids[$changeset->getFilename()]; $parser->setLeftSideCommentMapping($path_id, false); $parser->setRightSideCommentMapping($path_id, true); $parser->setWhitespaceMode( DifferentialChangesetParser::WHITESPACE_SHOW_ALL); $inlines = PhabricatorAuditInlineComment::loadDraftAndPublishedComments( $user, $drequest->loadCommit()->getPHID(), $path_id); if ($inlines) { foreach ($inlines as $inline) { $parser->parseInlineComment($inline); } $phids = mpull($inlines, 'getAuthorPHID'); $handles = $this->loadViewerHandles($phids); $parser->setHandles($handles); } $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); foreach ($inlines as $inline) { $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); $parser->setMarkupEngine($engine); $spec = $request->getStr('range'); list($range_s, $range_e, $mask) = DifferentialChangesetParser::parseRangeSpecification($spec); $output = $parser->render($range_s, $range_e, $mask); return id(new PhabricatorChangesetResponse()) ->setRenderedChangeset($output); } } diff --git a/src/applications/diffusion/controller/DiffusionHistoryController.php b/src/applications/diffusion/controller/DiffusionHistoryController.php index 88918ae19d..b1074f718a 100644 --- a/src/applications/diffusion/controller/DiffusionHistoryController.php +++ b/src/applications/diffusion/controller/DiffusionHistoryController.php @@ -1,173 +1,174 @@ <?php final class DiffusionHistoryController extends DiffusionController { public function shouldAllowPublic() { return true; } public function processRequest() { $drequest = $this->diffusionRequest; $request = $this->getRequest(); $viewer = $request->getUser(); $repository = $drequest->getRepository(); $page_size = $request->getInt('pagesize', 100); $offset = $request->getInt('offset', 0); $params = array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'offset' => $offset, - 'limit' => $page_size + 1); + 'limit' => $page_size + 1, + ); if (!$request->getBool('copies')) { $params['needDirectChanges'] = true; $params['needChildChanges'] = true; } $history_results = $this->callConduitWithDiffusionRequest( 'diffusion.historyquery', $params); $history = DiffusionPathChange::newFromConduit( $history_results['pathChanges']); $pager = new AphrontPagerView(); $pager->setPageSize($page_size); $pager->setOffset($offset); $history = $pager->sliceResults($history); $pager->setURI($request->getRequestURI(), 'offset'); $show_graph = !strlen($drequest->getPath()); $content = array(); $history_table = new DiffusionHistoryTableView(); $history_table->setUser($request->getUser()); $history_table->setDiffusionRequest($drequest); $history_table->setHistory($history); $history_table->loadRevisions(); $phids = $history_table->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $history_table->setHandles($handles); if ($show_graph) { $history_table->setParents($history_results['parents']); $history_table->setIsHead($offset == 0); } $history_panel = new AphrontPanelView(); $history_panel->appendChild($history_table); $history_panel->appendChild($pager); $history_panel->setNoBackground(); $content[] = $history_panel; $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setPolicyObject($repository) ->setHeader($this->renderPathLinks($drequest, $mode = 'history')); $actions = $this->buildActionView($drequest); $properties = $this->buildPropertyView($drequest, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'history', )); return $this->buildApplicationPage( array( $crumbs, $object_box, $content, ), array( 'title' => array( pht('History'), pht('%s Repository', $drequest->getRepository()->getCallsign()), ), )); } private function buildActionView(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); $view = id(new PhabricatorActionListView()) ->setUser($viewer); $browse_uri = $drequest->generateURI( array( 'action' => 'browse', )); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Browse Content')) ->setHref($browse_uri) ->setIcon('fa-files-o')); // TODO: Sometimes we do have a change view, we need to look at the most // recent history entry to figure it out. $request = $this->getRequest(); if ($request->getBool('copies')) { $branch_name = pht('Hide Copies/Branches'); $branch_uri = $request->getRequestURI() ->alter('offset', null) ->alter('copies', null); } else { $branch_name = pht('Show Copies/Branches'); $branch_uri = $request->getRequestURI() ->alter('offset', null) ->alter('copies', true); } $view->addAction( id(new PhabricatorActionView()) ->setName($branch_name) ->setIcon('fa-code-fork') ->setHref($branch_uri)); return $view; } protected function buildPropertyView( DiffusionRequest $drequest, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setActionList($actions); $stable_commit = $drequest->getStableCommit(); $callsign = $drequest->getRepository()->getCallsign(); $view->addProperty( pht('Commit'), phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $stable_commit, )), ), $drequest->getRepository()->formatCommitName($stable_commit))); return $view; } } diff --git a/src/applications/diffusion/controller/DiffusionLastModifiedController.php b/src/applications/diffusion/controller/DiffusionLastModifiedController.php index 7b96f6ab7f..f6bb0fb908 100644 --- a/src/applications/diffusion/controller/DiffusionLastModifiedController.php +++ b/src/applications/diffusion/controller/DiffusionLastModifiedController.php @@ -1,158 +1,160 @@ <?php final class DiffusionLastModifiedController extends DiffusionController { public function shouldAllowPublic() { return true; } public function processRequest() { $drequest = $this->getDiffusionRequest(); $request = $this->getRequest(); $viewer = $request->getUser(); $paths = $request->getStr('paths'); $paths = json_decode($paths, true); if (!is_array($paths)) { return new Aphront400Response(); } $modified_map = $this->callConduitWithDiffusionRequest( 'diffusion.lastmodifiedquery', array( 'paths' => array_fill_keys($paths, $drequest->getCommit()), )); if ($modified_map) { $commit_map = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($drequest->getRepository()) ->withIdentifiers(array_values($modified_map)) ->needCommitData(true) ->execute(); $commit_map = mpull($commit_map, null, 'getCommitIdentifier'); } else { $commit_map = array(); } $commits = array(); foreach ($paths as $path) { $modified_at = idx($modified_map, $path); if ($modified_at) { $commit = idx($commit_map, $modified_at); if ($commit) { $commits[$path] = $commit; } } } $phids = array(); foreach ($commits as $commit) { $data = $commit->getCommitData(); $phids[] = $data->getCommitDetail('authorPHID'); $phids[] = $data->getCommitDetail('committerPHID'); } $phids = array_filter($phids); $handles = $this->loadViewerHandles($phids); $branch = $drequest->loadBranch(); if ($branch && $commits) { $lint_query = id(new DiffusionLintCountQuery()) ->withBranchIDs(array($branch->getID())) ->withPaths(array_keys($commits)); if ($drequest->getLint()) { $lint_query->withCodes(array($drequest->getLint())); } $lint = $lint_query->execute(); } else { $lint = array(); } $output = array(); foreach ($commits as $path => $commit) { $prequest = clone $drequest; $prequest->setPath($path); $output[$path] = $this->renderColumns( $prequest, $handles, $commit, idx($lint, $path)); } return id(new AphrontAjaxResponse())->setContent($output); } private function renderColumns( DiffusionRequest $drequest, array $handles, PhabricatorRepositoryCommit $commit = null, $lint = null) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $viewer = $this->getRequest()->getUser(); if ($commit) { $epoch = $commit->getEpoch(); $modified = DiffusionView::linkCommit( $drequest->getRepository(), $commit->getCommitIdentifier()); $date = phabricator_date($epoch, $viewer); $time = phabricator_time($epoch, $viewer); } else { $modified = ''; $date = ''; $time = ''; } $data = $commit->getCommitData(); if ($data) { $author_phid = $data->getCommitDetail('authorPHID'); if ($author_phid && isset($handles[$author_phid])) { $author = $handles[$author_phid]->renderLink(); } else { $author = DiffusionView::renderName($data->getAuthorName()); } $committer = $data->getCommitDetail('committer'); if ($committer) { $committer_phid = $data->getCommitDetail('committerPHID'); if ($committer_phid && isset($handles[$committer_phid])) { $committer = $handles[$committer_phid]->renderLink(); } else { $committer = DiffusionView::renderName($committer); } if ($author != $committer) { $author = hsprintf('%s/%s', $author, $committer); } } $details = AphrontTableView::renderSingleDisplayLine($data->getSummary()); } else { $author = ''; $details = ''; } $return = array( 'commit' => $modified, 'date' => $date, 'time' => $time, 'author' => $author, 'details' => $details, ); if ($lint !== null) { $return['lint'] = phutil_tag( 'a', - array('href' => $drequest->generateURI(array( - 'action' => 'lint', - 'lint' => null, - ))), + array( + 'href' => $drequest->generateURI(array( + 'action' => 'lint', + 'lint' => null, + )), + ), number_format($lint)); } return $return; } } diff --git a/src/applications/diffusion/controller/DiffusionLintDetailsController.php b/src/applications/diffusion/controller/DiffusionLintDetailsController.php index 5d8cdef0b9..0669f818f6 100644 --- a/src/applications/diffusion/controller/DiffusionLintDetailsController.php +++ b/src/applications/diffusion/controller/DiffusionLintDetailsController.php @@ -1,139 +1,144 @@ <?php final class DiffusionLintDetailsController extends DiffusionController { public function processRequest() { $limit = 500; $offset = $this->getRequest()->getInt('offset', 0); $drequest = $this->getDiffusionRequest(); $branch = $drequest->loadBranch(); $messages = $this->loadLintMessages($branch, $limit, $offset); $is_dir = (substr('/'.$drequest->getPath(), -1) == '/'); $authors = $this->loadViewerHandles(ipull($messages, 'authorPHID')); $rows = array(); foreach ($messages as $message) { $path = phutil_tag( 'a', - array('href' => $drequest->generateURI(array( - 'action' => 'lint', - 'path' => $message['path'], - ))), + array( + 'href' => $drequest->generateURI(array( + 'action' => 'lint', + 'path' => $message['path'], + )), + ), substr($message['path'], strlen($drequest->getPath()) + 1)); $line = phutil_tag( 'a', - array('href' => $drequest->generateURI(array( - 'action' => 'browse', - 'path' => $message['path'], - 'line' => $message['line'], - 'commit' => $branch->getLintCommit(), - ))), + array( + 'href' => $drequest->generateURI(array( + 'action' => 'browse', + 'path' => $message['path'], + 'line' => $message['line'], + 'commit' => $branch->getLintCommit(), + )), + ), $message['line']); $author = $message['authorPHID']; if ($author && $authors[$author]) { $author = $authors[$author]->renderLink(); } $rows[] = array( $path, $line, $author, ArcanistLintSeverity::getStringForSeverity($message['severity']), $message['name'], $message['description'], ); } $table = id(new AphrontTableView($rows)) ->setHeaders(array( pht('Path'), pht('Line'), pht('Author'), pht('Severity'), pht('Name'), pht('Description'), )) ->setColumnClasses(array('', 'n')) ->setColumnVisibility(array($is_dir)); $content = array(); $pager = id(new AphrontPagerView()) ->setPageSize($limit) ->setOffset($offset) ->setHasMorePages(count($messages) >= $limit) ->setURI($this->getRequest()->getRequestURI(), 'offset'); $content[] = id(new AphrontPanelView()) ->setNoBackground(true) ->appendChild($table) ->appendChild($pager); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'lint', )); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => array( pht('Lint'), $drequest->getRepository()->getCallsign(), - ))); + ), + )); } private function loadLintMessages( PhabricatorRepositoryBranch $branch, $limit, $offset) { $drequest = $this->getDiffusionRequest(); if (!$branch) { return array(); } $conn = $branch->establishConnection('r'); $where = array( qsprintf($conn, 'branchID = %d', $branch->getID()), ); if ($drequest->getPath() != '') { $path = '/'.$drequest->getPath(); $is_dir = (substr($path, -1) == '/'); $where[] = ($is_dir ? qsprintf($conn, 'path LIKE %>', $path) : qsprintf($conn, 'path = %s', $path)); } if ($drequest->getLint() != '') { $where[] = qsprintf( $conn, 'code = %s', $drequest->getLint()); } return queryfx_all( $conn, 'SELECT * FROM %T WHERE %Q ORDER BY path, code, line LIMIT %d OFFSET %d', PhabricatorRepository::TABLE_LINTMESSAGE, implode(' AND ', $where), $limit, $offset); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php index 9d26926345..25acfab5a2 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php @@ -1,711 +1,712 @@ <?php final class DiffusionRepositoryController extends DiffusionController { public function shouldAllowPublic() { return true; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $content = array(); $crumbs = $this->buildCrumbs(); $content[] = $crumbs; $content[] = $this->buildPropertiesTable($drequest->getRepository()); // Before we do any work, make sure we're looking at a some content: we're // on a valid branch, and the repository is not empty. $page_has_content = false; $empty_title = null; $empty_message = null; // If this VCS supports branches, check that the selected branch actually // exists. if ($drequest->supportsBranches()) { // NOTE: Mercurial may have multiple branch heads with the same name. $ref_cursors = id(new PhabricatorRepositoryRefCursorQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withRefTypes(array(PhabricatorRepositoryRefCursor::TYPE_BRANCH)) ->withRefNames(array($drequest->getBranch())) ->execute(); if ($ref_cursors) { // This is a valid branch, so we necessarily have some content. $page_has_content = true; } else { $empty_title = pht('No Such Branch'); $empty_message = pht( 'There is no branch named "%s" in this repository.', $drequest->getBranch()); } } // If we didn't find any branches, check if there are any commits at all. // This can tailor the message for empty repositories. if (!$page_has_content) { $any_commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->setLimit(1) ->execute(); if ($any_commit) { if (!$drequest->supportsBranches()) { $page_has_content = true; } } else { $empty_title = pht('Empty Repository'); $empty_message = pht( 'This repository does not have any commits yet.'); } } if ($page_has_content) { $content[] = $this->buildNormalContent($drequest); } else { $content[] = id(new AphrontErrorView()) ->setTitle($empty_title) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ->setErrors(array($empty_message)); } return $this->buildApplicationPage( $content, array( 'title' => $drequest->getRepository()->getName(), )); } private function buildNormalContent(DiffusionRequest $drequest) { $repository = $drequest->getRepository(); $phids = array(); $content = array(); try { $history_results = $this->callConduitWithDiffusionRequest( 'diffusion.historyquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), 'offset' => 0, - 'limit' => 15)); + 'limit' => 15, + )); $history = DiffusionPathChange::newFromConduit( $history_results['pathChanges']); foreach ($history as $item) { $data = $item->getCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } if ($data->getCommitDetail('committerPHID')) { $phids[$data->getCommitDetail('committerPHID')] = true; } } } $history_exception = null; } catch (Exception $ex) { $history_results = null; $history = null; $history_exception = $ex; } try { $browse_results = DiffusionBrowseResultSet::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.browsequery', array( 'path' => $drequest->getPath(), 'commit' => $drequest->getCommit(), ))); $browse_paths = $browse_results->getPaths(); foreach ($browse_paths as $item) { $data = $item->getLastCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } if ($data->getCommitDetail('committerPHID')) { $phids[$data->getCommitDetail('committerPHID')] = true; } } } $browse_exception = null; } catch (Exception $ex) { $browse_results = null; $browse_paths = null; $browse_exception = $ex; } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); if ($browse_results) { $readme = $this->callConduitWithDiffusionRequest( 'diffusion.readmequery', array( 'paths' => $browse_results->getPathDicts(), 'commit' => $drequest->getStableCommit(), )); } else { $readme = null; } $content[] = $this->buildBrowseTable( $browse_results, $browse_paths, $browse_exception, $handles); $content[] = $this->buildHistoryTable( $history_results, $history, $history_exception, $handles); try { $content[] = $this->buildTagListTable($drequest); } catch (Exception $ex) { if (!$repository->isImporting()) { $content[] = $this->renderStatusMessage( pht('Unable to Load Tags'), $ex->getMessage()); } } try { $content[] = $this->buildBranchListTable($drequest); } catch (Exception $ex) { if (!$repository->isImporting()) { $content[] = $this->renderStatusMessage( pht('Unable to Load Branches'), $ex->getMessage()); } } if ($readme) { $box = new PHUIBoxView(); $box->appendChild($readme); $box->addPadding(PHUI::PADDING_LARGE); $panel = new PHUIObjectBoxView(); $panel->setHeaderText(pht('README')); $panel->appendChild($box); $content[] = $panel; } return $content; } private function buildPropertiesTable(PhabricatorRepository $repository) { $user = $this->getRequest()->getUser(); $header = id(new PHUIHeaderView()) ->setHeader($repository->getName()) ->setUser($user) ->setPolicyObject($repository); if (!$repository->isTracked()) { $header->setStatus('fa-ban', 'dark', pht('Inactive')); } else if ($repository->isImporting()) { $header->setStatus('fa-clock-o', 'indigo', pht('Importing...')); } else { $header->setStatus('fa-check', 'bluegrey', pht('Active')); } $actions = $this->buildActionList($repository); $view = id(new PHUIPropertyListView()) ->setUser($user); $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $repository->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); if ($project_phids) { $this->loadHandles($project_phids); $view->addProperty( pht('Projects'), $this->renderHandlesForPHIDs($project_phids)); } if ($repository->isHosted()) { $ssh_uri = $repository->getSSHCloneURIObject(); if ($ssh_uri) { $clone_uri = $this->renderCloneCommand( $repository, $ssh_uri, $repository->getServeOverSSH(), '/settings/panel/ssh/'); $view->addProperty( $repository->isSVN() ? pht('Checkout (SSH)') : pht('Clone (SSH)'), $clone_uri); } $http_uri = $repository->getHTTPCloneURIObject(); if ($http_uri) { $clone_uri = $this->renderCloneCommand( $repository, $http_uri, $repository->getServeOverHTTP(), PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth') ? '/settings/panel/vcspassword/' : null); $view->addProperty( $repository->isSVN() ? pht('Checkout (HTTP)') : pht('Clone (HTTP)'), $clone_uri); } } else { switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $view->addProperty( pht('Clone'), $this->renderCloneCommand( $repository, $repository->getPublicCloneURI())); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $view->addProperty( pht('Checkout'), $this->renderCloneCommand( $repository, $repository->getPublicCloneURI())); break; } } $description = $repository->getDetail('description'); if (strlen($description)) { $description = PhabricatorMarkupEngine::renderOneObject( $repository, 'description', $user); $view->addSectionHeader(pht('Description')); $view->addTextContent($description); } $view->setActionList($actions); return id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($view); } private function buildBranchListTable(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); if ($drequest->getBranch() === null) { return null; } $limit = 15; $branches = $this->callConduitWithDiffusionRequest( 'diffusion.branchquery', array( 'limit' => $limit + 1, )); if (!$branches) { return null; } $more_branches = (count($branches) > $limit); $branches = array_slice($branches, 0, $limit); $branches = DiffusionRepositoryRef::loadAllFromDictionaries($branches); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers(mpull($branches, 'getCommitIdentifier')) ->withRepository($drequest->getRepository()) ->execute(); $table = id(new DiffusionBranchTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setBranches($branches) ->setCommits($commits); $panel = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); $header->setHeader(pht('Branches')); if ($more_branches) { $header->setSubHeader(pht('Showing %d branches.', $limit)); } $icon = id(new PHUIIconView()) ->setIconFont('fa-code-fork'); $button = new PHUIButtonView(); $button->setText(pht('Show All Branches')); $button->setTag('a'); $button->setIcon($icon); $button->setHref($drequest->generateURI( array( 'action' => 'branches', ))); $header->addActionLink($button); $panel->setHeader($header); $panel->appendChild($table); return $panel; } private function buildTagListTable(DiffusionRequest $drequest) { $viewer = $this->getRequest()->getUser(); $tag_limit = 15; $tags = array(); try { $tags = DiffusionRepositoryTag::newFromConduit( $this->callConduitWithDiffusionRequest( 'diffusion.tagsquery', array( // On the home page, we want to find tags on any branch. 'commit' => null, 'limit' => $tag_limit + 1, ))); } catch (ConduitException $e) { if ($e->getMessage() != 'ERR-UNSUPPORTED-VCS') { throw $e; } } if (!$tags) { return null; } $more_tags = (count($tags) > $tag_limit); $tags = array_slice($tags, 0, $tag_limit); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers(mpull($tags, 'getCommitIdentifier')) ->withRepository($drequest->getRepository()) ->needCommitData(true) ->execute(); $view = id(new DiffusionTagListView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setTags($tags) ->setCommits($commits); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $panel = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); $header->setHeader(pht('Tags')); if ($more_tags) { $header->setSubHeader( pht('Showing the %d most recent tags.', $tag_limit)); } $icon = id(new PHUIIconView()) ->setIconFont('fa-tag'); $button = new PHUIButtonView(); $button->setText(pht('Show All Tags')); $button->setTag('a'); $button->setIcon($icon); $button->setHref($drequest->generateURI( array( 'action' => 'tags', ))); $header->addActionLink($button); $panel->setHeader($header); $panel->appendChild($view); return $panel; } private function buildActionList(PhabricatorRepository $repository) { $viewer = $this->getRequest()->getUser(); $view_uri = $this->getApplicationURI($repository->getCallsign().'/'); $edit_uri = $this->getApplicationURI($repository->getCallsign().'/edit/'); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($repository) ->setObjectURI($view_uri); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $repository, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Repository')) ->setIcon('fa-pencil') ->setHref($edit_uri) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); if ($repository->isHosted()) { $callsign = $repository->getCallsign(); $push_uri = $this->getApplicationURI( 'pushlog/?repositories=r'.$callsign); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View Push Logs')) ->setIcon('fa-list-alt') ->setHref($push_uri)); } return $view; } private function buildHistoryTable( $history_results, $history, $history_exception, array $handles) { $request = $this->getRequest(); $viewer = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); if ($history_exception) { if ($repository->isImporting()) { return $this->renderStatusMessage( pht('Still Importing...'), pht( 'This repository is still importing. History is not yet '. 'available.')); } else { return $this->renderStatusMessage( pht('Unable to Retrieve History'), $history_exception->getMessage()); } } $history_table = id(new DiffusionHistoryTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setHandles($handles) ->setHistory($history); // TODO: Super sketchy. $history_table->loadRevisions(); if ($history_results) { $history_table->setParents($history_results['parents']); } $history_table->setIsHead(true); $callsign = $drequest->getRepository()->getCallsign(); $icon = id(new PHUIIconView()) ->setIconFont('fa-list-alt'); $button = id(new PHUIButtonView()) ->setText(pht('View Full History')) ->setHref($drequest->generateURI( array( 'action' => 'history', ))) ->setTag('a') ->setIcon($icon); $panel = new PHUIObjectBoxView(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Recent Commits')) ->addActionLink($button); $panel->setHeader($header); $panel->appendChild($history_table); return $panel; } private function buildBrowseTable( $browse_results, $browse_paths, $browse_exception, array $handles) { require_celerity_resource('diffusion-icons-css'); $request = $this->getRequest(); $viewer = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); if ($browse_exception) { if ($repository->isImporting()) { // The history table renders a useful message. return null; } else { return $this->renderStatusMessage( pht('Unable to Retrieve Paths'), $browse_exception->getMessage()); } } $browse_table = id(new DiffusionBrowseTableView()) ->setUser($viewer) ->setDiffusionRequest($drequest) ->setHandles($handles); if ($browse_paths) { $browse_table->setPaths($browse_paths); } else { $browse_table->setPaths(array()); } $browse_uri = $drequest->generateURI(array('action' => 'browse')); $browse_panel = new PHUIObjectBoxView(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Repository')); $icon = id(new PHUIIconView()) ->setIconFont('fa-folder-open'); $button = new PHUIButtonView(); $button->setText(pht('Browse Repository')); $button->setTag('a'); $button->setIcon($icon); $button->setHref($browse_uri); $header->addActionLink($button); $browse_panel->setHeader($header); if ($repository->canUsePathTree()) { Javelin::initBehavior( 'diffusion-locate-file', array( 'controlID' => 'locate-control', 'inputID' => 'locate-input', 'browseBaseURI' => (string)$drequest->generateURI( array( 'action' => 'browse', )), 'uri' => (string)$drequest->generateURI( array( 'action' => 'pathtree', )), )); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTypeaheadControl()) ->setHardpointID('locate-control') ->setID('locate-input') ->setLabel(pht('Locate File'))); $form_box = id(new PHUIBoxView()) ->addClass('diffusion-locate-file-view') ->appendChild($form->buildLayoutView()); $browse_panel->appendChild($form_box); } $browse_panel->appendChild($browse_table); return $browse_panel; } private function renderCloneCommand( PhabricatorRepository $repository, $uri, $serve_mode = null, $manage_uri = null) { require_celerity_resource('diffusion-icons-css'); Javelin::initBehavior('select-on-click'); switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $command = csprintf( 'git clone %R', $uri); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $command = csprintf( 'hg clone %R', $uri); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: if ($repository->isHosted()) { $command = csprintf( 'svn checkout %R %R', $uri, $repository->getCloneName()); } else { $command = csprintf( 'svn checkout %R', $uri); } break; } $input = javelin_tag( 'input', array( 'type' => 'text', 'value' => (string)$command, 'class' => 'diffusion-clone-uri', 'sigil' => 'select-on-click', 'readonly' => 'true', )); $extras = array(); if ($serve_mode) { if ($serve_mode === PhabricatorRepository::SERVE_READONLY) { $extras[] = pht('(Read Only)'); } } if ($manage_uri) { if ($this->getRequest()->getUser()->isLoggedIn()) { $extras[] = phutil_tag( 'a', array( 'href' => $manage_uri, ), pht('Manage Credentials')); } } if ($extras) { $extras = phutil_implode_html(' ', $extras); $extras = phutil_tag( 'div', array( 'class' => 'diffusion-clone-extras', ), $extras); } return array($input, $extras); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php index 1f6bb5acc1..4457df8d23 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditBasicController.php @@ -1,170 +1,171 @@ <?php final class DiffusionRepositoryEditBasicController extends DiffusionRepositoryEditController { public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->needProjectPHIDs(true) ->withIDs(array($repository->getID())) ->executeOne(); if (!$repository) { return new Aphront404Response(); } $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $v_name = $repository->getName(); $v_desc = $repository->getDetail('description'); $v_clone_name = $repository->getDetail('clone-name'); $e_name = true; $errors = array(); if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_desc = $request->getStr('description'); $v_projects = $request->getArr('projectPHIDs'); if ($repository->isHosted()) { $v_clone_name = $request->getStr('cloneName'); } if (!strlen($v_name)) { $e_name = pht('Required'); $errors[] = pht('Repository name is required.'); } else { $e_name = null; } if (!$errors) { $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_name = PhabricatorRepositoryTransaction::TYPE_NAME; $type_desc = PhabricatorRepositoryTransaction::TYPE_DESCRIPTION; $type_edge = PhabricatorTransactions::TYPE_EDGE; $type_clone_name = PhabricatorRepositoryTransaction::TYPE_CLONE_NAME; $xactions[] = id(clone $template) ->setTransactionType($type_name) ->setNewValue($v_name); $xactions[] = id(clone $template) ->setTransactionType($type_desc) ->setNewValue($v_desc); $xactions[] = id(clone $template) ->setTransactionType($type_clone_name) ->setNewValue($v_clone_name); $xactions[] = id(clone $template) ->setTransactionType($type_edge) ->setMetadataValue( 'edge:type', PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) ->setNewValue( array( '=' => array_fuse($v_projects), )); id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($user) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Edit Basics')); $title = pht('Edit %s', $repository->getName()); $project_handles = $this->loadViewerHandles($repository->getProjectPHIDs()); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($v_name) ->setError($e_name)); if ($repository->isHosted()) { $form ->appendChild( id(new AphrontFormTextControl()) ->setName('cloneName') ->setLabel(pht('Clone/Checkout As')) ->setValue($v_clone_name) ->setCaption( pht( 'Optional directory name to use when cloning or checking out '. 'this repository.'))); } $form ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('description') ->setLabel(pht('Description')) ->setValue($v_desc)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorProjectDatasource()) ->setName('projectPHIDs') ->setLabel(pht('Projects')) ->setValue($project_handles)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save')) ->addCancelButton($edit_uri)) ->appendChild(id(new PHUIFormDividerControl())) ->appendRemarkupInstructions($this->getReadmeInstructions()); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) ->setFormErrors($errors); return $this->buildApplicationPage( array( $crumbs, - $object_box), + $object_box, + ), array( 'title' => $title, )); } private function getReadmeInstructions() { return pht(<<<EOTEXT You can also create a `README` file at the repository root (or in any subdirectory) to provide information about the repository. These formats are supported: | File Name | Rendered As... | |-----------------|----------------| | `README` | Plain Text | | `README.txt` | Plain Text | | `README.remarkup` | Remarkup | | `README.md` | Remarkup | | `README.rainbow` | \xC2\xA1Fiesta! | EOTEXT ); } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php index 64b3ae331d..a75f66500d 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditHostingController.php @@ -1,289 +1,290 @@ <?php final class DiffusionRepositoryEditHostingController extends DiffusionRepositoryEditController { private $serve; public function willProcessRequest(array $data) { parent::willProcessRequest($data); $this->serve = idx($data, 'serve'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $drequest = $this->diffusionRequest; $repository = $drequest->getRepository(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($repository->getID())) ->executeOne(); if (!$repository) { return new Aphront404Response(); } if (!$this->serve) { return $this->handleHosting($repository); } else { return $this->handleProtocols($repository); } } public function handleHosting(PhabricatorRepository $repository) { $request = $this->getRequest(); $user = $request->getUser(); $v_hosting = $repository->isHosted(); $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $next_uri = $this->getRepositoryControllerURI($repository, 'edit/serve/'); if ($request->isFormPost()) { $v_hosting = $request->getBool('hosting'); $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_hosting = PhabricatorRepositoryTransaction::TYPE_HOSTING; $xactions[] = id(clone $template) ->setTransactionType($type_hosting) ->setNewValue($v_hosting); id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($user) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($next_uri); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Edit Hosting')); $title = pht('Edit Hosting (%s)', $repository->getName()); $hosted_control = id(new AphrontFormRadioButtonControl()) ->setName('hosting') ->setLabel(pht('Hosting')) ->addButton( true, pht('Host Repository on Phabricator'), pht( 'Phabricator will host this repository. Users will be able to '. 'push commits to Phabricator. Phabricator will not pull '. 'changes from elsewhere.')) ->addButton( false, pht('Host Repository Elsewhere'), pht( 'Phabricator will pull updates to this repository from a master '. 'repository elsewhere (for example, on GitHub or Bitbucket). '. 'Users will not be able to push commits to this repository.')) ->setValue($v_hosting); $doc_href = PhabricatorEnv::getDoclink( 'Diffusion User Guide: Repository Hosting'); $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions( pht( 'Phabricator can host repositories, or it can track repositories '. 'hosted elsewhere (like on GitHub or Bitbucket). For information '. 'on configuring hosting, see [[ %s | Diffusion User Guide: '. 'Repository Hosting]]', $doc_href)) ->appendChild($hosted_control) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save and Continue')) ->addCancelButton($edit_uri)); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $title, )); } public function handleProtocols(PhabricatorRepository $repository) { $request = $this->getRequest(); $user = $request->getUser(); $type = $repository->getVersionControlSystem(); $is_svn = ($type == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN); $v_http_mode = $repository->getDetail( 'serve-over-http', PhabricatorRepository::SERVE_OFF); $v_ssh_mode = $repository->getDetail( 'serve-over-ssh', PhabricatorRepository::SERVE_OFF); $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); $prev_uri = $this->getRepositoryControllerURI($repository, 'edit/hosting/'); if ($request->isFormPost()) { $v_http_mode = $request->getStr('http'); $v_ssh_mode = $request->getStr('ssh'); $xactions = array(); $template = id(new PhabricatorRepositoryTransaction()); $type_http = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_HTTP; $type_ssh = PhabricatorRepositoryTransaction::TYPE_PROTOCOL_SSH; if (!$is_svn) { $xactions[] = id(clone $template) ->setTransactionType($type_http) ->setNewValue($v_http_mode); } $xactions[] = id(clone $template) ->setTransactionType($type_ssh) ->setNewValue($v_ssh_mode); id(new PhabricatorRepositoryEditor()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->setActor($user) ->applyTransactions($repository, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Edit Protocols')); $title = pht('Edit Protocols (%s)', $repository->getName()); $rw_message = pht( 'Phabricator will serve a read-write copy of this repository.'); if (!$repository->isHosted()) { $rw_message = array( $rw_message, phutil_tag('br'), phutil_tag('br'), pht( '%s: This repository is hosted elsewhere, so Phabricator can not '. 'perform writes. This mode will act like "Read Only" for '. 'repositories hosted elsewhere.', - phutil_tag('strong', array(), 'WARNING'))); + phutil_tag('strong', array(), 'WARNING')), + ); } $ssh_control = id(new AphrontFormRadioButtonControl()) ->setName('ssh') ->setLabel(pht('SSH')) ->setValue($v_ssh_mode) ->addButton( PhabricatorRepository::SERVE_OFF, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_OFF), pht('Phabricator will not serve this repository over SSH.')) ->addButton( PhabricatorRepository::SERVE_READONLY, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_READONLY), pht( 'Phabricator will serve a read-only copy of this repository '. 'over SSH.')) ->addButton( PhabricatorRepository::SERVE_READWRITE, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_READWRITE), $rw_message); $http_control = id(new AphrontFormRadioButtonControl()) ->setName('http') ->setLabel(pht('HTTP')) ->setValue($v_http_mode) ->addButton( PhabricatorRepository::SERVE_OFF, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_OFF), pht('Phabricator will not serve this repository over HTTP.')) ->addButton( PhabricatorRepository::SERVE_READONLY, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_READONLY), pht( 'Phabricator will serve a read-only copy of this repository '. 'over HTTP.')) ->addButton( PhabricatorRepository::SERVE_READWRITE, PhabricatorRepository::getProtocolAvailabilityName( PhabricatorRepository::SERVE_READWRITE), $rw_message); if ($is_svn) { $http_control = id(new AphrontFormMarkupControl()) ->setLabel(pht('HTTP')) ->setValue( phutil_tag( 'em', array(), pht( 'Phabricator does not currently support HTTP access to '. 'Subversion repositories.'))); } $form = id(new AphrontFormView()) ->setUser($user) ->appendRemarkupInstructions( pht( 'Phabricator can serve repositories over various protocols. You can '. 'configure server protocols here.')) ->appendChild($ssh_control); if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) { $form->appendRemarkupInstructions( pht( 'NOTE: The configuration setting [[ %s | %s ]] is currently '. 'disabled. You must enable it to activate authenticated access '. 'to repositories over HTTP.', '/config/edit/diffusion.allow-http-auth/', 'diffusion.allow-http-auth')); } $form ->appendChild($http_control) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Changes')) ->addCancelButton($prev_uri, pht('Back'))); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/diffusion/controller/DiffusionTagListController.php b/src/applications/diffusion/controller/DiffusionTagListController.php index 169d59d347..d4c2d5dc40 100644 --- a/src/applications/diffusion/controller/DiffusionTagListController.php +++ b/src/applications/diffusion/controller/DiffusionTagListController.php @@ -1,97 +1,98 @@ <?php final class DiffusionTagListController extends DiffusionController { public function shouldAllowPublic() { return true; } public function processRequest() { $drequest = $this->getDiffusionRequest(); $request = $this->getRequest(); $viewer = $request->getUser(); $repository = $drequest->getRepository(); $pager = new AphrontPagerView(); $pager->setURI($request->getRequestURI(), 'offset'); $pager->setOffset($request->getInt('offset')); $params = array( 'limit' => $pager->getPageSize() + 1, - 'offset' => $pager->getOffset()); + 'offset' => $pager->getOffset(), + ); if ($drequest->getSymbolicCommit()) { $is_commit = true; $params['commit'] = $drequest->getSymbolicCommit(); } else { $is_commit = false; } $tags = array(); try { $conduit_result = $this->callConduitWithDiffusionRequest( 'diffusion.tagsquery', $params); $tags = DiffusionRepositoryTag::newFromConduit($conduit_result); } catch (ConduitException $ex) { if ($ex->getMessage() != 'ERR-UNSUPPORTED-VCS') { throw $ex; } } $tags = $pager->sliceResults($tags); $content = null; if (!$tags) { $content = $this->renderStatusMessage( pht('No Tags'), $is_commit ? pht('This commit has no tags.') : pht('This repository has no tags.')); } else { $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers(mpull($tags, 'getCommitIdentifier')) ->needCommitData(true) ->execute(); $view = id(new DiffusionTagListView()) ->setTags($tags) ->setUser($viewer) ->setCommits($commits) ->setDiffusionRequest($drequest); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $panel = id(new AphrontPanelView()) ->setNoBackground(true) ->appendChild($view) ->appendChild($pager); $content = $panel; } $crumbs = $this->buildCrumbs( array( 'tags' => true, 'commit' => $drequest->getSymbolicCommit(), )); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => array( pht('Tags'), $repository->getCallsign().' Repository', ), 'device' => false, )); } } diff --git a/src/applications/diffusion/data/DiffusionBrowseResultSet.php b/src/applications/diffusion/data/DiffusionBrowseResultSet.php index 17f5a3fcde..c28254dd35 100644 --- a/src/applications/diffusion/data/DiffusionBrowseResultSet.php +++ b/src/applications/diffusion/data/DiffusionBrowseResultSet.php @@ -1,91 +1,92 @@ <?php final class DiffusionBrowseResultSet { const REASON_IS_FILE = 'is-file'; const REASON_IS_DELETED = 'is-deleted'; const REASON_IS_NONEXISTENT = 'nonexistent'; const REASON_BAD_COMMIT = 'bad-commit'; const REASON_IS_EMPTY = 'empty'; const REASON_IS_UNTRACKED_PARENT = 'untracked-parent'; private $paths; private $isValidResults; private $reasonForEmptyResultSet; private $existedAtCommit; private $deletedAtCommit; public function setPaths(array $paths) { assert_instances_of($paths, 'DiffusionRepositoryPath'); $this->paths = $paths; return $this; } public function getPaths() { return $this->paths; } public function setIsValidResults($is_valid) { $this->isValidResults = $is_valid; return $this; } public function isValidResults() { return $this->isValidResults; } public function setReasonForEmptyResultSet($reason) { $this->reasonForEmptyResultSet = $reason; return $this; } public function getReasonForEmptyResultSet() { return $this->reasonForEmptyResultSet; } public function setExistedAtCommit($existed_at_commit) { $this->existedAtCommit = $existed_at_commit; return $this; } public function getExistedAtCommit() { return $this->existedAtCommit; } public function setDeletedAtCommit($deleted_at_commit) { $this->deletedAtCommit = $deleted_at_commit; return $this; } public function getDeletedAtCommit() { return $this->deletedAtCommit; } public function toDictionary() { $paths = $this->getPathDicts(); return array( 'paths' => $paths, 'isValidResults' => $this->isValidResults(), 'reasonForEmptyResultSet' => $this->getReasonForEmptyResultSet(), 'existedAtCommit' => $this->getExistedAtCommit(), - 'deletedAtCommit' => $this->getDeletedAtCommit()); + 'deletedAtCommit' => $this->getDeletedAtCommit(), + ); } public function getPathDicts() { $paths = $this->getPaths(); if ($paths) { return mpull($paths, 'toDictionary'); } return array(); } public static function newFromConduit(array $data) { $paths = array(); $path_dicts = $data['paths']; foreach ($path_dicts as $dict) { $paths[] = DiffusionRepositoryPath::newFromDictionary($dict); } return id(new DiffusionBrowseResultSet()) ->setPaths($paths) ->setIsValidResults($data['isValidResults']) ->setReasonForEmptyResultSet($data['reasonForEmptyResultSet']) ->setExistedAtCommit($data['existedAtCommit']) ->setDeletedAtCommit($data['deletedAtCommit']); } } diff --git a/src/applications/diffusion/data/DiffusionFileContent.php b/src/applications/diffusion/data/DiffusionFileContent.php index 9c834650a2..3919d30ea0 100644 --- a/src/applications/diffusion/data/DiffusionFileContent.php +++ b/src/applications/diffusion/data/DiffusionFileContent.php @@ -1,63 +1,63 @@ <?php final class DiffusionFileContent { private $corpus; private $blameDict; private $revList; private $textList; public function setTextList(array $text_list) { $this->textList = $text_list; return $this; } public function getTextList() { if (!$this->textList) { return phutil_split_lines($this->getCorpus(), $retain_ends = false); } return $this->textList; } public function setRevList(array $rev_list) { $this->revList = $rev_list; return $this; } public function getRevList() { return $this->revList; } public function setBlameDict(array $blame_dict) { $this->blameDict = $blame_dict; return $this; } public function getBlameDict() { return $this->blameDict; } public function setCorpus($corpus) { $this->corpus = $corpus; return $this; } public function getCorpus() { return $this->corpus; } public function toDictionary() { return array( 'corpus' => $this->getCorpus(), 'blameDict' => $this->getBlameDict(), 'revList' => $this->getRevList(), - 'textList' => $this->getTextList() + 'textList' => $this->getTextList(), ); } public static function newFromConduit(array $dict) { return id(new DiffusionFileContent()) ->setCorpus($dict['corpus']) ->setBlameDict($dict['blameDict']) ->setRevList($dict['revList']) ->setTextList($dict['textList']); } } diff --git a/src/applications/diffusion/data/DiffusionPathChange.php b/src/applications/diffusion/data/DiffusionPathChange.php index ca95222591..188669126e 100644 --- a/src/applications/diffusion/data/DiffusionPathChange.php +++ b/src/applications/diffusion/data/DiffusionPathChange.php @@ -1,198 +1,199 @@ <?php final class DiffusionPathChange { private $path; private $commitIdentifier; private $commit; private $commitData; private $changeType; private $fileType; private $targetPath; private $targetCommitIdentifier; private $awayPaths = array(); final public function setPath($path) { $this->path = $path; return $this; } final public function getPath() { return $this->path; } public function setChangeType($change_type) { $this->changeType = $change_type; return $this; } public function getChangeType() { return $this->changeType; } public function setFileType($file_type) { $this->fileType = $file_type; return $this; } public function getFileType() { return $this->fileType; } public function setTargetPath($target_path) { $this->targetPath = $target_path; return $this; } public function getTargetPath() { return $this->targetPath; } public function setAwayPaths(array $away_paths) { $this->awayPaths = $away_paths; return $this; } public function getAwayPaths() { return $this->awayPaths; } final public function setCommitIdentifier($commit) { $this->commitIdentifier = $commit; return $this; } final public function getCommitIdentifier() { return $this->commitIdentifier; } final public function setTargetCommitIdentifier($target_commit_identifier) { $this->targetCommitIdentifier = $target_commit_identifier; return $this; } final public function getTargetCommitIdentifier() { return $this->targetCommitIdentifier; } final public function setCommit($commit) { $this->commit = $commit; return $this; } final public function getCommit() { return $this->commit; } final public function setCommitData($commit_data) { $this->commitData = $commit_data; return $this; } final public function getCommitData() { return $this->commitData; } final public function getEpoch() { if ($this->getCommit()) { return $this->getCommit()->getEpoch(); } return null; } final public function getAuthorName() { if ($this->getCommitData()) { return $this->getCommitData()->getAuthorName(); } return null; } final public function getSummary() { if (!$this->getCommitData()) { return null; } $message = $this->getCommitData()->getCommitMessage(); $first = idx(explode("\n", $message), 0); return substr($first, 0, 80); } final public static function convertToArcanistChanges(array $changes) { assert_instances_of($changes, 'DiffusionPathChange'); $direct = array(); $result = array(); foreach ($changes as $path) { $change = new ArcanistDiffChange(); $change->setCurrentPath($path->getPath()); $direct[] = $path->getPath(); $change->setType($path->getChangeType()); $file_type = $path->getFileType(); if ($file_type == DifferentialChangeType::FILE_NORMAL) { $file_type = DifferentialChangeType::FILE_TEXT; } $change->setFileType($file_type); $change->setOldPath($path->getTargetPath()); foreach ($path->getAwayPaths() as $away_path) { $change->addAwayPath($away_path); } $result[$path->getPath()] = $change; } return array_select_keys($result, $direct); } final public static function convertToDifferentialChangesets(array $changes) { assert_instances_of($changes, 'DiffusionPathChange'); $arcanist_changes = self::convertToArcanistChanges($changes); $diff = DifferentialDiff::newFromRawChanges($arcanist_changes); return $diff->getChangesets(); } public function toDictionary() { $commit = $this->getCommit(); if ($commit) { $commit_dict = $commit->toDictionary(); } else { $commit_dict = array(); } $commit_data = $this->getCommitData(); if ($commit_data) { $commit_data_dict = $commit_data->toDictionary(); } else { $commit_data_dict = array(); } return array( 'path' => $this->getPath(), 'commitIdentifier' => $this->getCommitIdentifier(), 'commit' => $commit_dict, 'commitData' => $commit_data_dict, 'fileType' => $this->getFileType(), 'changeType' => $this->getChangeType(), 'targetPath' => $this->getTargetPath(), 'targetCommitIdentifier' => $this->getTargetCommitIdentifier(), - 'awayPaths' => $this->getAwayPaths()); + 'awayPaths' => $this->getAwayPaths(), + ); } public static function newFromConduit(array $dicts) { $results = array(); foreach ($dicts as $dict) { $commit = PhabricatorRepositoryCommit::newFromDictionary($dict['commit']); $commit_data = PhabricatorRepositoryCommitData::newFromDictionary( $dict['commitData']); $results[] = id(new DiffusionPathChange()) ->setPath($dict['path']) ->setCommitIdentifier($dict['commitIdentifier']) ->setCommit($commit) ->setCommitData($commit_data) ->setFileType($dict['fileType']) ->setChangeType($dict['changeType']) ->setTargetPath($dict['targetPath']) ->setTargetCommitIdentifier($dict['targetCommitIdentifier']) ->setAwayPaths($dict['awayPaths']); } return $results; } } diff --git a/src/applications/diffusion/view/DiffusionBranchTableView.php b/src/applications/diffusion/view/DiffusionBranchTableView.php index c214d4b2dc..b1e14688f1 100644 --- a/src/applications/diffusion/view/DiffusionBranchTableView.php +++ b/src/applications/diffusion/view/DiffusionBranchTableView.php @@ -1,136 +1,136 @@ <?php final class DiffusionBranchTableView extends DiffusionView { private $branches; private $commits = array(); public function setBranches(array $branches) { assert_instances_of($branches, 'DiffusionRepositoryRef'); $this->branches = $branches; return $this; } public function setCommits(array $commits) { assert_instances_of($commits, 'PhabricatorRepositoryCommit'); $this->commits = mpull($commits, null, 'getCommitIdentifier'); return $this; } public function render() { $drequest = $this->getDiffusionRequest(); $current_branch = $drequest->getBranch(); $repository = $drequest->getRepository(); Javelin::initBehavior('phabricator-tooltips'); $doc_href = PhabricatorEnv::getDoclink('Diffusion User Guide: Autoclose'); $rows = array(); $rowc = array(); foreach ($this->branches as $branch) { $commit = idx($this->commits, $branch->getCommitIdentifier()); if ($commit) { $details = $commit->getSummary(); $datetime = phabricator_datetime($commit->getEpoch(), $this->user); } else { $datetime = null; $details = null; } switch ($repository->shouldSkipAutocloseBranch($branch->getShortName())) { case PhabricatorRepository::BECAUSE_REPOSITORY_IMPORTING: $icon = 'fa-times bluegrey'; $tip = pht('Repository Importing'); break; case PhabricatorRepository::BECAUSE_AUTOCLOSE_DISABLED: $icon = 'fa-times bluegrey'; $tip = pht('Repository Autoclose Disabled'); break; case PhabricatorRepository::BECAUSE_BRANCH_UNTRACKED: $icon = 'fa-times bluegrey'; $tip = pht('Branch Untracked'); break; case PhabricatorRepository::BECAUSE_BRANCH_NOT_AUTOCLOSE: $icon = 'fa-times bluegrey'; $tip = pht('Branch Autoclose Disabled'); break; case null: $icon = 'fa-check bluegrey'; $tip = pht('Autoclose Enabled'); break; default: $icon = 'fa-question'; $tip = pht('Status Unknown'); break; } $status_icon = id(new PHUIIconView()) ->setIconFont($icon) ->addSigil('has-tooltip') ->setHref($doc_href) ->setMetadata( array( 'tip' => $tip, 'size' => 200, )); $rows[] = array( phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'history', 'branch' => $branch->getShortName(), - )) + )), ), pht('History')), phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'browse', 'branch' => $branch->getShortName(), )), ), $branch->getShortName()), self::linkCommit( $drequest->getRepository(), $branch->getCommitIdentifier()), $status_icon, $datetime, AphrontTableView::renderSingleDisplayLine($details), ); if ($branch->getShortName() == $current_branch) { $rowc[] = 'highlighted'; } else { $rowc[] = null; } } $view = new AphrontTableView($rows); $view->setHeaders( array( pht('History'), pht('Branch'), pht('Head'), pht(''), pht('Modified'), pht('Details'), )); $view->setColumnClasses( array( '', 'pri', '', '', '', 'wide', )); $view->setRowClasses($rowc); return $view->render(); } } diff --git a/src/applications/diffusion/view/DiffusionHistoryTableView.php b/src/applications/diffusion/view/DiffusionHistoryTableView.php index b631a95f14..80220f7c21 100644 --- a/src/applications/diffusion/view/DiffusionHistoryTableView.php +++ b/src/applications/diffusion/view/DiffusionHistoryTableView.php @@ -1,399 +1,400 @@ <?php final class DiffusionHistoryTableView extends DiffusionView { private $history; private $revisions = array(); private $handles = array(); private $isHead; private $parents; private $buildCache; public function setHistory(array $history) { assert_instances_of($history, 'DiffusionPathChange'); $this->history = $history; $this->buildCache = null; return $this; } public function loadRevisions() { $commit_phids = array(); foreach ($this->history as $item) { if ($item->getCommit()) { $commit_phids[] = $item->getCommit()->getPHID(); } } // TODO: Get rid of this. $this->revisions = id(new DifferentialRevision()) ->loadIDsByCommitPHIDs($commit_phids); return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function getRequiredHandlePHIDs() { $phids = array(); foreach ($this->history as $item) { $data = $item->getCommitData(); if ($data) { if ($data->getCommitDetail('authorPHID')) { $phids[$data->getCommitDetail('authorPHID')] = true; } if ($data->getCommitDetail('committerPHID')) { $phids[$data->getCommitDetail('committerPHID')] = true; } } } return array_keys($phids); } public function setParents(array $parents) { $this->parents = $parents; return $this; } public function setIsHead($is_head) { $this->isHead = $is_head; return $this; } public function loadBuildablesOnDemand() { if ($this->buildCache !== null) { return $this->buildCache; } $commits_to_builds = array(); $commits = mpull($this->history, 'getCommit'); $commit_phids = mpull($commits, 'getPHID'); $buildables = id(new HarbormasterBuildableQuery()) ->setViewer($this->getUser()) ->withBuildablePHIDs($commit_phids) ->withManualBuildables(false) ->execute(); $this->buildCache = mpull($buildables, null, 'getBuildablePHID'); return $this->buildCache; } public function render() { $drequest = $this->getDiffusionRequest(); $handles = $this->handles; $graph = null; if ($this->parents) { $graph = $this->renderGraph(); } $show_builds = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorHarbormasterApplication', $this->getUser()); $rows = array(); $ii = 0; foreach ($this->history as $history) { $epoch = $history->getEpoch(); if ($epoch) { $date = phabricator_date($epoch, $this->user); $time = phabricator_time($epoch, $this->user); } else { $date = null; $time = null; } $data = $history->getCommitData(); $author_phid = $committer = $committer_phid = null; if ($data) { $author_phid = $data->getCommitDetail('authorPHID'); $committer_phid = $data->getCommitDetail('committerPHID'); $committer = $data->getCommitDetail('committer'); } if ($author_phid && isset($handles[$author_phid])) { $author = $handles[$author_phid]->renderLink(); } else { $author = self::renderName($history->getAuthorName()); } $different_committer = false; if ($committer_phid) { $different_committer = ($committer_phid != $author_phid); } else if ($committer != '') { $different_committer = ($committer != $history->getAuthorName()); } if ($different_committer) { if ($committer_phid && isset($handles[$committer_phid])) { $committer = $handles[$committer_phid]->renderLink(); } else { $committer = self::renderName($committer); } $author = hsprintf('%s/%s', $author, $committer); } // We can show details once the message and change have been imported. $partial_import = PhabricatorRepositoryCommit::IMPORTED_MESSAGE | PhabricatorRepositoryCommit::IMPORTED_CHANGE; $commit = $history->getCommit(); if ($commit && $commit->isPartiallyImported($partial_import) && $data) { $summary = AphrontTableView::renderSingleDisplayLine( $history->getSummary()); } else { $summary = phutil_tag('em', array(), "Importing\xE2\x80\xA6"); } $build = null; if ($show_builds) { $buildable_lookup = $this->loadBuildablesOnDemand(); $buildable = idx($buildable_lookup, $commit->getPHID()); if ($buildable !== null) { $icon = HarbormasterBuildable::getBuildableStatusIcon( $buildable->getBuildableStatus()); $color = HarbormasterBuildable::getBuildableStatusColor( $buildable->getBuildableStatus()); $name = HarbormasterBuildable::getBuildableStatusName( $buildable->getBuildableStatus()); $icon_view = id(new PHUIIconView()) ->setIconFont($icon.' '.$color); $tooltip_view = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', - 'meta' => array('tip' => $name)), + 'meta' => array('tip' => $name), + ), $icon_view); Javelin::initBehavior('phabricator-tooltips'); $href_view = phutil_tag( 'a', array('href' => '/'.$buildable->getMonogram()), $tooltip_view); $build = $href_view; $has_any_build = true; } } $rows[] = array( $graph ? $graph[$ii++] : null, self::linkCommit( $drequest->getRepository(), $history->getCommitIdentifier()), $build, ($commit ? self::linkRevision(idx($this->revisions, $commit->getPHID())) : null), $author, $summary, $date, $time, ); } $view = new AphrontTableView($rows); $view->setHeaders( array( '', pht('Commit'), '', pht('Revision'), pht('Author/Committer'), pht('Details'), pht('Date'), pht('Time'), )); $view->setColumnClasses( array( 'threads', 'n', 'icon', 'n', '', 'wide', '', 'right', )); $view->setColumnVisibility( array( $graph ? true : false, )); $view->setDeviceVisibility( array( $graph ? true : false, true, true, true, false, true, false, false, )); return $view->render(); } /** * Draw a merge/branch graph from the parent revision data. We're basically * building up a bunch of strings like this: * * ^ * |^ * o| * |o * o * * ...which form an ASCII representation of the graph we eventually want to * draw. * * NOTE: The actual implementation is black magic. */ private function renderGraph() { // This keeps our accumulated information about each line of the // merge/branch graph. $graph = array(); // This holds the next commit we're looking for in each column of the // graph. $threads = array(); // This is the largest number of columns any row has, i.e. the width of // the graph. $count = 0; foreach ($this->history as $key => $history) { $joins = array(); $splits = array(); $parent_list = $this->parents[$history->getCommitIdentifier()]; // Look for some thread which has this commit as the next commit. If // we find one, this commit goes on that thread. Otherwise, this commit // goes on a new thread. $line = ''; $found = false; $pos = count($threads); for ($n = 0; $n < $count; $n++) { if (empty($threads[$n])) { $line .= ' '; continue; } if ($threads[$n] == $history->getCommitIdentifier()) { if ($found) { $line .= ' '; $joins[] = $n; unset($threads[$n]); } else { $line .= 'o'; $found = true; $pos = $n; } } else { // We render a "|" for any threads which have a commit that we haven't // seen yet, this is later drawn as a vertical line. $line .= '|'; } } // If we didn't find the thread this commit goes on, start a new thread. // We use "o" to mark the commit for the rendering engine, or "^" to // indicate that there's nothing after it so the line from the commit // upward should not be drawn. if (!$found) { if ($this->isHead) { $line .= '^'; } else { $line .= 'o'; foreach ($graph as $k => $meta) { // Go back across all the lines we've already drawn and add a // "|" to the end, since this is connected to some future commit // we don't know about. for ($jj = strlen($meta['line']); $jj <= $count; $jj++) { $graph[$k]['line'] .= '|'; } } } } // Update the next commit on this thread to the commit's first parent. // This might have the effect of making a new thread. $threads[$pos] = head($parent_list); // If we made a new thread, increase the thread count. $count = max($pos + 1, $count); // Now, deal with splits (merges). I picked this terms opposite to the // underlying repository term to confuse you. foreach (array_slice($parent_list, 1) as $parent) { $found = false; // Try to find the other parent(s) in our existing threads. If we find // them, split to that thread. foreach ($threads as $idx => $thread_commit) { if ($thread_commit == $parent) { $found = true; $splits[] = $idx; } } // If we didn't find the parent, we don't know about it yet. Find the // first free thread and add it as the "next" commit in that thread. // This might create a new thread. if (!$found) { for ($n = 0; $n < $count; $n++) { if (empty($threads[$n])) { break; } } $threads[$n] = $parent; $splits[] = $n; $count = max($n + 1, $count); } } $graph[] = array( 'line' => $line, 'split' => $splits, 'join' => $joins, ); } // Render into tags for the behavior. foreach ($graph as $k => $meta) { $graph[$k] = javelin_tag( 'div', array( 'sigil' => 'commit-graph', 'meta' => $meta, ), ''); } Javelin::initBehavior( 'diffusion-commit-graph', array( 'count' => $count, )); return $graph; } } diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php index 6e68136ee8..b303a0cd0a 100644 --- a/src/applications/diviner/controller/DivinerAtomController.php +++ b/src/applications/diviner/controller/DivinerAtomController.php @@ -1,674 +1,676 @@ <?php final class DivinerAtomController extends DivinerController { private $bookName; private $atomType; private $atomName; private $atomContext; private $atomIndex; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->bookName = $data['book']; $this->atomType = $data['type']; $this->atomName = $data['name']; $this->atomContext = nonempty(idx($data, 'context'), null); $this->atomIndex = nonempty(idx($data, 'index'), null); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); require_celerity_resource('diviner-shared-css'); $book = id(new DivinerBookQuery()) ->setViewer($viewer) ->withNames(array($this->bookName)) ->executeOne(); if (!$book) { return new Aphront404Response(); } // TODO: This query won't load ghosts, because they'll fail `needAtoms()`. // Instead, we might want to load ghosts and render a message like // "this thing existed in an older version, but no longer does", especially // if we add content like comments. $symbol = id(new DivinerAtomQuery()) ->setViewer($viewer) ->withBookPHIDs(array($book->getPHID())) ->withTypes(array($this->atomType)) ->withNames(array($this->atomName)) ->withContexts(array($this->atomContext)) ->withIndexes(array($this->atomIndex)) ->needAtoms(true) ->needExtends(true) ->needChildren(true) ->executeOne(); if (!$symbol) { return new Aphront404Response(); } $atom = $symbol->getAtom(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( $book->getShortTitle(), '/book/'.$book->getName().'/'); $atom_short_title = $atom->getDocblockMetaValue( 'short', $symbol->getTitle()); $crumbs->addTextCrumb($atom_short_title); $header = id(new PHUIHeaderView()) ->setHeader($this->renderFullSignature($symbol)) ->addTag( id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE) ->setBackgroundColor(PHUITagView::COLOR_BLUE) ->setName(DivinerAtom::getAtomTypeNameString($atom->getType()))); $properties = id(new PHUIPropertyListView()); $group = $atom->getProperty('group'); if ($group) { $group_name = $book->getGroupName($group); } else { $group_name = null; } $this->buildDefined($properties, $symbol); $this->buildExtendsAndImplements($properties, $symbol); $warnings = $atom->getWarnings(); if ($warnings) { $warnings = id(new AphrontErrorView()) ->setErrors($warnings) ->setTitle(pht('Documentation Warnings')) ->setSeverity(AphrontErrorView::SEVERITY_WARNING); } $methods = $this->composeMethods($symbol); $field = 'default'; $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer) ->addObject($symbol, $field); foreach ($methods as $method) { foreach ($method['atoms'] as $matom) { $engine->addObject($matom, $field); } } $engine->process(); $content = $this->renderDocumentationText($symbol, $engine); $toc = $engine->getEngineMetadata( $symbol, $field, PhutilRemarkupHeaderBlockRule::KEY_HEADER_TOC, array()); $document = id(new PHUIDocumentView()) ->setBook($book->getTitle(), $group_name) ->setHeader($header) ->addClass('diviner-view') ->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS) ->appendChild($properties) ->appendChild($warnings) ->appendChild($content); $document->appendChild($this->buildParametersAndReturn(array($symbol))); if ($methods) { $tasks = $this->composeTasks($symbol); if ($tasks) { $methods_by_task = igroup($methods, 'task'); // Add phantom tasks for methods which have a "@task" name that isn't // documented anywhere, or methods that have no "@task" name. foreach ($methods_by_task as $task => $ignored) { if (empty($tasks[$task])) { $tasks[$task] = array( 'name' => $task, 'title' => $task ? $task : pht('Other Methods'), 'defined' => $symbol, ); } } $section = id(new DivinerSectionView()) ->setHeader(pht('Tasks')); foreach ($tasks as $spec) { $section->addContent( id(new PHUIHeaderView()) ->setNoBackground(true) ->setHeader($spec['title'])); $task_methods = idx($methods_by_task, $spec['name'], array()); $inner_box = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE_LEFT) ->addPadding(PHUI::PADDING_LARGE_RIGHT) ->addPadding(PHUI::PADDING_LARGE_BOTTOM); $box_content = array(); if ($task_methods) { $list_items = array(); foreach ($task_methods as $task_method) { $atom = last($task_method['atoms']); $item = $this->renderFullSignature($atom, true); if (strlen($atom->getSummary())) { $item = array( $item, " \xE2\x80\x94 ", - $atom->getSummary()); + $atom->getSummary(), + ); } $list_items[] = phutil_tag('li', array(), $item); } $box_content[] = phutil_tag( 'ul', array( 'class' => 'diviner-list', ), $list_items); } else { $no_methods = pht('No methods for this task.'); $box_content = phutil_tag('em', array(), $no_methods); } $inner_box->appendChild($box_content); $section->addContent($inner_box); } $document->appendChild($section); } $section = id(new DivinerSectionView()) ->setHeader(pht('Methods')); foreach ($methods as $spec) { $matom = last($spec['atoms']); $method_header = id(new PHUIHeaderView()) ->setNoBackground(true); $inherited = $spec['inherited']; if ($inherited) { $method_header->addTag( id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE) ->setBackgroundColor(PHUITagView::COLOR_GREY) ->setName(pht('Inherited'))); } $method_header->setHeader($this->renderFullSignature($matom)); $section->addContent( array( $method_header, $this->renderMethodDocumentationText($symbol, $spec, $engine), $this->buildParametersAndReturn($spec['atoms']), )); } $document->appendChild($section); } if ($toc) { $side = new PHUIListView(); $side->addMenuItem( id(new PHUIListItemView()) ->setName(pht('Contents')) ->setType(PHUIListItemView::TYPE_LABEL)); foreach ($toc as $key => $entry) { $side->addMenuItem( id(new PHUIListItemView()) ->setName($entry[1]) ->setHref('#'.$key)); } $document->setSideNav($side, PHUIDocumentView::NAV_TOP); } return $this->buildApplicationPage( array( $crumbs, $document, ), array( 'title' => $symbol->getTitle(), )); } private function buildExtendsAndImplements( PHUIPropertyListView $view, DivinerLiveSymbol $symbol) { $lineage = $this->getExtendsLineage($symbol); if ($lineage) { $tags = array(); foreach ($lineage as $item) { $tags[] = $this->renderAtomTag($item); } $caret = phutil_tag('span', array('class' => 'caret-right msl msr')); $tags = phutil_implode_html($caret, $tags); $view->addProperty(pht('Extends'), $tags); } $implements = $this->getImplementsLineage($symbol); if ($implements) { $items = array(); foreach ($implements as $spec) { $via = $spec['via']; $iface = $spec['interface']; if ($via == $symbol) { $items[] = $this->renderAtomTag($iface); } else { $items[] = array( $this->renderAtomTag($iface), " \xE2\x97\x80 ", - $this->renderAtomTag($via)); + $this->renderAtomTag($via), + ); } } $view->addProperty( pht('Implements'), phutil_implode_html(phutil_tag('br'), $items)); } } private function renderAtomTag(DivinerLiveSymbol $symbol) { return id(new PHUITagView()) ->setType(PHUITagView::TYPE_OBJECT) ->setName($symbol->getName()) ->setHref($symbol->getURI()); } private function getExtendsLineage(DivinerLiveSymbol $symbol) { foreach ($symbol->getExtends() as $extends) { if ($extends->getType() == 'class') { $lineage = $this->getExtendsLineage($extends); $lineage[] = $extends; return $lineage; } } return array(); } private function getImplementsLineage(DivinerLiveSymbol $symbol) { $implements = array(); // Do these first so we get interfaces ordered from most to least specific. foreach ($symbol->getExtends() as $extends) { if ($extends->getType() == 'interface') { $implements[$extends->getName()] = array( 'interface' => $extends, 'via' => $symbol, ); } } // Now do parent interfaces. foreach ($symbol->getExtends() as $extends) { if ($extends->getType() == 'class') { $implements += $this->getImplementsLineage($extends); } } return $implements; } private function buildDefined( PHUIPropertyListView $view, DivinerLiveSymbol $symbol) { $atom = $symbol->getAtom(); $defined = $atom->getFile().':'.$atom->getLine(); $link = $symbol->getBook()->getConfig('uri.source'); if ($link) { $link = strtr( $link, array( '%%' => '%', '%f' => phutil_escape_uri($atom->getFile()), '%l' => phutil_escape_uri($atom->getLine()), )); $defined = phutil_tag( 'a', array( 'href' => $link, 'target' => '_blank', ), $defined); } $view->addProperty(pht('Defined'), $defined); } private function composeMethods(DivinerLiveSymbol $symbol) { $methods = $this->findMethods($symbol); if (!$methods) { return $methods; } foreach ($methods as $name => $method) { // Check for "@task" on each parent, to find the most recently declared // "@task". $task = null; foreach ($method['atoms'] as $key => $method_symbol) { $atom = $method_symbol->getAtom(); if ($atom->getDocblockMetaValue('task')) { $task = $atom->getDocblockMetaValue('task'); } } $methods[$name]['task'] = $task; // Set 'inherited' if this atom has no implementation of the method. if (last($method['implementations']) !== $symbol) { $methods[$name]['inherited'] = true; } else { $methods[$name]['inherited'] = false; } } return $methods; } private function findMethods(DivinerLiveSymbol $symbol) { $child_specs = array(); foreach ($symbol->getExtends() as $extends) { if ($extends->getType() == DivinerAtom::TYPE_CLASS) { $child_specs = $this->findMethods($extends); } } foreach ($symbol->getChildren() as $child) { if ($child->getType() == DivinerAtom::TYPE_METHOD) { $name = $child->getName(); if (isset($child_specs[$name])) { $child_specs[$name]['atoms'][] = $child; $child_specs[$name]['implementations'][] = $symbol; } else { $child_specs[$name] = array( 'atoms' => array($child), 'defined' => $symbol, 'implementations' => array($symbol), ); } } } return $child_specs; } private function composeTasks(DivinerLiveSymbol $symbol) { $extends_task_specs = array(); foreach ($symbol->getExtends() as $extends) { $extends_task_specs += $this->composeTasks($extends); } $task_specs = array(); $tasks = $symbol->getAtom()->getDocblockMetaValue('task'); if (strlen($tasks)) { $tasks = phutil_split_lines($tasks, $retain_endings = false); foreach ($tasks as $task) { list($name, $title) = explode(' ', $task, 2); $name = trim($name); $title = trim($title); $task_specs[$name] = array( 'name' => $name, 'title' => $title, 'defined' => $symbol, ); } } $specs = $task_specs + $extends_task_specs; // Reorder "@tasks" in original declaration order. Basically, we want to // use the documentation of the closest subclass, but put tasks which // were declared by parents first. $keys = array_keys($extends_task_specs); $specs = array_select_keys($specs, $keys) + $specs; return $specs; } private function renderFullSignature( DivinerLiveSymbol $symbol, $is_link = false) { switch ($symbol->getType()) { case DivinerAtom::TYPE_CLASS: case DivinerAtom::TYPE_INTERFACE: case DivinerAtom::TYPE_METHOD: case DivinerAtom::TYPE_FUNCTION: break; default: return $symbol->getTitle(); } $atom = $symbol->getAtom(); $out = array(); if ($atom->getProperty('final')) { $out[] = 'final'; } if ($atom->getProperty('abstract')) { $out[] = 'abstract'; } if ($atom->getProperty('access')) { $out[] = $atom->getProperty('access'); } if ($atom->getProperty('static')) { $out[] = 'static'; } switch ($symbol->getType()) { case DivinerAtom::TYPE_CLASS: case DivinerAtom::TYPE_INTERFACE: $out[] = $symbol->getType(); break; case DivinerAtom::TYPE_FUNCTION: switch ($atom->getLanguage()) { case 'php': $out[] = $symbol->getType(); break; } break; case DivinerAtom::TYPE_METHOD: switch ($atom->getLanguage()) { case 'php': $out[] = DivinerAtom::TYPE_FUNCTION; break; } break; } $anchor = null; switch ($symbol->getType()) { case DivinerAtom::TYPE_METHOD: $anchor = $symbol->getType().'/'.$symbol->getName(); break; default: break; } $out[] = phutil_tag( $anchor ? 'a' : 'span', array( 'class' => 'diviner-atom-signature-name', 'href' => $anchor ? '#'.$anchor : null, 'name' => $is_link ? null : $anchor, ), $symbol->getName()); $out = phutil_implode_html(' ', $out); $parameters = $atom->getProperty('parameters'); if ($parameters !== null) { $pout = array(); foreach ($parameters as $parameter) { $pout[] = idx($parameter, 'name', '...'); } $out = array($out, '('.implode(', ', $pout).')'); } return phutil_tag( 'span', array( 'class' => 'diviner-atom-signature', ), $out); } private function buildParametersAndReturn(array $symbols) { assert_instances_of($symbols, 'DivinerLiveSymbol'); $symbols = array_reverse($symbols); $out = array(); $collected_parameters = null; foreach ($symbols as $symbol) { $parameters = $symbol->getAtom()->getProperty('parameters'); if ($parameters !== null) { if ($collected_parameters === null) { $collected_parameters = array(); } foreach ($parameters as $key => $parameter) { if (isset($collected_parameters[$key])) { $collected_parameters[$key] += $parameter; } else { $collected_parameters[$key] = $parameter; } } } } if (nonempty($parameters)) { $out[] = id(new DivinerParameterTableView()) ->setHeader(pht('Parameters')) ->setParameters($parameters); } $collected_return = null; foreach ($symbols as $symbol) { $return = $symbol->getAtom()->getProperty('return'); if ($return) { if ($collected_return) { $collected_return += $return; } else { $collected_return = $return; } } } if (nonempty($return)) { $out[] = id(new DivinerReturnTableView()) ->setHeader(pht('Return')) ->setReturn($collected_return); } return $out; } private function renderDocumentationText( DivinerLiveSymbol $symbol, PhabricatorMarkupEngine $engine) { $field = 'default'; $content = $engine->getOutput($symbol, $field); if (strlen(trim($symbol->getMarkupText($field)))) { $content = phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $content); } else { $atom = $symbol->getAtom(); $content = phutil_tag( 'div', array( 'class' => 'diviner-message-not-documented', ), DivinerAtom::getThisAtomIsNotDocumentedString($atom->getType())); } return $content; } private function renderMethodDocumentationText( DivinerLiveSymbol $parent, array $spec, PhabricatorMarkupEngine $engine) { $symbols = array_values($spec['atoms']); $implementations = array_values($spec['implementations']); $field = 'default'; $out = array(); foreach ($symbols as $key => $symbol) { $impl = $implementations[$key]; if ($impl !== $parent) { if (!strlen(trim($symbol->getMarkupText($field)))) { continue; } } $doc = $this->renderDocumentationText($symbol, $engine); if (($impl !== $parent) || $out) { $where = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_MEDIUM_LEFT) ->addPadding(PHUI::PADDING_MEDIUM_RIGHT) ->addClass('diviner-method-implementation-header') ->appendChild($impl->getName()); $doc = array($where, $doc); if ($impl !== $parent) { $doc = phutil_tag( 'div', array( 'class' => 'diviner-method-implementation-inherited', ), $doc); } } $out[] = $doc; } // If we only have inherited implementations but none have documentation, // render the last one here so we get the "this thing has no documentation" // element. if (!$out) { $out[] = $this->renderDocumentationText($symbol, $engine); } return $out; } } diff --git a/src/applications/diviner/view/DivinerBookItemView.php b/src/applications/diviner/view/DivinerBookItemView.php index c90c067dce..b396dcc5d1 100644 --- a/src/applications/diviner/view/DivinerBookItemView.php +++ b/src/applications/diviner/view/DivinerBookItemView.php @@ -1,68 +1,68 @@ <?php final class DivinerBookItemView extends AphrontTagView { private $title; private $subtitle; private $type; private $href; public function setTitle($title) { $this->title = $title; return $this; } public function setSubtitle($subtitle) { $this->subtitle = $subtitle; return $this; } public function setType($type) { $this->type = $type; return $this; } public function setHref($href) { $this->href = $href; return $this; } public function getTagName() { return 'a'; } public function getTagAttributes() { return array( 'class' => 'diviner-book-item', 'href' => $this->href, ); } public function getTagContent() { require_celerity_resource('diviner-shared-css'); $title = phutil_tag( 'span', array( - 'class' => 'diviner-book-item-title' + 'class' => 'diviner-book-item-title', ), $this->title); $subtitle = phutil_tag( 'span', array( - 'class' => 'diviner-book-item-subtitle' + 'class' => 'diviner-book-item-subtitle', ), $this->subtitle); $type = phutil_tag( 'span', array( - 'class' => 'diviner-book-item-type' + 'class' => 'diviner-book-item-type', ), $this->type); return array($title, $type, $subtitle); } } diff --git a/src/applications/diviner/view/DivinerParameterTableView.php b/src/applications/diviner/view/DivinerParameterTableView.php index 990e02db2c..f5eaddbfd6 100644 --- a/src/applications/diviner/view/DivinerParameterTableView.php +++ b/src/applications/diviner/view/DivinerParameterTableView.php @@ -1,83 +1,84 @@ <?php final class DivinerParameterTableView extends AphrontTagView { private $parameters; private $header; public function setParameters(array $parameters) { $this->parameters = $parameters; return $this; } public function setHeader($text) { $this->header = $text; return $this; } public function getTagName() { return 'div'; } public function getTagAttributes() { return array( 'class' => 'diviner-table-view', ); } public function getTagContent() { require_celerity_resource('diviner-shared-css'); $rows = array(); foreach ($this->parameters as $param) { $cells = array(); $type = idx($param, 'doctype'); if (!$type) { $type = idx($param, 'type'); } $name = idx($param, 'name'); $docs = idx($param, 'docs'); $cells[] = phutil_tag( 'td', array( 'class' => 'diviner-parameter-table-type diviner-monospace', ), $type); $cells[] = phutil_tag( 'td', array( 'class' => 'diviner-parameter-table-name diviner-monospace', ), $name); $cells[] = phutil_tag( 'td', array( 'class' => 'diviner-parameter-table-docs', ), $docs); $rows[] = phutil_tag('tr', array(), $cells); } $table = phutil_tag( 'table', array( - 'class' => 'diviner-parameter-table-view'), + 'class' => 'diviner-parameter-table-view', + ), $rows); $header = phutil_tag( 'span', array( - 'class' => 'diviner-table-header' + 'class' => 'diviner-table-header', ), $this->header); return array($header, $table); } } diff --git a/src/applications/diviner/view/DivinerReturnTableView.php b/src/applications/diviner/view/DivinerReturnTableView.php index a85f691edb..fa1f450200 100644 --- a/src/applications/diviner/view/DivinerReturnTableView.php +++ b/src/applications/diviner/view/DivinerReturnTableView.php @@ -1,77 +1,78 @@ <?php final class DivinerReturnTableView extends AphrontTagView { private $return; private $header; public function setReturn(array $return) { $this->return = $return; return $this; } public function setHeader($text) { $this->header = $text; return $this; } public function getTagName() { return 'div'; } public function getTagAttributes() { return array( 'class' => 'diviner-table-view', ); } public function getTagContent() { require_celerity_resource('diviner-shared-css'); $return = $this->return; $type = idx($return, 'doctype'); if (!$type) { $type = idx($return, 'type'); } $docs = idx($return, 'docs'); $cells = array(); $cells[] = phutil_tag( 'td', array( 'class' => 'diviner-return-table-type diviner-monospace', ), $type); $cells[] = phutil_tag( 'td', array( 'class' => 'diviner-return-table-docs', ), $docs); $rows = phutil_tag( 'tr', array(), $cells); $table = phutil_tag( 'table', array( - 'class' => 'diviner-return-table-view'), + 'class' => 'diviner-return-table-view', + ), $rows); $header = phutil_tag( 'span', array( - 'class' => 'diviner-table-header' + 'class' => 'diviner-table-header', ), $this->header); return array($header, $table); } } diff --git a/src/applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php b/src/applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php index a31e8d3974..8a951af73a 100644 --- a/src/applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php +++ b/src/applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php @@ -1,37 +1,37 @@ <?php final class DoorkeeperBridgeJIRATestCase extends PhabricatorTestCase { public function testJIRABridgeRestAPIURIConversion() { $map = array( array( // Installed at domain root. 'http://jira.example.com/rest/api/2/issue/1', 'TP-1', - 'http://jira.example.com/browse/TP-1' + 'http://jira.example.com/browse/TP-1', ), array( // Installed on path. 'http://jira.example.com/jira/rest/api/2/issue/1', 'TP-1', - 'http://jira.example.com/jira/browse/TP-1' + 'http://jira.example.com/jira/browse/TP-1', ), array( // A URI we don't understand. 'http://jira.example.com/wake/cli/3/task/1', 'TP-1', null, ), ); foreach ($map as $inputs) { list($rest_uri, $object_id, $expect) = $inputs; $this->assertEqual( $expect, DoorkeeperBridgeJIRA::getJIRAIssueBrowseURIFromJIRARestURI( $rest_uri, $object_id)); } } } diff --git a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php index 800d18a287..70ae226065 100644 --- a/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php +++ b/src/applications/doorkeeper/option/PhabricatorAsanaConfigOptions.php @@ -1,153 +1,153 @@ <?php final class PhabricatorAsanaConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Integration with Asana'); } public function getDescription() { return pht('Asana integration options.'); } public function getOptions() { return array( $this->newOption('asana.workspace-id', 'string', null) ->setSummary(pht('Asana Workspace ID to publish into.')) ->setDescription( pht( 'To enable synchronization into Asana, enter an Asana Workspace '. 'ID here.'. "\n\n". "NOTE: This feature is new and experimental.")), $this->newOption('asana.project-ids', 'wild', null) ->setSummary(pht('Optional Asana projects to use as application tags.')) ->setDescription( pht( 'When Phabricator creates tasks in Asana, it can add the tasks '. 'to Asana projects based on which application the corresponding '. 'object in Phabricator comes from. For example, you can add code '. 'reviews in Asana to a "Differential" project.'. "\n\n". - 'NOTE: This feature is new and experimental.')) + 'NOTE: This feature is new and experimental.')), ); } public function renderContextualDescription( PhabricatorConfigOption $option, AphrontRequest $request) { switch ($option->getKey()) { case 'asana.workspace-id': break; case 'asana.project-ids': return $this->renderContextualProjectDescription($option, $request); default: return parent::renderContextualDescription($option, $request); } $viewer = $request->getUser(); $provider = PhabricatorAsanaAuthProvider::getAsanaProvider(); if (!$provider) { return null; } $account = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withAccountTypes(array($provider->getProviderType())) ->withAccountDomains(array($provider->getProviderDomain())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$account) { return null; } $token = $provider->getOAuthAccessToken($account); if (!$token) { return null; } try { $workspaces = id(new PhutilAsanaFuture()) ->setAccessToken($token) ->setRawAsanaQuery('workspaces') ->resolve(); } catch (Exception $ex) { return null; } if (!$workspaces) { return null; } $out = array(); $out[] = pht('| Workspace ID | Workspace Name |'); $out[] = '| ------------ | -------------- |'; foreach ($workspaces as $workspace) { $out[] = sprintf('| `%s` | `%s` |', $workspace['id'], $workspace['name']); } $out = implode("\n", $out); $out = pht( "The Asana Workspaces your linked account has access to are:\n\n%s", $out); return PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($out), 'default', $viewer); } private function renderContextualProjectDescription( PhabricatorConfigOption $option, AphrontRequest $request) { $viewer = $request->getUser(); $publishers = id(new PhutilSymbolLoader()) ->setAncestorClass('DoorkeeperFeedStoryPublisher') ->loadObjects(); $out = array(); $out[] = pht( 'To specify projects to add tasks to, enter a JSON map with publisher '. 'class names as keys and a list of project IDs as values. For example, '. 'to put Differential tasks into Asana projects with IDs `123` and '. '`456`, enter:'. "\n\n". " lang=txt\n". " {\n". " \"DifferentialDoorkeeperRevisionFeedStoryPublisher\" : [123, 456]\n". " }\n"); $out[] = pht('Available publishers class names are:'); foreach ($publishers as $publisher) { $out[] = ' - `'.get_class($publisher).'`'; } $out[] = pht( 'You can find an Asana project ID by clicking the project in Asana and '. 'then examining the URL:'. "\n\n". " lang=txt\n". " https://app.asana.com/0/12345678901234567890/111111111111111111\n". " ^^^^^^^^^^^^^^^^^^^^\n". " This is the ID to use.\n"); $out = implode("\n", $out); return PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($out), 'default', $viewer); } } diff --git a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php index 9ee4830989..e256ed4bb9 100644 --- a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php @@ -1,124 +1,126 @@ <?php final class DrydockPreallocatedHostBlueprintImplementation extends DrydockBlueprintImplementation { public function isEnabled() { return true; } public function getBlueprintName() { return pht('Preallocated Remote Hosts'); } public function getDescription() { return pht('Allows Drydock to run on specific remote hosts you configure.'); } public function canAllocateMoreResources(array $pool) { return false; } protected function executeAllocateResource(DrydockLease $lease) { throw new Exception("Preallocated hosts can't be dynamically allocated."); } protected function canAllocateLease( DrydockResource $resource, DrydockLease $lease) { return $lease->getAttribute('platform') === $resource->getAttribute('platform'); } protected function shouldAllocateLease( DrydockResource $resource, DrydockLease $lease, array $other_leases) { return true; } protected function executeAcquireLease( DrydockResource $resource, DrydockLease $lease) { // Because preallocated resources are manually created, we should verify // we have all the information we need. PhutilTypeSpec::checkMap( $resource->getAttributesForTypeSpec( array('platform', 'host', 'port', 'credential', 'path')), array( 'platform' => 'string', 'host' => 'string', 'port' => 'string', // Value is a string from the command line 'credential' => 'string', 'path' => 'string', )); $v_platform = $resource->getAttribute('platform'); $v_path = $resource->getAttribute('path'); // Similar to DrydockLocalHostBlueprint, we create a folder // on the remote host that the lease can use. $lease_id = $lease->getID(); // Can't use DIRECTORY_SEPERATOR here because that is relevant to // the platform we're currently running on, not the platform we are // remoting to. $separator = '/'; if ($v_platform === 'windows') { $separator = '\\'; } // Clean up the directory path a little. $base_path = rtrim($v_path, '/'); $base_path = rtrim($base_path, '\\'); $full_path = $base_path.$separator.$lease_id; $cmd = $lease->getInterface('command'); if ($v_platform !== 'windows') { $cmd->execx('mkdir %s', $full_path); } else { // Windows is terrible. The mkdir command doesn't even support putting // the path in quotes. IN QUOTES. ARGUHRGHUGHHGG!! Do some terribly // inaccurate sanity checking since we can't safely escape the path. if (preg_match('/^[A-Z]\\:\\\\[a-zA-Z0-9\\\\\\ ]/', $full_path) === 0) { throw new Exception( 'Unsafe path detected for Windows platform: "'.$full_path.'".'); } $cmd->execx('mkdir %C', $full_path); } $lease->setAttribute('path', $full_path); } public function getType() { return 'host'; } public function getInterface( DrydockResource $resource, DrydockLease $lease, $type) { switch ($type) { case 'command': return id(new DrydockSSHCommandInterface()) ->setConfiguration(array( 'host' => $resource->getAttribute('host'), 'port' => $resource->getAttribute('port'), 'credential' => $resource->getAttribute('credential'), - 'platform' => $resource->getAttribute('platform'))) + 'platform' => $resource->getAttribute('platform'), + )) ->setWorkingDirectory($lease->getAttribute('path')); case 'filesystem': return id(new DrydockSFTPFilesystemInterface()) ->setConfiguration(array( 'host' => $resource->getAttribute('host'), 'port' => $resource->getAttribute('port'), - 'credential' => $resource->getAttribute('credential'))); + 'credential' => $resource->getAttribute('credential'), + )); } throw new Exception("No interface of type '{$type}'."); } } diff --git a/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php b/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php index 0aa6d99f27..c5f9b4a59c 100644 --- a/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php +++ b/src/applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php @@ -1,66 +1,66 @@ <?php final class PhabricatorFactManagementCursorsWorkflow extends PhabricatorFactManagementWorkflow { public function didConstruct() { $this ->setName('cursors') ->setSynopsis(pht('Show a list of fact iterators and cursors.')) ->setExamples( "**cursors**\n". "**cursors** --reset __cursor__") ->setArguments( array( array( 'name' => 'reset', 'param' => 'cursor', 'repeat' => true, 'help' => 'Reset cursor __cursor__.', - ) + ), )); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $reset = $args->getArg('reset'); if ($reset) { foreach ($reset as $name) { $cursor = id(new PhabricatorFactCursor())->loadOneWhere( 'name = %s', $name); if ($cursor) { $console->writeOut("%s\n", pht('Resetting cursor %s...', $name)); $cursor->delete(); } else { $console->writeErr( "%s\n", pht('Cursor %s does not exist or is already reset.', $name)); } } return 0; } $iterator_map = PhabricatorFactDaemon::getAllApplicationIterators(); if (!$iterator_map) { $console->writeErr("%s\n", pht('No cursors.')); return 0; } $cursors = id(new PhabricatorFactCursor())->loadAllWhere( 'name IN (%Ls)', array_keys($iterator_map)); $cursors = mpull($cursors, 'getPosition', 'getName'); foreach ($iterator_map as $iterator_name => $iterator) { $console->writeOut( "%s (%s)\n", $iterator_name, idx($cursors, $iterator_name, 'start')); } return 0; } } diff --git a/src/applications/feed/conduit/FeedQueryConduitAPIMethod.php b/src/applications/feed/conduit/FeedQueryConduitAPIMethod.php index 5930e0a2c8..9612122b40 100644 --- a/src/applications/feed/conduit/FeedQueryConduitAPIMethod.php +++ b/src/applications/feed/conduit/FeedQueryConduitAPIMethod.php @@ -1,145 +1,145 @@ <?php final class FeedQueryConduitAPIMethod extends FeedConduitAPIMethod { public function getAPIMethodName() { return 'feed.query'; } public function getMethodStatus() { return self::METHOD_STATUS_UNSTABLE; } public function getMethodDescription() { return 'Query the feed for stories'; } private function getDefaultLimit() { return 100; } public function defineParamTypes() { return array( 'filterPHIDs' => 'optional list <phid>', 'limit' => 'optional int (default '.$this->getDefaultLimit().')', 'after' => 'optional int', 'before' => 'optional int', 'view' => 'optional string (data, html, html-summary, text)', ); } private function getSupportedViewTypes() { return array( 'html' => 'Full HTML presentation of story', 'data' => 'Dictionary with various data of the story', 'html-summary' => 'Story contains only the title of the story', 'text' => 'Simple one-line plain text representation of story', ); } public function defineErrorTypes() { $view_types = array_keys($this->getSupportedViewTypes()); $view_types = implode(', ', $view_types); return array( 'ERR-UNKNOWN-TYPE' => 'Unsupported view type, possibles are: '.$view_types, ); } public function defineReturnType() { return 'nonempty dict'; } protected function execute(ConduitAPIRequest $request) { $results = array(); $user = $request->getUser(); $view_type = $request->getValue('view'); if (!$view_type) { $view_type = 'data'; } $limit = $request->getValue('limit'); if (!$limit) { $limit = $this->getDefaultLimit(); } $filter_phids = $request->getValue('filterPHIDs'); if (!$filter_phids) { $filter_phids = array(); } $query = id(new PhabricatorFeedQuery()) ->setLimit($limit) ->setFilterPHIDs($filter_phids) ->setViewer($user); $after = $request->getValue('after'); if (strlen($after)) { $query->setAfterID($after); } $before = $request->getValue('before'); if (strlen($before)) { $query->setBeforeID($before); } $stories = $query->execute(); if ($stories) { foreach ($stories as $story) { $story_data = $story->getStoryData(); $data = null; try { $view = $story->renderView(); } catch (Exception $ex) { // When stories fail to render, just fail that story. phlog($ex); continue; } $view->setEpoch($story->getEpoch()); $view->setUser($user); switch ($view_type) { case 'html': $data = $view->render(); break; case 'html-summary': $data = $view->render(); break; case 'data': $data = array( 'class' => $story_data->getStoryType(), 'epoch' => $story_data->getEpoch(), 'authorPHID' => $story_data->getAuthorPHID(), 'chronologicalKey' => $story_data->getChronologicalKey(), 'data' => $story_data->getStoryData(), ); break; case 'text': $data = array( 'class' => $story_data->getStoryType(), 'epoch' => $story_data->getEpoch(), 'authorPHID' => $story_data->getAuthorPHID(), 'chronologicalKey' => $story_data->getChronologicalKey(), 'objectPHID' => $story->getPrimaryObjectPHID(), - 'text' => $story->renderText() + 'text' => $story->renderText(), ); break; default: throw new ConduitException('ERR-UNKNOWN-TYPE'); } $results[$story_data->getPHID()] = $data; } } return $results; } } diff --git a/src/applications/files/PhabricatorImageTransformer.php b/src/applications/files/PhabricatorImageTransformer.php index e13494dd67..95cf1fac7a 100644 --- a/src/applications/files/PhabricatorImageTransformer.php +++ b/src/applications/files/PhabricatorImageTransformer.php @@ -1,661 +1,661 @@ <?php /** * @task enormous Detecting Enormous Images * @task save Saving Image Data */ final class PhabricatorImageTransformer { public function executeMemeTransform( PhabricatorFile $file, $upper_text, $lower_text) { $image = $this->applyMemeToFile($file, $upper_text, $lower_text); return PhabricatorFile::newFromFileData( $image, array( 'name' => 'meme-'.$file->getName(), 'ttl' => time() + 60 * 60 * 24, 'canCDN' => true, )); } public function executeThumbTransform( PhabricatorFile $file, $x, $y) { $image = $this->crudelyScaleTo($file, $x, $y); return PhabricatorFile::newFromFileData( $image, array( 'name' => 'thumb-'.$file->getName(), 'canCDN' => true, )); } public function executeProfileTransform( PhabricatorFile $file, $x, $min_y, $max_y) { $image = $this->crudelyCropTo($file, $x, $min_y, $max_y); return PhabricatorFile::newFromFileData( $image, array( 'name' => 'profile-'.$file->getName(), 'canCDN' => true, )); } public function executePreviewTransform( PhabricatorFile $file, $size) { $image = $this->generatePreview($file, $size); return PhabricatorFile::newFromFileData( $image, array( 'name' => 'preview-'.$file->getName(), 'canCDN' => true, )); } public function executeConpherenceTransform( PhabricatorFile $file, $top, $left, $width, $height) { $image = $this->crasslyCropTo( $file, $top, $left, $width, $height); return PhabricatorFile::newFromFileData( $image, array( 'name' => 'conpherence-'.$file->getName(), 'canCDN' => true, )); } private function crudelyCropTo(PhabricatorFile $file, $x, $min_y, $max_y) { $data = $file->loadFileData(); $img = imagecreatefromstring($data); $sx = imagesx($img); $sy = imagesy($img); $scaled_y = ($x / $sx) * $sy; if ($scaled_y > $max_y) { // This image is very tall and thin. $scaled_y = $max_y; } else if ($scaled_y < $min_y) { // This image is very short and wide. $scaled_y = $min_y; } $cropped = $this->applyScaleWithImagemagick($file, $x, $scaled_y); if ($cropped != null) { return $cropped; } $img = $this->applyScaleTo( $file, $x, $scaled_y); return self::saveImageDataInAnyFormat($img, $file->getMimeType()); } private function crasslyCropTo(PhabricatorFile $file, $top, $left, $w, $h) { $data = $file->loadFileData(); $src = imagecreatefromstring($data); $dst = $this->getBlankDestinationFile($w, $h); $scale = self::getScaleForCrop($file, $w, $h); $orig_x = $left / $scale; $orig_y = $top / $scale; $orig_w = $w / $scale; $orig_h = $h / $scale; imagecopyresampled( $dst, $src, 0, 0, $orig_x, $orig_y, $w, $h, $orig_w, $orig_h); return self::saveImageDataInAnyFormat($dst, $file->getMimeType()); } /** * Very crudely scale an image up or down to an exact size. */ private function crudelyScaleTo(PhabricatorFile $file, $dx, $dy) { $scaled = $this->applyScaleWithImagemagick($file, $dx, $dy); if ($scaled != null) { return $scaled; } $dst = $this->applyScaleTo($file, $dx, $dy); return self::saveImageDataInAnyFormat($dst, $file->getMimeType()); } private function getBlankDestinationFile($dx, $dy) { $dst = imagecreatetruecolor($dx, $dy); imagesavealpha($dst, true); imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127)); return $dst; } private function applyScaleTo(PhabricatorFile $file, $dx, $dy) { $data = $file->loadFileData(); $src = imagecreatefromstring($data); $x = imagesx($src); $y = imagesy($src); $scale = min(($dx / $x), ($dy / $y), 1); $sdx = $scale * $x; $sdy = $scale * $y; $dst = $this->getBlankDestinationFile($dx, $dy); imagesavealpha($dst, true); imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127)); imagecopyresampled( $dst, $src, ($dx - $sdx) / 2, ($dy - $sdy) / 2, 0, 0, $sdx, $sdy, $x, $y); return $dst; } public static function getPreviewDimensions(PhabricatorFile $file, $size) { $metadata = $file->getMetadata(); $x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH); $y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT); if (!$x || !$y) { $data = $file->loadFileData(); $src = imagecreatefromstring($data); $x = imagesx($src); $y = imagesy($src); } $scale = min($size / $x, $size / $y, 1); $dx = max($size / 4, $scale * $x); $dy = max($size / 4, $scale * $y); $sdx = $scale * $x; $sdy = $scale * $y; return array( 'x' => $x, 'y' => $y, 'dx' => $dx, 'dy' => $dy, 'sdx' => $sdx, - 'sdy' => $sdy + 'sdy' => $sdy, ); } public static function getScaleForCrop( PhabricatorFile $file, $des_width, $des_height) { $metadata = $file->getMetadata(); $width = $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH]; $height = $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT]; if ($height < $des_height) { $scale = $height / $des_height; } else if ($width < $des_width) { $scale = $width / $des_width; } else { $scale_x = $des_width / $width; $scale_y = $des_height / $height; $scale = max($scale_x, $scale_y); } return $scale; } private function generatePreview(PhabricatorFile $file, $size) { $data = $file->loadFileData(); $src = imagecreatefromstring($data); $dimensions = self::getPreviewDimensions($file, $size); $x = $dimensions['x']; $y = $dimensions['y']; $dx = $dimensions['dx']; $dy = $dimensions['dy']; $sdx = $dimensions['sdx']; $sdy = $dimensions['sdy']; $dst = $this->getBlankDestinationFile($dx, $dy); imagecopyresampled( $dst, $src, ($dx - $sdx) / 2, ($dy - $sdy) / 2, 0, 0, $sdx, $sdy, $x, $y); return self::saveImageDataInAnyFormat($dst, $file->getMimeType()); } private function applyMemeToFile( PhabricatorFile $file, $upper_text, $lower_text) { $data = $file->loadFileData(); $img_type = $file->getMimeType(); $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick'); if ($img_type != 'image/gif' || $imagemagick == false) { return $this->applyMemeTo( $data, $upper_text, $lower_text, $img_type); } $data = $file->loadFileData(); $input = new TempFile(); Filesystem::writeFile($input, $data); list($out) = execx('convert %s info:', $input); $split = phutil_split_lines($out); if (count($split) > 1) { return $this->applyMemeWithImagemagick( $input, $upper_text, $lower_text, count($split), $img_type); } else { return $this->applyMemeTo($data, $upper_text, $lower_text, $img_type); } } private function applyMemeTo( $data, $upper_text, $lower_text, $mime_type) { $img = imagecreatefromstring($data); // Some PNGs have color palettes, and allocating the dark border color // fails and gives us whatever's first in the color table. Copy the image // to a fresh truecolor canvas before working with it. $truecolor = imagecreatetruecolor(imagesx($img), imagesy($img)); imagecopy($truecolor, $img, 0, 0, 0, 0, imagesx($img), imagesy($img)); $img = $truecolor; $phabricator_root = dirname(phutil_get_library_root('phabricator')); $font_root = $phabricator_root.'/resources/font/'; $font_path = $font_root.'tuffy.ttf'; if (Filesystem::pathExists($font_root.'impact.ttf')) { $font_path = $font_root.'impact.ttf'; } $text_color = imagecolorallocate($img, 255, 255, 255); $border_color = imagecolorallocatealpha($img, 0, 0, 0, 110); $border_width = 4; $font_max = 200; $font_min = 5; for ($i = $font_max; $i > $font_min; $i--) { $fit = $this->doesTextBoundingBoxFitInImage( $img, $upper_text, $i, $font_path); if ($fit['doesfit']) { $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2; $y = $fit['txtheight'] + 10; $this->makeImageWithTextBorder($img, $i, $x, $y, $text_color, $border_color, $border_width, $font_path, $upper_text); break; } } for ($i = $font_max; $i > $font_min; $i--) { $fit = $this->doesTextBoundingBoxFitInImage($img, $lower_text, $i, $font_path); if ($fit['doesfit']) { $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2; $y = $fit['imgheight'] - 10; $this->makeImageWithTextBorder( $img, $i, $x, $y, $text_color, $border_color, $border_width, $font_path, $lower_text); break; } } return self::saveImageDataInAnyFormat($img, $mime_type); } private function makeImageWithTextBorder($img, $font_size, $x, $y, $color, $stroke_color, $bw, $font, $text) { $angle = 0; $bw = abs($bw); for ($c1 = $x - $bw; $c1 <= $x + $bw; $c1++) { for ($c2 = $y - $bw; $c2 <= $y + $bw; $c2++) { if (!(($c1 == $x - $bw || $x + $bw) && $c2 == $y - $bw || $c2 == $y + $bw)) { $bg = imagettftext($img, $font_size, $angle, $c1, $c2, $stroke_color, $font, $text); } } } imagettftext($img, $font_size, $angle, $x , $y, $color , $font, $text); } private function doesTextBoundingBoxFitInImage($img, $text, $font_size, $font_path) { // Default Angle = 0 $angle = 0; $bbox = imagettfbbox($font_size, $angle, $font_path, $text); $text_height = abs($bbox[3] - $bbox[5]); $text_width = abs($bbox[0] - $bbox[2]); return array( 'doesfit' => ($text_height * 1.05 <= imagesy($img) / 2 && $text_width * 1.05 <= imagesx($img)), 'txtwidth' => $text_width, 'txtheight' => $text_height, 'imgwidth' => imagesx($img), 'imgheight' => imagesy($img), ); } private function applyScaleWithImagemagick(PhabricatorFile $file, $dx, $dy) { $img_type = $file->getMimeType(); $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick'); if ($img_type != 'image/gif' || $imagemagick == false) { return null; } $data = $file->loadFileData(); $src = imagecreatefromstring($data); $x = imagesx($src); $y = imagesy($src); if (self::isEnormousGIF($x, $y)) { return null; } $scale = min(($dx / $x), ($dy / $y), 1); $sdx = $scale * $x; $sdy = $scale * $y; $input = new TempFile(); Filesystem::writeFile($input, $data); $resized = new TempFile(); $future = new ExecFuture( 'convert %s -coalesce -resize %sX%s%s %s', $input, $sdx, $sdy, '!', $resized); // Don't spend more than 10 seconds resizing; just fail if it takes longer // than that. $future->setTimeout(10)->resolvex(); return Filesystem::readFile($resized); } private function applyMemeWithImagemagick( $input, $above, $below, $count, $img_type) { $output = new TempFile(); $future = new ExecFuture( 'convert %s -coalesce +adjoin %s_%s', $input, $input, '%09d'); $future->setTimeout(10)->resolvex(); $output_files = array(); for ($ii = 0; $ii < $count; $ii++) { $frame_name = sprintf('%s_%09d', $input, $ii); $output_name = sprintf('%s_%09d', $output, $ii); $output_files[] = $output_name; $frame_data = Filesystem::readFile($frame_name); $memed_frame_data = $this->applyMemeTo( $frame_data, $above, $below, $img_type); Filesystem::writeFile($output_name, $memed_frame_data); } $future = new ExecFuture('convert -loop 0 %Ls %s', $output_files, $output); $future->setTimeout(10)->resolvex(); return Filesystem::readFile($output); } /* -( Detecting Enormous Files )------------------------------------------- */ /** * Determine if an image is enormous (too large to transform). * * Attackers can perform a denial of service attack by uploading highly * compressible images with enormous dimensions but a very small filesize. * Transforming them (e.g., into thumbnails) may consume huge quantities of * memory and CPU relative to the resources required to transmit the file. * * In general, we respond to these images by declining to transform them, and * using a default thumbnail instead. * * @param int Width of the image, in pixels. * @param int Height of the image, in pixels. * @return bool True if this image is enormous (too large to transform). * @task enormous */ public static function isEnormousImage($x, $y) { // This is just a sanity check, but if we don't have valid dimensions we // shouldn't be trying to transform the file. if (($x <= 0) || ($y <= 0)) { return true; } return ($x * $y) > (4096 * 4096); } /** * Determine if a GIF is enormous (too large to transform). * * For discussion, see @{method:isEnormousImage}. We need to be more * careful about GIFs, because they can also have a large number of frames * despite having a very small filesize. We're more conservative about * calling GIFs enormous than about calling images in general enormous. * * @param int Width of the GIF, in pixels. * @param int Height of the GIF, in pixels. * @return bool True if this image is enormous (too large to transform). * @task enormous */ public static function isEnormousGIF($x, $y) { if (self::isEnormousImage($x, $y)) { return true; } return ($x * $y) > (800 * 800); } /* -( Saving Image Data )-------------------------------------------------- */ /** * Save an image resource to a string representation suitable for storage or * transmission as an image file. * * Optionally, you can specify a preferred MIME type like `"image/png"`. * Generally, you should specify the MIME type of the original file if you're * applying file transformations. The MIME type may not be honored if * Phabricator can not encode images in the given format (based on available * extensions), but can save images in another format. * * @param resource GD image resource. * @param string? Optionally, preferred mime type. * @return string Bytes of an image file. * @task save */ public static function saveImageDataInAnyFormat($data, $preferred_mime = '') { $preferred = null; switch ($preferred_mime) { case 'image/gif': $preferred = self::saveImageDataAsGIF($data); break; case 'image/png': $preferred = self::saveImageDataAsPNG($data); break; } if ($preferred !== null) { return $preferred; } $data = self::saveImageDataAsJPG($data); if ($data !== null) { return $data; } $data = self::saveImageDataAsPNG($data); if ($data !== null) { return $data; } $data = self::saveImageDataAsGIF($data); if ($data !== null) { return $data; } throw new Exception(pht('Failed to save image data into any format.')); } /** * Save an image in PNG format, returning the file data as a string. * * @param resource GD image resource. * @return string|null PNG file as a string, or null on failure. * @task save */ private static function saveImageDataAsPNG($image) { if (!function_exists('imagepng')) { return null; } ob_start(); $result = imagepng($image, null, 9); $output = ob_get_clean(); if (!$result) { return null; } return $output; } /** * Save an image in GIF format, returning the file data as a string. * * @param resource GD image resource. * @return string|null GIF file as a string, or null on failure. * @task save */ private static function saveImageDataAsGIF($image) { if (!function_exists('imagegif')) { return null; } ob_start(); $result = imagegif($image); $output = ob_get_clean(); if (!$result) { return null; } return $output; } /** * Save an image in JPG format, returning the file data as a string. * * @param resource GD image resource. * @return string|null JPG file as a string, or null on failure. * @task save */ private static function saveImageDataAsJPG($image) { if (!function_exists('imagejpeg')) { return null; } ob_start(); $result = imagejpeg($image); $output = ob_get_clean(); if (!$result) { return null; } return $output; } } diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php index f7a3d78627..d605046cf9 100644 --- a/src/applications/files/config/PhabricatorFilesConfigOptions.php +++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php @@ -1,196 +1,196 @@ <?php final class PhabricatorFilesConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Files'); } public function getDescription() { return pht('Configure files and file storage.'); } public function getOptions() { $viewable_default = array( 'image/jpeg' => 'image/jpeg', 'image/jpg' => 'image/jpg', 'image/png' => 'image/png', 'image/gif' => 'image/gif', 'text/plain' => 'text/plain; charset=utf-8', 'text/x-diff' => 'text/plain; charset=utf-8', // ".ico" favicon files, which have mime type diversity. See: // http://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type 'image/x-ico' => 'image/x-icon', 'image/x-icon' => 'image/x-icon', 'image/vnd.microsoft.icon' => 'image/x-icon', 'audio/x-wav' => 'audio/x-wav', 'application/ogg' => 'application/ogg', 'audio/mpeg' => 'audio/mpeg', ); $image_default = array( 'image/jpeg' => true, 'image/jpg' => true, 'image/png' => true, 'image/gif' => true, 'image/x-ico' => true, 'image/x-icon' => true, 'image/vnd.microsoft.icon' => true, ); $audio_default = array( 'audio/x-wav' => true, 'application/ogg' => true, 'audio/mpeg' => true, ); // largely lifted from http://en.wikipedia.org/wiki/Internet_media_type $icon_default = array( // audio file icon 'audio/basic' => 'docs_audio', 'audio/L24' => 'docs_audio', 'audio/mp4' => 'docs_audio', 'audio/mpeg' => 'docs_audio', 'audio/ogg' => 'docs_audio', 'audio/vorbis' => 'docs_audio', 'audio/vnd.rn-realaudio' => 'docs_audio', 'audio/vnd.wave' => 'docs_audio', 'audio/webm' => 'docs_audio', // movie file icon 'video/mpeg' => 'docs_movie', 'video/mp4' => 'docs_movie', 'video/ogg' => 'docs_movie', 'video/quicktime' => 'docs_movie', 'video/webm' => 'docs_movie', 'video/x-matroska' => 'docs_movie', 'video/x-ms-wmv' => 'docs_movie', 'video/x-flv' => 'docs_movie', // pdf file icon 'application/pdf' => 'docs_pdf', // zip file icon 'application/zip' => 'docs_zip', // msword icon 'application/msword' => 'docs_doc', ) + array_fill_keys(array_keys($image_default), 'docs_image'); return array( $this->newOption('files.viewable-mime-types', 'wild', $viewable_default) ->setSummary( pht('Configure which MIME types are viewable in the browser.')) ->setDescription( pht( 'Configure which uploaded file types may be viewed directly '. 'in the browser. Other file types will be downloaded instead '. 'of displayed. This is mainly a usability consideration, since '. 'browsers tend to freak out when viewing enormous binary files.'. "\n\n". 'The keys in this map are vieweable MIME types; the values are '. 'the MIME types they are delivered as when they are viewed in '. 'the browser.')), $this->newOption('files.image-mime-types', 'set', $image_default) ->setSummary(pht('Configure which MIME types are images.')) ->setDescription( pht( 'List of MIME types which can be used as the `src` for an '. '`<img />` tag.')), $this->newOption('files.audio-mime-types', 'set', $audio_default) ->setSummary(pht('Configure which MIME types are audio.')) ->setDescription( pht( 'List of MIME types which can be used to render an '. '`<audio />` tag.')), $this->newOption('files.icon-mime-types', 'wild', $icon_default) ->setSummary(pht('Configure which MIME types map to which icons.')) ->setDescription( pht( 'Map of MIME type to icon name. MIME types which can not be '. 'found default to icon `doc_files`.')), $this->newOption('storage.mysql-engine.max-size', 'int', 1000000) ->setSummary( pht( 'Configure the largest file which will be put into the MySQL '. 'storage engine.')), $this->newOption('storage.local-disk.path', 'string', null) ->setLocked(true) ->setSummary(pht('Local storage disk path.')) ->setDescription( pht( "Phabricator provides a local disk storage engine, which just ". "writes files to some directory on local disk. The webserver ". "must have read/write permissions on this directory. This is ". "straightforward and suitable for most installs, but will not ". "scale past one web frontend unless the path is actually an NFS ". "mount, since you'll end up with some of the files written to ". "each web frontend and no way for them to share. To use the ". "local disk storage engine, specify the path to a directory ". "here. To disable it, specify null.")), $this->newOption('storage.s3.bucket', 'string', null) ->setSummary(pht('Amazon S3 bucket.')) ->setDescription( pht( "Set this to a valid Amazon S3 bucket to store files there. You ". "must also configure S3 access keys in the 'Amazon Web Services' ". "group.")), $this->newOption( 'storage.engine-selector', 'class', 'PhabricatorDefaultFileStorageEngineSelector') ->setBaseClass('PhabricatorFileStorageEngineSelector') ->setSummary(pht('Storage engine selector.')) ->setDescription( pht( 'Phabricator uses a storage engine selector to choose which '. 'storage engine to use when writing file data. If you add new '. 'storage engines or want to provide very custom rules (e.g., '. 'write images to one storage engine and other files to a '. 'different one), you can provide an alternate implementation '. 'here. The default engine will use choose MySQL, Local Disk, and '. 'S3, in that order, if they have valid configurations above and '. 'a file fits within configured limits.')), $this->newOption('storage.upload-size-limit', 'string', null) ->setSummary( pht('Limit to users in interfaces which allow uploading.')) ->setDescription( pht( "Set the size of the largest file a user may upload. This is ". "used to render text like 'Maximum file size: 10MB' on ". "interfaces where users can upload files, and files larger than ". "this size will be rejected. \n\n". "NOTE: **Setting this to a large size is NOT sufficient to ". "allow users to upload large files. You must also configure a ". "number of other settings.** To configure file upload limits, ". "consult the article 'Configuring File Upload Limits' in the ". "documentation. Once you've configured some limit across all ". "levels of the server, you can set this limit to an appropriate ". "value and the UI will then reflect the actual configured ". "limit.\n\n". "Specify this limit in bytes, or using a 'K', 'M', or 'G' ". "suffix.")) ->addExample('10M', pht('Allow Uploads 10MB or Smaller')), $this->newOption( 'metamta.files.public-create-email', 'string', null) ->setDescription(pht('Allow uploaded files via email.')), $this->newOption( 'metamta.files.subject-prefix', 'string', '[File]') ->setDescription(pht('Subject prefix for Files email.')), $this->newOption('files.enable-imagemagick', 'bool', false) ->setBoolOptions( array( pht('Enable'), - pht('Disable') + pht('Disable'), ))->setDescription( pht("This option will enable animated gif images". "to be set as profile pictures. The 'convert' binary ". "should be available to the webserver for this to work")), ); } } diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php index bcf513390c..cf7e9f2b4e 100644 --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -1,310 +1,311 @@ <?php final class PhabricatorFileInfoController extends PhabricatorFileController { private $phid; private $id; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->phid = idx($data, 'phid'); $this->id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->phid) { $file = id(new PhabricatorFileQuery()) ->setViewer($user) ->withPHIDs(array($this->phid)) ->executeOne(); if (!$file) { return new Aphront404Response(); } return id(new AphrontRedirectResponse())->setURI($file->getInfoURI()); } $file = id(new PhabricatorFileQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$file) { return new Aphront404Response(); } $phid = $file->getPHID(); $xactions = id(new PhabricatorFileTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($phid)) ->execute(); $handle_phids = array_merge( array($file->getAuthorPHID()), $file->getObjectPHIDs()); $this->loadHandles($handle_phids); $header = id(new PHUIHeaderView()) ->setUser($user) ->setPolicyObject($file) ->setHeader($file->getName()); $ttl = $file->getTTL(); if ($ttl !== null) { $ttl_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_OBJECT) ->setName(pht('Temporary')); $header->addTag($ttl_tag); } $actions = $this->buildActionView($file); $timeline = $this->buildTransactionView($file, $xactions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); $crumbs->addTextCrumb( 'F'.$file->getID(), $this->getApplicationURI("/info/{$phid}/")); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header); $this->buildPropertyViews($object_box, $file, $actions); return $this->buildApplicationPage( array( $crumbs, $object_box, - $timeline + $timeline, ), array( 'title' => $file->getName(), 'pageObjects' => array($file->getPHID()), )); } private function buildTransactionView( PhabricatorFile $file, array $xactions) { $user = $this->getRequest()->getUser(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($file->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $add_comment_header = $is_serious ? pht('Add Comment') : pht('Question File Integrity'); $draft = PhabricatorDraft::newFromUserAndKey($user, $file->getPHID()); $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setObjectPHID($file->getPHID()) ->setDraft($draft) ->setHeaderText($add_comment_header) ->setAction($this->getApplicationURI('/comment/'.$file->getID().'/')) ->setSubmitButtonName(pht('Add Comment')); return array( $timeline, - $add_comment_form); + $add_comment_form, + ); } private function buildActionView(PhabricatorFile $file) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $file->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $file, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($file); if ($file->isViewableInBrowser()) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View File')) ->setIcon('fa-file-o') ->setHref($file->getViewURI())); } else { $view->addAction( id(new PhabricatorActionView()) ->setUser($viewer) ->setRenderAsForm(true) ->setDownload(true) ->setName(pht('Download File')) ->setIcon('fa-download') ->setHref($file->getViewURI())); } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit File')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("/edit/{$id}/")) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete File')) ->setIcon('fa-times') ->setHref($this->getApplicationURI("/delete/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit)); return $view; } private function buildPropertyViews( PHUIObjectBoxView $box, PhabricatorFile $file, PhabricatorActionListView $actions) { $request = $this->getRequest(); $user = $request->getUser(); $properties = id(new PHUIPropertyListView()); $properties->setActionList($actions); $box->addPropertyList($properties, pht('Details')); if ($file->getAuthorPHID()) { $properties->addProperty( pht('Author'), $this->getHandle($file->getAuthorPHID())->renderLink()); } $properties->addProperty( pht('Created'), phabricator_datetime($file->getDateCreated(), $user)); $finfo = id(new PHUIPropertyListView()); $box->addPropertyList($finfo, pht('File Info')); $finfo->addProperty( pht('Size'), phutil_format_bytes($file->getByteSize())); $finfo->addProperty( pht('Mime Type'), $file->getMimeType()); $width = $file->getImageWidth(); if ($width) { $finfo->addProperty( pht('Width'), pht('%s px', new PhutilNumber($width))); } $height = $file->getImageHeight(); if ($height) { $finfo->addProperty( pht('Height'), pht('%s px', new PhutilNumber($height))); } $is_image = $file->isViewableImage(); if ($is_image) { $image_string = pht('Yes'); $cache_string = $file->getCanCDN() ? pht('Yes') : pht('No'); } else { $image_string = pht('No'); $cache_string = pht('Not Applicable'); } $finfo->addProperty(pht('Viewable Image'), $image_string); $finfo->addProperty(pht('Cacheable'), $cache_string); $storage_properties = new PHUIPropertyListView(); $box->addPropertyList($storage_properties, pht('Storage')); $storage_properties->addProperty( pht('Engine'), $file->getStorageEngine()); $storage_properties->addProperty( pht('Format'), $file->getStorageFormat()); $storage_properties->addProperty( pht('Handle'), $file->getStorageHandle()); $phids = $file->getObjectPHIDs(); if ($phids) { $attached = new PHUIPropertyListView(); $box->addPropertyList($attached, pht('Attached')); $attached->addProperty( pht('Attached To'), $this->renderHandlesForPHIDs($phids)); } if ($file->isViewableImage()) { $image = phutil_tag( 'img', array( 'src' => $file->getViewURI(), 'class' => 'phui-property-list-image', )); $linked_image = phutil_tag( 'a', array( 'href' => $file->getViewURI(), ), $image); $media = id(new PHUIPropertyListView()) ->addImageContent($linked_image); $box->addPropertyList($media); } else if ($file->isAudio()) { $audio = phutil_tag( 'audio', array( 'controls' => 'controls', 'class' => 'phui-property-list-audio', ), phutil_tag( 'source', array( 'src' => $file->getViewURI(), 'type' => $file->getMimeType(), ))); $media = id(new PHUIPropertyListView()) ->addImageContent($audio); $box->addPropertyList($media); } } } diff --git a/src/applications/flag/query/PhabricatorFlagSearchEngine.php b/src/applications/flag/query/PhabricatorFlagSearchEngine.php index 8dff52e47c..132891d6da 100644 --- a/src/applications/flag/query/PhabricatorFlagSearchEngine.php +++ b/src/applications/flag/query/PhabricatorFlagSearchEngine.php @@ -1,180 +1,181 @@ <?php final class PhabricatorFlagSearchEngine extends PhabricatorApplicationSearchEngine { public function getResultTypeDescription() { return pht('Flags'); } public function getApplicationClassName() { return 'PhabricatorFlagsApplication'; } public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); $saved->setParameter('colors', $request->getArr('colors')); $saved->setParameter('group', $request->getStr('group')); $saved->setParameter('objectFilter', $request->getStr('objectFilter')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorFlagQuery()) ->needHandles(true) ->withOwnerPHIDs(array($this->requireViewer()->getPHID())); $colors = $saved->getParameter('colors'); if ($colors) { $query->withColors($colors); } $group = $saved->getParameter('group'); $options = $this->getGroupOptions(); if ($group && isset($options[$group])) { $query->setGroupBy($group); } $object_filter = $saved->getParameter('objectFilter'); $objects = $this->getObjectFilterOptions(); if ($object_filter && isset($objects[$object_filter])) { $query->withTypes(array($object_filter)); } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved_query) { $form ->appendChild( id(new PhabricatorFlagSelectControl()) ->setName('colors') ->setLabel(pht('Colors')) ->setValue($saved_query->getParameter('colors', array()))) ->appendChild( id(new AphrontFormSelectControl()) ->setName('group') ->setLabel(pht('Group By')) ->setValue($saved_query->getParameter('group')) ->setOptions($this->getGroupOptions())) ->appendChild( id(new AphrontFormSelectControl()) ->setName('objectFilter') ->setLabel(pht('Object Type')) ->setValue($saved_query->getParameter('objectFilter')) ->setOptions($this->getObjectFilterOptions())); } protected function getURI($path) { return '/flag/'.$path; } public function getBuiltinQueryNames() { $names = array( 'all' => pht('Flagged'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } private function getGroupOptions() { return array( PhabricatorFlagQuery::GROUP_NONE => pht('None'), PhabricatorFlagQuery::GROUP_COLOR => pht('Color'), ); } private function getObjectFilterOptions() { $objects = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorFlaggableInterface') ->loadObjects(); $all_types = PhabricatorPHIDType::getAllTypes(); $options = array(); foreach ($objects as $object) { $phid = $object->generatePHID(); $phid_type = phid_get_type($phid); $type_object = idx($all_types, $phid_type); if ($type_object) { $options[$phid_type] = $type_object->getTypeName(); } } // sort it alphabetically... asort($options); $default_option = array( - 0 => pht('All Object Types')); + 0 => pht('All Object Types'), + ); // ...and stick the default option on front $options = array_merge($default_option, $options); return $options; } protected function renderResultList( array $flags, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($flags, 'PhabricatorFlag'); $viewer = $this->requireViewer(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($flags as $flag) { $id = $flag->getID(); $phid = $flag->getObjectPHID(); $class = PhabricatorFlagColor::getCSSClass($flag->getColor()); $flag_icon = phutil_tag( 'div', array( 'class' => 'phabricator-flag-icon '.$class, ), ''); $item = id(new PHUIObjectItemView()) ->addHeadIcon($flag_icon) ->setHeader($flag->getHandle()->renderLink()); $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("edit/{$phid}/")) ->setWorkflow(true)); $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-times') ->setHref($this->getApplicationURI("delete/{$id}/")) ->setWorkflow(true)); if ($flag->getNote()) { $item->addAttribute($flag->getNote()); } $item->addIcon( 'none', phabricator_datetime($flag->getDateCreated(), $viewer)); $list->addItem($item); } return $list; } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php index 05433c074a..6a3b3f9cd1 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -1,534 +1,537 @@ <?php final class HarbormasterBuildViewController extends HarbormasterController { private $id; public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $this->id; $generation = $request->getInt('g'); $build = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$build) { return new Aphront404Response(); } require_celerity_resource('harbormaster-css'); $title = pht('Build %d', $id); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setPolicyObject($build); if ($build->isRestarting()) { $header->setStatus('fa-exclamation-triangle', 'red', pht('Restarting')); } else if ($build->isStopping()) { $header->setStatus('fa-exclamation-triangle', 'red', pht('Pausing')); } else if ($build->isResuming()) { $header->setStatus('fa-exclamation-triangle', 'red', pht('Resuming')); } $box = id(new PHUIObjectBoxView()) ->setHeader($header); $actions = $this->buildActionList($build); $this->buildPropertyLists($box, $build, $actions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( $build->getBuildable()->getMonogram(), '/'.$build->getBuildable()->getMonogram()); $crumbs->addTextCrumb($title); if ($generation === null || $generation > $build->getBuildGeneration() || $generation < 0) { $generation = $build->getBuildGeneration(); } $build_targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($viewer) ->needBuildSteps(true) ->withBuildPHIDs(array($build->getPHID())) ->withBuildGenerations(array($generation)) ->execute(); if ($build_targets) { $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(mpull($build_targets, 'getPHID')) ->execute(); $messages = mgroup($messages, 'getBuildTargetPHID'); } else { $messages = array(); } $targets = array(); foreach ($build_targets as $build_target) { $header = id(new PHUIHeaderView()) ->setHeader($build_target->getName()) ->setUser($viewer); $target_box = id(new PHUIObjectBoxView()) ->setHeader($header); $properties = new PHUIPropertyListView(); $status_view = new PHUIStatusListView(); $item = new PHUIStatusItemView(); $status = $build_target->getTargetStatus(); $status_name = HarbormasterBuildTarget::getBuildTargetStatusName($status); $icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status); $color = HarbormasterBuildTarget::getBuildTargetStatusColor($status); $item->setTarget($status_name); $item->setIcon($icon, $color); $status_view->addItem($item); $properties->addProperty(pht('Name'), $build_target->getName()); if ($build_target->getDateStarted() !== null) { $properties->addProperty( pht('Started'), phabricator_datetime($build_target->getDateStarted(), $viewer)); if ($build_target->isComplete()) { $properties->addProperty( pht('Completed'), phabricator_datetime($build_target->getDateCompleted(), $viewer)); $properties->addProperty( pht('Duration'), phutil_format_relative_time_detailed( $build_target->getDateCompleted() - $build_target->getDateStarted())); } else { $properties->addProperty( pht('Elapsed'), phutil_format_relative_time_detailed( time() - $build_target->getDateStarted())); } } $properties->addProperty(pht('Status'), $status_view); $target_box->addPropertyList($properties, pht('Overview')); $step = $build_target->getBuildStep(); if ($step) { $description = $step->getDescription(); if ($description) { $rendered = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff()) ->setContent($description) ->setPreserveLinebreaks(true), 'default', $viewer); $properties->addSectionHeader(pht('Description')); $properties->addTextContent($rendered); } } else { $target_box->setFormErrors( array( pht( 'This build step has since been deleted on the build plan. '. 'Some information may be omitted.'), )); } $details = $build_target->getDetails(); if ($details) { $properties = new PHUIPropertyListView(); foreach ($details as $key => $value) { $properties->addProperty($key, $value); } $target_box->addPropertyList($properties, pht('Configuration')); } $variables = $build_target->getVariables(); if ($variables) { $properties = new PHUIPropertyListView(); foreach ($variables as $key => $value) { $properties->addProperty($key, $value); } $target_box->addPropertyList($properties, pht('Variables')); } $artifacts = $this->buildArtifacts($build_target); if ($artifacts) { $properties = new PHUIPropertyListView(); $properties->addRawContent($artifacts); $target_box->addPropertyList($properties, pht('Artifacts')); } $build_messages = idx($messages, $build_target->getPHID(), array()); if ($build_messages) { $properties = new PHUIPropertyListView(); $properties->addRawContent($this->buildMessages($build_messages)); $target_box->addPropertyList($properties, pht('Messages')); } $properties = new PHUIPropertyListView(); $properties->addProperty('Build Target ID', $build_target->getID()); $target_box->addPropertyList($properties, pht('Metadata')); $targets[] = $target_box; $targets[] = $this->buildLog($build, $build_target); } $xactions = id(new HarbormasterBuildTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($build->getPHID())) ->execute(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($build->getPHID()) ->setTransactions($xactions); return $this->buildApplicationPage( array( $crumbs, $box, $targets, $timeline, ), array( 'title' => $title, )); } private function buildArtifacts( HarbormasterBuildTarget $build_target) { $request = $this->getRequest(); $viewer = $request->getUser(); $artifacts = id(new HarbormasterBuildArtifactQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(array($build_target->getPHID())) ->execute(); if (count($artifacts) === 0) { return null; } $list = id(new PHUIObjectItemListView()) ->setFlush(true); foreach ($artifacts as $artifact) { $item = $artifact->getObjectItemView($viewer); if ($item !== null) { $list->addItem($item); } } return $list; } private function buildLog( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $request = $this->getRequest(); $viewer = $request->getUser(); $limit = $request->getInt('l', 25); $logs = id(new HarbormasterBuildLogQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(array($build_target->getPHID())) ->execute(); $empty_logs = array(); $log_boxes = array(); foreach ($logs as $log) { $start = 1; $lines = preg_split("/\r\n|\r|\n/", $log->getLogText()); if ($limit !== 0) { $start = count($lines) - $limit; if ($start >= 1) { $lines = array_slice($lines, -$limit, $limit); } else { $start = 1; } } $id = null; $is_empty = false; if (count($lines) === 1 && trim($lines[0]) === '') { // Prevent Harbormaster from showing empty build logs. $id = celerity_generate_unique_node_id(); $empty_logs[] = $id; $is_empty = true; } $log_view = new ShellLogView(); $log_view->setLines($lines); $log_view->setStart($start); $header = id(new PHUIHeaderView()) ->setHeader(pht( 'Build Log %d (%s - %s)', $log->getID(), $log->getLogSource(), $log->getLogType())) ->setSubheader($this->createLogHeader($build, $log)) ->setUser($viewer); $log_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setForm($log_view); if ($is_empty) { $log_box = phutil_tag( 'div', array( 'style' => 'display: none', - 'id' => $id), + 'id' => $id, + ), $log_box); } $log_boxes[] = $log_box; } if ($empty_logs) { $hide_id = celerity_generate_unique_node_id(); Javelin::initBehavior('phabricator-reveal-content'); $expand = phutil_tag( 'div', array( 'id' => $hide_id, 'class' => 'harbormaster-empty-logs-are-hidden mlr mlt mll', ), array( pht( '%s empty logs are hidden.', new PhutilNumber(count($empty_logs))), ' ', javelin_tag( 'a', array( 'href' => '#', 'sigil' => 'reveal-content', 'meta' => array( 'showIDs' => $empty_logs, 'hideIDs' => array($hide_id), ), ), pht('Show all logs.')), )); array_unshift($log_boxes, $expand); } return $log_boxes; } private function createLogHeader($build, $log) { $request = $this->getRequest(); $limit = $request->getInt('l', 25); $lines_25 = $this->getApplicationURI('/build/'.$build->getID().'/?l=25'); $lines_50 = $this->getApplicationURI('/build/'.$build->getID().'/?l=50'); $lines_100 = $this->getApplicationURI('/build/'.$build->getID().'/?l=100'); $lines_0 = $this->getApplicationURI('/build/'.$build->getID().'/?l=0'); $link_25 = phutil_tag('a', array('href' => $lines_25), pht('25')); $link_50 = phutil_tag('a', array('href' => $lines_50), pht('50')); $link_100 = phutil_tag('a', array('href' => $lines_100), pht('100')); $link_0 = phutil_tag('a', array('href' => $lines_0), pht('Unlimited')); if ($limit === 25) { $link_25 = phutil_tag('strong', array(), $link_25); } else if ($limit === 50) { $link_50 = phutil_tag('strong', array(), $link_50); } else if ($limit === 100) { $link_100 = phutil_tag('strong', array(), $link_100); } else if ($limit === 0) { $link_0 = phutil_tag('strong', array(), $link_0); } return phutil_tag( 'span', array(), array( $link_25, ' - ', $link_50, ' - ', $link_100, ' - ', $link_0, - ' Lines')); + ' Lines', + )); } private function buildActionList(HarbormasterBuild $build) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $build->getID(); $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($build) ->setObjectURI("/build/{$id}"); $can_restart = $build->canRestartBuild(); $can_stop = $build->canStopBuild(); $can_resume = $build->canResumeBuild(); $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Restart Build')) ->setIcon('fa-repeat') ->setHref($this->getApplicationURI('/build/restart/'.$id.'/')) ->setDisabled(!$can_restart) ->setWorkflow(true)); if ($build->canResumeBuild()) { $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Resume Build')) ->setIcon('fa-play') ->setHref($this->getApplicationURI('/build/resume/'.$id.'/')) ->setDisabled(!$can_resume) ->setWorkflow(true)); } else { $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Pause Build')) ->setIcon('fa-pause') ->setHref($this->getApplicationURI('/build/stop/'.$id.'/')) ->setDisabled(!$can_stop) ->setWorkflow(true)); } return $list; } private function buildPropertyLists( PHUIObjectBoxView $box, HarbormasterBuild $build, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($build) ->setActionList($actions); $box->addPropertyList($properties); $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array( $build->getBuildablePHID(), - $build->getBuildPlanPHID())) + $build->getBuildPlanPHID(), + )) ->execute(); $properties->addProperty( pht('Buildable'), $handles[$build->getBuildablePHID()]->renderLink()); $properties->addProperty( pht('Build Plan'), $handles[$build->getBuildPlanPHID()]->renderLink()); $properties->addProperty( pht('Restarts'), $build->getBuildGeneration()); $properties->addProperty( pht('Status'), $this->getStatus($build)); } private function getStatus(HarbormasterBuild $build) { $status_view = new PHUIStatusListView(); $item = new PHUIStatusItemView(); if ($build->isStopping()) { $status_name = pht('Pausing'); $icon = PHUIStatusItemView::ICON_RIGHT; $color = 'dark'; } else { $status = $build->getBuildStatus(); $status_name = HarbormasterBuild::getBuildStatusName($status); $icon = HarbormasterBuild::getBuildStatusIcon($status); $color = HarbormasterBuild::getBuildStatusColor($status); } $item->setTarget($status_name); $item->setIcon($icon, $color); $status_view->addItem($item); return $status_view; } private function buildMessages(array $messages) { $viewer = $this->getRequest()->getUser(); if ($messages) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(mpull($messages, 'getAuthorPHID')) ->execute(); } else { $handles = array(); } $rows = array(); foreach ($messages as $message) { $rows[] = array( $message->getID(), $handles[$message->getAuthorPHID()]->renderLink(), $message->getType(), $message->getIsConsumed() ? pht('Consumed') : null, phabricator_datetime($message->getDateCreated(), $viewer), ); } $table = new AphrontTableView($rows); $table->setNoDataString(pht('No messages for this build target.')); $table->setHeaders( array( pht('ID'), pht('From'), pht('Type'), pht('Consumed'), pht('Received'), )); $table->setColumnClasses( array( '', '', 'wide', '', 'date', )); return $table; } } diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index 1ce17bf4f4..9f52e93a09 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -1,479 +1,481 @@ <?php final class HarbormasterPlanViewController extends HarbormasterPlanController { private $id; public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $this->id; $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$plan) { return new Aphront404Response(); } $xactions = id(new HarbormasterBuildPlanTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($plan->getPHID())) ->execute(); $xaction_view = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($plan->getPHID()) ->setTransactions($xactions) ->setShouldTerminate(true); $title = pht('Plan %d', $id); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setPolicyObject($plan); $box = id(new PHUIObjectBoxView()) ->setHeader($header); $actions = $this->buildActionList($plan); $this->buildPropertyLists($box, $plan, $actions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Plan %d', $id)); list($step_list, $has_any_conflicts, $would_deadlock) = $this->buildStepList($plan); if ($would_deadlock) { $box->setFormErrors( array( pht( 'This build plan will deadlock when executed, due to '. 'circular dependencies present in the build plan. '. 'Examine the step list and resolve the deadlock.'), )); } else if ($has_any_conflicts) { // A deadlocking build will also cause all the artifacts to be // invalid, so we just skip showing this message if that's the // case. $box->setFormErrors( array( pht( 'This build plan has conflicts in one or more build steps. '. 'Examine the step list and resolve the listed errors.'), )); } return $this->buildApplicationPage( array( $crumbs, $box, $step_list, $xaction_view, ), array( 'title' => $title, )); } private function buildStepList(HarbormasterBuildPlan $plan) { $request = $this->getRequest(); $viewer = $request->getUser(); $run_order = HarbormasterBuildGraph::determineDependencyExecution($plan); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($viewer) ->withBuildPlanPHIDs(array($plan->getPHID())) ->execute(); $steps = mpull($steps, null, 'getPHID'); $can_edit = $this->hasApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); $step_list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setNoDataString( pht('This build plan does not have any build steps yet.')); $i = 1; $last_depth = 0; $has_any_conflicts = false; $is_deadlocking = false; foreach ($run_order as $run_ref) { $step = $steps[$run_ref['node']->getPHID()]; $depth = $run_ref['depth'] + 1; if ($last_depth !== $depth) { $last_depth = $depth; $i = 1; } else { $i++; } $implementation = null; try { $implementation = $step->getStepImplementation(); } catch (Exception $ex) { // We can't initialize the implementation. This might be because // it's been renamed or no longer exists. $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Step %d.%d', $depth, $i)) ->setHeader(pht('Unknown Implementation')) ->setBarColor('red') ->addAttribute(pht( 'This step has an invalid implementation (%s).', $step->getClassName())) ->addAction( id(new PHUIListItemView()) ->setIcon('fa-times') ->addSigil('harbormaster-build-step-delete') ->setWorkflow(true) ->setRenderNameAsTooltip(true) ->setName(pht('Delete')) ->setHref( $this->getApplicationURI('step/delete/'.$step->getID().'/'))); $step_list->addItem($item); continue; } $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Step %d.%d', $depth, $i)) ->setHeader($step->getName()); $item->addAttribute($implementation->getDescription()); $step_id = $step->getID(); $edit_uri = $this->getApplicationURI("step/edit/{$step_id}/"); $delete_uri = $this->getApplicationURI("step/delete/{$step_id}/"); if ($can_edit) { $item->setHref($edit_uri); } $item ->setHref($edit_uri) ->addAction( id(new PHUIListItemView()) ->setIcon('fa-times') ->addSigil('harbormaster-build-step-delete') ->setWorkflow(true) ->setDisabled(!$can_edit) ->setHref( $this->getApplicationURI('step/delete/'.$step->getID().'/'))); $depends = $step->getStepImplementation()->getDependencies($step); $inputs = $step->getStepImplementation()->getArtifactInputs(); $outputs = $step->getStepImplementation()->getArtifactOutputs(); $has_conflicts = false; if ($depends || $inputs || $outputs) { $available_artifacts = HarbormasterBuildStepImplementation::getAvailableArtifacts( $plan, $step, null); $available_artifacts = ipull($available_artifacts, 'type'); list($depends_ui, $has_conflicts) = $this->buildDependsOnList( $depends, pht('Depends On'), $steps); list($inputs_ui, $has_conflicts) = $this->buildArtifactList( $inputs, 'in', pht('Input Artifacts'), $available_artifacts); list($outputs_ui) = $this->buildArtifactList( $outputs, 'out', pht('Output Artifacts'), array()); $item->appendChild( phutil_tag( 'div', array( 'class' => 'harbormaster-artifact-io', ), array( $depends_ui, $inputs_ui, $outputs_ui, ))); } if ($has_conflicts) { $has_any_conflicts = true; $item->setBarColor('red'); } if ($run_ref['cycle']) { $is_deadlocking = true; } if ($is_deadlocking) { $item->setBarColor('red'); } $step_list->addItem($item); } return array($step_list, $has_any_conflicts, $is_deadlocking); } private function buildActionList(HarbormasterBuildPlan $plan) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $plan->getID(); $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($plan) ->setObjectURI($this->getApplicationURI("plan/{$id}/")); $can_edit = $this->hasApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Plan')) ->setHref($this->getApplicationURI("plan/edit/{$id}/")) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit) ->setIcon('fa-pencil')); if ($plan->isDisabled()) { $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Enable Plan')) ->setHref($this->getApplicationURI("plan/disable/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setIcon('fa-check')); } else { $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Disable Plan')) ->setHref($this->getApplicationURI("plan/disable/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setIcon('fa-ban')); } $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Add Build Step')) ->setHref($this->getApplicationURI("step/add/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setIcon('fa-plus')); $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Run Plan Manually')) ->setHref($this->getApplicationURI("plan/run/{$id}/")) ->setWorkflow(true) ->setDisabled(!$can_edit) ->setIcon('fa-play-circle')); return $list; } private function buildPropertyLists( PHUIObjectBoxView $box, HarbormasterBuildPlan $plan, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($plan) ->setActionList($actions); $box->addPropertyList($properties); $properties->addProperty( pht('Created'), phabricator_datetime($plan->getDateCreated(), $viewer)); } private function buildArtifactList( array $artifacts, $kind, $name, array $available_artifacts) { $has_conflicts = false; if (!$artifacts) { return array(null, $has_conflicts); } $this->requireResource('harbormaster-css'); $header = phutil_tag( 'div', array( 'class' => 'harbormaster-artifact-summary-header', ), $name); $is_input = ($kind == 'in'); $list = new PHUIStatusListView(); foreach ($artifacts as $artifact) { $error = null; $key = idx($artifact, 'key'); if (!strlen($key)) { $bound = phutil_tag('em', array(), pht('(null)')); if ($is_input) { // This is an unbound input. For now, all inputs are always required. $icon = PHUIStatusItemView::ICON_WARNING; $color = 'red'; $icon_label = pht('Required Input'); $has_conflicts = true; $error = pht('This input is required, but not configured.'); } else { // This is an unnamed output. Outputs do not necessarily need to be // named. $icon = PHUIStatusItemView::ICON_OPEN; $color = 'bluegrey'; $icon_label = pht('Unused Output'); } } else { $bound = phutil_tag('strong', array(), $key); if ($is_input) { if (isset($available_artifacts[$key])) { if ($available_artifacts[$key] == idx($artifact, 'type')) { $icon = PHUIStatusItemView::ICON_ACCEPT; $color = 'green'; $icon_label = pht('Valid Input'); } else { $icon = PHUIStatusItemView::ICON_WARNING; $color = 'red'; $icon_label = pht('Bad Input Type'); $has_conflicts = true; $error = pht( 'This input is bound to the wrong artifact type. It is bound '. 'to a "%s" artifact, but should be bound to a "%s" artifact.', $available_artifacts[$key], idx($artifact, 'type')); } } else { $icon = PHUIStatusItemView::ICON_QUESTION; $color = 'red'; $icon_label = pht('Unknown Input'); $has_conflicts = true; $error = pht( 'This input is bound to an artifact ("%s") which does not exist '. 'at this stage in the build process.', $key); } } else { $icon = PHUIStatusItemView::ICON_DOWN; $color = 'green'; $icon_label = pht('Valid Output'); } } if ($error) { $note = array( phutil_tag('strong', array(), pht('ERROR:')), ' ', - $error); + $error, + ); } else { $note = $bound; } $list->addItem( id(new PHUIStatusItemView()) ->setIcon($icon, $color, $icon_label) ->setTarget($artifact['name']) ->setNote($note)); } $ui = array( $header, $list, ); return array($ui, $has_conflicts); } private function buildDependsOnList( array $step_phids, $name, array $steps) { $has_conflicts = false; if (count($step_phids) === 0) { return null; } $this->requireResource('harbormaster-css'); $steps = mpull($steps, null, 'getPHID'); $header = phutil_tag( 'div', array( 'class' => 'harbormaster-artifact-summary-header', ), $name); $list = new PHUIStatusListView(); foreach ($step_phids as $step_phid) { $error = null; if (idx($steps, $step_phid) === null) { $icon = PHUIStatusItemView::ICON_WARNING; $color = 'red'; $icon_label = pht('Missing Dependency'); $has_conflicts = true; $error = pht( 'This dependency specifies a build step which doesn\'t exist.'); } else { $bound = phutil_tag( 'strong', array(), idx($steps, $step_phid)->getName()); $icon = PHUIStatusItemView::ICON_ACCEPT; $color = 'green'; $icon_label = pht('Valid Input'); } if ($error) { $note = array( phutil_tag('strong', array(), pht('ERROR:')), ' ', - $error); + $error, + ); } else { $note = $bound; } $list->addItem( id(new PHUIStatusItemView()) ->setIcon($icon, $color, $icon_label) ->setTarget(pht('Build Step')) ->setNote($note)); } $ui = array( $header, $list, ); return array($ui, $has_conflicts); } } diff --git a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php index eb383d0b06..0d9d26c6aa 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php @@ -1,114 +1,114 @@ <?php final class HarbormasterBuildTransactionEditor extends PhabricatorApplicationTransactionEditor { public function getEditorApplicationClass() { return 'PhabricatorHarbormasterApplication'; } public function getEditorObjectsDescription() { return pht('Harbormaster Builds'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = HarbormasterBuildTransaction::TYPE_CREATE; $types[] = HarbormasterBuildTransaction::TYPE_COMMAND; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: case HarbormasterBuildTransaction::TYPE_COMMAND: return null; } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: return true; case HarbormasterBuildTransaction::TYPE_COMMAND: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: return; case HarbormasterBuildTransaction::TYPE_COMMAND: return $this->executeBuildCommand($object, $xaction); } return parent::applyCustomInternalTransaction($object, $xaction); } private function executeBuildCommand( HarbormasterBuild $build, HarbormasterBuildTransaction $xaction) { $command = $xaction->getNewValue(); switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: $issuable = $build->canRestartBuild(); break; case HarbormasterBuildCommand::COMMAND_STOP: $issuable = $build->canStopBuild(); break; case HarbormasterBuildCommand::COMMAND_RESUME: $issuable = $build->canResumeBuild(); break; default: throw new Exception("Unknown command $command"); } if (!$issuable) { return; } id(new HarbormasterBuildCommand()) ->setAuthorPHID($xaction->getAuthorPHID()) ->setTargetPHID($build->getPHID()) ->setCommand($command) ->save(); PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( - 'buildID' => $build->getID() + 'buildID' => $build->getID(), )); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: case HarbormasterBuildTransaction::TYPE_COMMAND: return; } return parent::applyCustomExternalTransaction($object, $xaction); } } diff --git a/src/applications/harbormaster/engine/HarbormasterBuildGraph.php b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php index a696a2b3d6..757f112d05 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildGraph.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php @@ -1,61 +1,62 @@ <?php /** * Directed graph representing a build plan */ final class HarbormasterBuildGraph extends AbstractDirectedGraph { private $stepMap; public static function determineDependencyExecution( HarbormasterBuildPlan $plan) { $steps = id(new HarbormasterBuildStepQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildPlanPHIDs(array($plan->getPHID())) ->execute(); $steps_by_phid = mpull($steps, null, 'getPHID'); $step_phids = mpull($steps, 'getPHID'); if (count($steps) === 0) { return array(); } $graph = id(new HarbormasterBuildGraph($steps_by_phid)) ->addNodes($step_phids); $raw_results = $graph->getBestEffortTopographicallySortedNodes(); $results = array(); foreach ($raw_results as $node) { $results[] = array( 'node' => $steps_by_phid[$node['node']], 'depth' => $node['depth'], - 'cycle' => $node['cycle']); + 'cycle' => $node['cycle'], + ); } return $results; } public function __construct($step_map) { $this->stepMap = $step_map; } protected function loadEdges(array $nodes) { $map = array(); foreach ($nodes as $node) { $step = $this->stepMap[$node]; $deps = $step->getStepImplementation()->getDependencies($step); $map[$node] = array(); foreach ($deps as $dep) { $map[$node][] = $dep; } } return $map; } } diff --git a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php index 90efde27ec..28e14c964e 100644 --- a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php @@ -1,69 +1,70 @@ <?php final class HarbormasterLeaseHostBuildStepImplementation extends HarbormasterBuildStepImplementation { public function getName() { return pht('Lease Host'); } public function getGenericDescription() { return pht('Obtain a lease on a Drydock host for performing builds.'); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); // Create the lease. $lease = id(new DrydockLease()) ->setResourceType('host') ->setAttributes( array( 'platform' => $settings['platform'], )) ->queueForActivation(); // Wait until the lease is fulfilled. // TODO: This will throw an exception if the lease can't be fulfilled; // we should treat that as build failure not build error. $lease->waitUntilActive(); // Create the associated artifact. $artifact = $build->createArtifact( $build_target, $settings['name'], HarbormasterBuildArtifact::TYPE_HOST); $artifact->setArtifactData(array( - 'drydock-lease' => $lease->getID())); + 'drydock-lease' => $lease->getID(), + )); $artifact->save(); } public function getArtifactOutputs() { return array( array( 'name' => pht('Leased Host'), 'key' => $this->getSetting('name'), 'type' => HarbormasterBuildArtifact::TYPE_HOST, ), ); } public function getFieldSpecifications() { return array( 'name' => array( 'name' => pht('Artifact Name'), 'type' => 'text', 'required' => true, ), 'platform' => array( 'name' => pht('Platform'), 'type' => 'text', 'required' => true, ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php index 1e76e65819..4172da8917 100644 --- a/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php @@ -1,92 +1,93 @@ <?php final class HarbormasterUploadArtifactBuildStepImplementation extends HarbormasterBuildStepImplementation { public function getName() { return pht('Upload File'); } public function getGenericDescription() { return pht('Upload a file from a host to Phabricator.'); } public function getDescription() { return pht( 'Upload %s from %s.', $this->formatSettingForDescription('path'), $this->formatSettingForDescription('hostartifact')); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); $variables = $build_target->getVariables(); $path = $this->mergeVariables( 'vsprintf', $settings['path'], $variables); $artifact = $build->loadArtifact($settings['hostartifact']); $lease = $artifact->loadDrydockLease(); $interface = $lease->getInterface('filesystem'); // TODO: Handle exceptions. $file = $interface->saveFile($path, $settings['name']); // Insert the artifact record. $artifact = $build->createArtifact( $build_target, $settings['name'], HarbormasterBuildArtifact::TYPE_FILE); $artifact->setArtifactData(array( - 'filePHID' => $file->getPHID())); + 'filePHID' => $file->getPHID(), + )); $artifact->save(); } public function getArtifactInputs() { return array( array( 'name' => pht('Upload From Host'), 'key' => $this->getSetting('hostartifact'), 'type' => HarbormasterBuildArtifact::TYPE_HOST, ), ); } public function getArtifactOutputs() { return array( array( 'name' => pht('Uploaded File'), 'key' => $this->getSetting('name'), 'type' => HarbormasterBuildArtifact::TYPE_FILE, ), ); } public function getFieldSpecifications() { return array( 'path' => array( 'name' => pht('Path'), 'type' => 'text', 'required' => true, ), 'name' => array( 'name' => pht('Local Name'), 'type' => 'text', 'required' => true, ), 'hostartifact' => array( 'name' => pht('Host Artifact'), 'type' => 'text', 'required' => true, ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php index 16df73d120..d3e4e92525 100644 --- a/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php @@ -1,111 +1,111 @@ <?php final class HarbormasterWaitForPreviousBuildStepImplementation extends HarbormasterBuildStepImplementation { public function getName() { return pht('Wait for Previous Commits to Build'); } public function getGenericDescription() { return pht( 'Wait for previous commits to finish building the current plan '. 'before continuing.'); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { // We can only wait when building against commits. $buildable = $build->getBuildable(); $object = $buildable->getBuildableObject(); if (!($object instanceof PhabricatorRepositoryCommit)) { return; } // Block until all previous builds of the same build plan have // finished. $plan = $build->getBuildPlan(); $existing_logs = id(new HarbormasterBuildLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildTargetPHIDs(array($build_target->getPHID())) ->execute(); if ($existing_logs) { $log = head($existing_logs); } else { $log = $build->createLog($build_target, 'waiting', 'blockers'); } $blockers = $this->getBlockers($object, $plan, $build); if ($blockers) { $log->start(); $log->append("Blocked by: ".implode(',', $blockers)."\n"); $log->finalize(); } if ($blockers) { throw new PhabricatorWorkerYieldException(15); } } private function getBlockers( PhabricatorRepositoryCommit $commit, HarbormasterBuildPlan $plan, HarbormasterBuild $source) { $call = new ConduitCall( 'diffusion.commitparentsquery', array( 'commit' => $commit->getCommitIdentifier(), - 'callsign' => $commit->getRepository()->getCallsign() + 'callsign' => $commit->getRepository()->getCallsign(), )); $call->setUser(PhabricatorUser::getOmnipotentUser()); $parents = $call->execute(); $parents = id(new DiffusionCommitQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRepository($commit->getRepository()) ->withIdentifiers($parents) ->execute(); $blockers = array(); $build_objects = array(); foreach ($parents as $parent) { if (!$parent->isImported()) { $blockers[] = pht('Commit %s', $parent->getCommitIdentifier()); } else { $build_objects[] = $parent->getPHID(); } } if ($build_objects) { $buildables = id(new HarbormasterBuildableQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildablePHIDs($build_objects) ->withManualBuildables(false) ->execute(); $buildable_phids = mpull($buildables, 'getPHID'); if ($buildable_phids) { $builds = id(new HarbormasterBuildQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildablePHIDs($buildable_phids) ->withBuildPlanPHIDs(array($plan->getPHID())) ->execute(); foreach ($builds as $build) { if (!$build->isComplete()) { $blockers[] = pht('Build %d', $build->getID()); } } } } return $blockers; } } diff --git a/src/applications/harbormaster/storage/HarbormasterBuildable.php b/src/applications/harbormaster/storage/HarbormasterBuildable.php index 3e14167ac3..f7e6ab51f7 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildable.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildable.php @@ -1,278 +1,278 @@ <?php final class HarbormasterBuildable extends HarbormasterDAO implements PhabricatorPolicyInterface, HarbormasterBuildableInterface { protected $buildablePHID; protected $containerPHID; protected $buildableStatus; protected $isManualBuildable; private $buildableObject = self::ATTACHABLE; private $containerObject = self::ATTACHABLE; private $buildableHandle = self::ATTACHABLE; private $containerHandle = self::ATTACHABLE; private $builds = self::ATTACHABLE; const STATUS_BUILDING = 'building'; const STATUS_PASSED = 'passed'; const STATUS_FAILED = 'failed'; public static function getBuildableStatusName($status) { switch ($status) { case self::STATUS_BUILDING: return pht('Building'); case self::STATUS_PASSED: return pht('Passed'); case self::STATUS_FAILED: return pht('Failed'); default: return pht('Unknown'); } } public static function getBuildableStatusIcon($status) { switch ($status) { case self::STATUS_BUILDING: return PHUIStatusItemView::ICON_RIGHT; case self::STATUS_PASSED: return PHUIStatusItemView::ICON_ACCEPT; case self::STATUS_FAILED: return PHUIStatusItemView::ICON_REJECT; default: return PHUIStatusItemView::ICON_QUESTION; } } public static function getBuildableStatusColor($status) { switch ($status) { case self::STATUS_BUILDING: return 'blue'; case self::STATUS_PASSED: return 'green'; case self::STATUS_FAILED: return 'red'; default: return 'bluegrey'; } } public static function initializeNewBuildable(PhabricatorUser $actor) { return id(new HarbormasterBuildable()) ->setIsManualBuildable(0) ->setBuildableStatus(self::STATUS_BUILDING); } public function getMonogram() { return 'B'.$this->getID(); } /** * Returns an existing buildable for the object's PHID or creates a * new buildable implicitly if needed. */ public static function createOrLoadExisting( PhabricatorUser $actor, $buildable_object_phid, $container_object_phid) { $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($actor) ->withBuildablePHIDs(array($buildable_object_phid)) ->withManualBuildables(false) ->setLimit(1) ->executeOne(); if ($buildable) { return $buildable; } $buildable = HarbormasterBuildable::initializeNewBuildable($actor) ->setBuildablePHID($buildable_object_phid) ->setContainerPHID($container_object_phid); $buildable->save(); return $buildable; } /** * Looks up the plan PHIDs and applies the plans to the specified * object identified by it's PHID. */ public static function applyBuildPlans( $phid, $container_phid, array $plan_phids) { if (count($plan_phids) === 0) { return; } // Skip all of this logic if the Harbormaster application // isn't currently installed. $harbormaster_app = 'PhabricatorHarbormasterApplication'; if (!PhabricatorApplication::isClassInstalled($harbormaster_app)) { return; } $buildable = HarbormasterBuildable::createOrLoadExisting( PhabricatorUser::getOmnipotentUser(), $phid, $container_phid); $plans = id(new HarbormasterBuildPlanQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($plan_phids) ->execute(); foreach ($plans as $plan) { if ($plan->isDisabled()) { // TODO: This should be communicated more clearly -- maybe we should // create the build but set the status to "disabled" or "derelict". continue; } $buildable->applyPlan($plan); } } public function applyPlan(HarbormasterBuildPlan $plan) { $viewer = PhabricatorUser::getOmnipotentUser(); $build = HarbormasterBuild::initializeNewBuild($viewer) ->setBuildablePHID($this->getPHID()) ->setBuildPlanPHID($plan->getPHID()) ->setBuildStatus(HarbormasterBuild::STATUS_PENDING) ->save(); PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( - 'buildID' => $build->getID() + 'buildID' => $build->getID(), )); return $build; } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'containerPHID' => 'phid?', 'buildableStatus' => 'text32', 'isManualBuildable' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_container' => array( 'columns' => array('containerPHID'), ), 'key_manual' => array( 'columns' => array('isManualBuildable'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildablePHIDType::TYPECONST); } public function attachBuildableObject($buildable_object) { $this->buildableObject = $buildable_object; return $this; } public function getBuildableObject() { return $this->assertAttached($this->buildableObject); } public function attachContainerObject($container_object) { $this->containerObject = $container_object; return $this; } public function getContainerObject() { return $this->assertAttached($this->containerObject); } public function attachContainerHandle($container_handle) { $this->containerHandle = $container_handle; return $this; } public function getContainerHandle() { return $this->assertAttached($this->containerHandle); } public function attachBuildableHandle($buildable_handle) { $this->buildableHandle = $buildable_handle; return $this; } public function getBuildableHandle() { return $this->assertAttached($this->buildableHandle); } public function attachBuilds(array $builds) { assert_instances_of($builds, 'HarbormasterBuild'); $this->builds = $builds; return $this; } public function getBuilds() { return $this->assertAttached($this->builds); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBuildableObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildableObject()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A buildable inherits policies from the underlying object.'); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildablePHID() { // NOTE: This is essentially just for convenience, as it allows you create // a copy of a buildable by specifying `B123` without bothering to go // look up the underlying object. return $this->getBuildablePHID(); } public function getHarbormasterContainerPHID() { return $this->getContainerPHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } } diff --git a/src/applications/harbormaster/view/ShellLogView.php b/src/applications/harbormaster/view/ShellLogView.php index feaa8ffc07..a78840741c 100644 --- a/src/applications/harbormaster/view/ShellLogView.php +++ b/src/applications/harbormaster/view/ShellLogView.php @@ -1,109 +1,109 @@ <?php final class ShellLogView extends AphrontView { private $start = 1; private $lines; private $limit; private $highlights = array(); public function setStart($start) { $this->start = $start; return $this; } public function setLimit($limit) { $this->limit = $limit; return $this; } public function setLines(array $lines) { $this->lines = $lines; return $this; } public function setHighlights(array $highlights) { $this->highlights = array_fuse($highlights); return $this; } public function render() { require_celerity_resource('phabricator-source-code-view-css'); require_celerity_resource('syntax-highlighting-css'); Javelin::initBehavior('phabricator-oncopy', array()); $line_number = $this->start; $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; $content_line = $line; } $row_attributes = array(); if (isset($this->highlights[$line_number])) { $row_attributes['class'] = 'phabricator-source-highlight'; } // TODO: Provide nice links. $th = phutil_tag( 'th', array( 'class' => 'phabricator-source-line', 'style' => 'background-color: #fff;', ), $content_number); $td = phutil_tag( 'td', array('class' => 'phabricator-source-code'), $content_line); $rows[] = phutil_tag( 'tr', $row_attributes, array($th, $td)); if ($hit_limit) { break; } $line_number++; } $classes = array(); $classes[] = 'phabricator-source-code-view'; $classes[] = 'remarkup-code'; $classes[] = 'PhabricatorMonospaced'; return phutil_tag( 'div', array( 'class' => 'phabricator-source-code-container', - 'style' => 'background-color: black; color: white;' + 'style' => 'background-color: black; color: white;', ), phutil_tag( 'table', array( 'class' => implode(' ', $classes), - 'style' => 'background-color: black' + 'style' => 'background-color: black', ), phutil_implode_html('', $rows))); } } diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index b12ffebdf9..147ebaee11 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -1,1454 +1,1458 @@ <?php /** * @task customfield Custom Field Integration */ abstract class HeraldAdapter { const FIELD_TITLE = 'title'; const FIELD_BODY = 'body'; const FIELD_AUTHOR = 'author'; const FIELD_ASSIGNEE = 'assignee'; const FIELD_REVIEWER = 'reviewer'; const FIELD_REVIEWERS = 'reviewers'; const FIELD_COMMITTER = 'committer'; const FIELD_CC = 'cc'; const FIELD_TAGS = 'tags'; const FIELD_DIFF_FILE = 'diff-file'; const FIELD_DIFF_CONTENT = 'diff-content'; const FIELD_DIFF_ADDED_CONTENT = 'diff-added-content'; const FIELD_DIFF_REMOVED_CONTENT = 'diff-removed-content'; const FIELD_DIFF_ENORMOUS = 'diff-enormous'; const FIELD_REPOSITORY = 'repository'; const FIELD_REPOSITORY_PROJECTS = 'repository-projects'; const FIELD_RULE = 'rule'; const FIELD_AFFECTED_PACKAGE = 'affected-package'; const FIELD_AFFECTED_PACKAGE_OWNER = 'affected-package-owner'; const FIELD_CONTENT_SOURCE = 'contentsource'; const FIELD_ALWAYS = 'always'; const FIELD_AUTHOR_PROJECTS = 'authorprojects'; const FIELD_PROJECTS = 'projects'; const FIELD_PUSHER = 'pusher'; const FIELD_PUSHER_PROJECTS = 'pusher-projects'; const FIELD_DIFFERENTIAL_REVISION = 'differential-revision'; const FIELD_DIFFERENTIAL_REVIEWERS = 'differential-reviewers'; const FIELD_DIFFERENTIAL_CCS = 'differential-ccs'; const FIELD_DIFFERENTIAL_ACCEPTED = 'differential-accepted'; const FIELD_IS_MERGE_COMMIT = 'is-merge-commit'; const FIELD_BRANCHES = 'branches'; const FIELD_AUTHOR_RAW = 'author-raw'; const FIELD_COMMITTER_RAW = 'committer-raw'; const FIELD_IS_NEW_OBJECT = 'new-object'; const FIELD_TASK_PRIORITY = 'taskpriority'; const FIELD_ARCANIST_PROJECT = 'arcanist-project'; const FIELD_PUSHER_IS_COMMITTER = 'pusher-is-committer'; const CONDITION_CONTAINS = 'contains'; const CONDITION_NOT_CONTAINS = '!contains'; const CONDITION_IS = 'is'; const CONDITION_IS_NOT = '!is'; const CONDITION_IS_ANY = 'isany'; const CONDITION_IS_NOT_ANY = '!isany'; const CONDITION_INCLUDE_ALL = 'all'; const CONDITION_INCLUDE_ANY = 'any'; const CONDITION_INCLUDE_NONE = 'none'; const CONDITION_IS_ME = 'me'; const CONDITION_IS_NOT_ME = '!me'; const CONDITION_REGEXP = 'regexp'; const CONDITION_RULE = 'conditions'; const CONDITION_NOT_RULE = '!conditions'; const CONDITION_EXISTS = 'exists'; const CONDITION_NOT_EXISTS = '!exists'; const CONDITION_UNCONDITIONALLY = 'unconditionally'; const CONDITION_NEVER = 'never'; const CONDITION_REGEXP_PAIR = 'regexp-pair'; const CONDITION_HAS_BIT = 'bit'; const CONDITION_NOT_BIT = '!bit'; const CONDITION_IS_TRUE = 'true'; const CONDITION_IS_FALSE = 'false'; const ACTION_ADD_CC = 'addcc'; const ACTION_REMOVE_CC = 'remcc'; const ACTION_EMAIL = 'email'; const ACTION_NOTHING = 'nothing'; const ACTION_AUDIT = 'audit'; const ACTION_FLAG = 'flag'; const ACTION_ASSIGN_TASK = 'assigntask'; const ACTION_ADD_PROJECTS = 'addprojects'; const ACTION_ADD_REVIEWERS = 'addreviewers'; const ACTION_ADD_BLOCKING_REVIEWERS = 'addblockingreviewers'; const ACTION_APPLY_BUILD_PLANS = 'applybuildplans'; const ACTION_BLOCK = 'block'; const ACTION_REQUIRE_SIGNATURE = 'signature'; const VALUE_TEXT = 'text'; const VALUE_NONE = 'none'; const VALUE_EMAIL = 'email'; const VALUE_USER = 'user'; const VALUE_TAG = 'tag'; const VALUE_RULE = 'rule'; const VALUE_REPOSITORY = 'repository'; const VALUE_OWNERS_PACKAGE = 'package'; const VALUE_PROJECT = 'project'; const VALUE_FLAG_COLOR = 'flagcolor'; const VALUE_CONTENT_SOURCE = 'contentsource'; const VALUE_USER_OR_PROJECT = 'userorproject'; const VALUE_BUILD_PLAN = 'buildplan'; const VALUE_TASK_PRIORITY = 'taskpriority'; const VALUE_ARCANIST_PROJECT = 'arcanistprojects'; const VALUE_LEGAL_DOCUMENTS = 'legaldocuments'; private $contentSource; private $isNewObject; private $customFields = false; private $customActions = null; private $queuedTransactions = array(); public function getCustomActions() { if ($this->customActions === null) { $custom_actions = id(new PhutilSymbolLoader()) ->setAncestorClass('HeraldCustomAction') ->loadObjects(); foreach ($custom_actions as $key => $object) { if (!$object->appliesToAdapter($this)) { unset($custom_actions[$key]); } } $this->customActions = array(); foreach ($custom_actions as $action) { $key = $action->getActionKey(); if (array_key_exists($key, $this->customActions)) { throw new Exception( 'More than one Herald custom action implementation '. 'handles the action key: \''.$key.'\'.'); } $this->customActions[$key] = $action; } } return $this->customActions; } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function getContentSource() { return $this->contentSource; } public function getIsNewObject() { if (is_bool($this->isNewObject)) { return $this->isNewObject; } throw new Exception(pht('You must setIsNewObject to a boolean first!')); } public function setIsNewObject($new) { $this->isNewObject = (bool) $new; return $this; } abstract public function getPHID(); abstract public function getHeraldName(); public function getHeraldField($field_name) { switch ($field_name) { case self::FIELD_RULE: return null; case self::FIELD_CONTENT_SOURCE: return $this->getContentSource()->getSource(); case self::FIELD_ALWAYS: return true; case self::FIELD_IS_NEW_OBJECT: return $this->getIsNewObject(); default: if ($this->isHeraldCustomKey($field_name)) { return $this->getCustomFieldValue($field_name); } throw new Exception( "Unknown field '{$field_name}'!"); } } abstract public function applyHeraldEffects(array $effects); protected function handleCustomHeraldEffect(HeraldEffect $effect) { $custom_action = idx($this->getCustomActions(), $effect->getAction()); if ($custom_action !== null) { return $custom_action->applyEffect( $this, $this->getObject(), $effect); } return null; } public function isAvailableToUser(PhabricatorUser $viewer) { $applications = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) ->withInstalled(true) ->withClasses(array($this->getAdapterApplicationClass())) ->execute(); return !empty($applications); } public function queueTransaction($transaction) { $this->queuedTransactions[] = $transaction; } public function getQueuedTransactions() { return $this->queuedTransactions; } /** * NOTE: You generally should not override this; it exists to support legacy * adapters which had hard-coded content types. */ public function getAdapterContentType() { return get_class($this); } abstract public function getAdapterContentName(); abstract public function getAdapterContentDescription(); abstract public function getAdapterApplicationClass(); abstract public function getObject(); public function supportsRuleType($rule_type) { return false; } public function canTriggerOnObject($object) { return false; } public function explainValidTriggerObjects() { return pht('This adapter can not trigger on objects.'); } public function getTriggerObjectPHIDs() { return array($this->getPHID()); } public function getAdapterSortKey() { return sprintf( '%08d%s', $this->getAdapterSortOrder(), $this->getAdapterContentName()); } public function getAdapterSortOrder() { return 1000; } /* -( Fields )------------------------------------------------------------- */ public function getFields() { $fields = array(); $fields[] = self::FIELD_ALWAYS; $fields[] = self::FIELD_RULE; $custom_fields = $this->getCustomFields(); if ($custom_fields) { foreach ($custom_fields->getFields() as $custom_field) { $key = $custom_field->getFieldKey(); $fields[] = $this->getHeraldKeyFromCustomKey($key); } } return $fields; } public function getFieldNameMap() { return array( self::FIELD_TITLE => pht('Title'), self::FIELD_BODY => pht('Body'), self::FIELD_AUTHOR => pht('Author'), self::FIELD_ASSIGNEE => pht('Assignee'), self::FIELD_COMMITTER => pht('Committer'), self::FIELD_REVIEWER => pht('Reviewer'), self::FIELD_REVIEWERS => pht('Reviewers'), self::FIELD_CC => pht('CCs'), self::FIELD_TAGS => pht('Tags'), self::FIELD_DIFF_FILE => pht('Any changed filename'), self::FIELD_DIFF_CONTENT => pht('Any changed file content'), self::FIELD_DIFF_ADDED_CONTENT => pht('Any added file content'), self::FIELD_DIFF_REMOVED_CONTENT => pht('Any removed file content'), self::FIELD_DIFF_ENORMOUS => pht('Change is enormous'), self::FIELD_REPOSITORY => pht('Repository'), self::FIELD_REPOSITORY_PROJECTS => pht('Repository\'s projects'), self::FIELD_RULE => pht('Another Herald rule'), self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'), self::FIELD_AFFECTED_PACKAGE_OWNER => pht("Any affected package's owner"), self::FIELD_CONTENT_SOURCE => pht('Content Source'), self::FIELD_ALWAYS => pht('Always'), self::FIELD_AUTHOR_PROJECTS => pht("Author's projects"), self::FIELD_PROJECTS => pht('Projects'), self::FIELD_PUSHER => pht('Pusher'), self::FIELD_PUSHER_PROJECTS => pht("Pusher's projects"), self::FIELD_DIFFERENTIAL_REVISION => pht('Differential revision'), self::FIELD_DIFFERENTIAL_REVIEWERS => pht('Differential reviewers'), self::FIELD_DIFFERENTIAL_CCS => pht('Differential CCs'), self::FIELD_DIFFERENTIAL_ACCEPTED => pht('Accepted Differential revision'), self::FIELD_IS_MERGE_COMMIT => pht('Commit is a merge'), self::FIELD_BRANCHES => pht('Commit\'s branches'), self::FIELD_AUTHOR_RAW => pht('Raw author name'), self::FIELD_COMMITTER_RAW => pht('Raw committer name'), self::FIELD_IS_NEW_OBJECT => pht('Is newly created?'), self::FIELD_TASK_PRIORITY => pht('Task priority'), self::FIELD_ARCANIST_PROJECT => pht('Arcanist Project'), self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'), ) + $this->getCustomFieldNameMap(); } /* -( Conditions )--------------------------------------------------------- */ public function getConditionNameMap() { return array( self::CONDITION_CONTAINS => pht('contains'), self::CONDITION_NOT_CONTAINS => pht('does not contain'), self::CONDITION_IS => pht('is'), self::CONDITION_IS_NOT => pht('is not'), self::CONDITION_IS_ANY => pht('is any of'), self::CONDITION_IS_TRUE => pht('is true'), self::CONDITION_IS_FALSE => pht('is false'), self::CONDITION_IS_NOT_ANY => pht('is not any of'), self::CONDITION_INCLUDE_ALL => pht('include all of'), self::CONDITION_INCLUDE_ANY => pht('include any of'), self::CONDITION_INCLUDE_NONE => pht('do not include'), self::CONDITION_IS_ME => pht('is myself'), self::CONDITION_IS_NOT_ME => pht('is not myself'), self::CONDITION_REGEXP => pht('matches regexp'), self::CONDITION_RULE => pht('matches:'), self::CONDITION_NOT_RULE => pht('does not match:'), self::CONDITION_EXISTS => pht('exists'), self::CONDITION_NOT_EXISTS => pht('does not exist'), self::CONDITION_UNCONDITIONALLY => '', // don't show anything! self::CONDITION_NEVER => '', // don't show anything! self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'), self::CONDITION_HAS_BIT => pht('has bit'), self::CONDITION_NOT_BIT => pht('lacks bit'), ); } public function getConditionsForField($field) { switch ($field) { case self::FIELD_TITLE: case self::FIELD_BODY: case self::FIELD_COMMITTER_RAW: case self::FIELD_AUTHOR_RAW: return array( self::CONDITION_CONTAINS, self::CONDITION_NOT_CONTAINS, self::CONDITION_IS, self::CONDITION_IS_NOT, self::CONDITION_REGEXP, ); case self::FIELD_REVIEWER: case self::FIELD_PUSHER: case self::FIELD_TASK_PRIORITY: case self::FIELD_ARCANIST_PROJECT: return array( self::CONDITION_IS_ANY, self::CONDITION_IS_NOT_ANY, ); case self::FIELD_REPOSITORY: case self::FIELD_ASSIGNEE: case self::FIELD_AUTHOR: case self::FIELD_COMMITTER: return array( self::CONDITION_IS_ANY, self::CONDITION_IS_NOT_ANY, self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_TAGS: case self::FIELD_REVIEWERS: case self::FIELD_CC: case self::FIELD_AUTHOR_PROJECTS: case self::FIELD_PROJECTS: case self::FIELD_AFFECTED_PACKAGE: case self::FIELD_AFFECTED_PACKAGE_OWNER: case self::FIELD_PUSHER_PROJECTS: case self::FIELD_REPOSITORY_PROJECTS: return array( self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_DIFF_FILE: case self::FIELD_BRANCHES: return array( self::CONDITION_CONTAINS, self::CONDITION_REGEXP, ); case self::FIELD_DIFF_CONTENT: case self::FIELD_DIFF_ADDED_CONTENT: case self::FIELD_DIFF_REMOVED_CONTENT: return array( self::CONDITION_CONTAINS, self::CONDITION_REGEXP, self::CONDITION_REGEXP_PAIR, ); case self::FIELD_RULE: return array( self::CONDITION_RULE, self::CONDITION_NOT_RULE, ); case self::FIELD_CONTENT_SOURCE: return array( self::CONDITION_IS, self::CONDITION_IS_NOT, ); case self::FIELD_ALWAYS: return array( self::CONDITION_UNCONDITIONALLY, ); case self::FIELD_DIFFERENTIAL_REVIEWERS: return array( self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_DIFFERENTIAL_CCS: return array( self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_DIFFERENTIAL_REVISION: case self::FIELD_DIFFERENTIAL_ACCEPTED: return array( self::CONDITION_EXISTS, self::CONDITION_NOT_EXISTS, ); case self::FIELD_IS_MERGE_COMMIT: case self::FIELD_DIFF_ENORMOUS: case self::FIELD_IS_NEW_OBJECT: case self::FIELD_PUSHER_IS_COMMITTER: return array( self::CONDITION_IS_TRUE, self::CONDITION_IS_FALSE, ); default: if ($this->isHeraldCustomKey($field)) { return $this->getCustomFieldConditions($field); } throw new Exception( "This adapter does not define conditions for field '{$field}'!"); } } public function doesConditionMatch( HeraldEngine $engine, HeraldRule $rule, HeraldCondition $condition, $field_value) { $condition_type = $condition->getFieldCondition(); $condition_value = $condition->getValue(); switch ($condition_type) { case self::CONDITION_CONTAINS: // "Contains" can take an array of strings, as in "Any changed // filename" for diffs. foreach ((array)$field_value as $value) { if (stripos($value, $condition_value) !== false) { return true; } } return false; case self::CONDITION_NOT_CONTAINS: return (stripos($field_value, $condition_value) === false); case self::CONDITION_IS: return ($field_value == $condition_value); case self::CONDITION_IS_NOT: return ($field_value != $condition_value); case self::CONDITION_IS_ME: return ($field_value == $rule->getAuthorPHID()); case self::CONDITION_IS_NOT_ME: return ($field_value != $rule->getAuthorPHID()); case self::CONDITION_IS_ANY: if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( 'Expected condition value to be an array.'); } $condition_value = array_fuse($condition_value); return isset($condition_value[$field_value]); case self::CONDITION_IS_NOT_ANY: if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( 'Expected condition value to be an array.'); } $condition_value = array_fuse($condition_value); return !isset($condition_value[$field_value]); case self::CONDITION_INCLUDE_ALL: if (!is_array($field_value)) { throw new HeraldInvalidConditionException( 'Object produced non-array value!'); } if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( 'Expected condition value to be an array.'); } $have = array_select_keys(array_fuse($field_value), $condition_value); return (count($have) == count($condition_value)); case self::CONDITION_INCLUDE_ANY: return (bool)array_select_keys( array_fuse($field_value), $condition_value); case self::CONDITION_INCLUDE_NONE: return !array_select_keys( array_fuse($field_value), $condition_value); case self::CONDITION_EXISTS: case self::CONDITION_IS_TRUE: return (bool)$field_value; case self::CONDITION_NOT_EXISTS: case self::CONDITION_IS_FALSE: return !$field_value; case self::CONDITION_UNCONDITIONALLY: return (bool)$field_value; case self::CONDITION_NEVER: return false; case self::CONDITION_REGEXP: foreach ((array)$field_value as $value) { // We add the 'S' flag because we use the regexp multiple times. // It shouldn't cause any troubles if the flag is already there // - /.*/S is evaluated same as /.*/SS. $result = @preg_match($condition_value.'S', $value); if ($result === false) { throw new HeraldInvalidConditionException( 'Regular expression is not valid!'); } if ($result) { return true; } } return false; case self::CONDITION_REGEXP_PAIR: // Match a JSON-encoded pair of regular expressions against a // dictionary. The first regexp must match the dictionary key, and the // second regexp must match the dictionary value. If any key/value pair // in the dictionary matches both regexps, the condition is satisfied. $regexp_pair = json_decode($condition_value, true); if (!is_array($regexp_pair)) { throw new HeraldInvalidConditionException( 'Regular expression pair is not valid JSON!'); } if (count($regexp_pair) != 2) { throw new HeraldInvalidConditionException( 'Regular expression pair is not a pair!'); } $key_regexp = array_shift($regexp_pair); $value_regexp = array_shift($regexp_pair); foreach ((array)$field_value as $key => $value) { $key_matches = @preg_match($key_regexp, $key); if ($key_matches === false) { throw new HeraldInvalidConditionException( 'First regular expression is invalid!'); } if ($key_matches) { $value_matches = @preg_match($value_regexp, $value); if ($value_matches === false) { throw new HeraldInvalidConditionException( 'Second regular expression is invalid!'); } if ($value_matches) { return true; } } } return false; case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: $rule = $engine->getRule($condition_value); if (!$rule) { throw new HeraldInvalidConditionException( 'Condition references a rule which does not exist!'); } $is_not = ($condition_type == self::CONDITION_NOT_RULE); $result = $engine->doesRuleMatch($rule, $this); if ($is_not) { $result = !$result; } return $result; case self::CONDITION_HAS_BIT: return (($condition_value & $field_value) === (int) $condition_value); case self::CONDITION_NOT_BIT: return (($condition_value & $field_value) !== (int) $condition_value); default: throw new HeraldInvalidConditionException( "Unknown condition '{$condition_type}'."); } } public function willSaveCondition(HeraldCondition $condition) { $condition_type = $condition->getFieldCondition(); $condition_value = $condition->getValue(); switch ($condition_type) { case self::CONDITION_REGEXP: $ok = @preg_match($condition_value, ''); if ($ok === false) { throw new HeraldInvalidConditionException( pht( 'The regular expression "%s" is not valid. Regular expressions '. 'must have enclosing characters (e.g. "@/path/to/file@", not '. '"/path/to/file") and be syntactically correct.', $condition_value)); } break; case self::CONDITION_REGEXP_PAIR: $json = json_decode($condition_value, true); if (!is_array($json)) { throw new HeraldInvalidConditionException( pht( 'The regular expression pair "%s" is not valid JSON. Enter a '. 'valid JSON array with two elements.', $condition_value)); } if (count($json) != 2) { throw new HeraldInvalidConditionException( pht( 'The regular expression pair "%s" must have exactly two '. 'elements.', $condition_value)); } $key_regexp = array_shift($json); $val_regexp = array_shift($json); $key_ok = @preg_match($key_regexp, ''); if ($key_ok === false) { throw new HeraldInvalidConditionException( pht( 'The first regexp in the regexp pair, "%s", is not a valid '. 'regexp.', $key_regexp)); } $val_ok = @preg_match($val_regexp, ''); if ($val_ok === false) { throw new HeraldInvalidConditionException( pht( 'The second regexp in the regexp pair, "%s", is not a valid '. 'regexp.', $val_regexp)); } break; case self::CONDITION_CONTAINS: case self::CONDITION_NOT_CONTAINS: case self::CONDITION_IS: case self::CONDITION_IS_NOT: case self::CONDITION_IS_ANY: case self::CONDITION_IS_NOT_ANY: case self::CONDITION_INCLUDE_ALL: case self::CONDITION_INCLUDE_ANY: case self::CONDITION_INCLUDE_NONE: case self::CONDITION_IS_ME: case self::CONDITION_IS_NOT_ME: case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: case self::CONDITION_NEVER: case self::CONDITION_HAS_BIT: case self::CONDITION_NOT_BIT: case self::CONDITION_IS_TRUE: case self::CONDITION_IS_FALSE: // No explicit validation for these types, although there probably // should be in some cases. break; default: throw new HeraldInvalidConditionException( pht( 'Unknown condition "%s"!', $condition_type)); } } /* -( Actions )------------------------------------------------------------ */ public function getCustomActionsForRuleType($rule_type) { $results = array(); foreach ($this->getCustomActions() as $custom_action) { if ($custom_action->appliesToRuleType($rule_type)) { $results[] = $custom_action; } } return $results; } public function getActions($rule_type) { $custom_actions = $this->getCustomActionsForRuleType($rule_type); return mpull($custom_actions, 'getActionKey'); } public function getActionNameMap($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: $standard = array( self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_ADD_CC => pht('Add emails to CC'), self::ACTION_REMOVE_CC => pht('Remove emails from CC'), self::ACTION_EMAIL => pht('Send an email to'), self::ACTION_AUDIT => pht('Trigger an Audit by'), self::ACTION_FLAG => pht('Mark with flag'), self::ACTION_ASSIGN_TASK => pht('Assign task to'), self::ACTION_ADD_PROJECTS => pht('Add projects'), self::ACTION_ADD_REVIEWERS => pht('Add reviewers'), self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add blocking reviewers'), self::ACTION_APPLY_BUILD_PLANS => pht('Run build plans'), self::ACTION_REQUIRE_SIGNATURE => pht('Require legal signatures'), self::ACTION_BLOCK => pht('Block change with message'), ); break; case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: $standard = array( self::ACTION_NOTHING => pht('Do nothing'), self::ACTION_ADD_CC => pht('Add me to CC'), self::ACTION_REMOVE_CC => pht('Remove me from CC'), self::ACTION_EMAIL => pht('Send me an email'), self::ACTION_AUDIT => pht('Trigger an Audit by me'), self::ACTION_FLAG => pht('Mark with flag'), self::ACTION_ASSIGN_TASK => pht('Assign task to me'), self::ACTION_ADD_PROJECTS => pht('Add projects'), self::ACTION_ADD_REVIEWERS => pht('Add me as a reviewer'), self::ACTION_ADD_BLOCKING_REVIEWERS => pht('Add me as a blocking reviewer'), ); break; default: throw new Exception("Unknown rule type '{$rule_type}'!"); } $custom_actions = $this->getCustomActionsForRuleType($rule_type); $standard += mpull($custom_actions, 'getActionName', 'getActionKey'); return $standard; } public function willSaveAction( HeraldRule $rule, HeraldAction $action) { $target = $action->getTarget(); if (is_array($target)) { $target = array_keys($target); } $author_phid = $rule->getAuthorPHID(); $rule_type = $rule->getRuleType(); if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) { switch ($action->getAction()) { case self::ACTION_EMAIL: case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_AUDIT: case self::ACTION_ASSIGN_TASK: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: // For personal rules, force these actions to target the rule owner. $target = array($author_phid); break; case self::ACTION_FLAG: // Make sure flag color is valid; set to blue if not. $color_map = PhabricatorFlagColor::getColorNameMap(); if (empty($color_map[$target])) { $target = PhabricatorFlagColor::COLOR_BLUE; } break; case self::ACTION_BLOCK: case self::ACTION_NOTHING: break; default: throw new HeraldInvalidActionException( pht( 'Unrecognized action type "%s"!', $action->getAction())); } } $action->setTarget($target); } /* -( Values )------------------------------------------------------------- */ public function getValueTypeForFieldAndCondition($field, $condition) { if ($this->isHeraldCustomKey($field)) { $value_type = $this->getCustomFieldValueTypeForFieldAndCondition( $field, $condition); if ($value_type !== null) { return $value_type; } } switch ($condition) { case self::CONDITION_CONTAINS: case self::CONDITION_NOT_CONTAINS: case self::CONDITION_REGEXP: case self::CONDITION_REGEXP_PAIR: return self::VALUE_TEXT; case self::CONDITION_IS: case self::CONDITION_IS_NOT: switch ($field) { case self::FIELD_CONTENT_SOURCE: return self::VALUE_CONTENT_SOURCE; default: return self::VALUE_TEXT; } break; case self::CONDITION_IS_ANY: case self::CONDITION_IS_NOT_ANY: switch ($field) { case self::FIELD_REPOSITORY: return self::VALUE_REPOSITORY; case self::FIELD_TASK_PRIORITY: return self::VALUE_TASK_PRIORITY; case self::FIELD_ARCANIST_PROJECT: return self::VALUE_ARCANIST_PROJECT; default: return self::VALUE_USER; } break; case self::CONDITION_INCLUDE_ALL: case self::CONDITION_INCLUDE_ANY: case self::CONDITION_INCLUDE_NONE: switch ($field) { case self::FIELD_REPOSITORY: return self::VALUE_REPOSITORY; case self::FIELD_CC: return self::VALUE_EMAIL; case self::FIELD_TAGS: return self::VALUE_TAG; case self::FIELD_AFFECTED_PACKAGE: return self::VALUE_OWNERS_PACKAGE; case self::FIELD_AUTHOR_PROJECTS: case self::FIELD_PUSHER_PROJECTS: case self::FIELD_PROJECTS: case self::FIELD_REPOSITORY_PROJECTS: return self::VALUE_PROJECT; case self::FIELD_REVIEWERS: return self::VALUE_USER_OR_PROJECT; default: return self::VALUE_USER; } break; case self::CONDITION_IS_ME: case self::CONDITION_IS_NOT_ME: case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: case self::CONDITION_NEVER: case self::CONDITION_IS_TRUE: case self::CONDITION_IS_FALSE: return self::VALUE_NONE; case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: return self::VALUE_RULE; default: throw new Exception("Unknown condition '{$condition}'."); } } public function getValueTypeForAction($action, $rule_type) { $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); if ($is_personal) { switch ($action) { case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_EMAIL: case self::ACTION_NOTHING: case self::ACTION_AUDIT: case self::ACTION_ASSIGN_TASK: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: return self::VALUE_NONE; case self::ACTION_FLAG: return self::VALUE_FLAG_COLOR; case self::ACTION_ADD_PROJECTS: return self::VALUE_PROJECT; } } else { switch ($action) { case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_EMAIL: return self::VALUE_EMAIL; case self::ACTION_NOTHING: return self::VALUE_NONE; case self::ACTION_ADD_PROJECTS: return self::VALUE_PROJECT; case self::ACTION_FLAG: return self::VALUE_FLAG_COLOR; case self::ACTION_ASSIGN_TASK: return self::VALUE_USER; case self::ACTION_AUDIT: case self::ACTION_ADD_REVIEWERS: case self::ACTION_ADD_BLOCKING_REVIEWERS: return self::VALUE_USER_OR_PROJECT; case self::ACTION_APPLY_BUILD_PLANS: return self::VALUE_BUILD_PLAN; case self::ACTION_REQUIRE_SIGNATURE: return self::VALUE_LEGAL_DOCUMENTS; case self::ACTION_BLOCK: return self::VALUE_TEXT; } } $custom_action = idx($this->getCustomActions(), $action); if ($custom_action !== null) { return $custom_action->getActionType(); } throw new Exception("Unknown or invalid action '".$action."'."); } /* -( Repetition )--------------------------------------------------------- */ public function getRepetitionOptions() { return array( HeraldRepetitionPolicyConfig::EVERY, ); } public static function applyFlagEffect(HeraldEffect $effect, $phid) { $color = $effect->getTarget(); // TODO: Silly that we need to load this again here. $rule = id(new HeraldRule())->load($effect->getRuleID()); $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $rule->getAuthorPHID()); $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid); if ($flag) { return new HeraldApplyTranscript( $effect, false, pht('Object already flagged.')); } $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($phid)) ->executeOne(); $flag = new PhabricatorFlag(); $flag->setOwnerPHID($user->getPHID()); $flag->setType($handle->getType()); $flag->setObjectPHID($handle->getPHID()); // TOOD: Should really be transcript PHID, but it doesn't exist yet. $flag->setReasonPHID($user->getPHID()); $flag->setColor($color); $flag->setNote( pht('Flagged by Herald Rule "%s".', $rule->getName())); $flag->save(); return new HeraldApplyTranscript( $effect, true, pht('Added flag.')); } public static function getAllAdapters() { static $adapters; if (!$adapters) { $adapters = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); $adapters = msort($adapters, 'getAdapterSortKey'); } return $adapters; } public static function getAdapterForContentType($content_type) { $adapters = self::getAllAdapters(); foreach ($adapters as $adapter) { if ($adapter->getAdapterContentType() == $content_type) { return $adapter; } } throw new Exception( pht( 'No adapter exists for Herald content type "%s".', $content_type)); } public static function getEnabledAdapterMap(PhabricatorUser $viewer) { $map = array(); $adapters = HeraldAdapter::getAllAdapters(); foreach ($adapters as $adapter) { if (!$adapter->isAvailableToUser($viewer)) { continue; } $type = $adapter->getAdapterContentType(); $name = $adapter->getAdapterContentName(); $map[$type] = $name; } return $map; } public function renderRuleAsText(HeraldRule $rule, array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); require_celerity_resource('herald-css'); $icon = id(new PHUIIconView()) ->setIconFont('fa-chevron-circle-right lightgreytext') ->addClass('herald-list-icon'); if ($rule->getMustMatchAll()) { $match_text = pht('When all of these conditions are met:'); } else { $match_text = pht('When any of these conditions are met:'); } $match_title = phutil_tag( 'p', array( - 'class' => 'herald-list-description' + 'class' => 'herald-list-description', ), $match_text); $match_list = array(); foreach ($rule->getConditions() as $condition) { $match_list[] = phutil_tag( 'div', array( - 'class' => 'herald-list-item' + 'class' => 'herald-list-item', ), array( $icon, - $this->renderConditionAsText($condition, $handles))); + $this->renderConditionAsText($condition, $handles), + )); } $integer_code_for_every = HeraldRepetitionPolicyConfig::toInt( HeraldRepetitionPolicyConfig::EVERY); if ($rule->getRepetitionPolicy() == $integer_code_for_every) { $action_text = pht('Take these actions every time this rule matches:'); } else { $action_text = pht('Take these actions the first time this rule matches:'); } $action_title = phutil_tag( 'p', array( - 'class' => 'herald-list-description' + 'class' => 'herald-list-description', ), $action_text); $action_list = array(); foreach ($rule->getActions() as $action) { $action_list[] = phutil_tag( 'div', array( - 'class' => 'herald-list-item' + 'class' => 'herald-list-item', ), array( $icon, - $this->renderActionAsText($action, $handles))); } + $this->renderActionAsText($action, $handles), + )); + } return array( $match_title, $match_list, $action_title, - $action_list); + $action_list, + ); } private function renderConditionAsText( HeraldCondition $condition, array $handles) { $field_type = $condition->getFieldName(); $default = $this->isHeraldCustomKey($field_type) ? pht('(Unknown Custom Field "%s")', $field_type) : pht('(Unknown Field "%s")', $field_type); $field_name = idx($this->getFieldNameMap(), $field_type, $default); $condition_type = $condition->getFieldCondition(); $condition_name = idx($this->getConditionNameMap(), $condition_type); $value = $this->renderConditionValueAsText($condition, $handles); return hsprintf(' %s %s %s', $field_name, $condition_name, $value); } private function renderActionAsText( HeraldAction $action, array $handles) { $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL; $action_type = $action->getAction(); $action_name = idx($this->getActionNameMap($rule_global), $action_type); $target = $this->renderActionTargetAsText($action, $handles); return hsprintf(' %s %s', $action_name, $target); } private function renderConditionValueAsText( HeraldCondition $condition, array $handles) { $value = $condition->getValue(); if (!is_array($value)) { $value = array($value); } switch ($condition->getFieldName()) { case self::FIELD_TASK_PRIORITY: $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); foreach ($value as $index => $val) { $name = idx($priority_map, $val); if ($name) { $value[$index] = $name; } } break; case HeraldPreCommitRefAdapter::FIELD_REF_CHANGE: $change_map = PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions(); foreach ($value as $index => $val) { $name = idx($change_map, $val); if ($name) { $value[$index] = $name; } } break; default: foreach ($value as $index => $val) { $handle = idx($handles, $val); if ($handle) { $value[$index] = $handle->renderLink(); } } break; } $value = phutil_implode_html(', ', $value); return $value; } private function renderActionTargetAsText( HeraldAction $action, array $handles) { $target = $action->getTarget(); if (!is_array($target)) { $target = array($target); } foreach ($target as $index => $val) { switch ($action->getAction()) { case self::ACTION_FLAG: $target[$index] = PhabricatorFlagColor::getColorName($val); break; default: $handle = idx($handles, $val); if ($handle) { $target[$index] = $handle->renderLink(); } break; } } $target = phutil_implode_html(', ', $target); return $target; } /** * Given a @{class:HeraldRule}, this function extracts all the phids that * we'll want to load as handles later. * * This function performs a somewhat hacky approach to figuring out what * is and is not a phid - try to get the phid type and if the type is * *not* unknown assume its a valid phid. * * Don't try this at home. Use more strongly typed data at home. * * Think of the children. */ public static function getHandlePHIDs(HeraldRule $rule) { $phids = array($rule->getAuthorPHID()); foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); if (!is_array($value)) { $value = array($value); } foreach ($value as $val) { if (phid_get_type($val) != PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { $phids[] = $val; } } } foreach ($rule->getActions() as $action) { $target = $action->getTarget(); if (!is_array($target)) { $target = array($target); } foreach ($target as $val) { if (phid_get_type($val) != PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN) { $phids[] = $val; } } } if ($rule->isObjectRule()) { $phids[] = $rule->getTriggerObjectPHID(); } return $phids; } /* -( Custom Field Integration )------------------------------------------- */ /** * Return an object which custom fields can be generated from while editing * rules. Adapters must return an object from this method to enable custom * field rules. * * Normally, you'll return an empty version of the adapted object, assuming * it implements @{interface:PhabricatorCustomFieldInterface}: * * return new ApplicationObject(); * * This is normally the only adapter method you need to override to enable * Herald rules to run against custom fields. * * @return null|PhabricatorCustomFieldInterface Template object. * @task customfield */ protected function getCustomFieldTemplateObject() { return null; } /** * Returns the prefix used to namespace Herald fields which are based on * custom fields. * * @return string Key prefix. * @task customfield */ private function getCustomKeyPrefix() { return 'herald.custom/'; } /** * Determine if a field key is based on a custom field or a regular internal * field. * * @param string Field key. * @return bool True if the field key is based on a custom field. * @task customfield */ private function isHeraldCustomKey($key) { $prefix = $this->getCustomKeyPrefix(); return (strncmp($key, $prefix, strlen($prefix)) == 0); } /** * Convert a custom field key into a Herald field key. * * @param string Custom field key. * @return string Herald field key. * @task customfield */ private function getHeraldKeyFromCustomKey($key) { return $this->getCustomKeyPrefix().$key; } /** * Get custom fields for this adapter, if appliable. This will either return * a field list or `null` if the adapted object does not implement custom * fields or the adapter does not support them. * * @return PhabricatorCustomFieldList|null List of fields, or `null`. * @task customfield */ private function getCustomFields() { if ($this->customFields === false) { $this->customFields = null; $template_object = $this->getCustomFieldTemplateObject(); if ($template_object) { $object = $this->getObject(); if (!$object) { $object = $template_object; } $fields = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_HERALD); $fields->setViewer(PhabricatorUser::getOmnipotentUser()); $fields->readFieldsFromStorage($object); $this->customFields = $fields; } } return $this->customFields; } /** * Get a custom field by Herald field key, or `null` if it does not exist * or custom fields are not supported. * * @param string Herald field key. * @return PhabricatorCustomField|null Matching field, if it exists. * @task customfield */ private function getCustomField($herald_field_key) { $fields = $this->getCustomFields(); if (!$fields) { return null; } foreach ($fields->getFields() as $custom_field) { $key = $custom_field->getFieldKey(); if ($this->getHeraldKeyFromCustomKey($key) == $herald_field_key) { return $custom_field; } } return null; } /** * Get the field map for custom fields. * * @return map<string, string> Map of Herald field keys to field names. * @task customfield */ private function getCustomFieldNameMap() { $fields = $this->getCustomFields(); if (!$fields) { return array(); } $map = array(); foreach ($fields->getFields() as $field) { $key = $field->getFieldKey(); $name = $field->getHeraldFieldName(); $map[$this->getHeraldKeyFromCustomKey($key)] = $name; } return $map; } /** * Get the value for a custom field. * * @param string Herald field key. * @return wild Custom field value. * @task customfield */ private function getCustomFieldValue($field_key) { $field = $this->getCustomField($field_key); if (!$field) { return null; } return $field->getHeraldFieldValue(); } /** * Get the Herald conditions for a custom field. * * @param string Herald field key. * @return list<const> List of Herald conditions. * @task customfield */ private function getCustomFieldConditions($field_key) { $field = $this->getCustomField($field_key); if (!$field) { return array( self::CONDITION_NEVER, ); } return $field->getHeraldFieldConditions(); } /** * Get the Herald value type for a custom field and condition. * * @param string Herald field key. * @param const Herald condition constant. * @return const|null Herald value type constant, or null to use the default. * @task customfield */ private function getCustomFieldValueTypeForFieldAndCondition( $field_key, $condition) { $field = $this->getCustomField($field_key); if (!$field) { return self::VALUE_NONE; } return $field->getHeraldFieldValueType($condition); } } diff --git a/src/applications/herald/adapter/HeraldCommitAdapter.php b/src/applications/herald/adapter/HeraldCommitAdapter.php index 6f87bd937f..e52ad18fa6 100644 --- a/src/applications/herald/adapter/HeraldCommitAdapter.php +++ b/src/applications/herald/adapter/HeraldCommitAdapter.php @@ -1,562 +1,562 @@ <?php final class HeraldCommitAdapter extends HeraldAdapter { const FIELD_NEED_AUDIT_FOR_PACKAGE = 'need-audit-for-package'; const FIELD_REPOSITORY_AUTOCLOSE_BRANCH = 'repository-autoclose-branch'; protected $diff; protected $revision; protected $repository; protected $commit; protected $commitData; private $commitDiff; protected $emailPHIDs = array(); protected $addCCPHIDs = array(); protected $auditMap = array(); protected $buildPlans = array(); protected $affectedPaths; protected $affectedRevision; protected $affectedPackages; protected $auditNeededPackages; public function getAdapterApplicationClass() { return 'PhabricatorDiffusionApplication'; } public function getObject() { return $this->commit; } public function getAdapterContentType() { return 'commit'; } public function getAdapterContentName() { return pht('Commits'); } public function getAdapterContentDescription() { return pht( "React to new commits appearing in tracked repositories.\n". "Commit rules can send email, flag commits, trigger audits, ". "and run build plans."); } public function supportsRuleType($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return true; default: return false; } } public function canTriggerOnObject($object) { if ($object instanceof PhabricatorRepository) { return true; } if ($object instanceof PhabricatorProject) { return true; } return false; } public function getTriggerObjectPHIDs() { return array_merge( array( $this->repository->getPHID(), $this->getPHID(), ), $this->repository->getProjectPHIDs()); } public function explainValidTriggerObjects() { return pht('This rule can trigger for **repositories** and **projects**.'); } public function getFieldNameMap() { return array( self::FIELD_NEED_AUDIT_FOR_PACKAGE => pht('Affected packages that need audit'), self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH => pht('Commit is on closing branch'), ) + parent::getFieldNameMap(); } public function getFields() { return array_merge( array( self::FIELD_BODY, self::FIELD_AUTHOR, self::FIELD_COMMITTER, self::FIELD_REVIEWER, self::FIELD_REPOSITORY, self::FIELD_REPOSITORY_PROJECTS, self::FIELD_DIFF_FILE, self::FIELD_DIFF_CONTENT, self::FIELD_DIFF_ADDED_CONTENT, self::FIELD_DIFF_REMOVED_CONTENT, self::FIELD_DIFF_ENORMOUS, self::FIELD_AFFECTED_PACKAGE, self::FIELD_AFFECTED_PACKAGE_OWNER, self::FIELD_NEED_AUDIT_FOR_PACKAGE, self::FIELD_DIFFERENTIAL_REVISION, self::FIELD_DIFFERENTIAL_ACCEPTED, self::FIELD_DIFFERENTIAL_REVIEWERS, self::FIELD_DIFFERENTIAL_CCS, self::FIELD_BRANCHES, self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH, ), parent::getFields()); } public function getConditionsForField($field) { switch ($field) { case self::FIELD_NEED_AUDIT_FOR_PACKAGE: return array( self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH: return array( self::CONDITION_UNCONDITIONALLY, ); } return parent::getConditionsForField($field); } public function getActions($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: return array_merge( array( self::ACTION_ADD_CC, self::ACTION_EMAIL, self::ACTION_AUDIT, self::ACTION_APPLY_BUILD_PLANS, - self::ACTION_NOTHING + self::ACTION_NOTHING, ), parent::getActions($rule_type)); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array_merge( array( self::ACTION_ADD_CC, self::ACTION_EMAIL, self::ACTION_FLAG, self::ACTION_AUDIT, self::ACTION_NOTHING, ), parent::getActions($rule_type)); } } public function getValueTypeForFieldAndCondition($field, $condition) { switch ($field) { case self::FIELD_DIFFERENTIAL_CCS: return self::VALUE_EMAIL; case self::FIELD_NEED_AUDIT_FOR_PACKAGE: return self::VALUE_OWNERS_PACKAGE; } return parent::getValueTypeForFieldAndCondition($field, $condition); } public static function newLegacyAdapter( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $commit_data) { $object = new HeraldCommitAdapter(); $commit->attachRepository($repository); $object->repository = $repository; $object->commit = $commit; $object->commitData = $commit_data; return $object; } public function setCommit(PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withIDs(array($commit->getRepositoryID())) ->needProjectPHIDs(true) ->executeOne(); if (!$repository) { throw new Exception(pht('Unable to load repository!')); } $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); if (!$data) { throw new Exception(pht('Unable to load commit data!')); } $this->commit = clone $commit; $this->commit->attachRepository($repository); $this->commit->attachCommitData($data); $this->repository = $repository; $this->commitData = $data; return $this; } public function getPHID() { return $this->commit->getPHID(); } public function getEmailPHIDs() { return array_keys($this->emailPHIDs); } public function getAddCCMap() { return $this->addCCPHIDs; } public function getAuditMap() { return $this->auditMap; } public function getBuildPlans() { return $this->buildPlans; } public function getHeraldName() { return 'r'. $this->repository->getCallsign(). $this->commit->getCommitIdentifier(); } public function loadAffectedPaths() { if ($this->affectedPaths === null) { $result = PhabricatorOwnerPathQuery::loadAffectedPaths( $this->repository, $this->commit, PhabricatorUser::getOmnipotentUser()); $this->affectedPaths = $result; } return $this->affectedPaths; } public function loadAffectedPackages() { if ($this->affectedPackages === null) { $packages = PhabricatorOwnersPackage::loadAffectedPackages( $this->repository, $this->loadAffectedPaths()); $this->affectedPackages = $packages; } return $this->affectedPackages; } public function loadAuditNeededPackage() { if ($this->auditNeededPackages === null) { $status_arr = array( PhabricatorAuditStatusConstants::AUDIT_REQUIRED, PhabricatorAuditStatusConstants::CONCERNED, ); $requests = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere( 'commitPHID = %s AND auditStatus IN (%Ls)', $this->commit->getPHID(), $status_arr); $packages = mpull($requests, 'getAuditorPHID'); $this->auditNeededPackages = $packages; } return $this->auditNeededPackages; } public function loadDifferentialRevision() { if ($this->affectedRevision === null) { $this->affectedRevision = false; $data = $this->commitData; $revision_id = $data->getCommitDetail('differential.revisionID'); if ($revision_id) { // NOTE: The Herald rule owner might not actually have access to // the revision, and can control which revision a commit is // associated with by putting text in the commit message. However, // the rules they can write against revisions don't actually expose // anything interesting, so it seems reasonable to load unconditionally // here. $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->needRelationships(true) ->needReviewerStatus(true) ->executeOne(); if ($revision) { $this->affectedRevision = $revision; } } } return $this->affectedRevision; } public static function getEnormousByteLimit() { return 1024 * 1024 * 1024; // 1GB } public static function getEnormousTimeLimit() { return 60 * 15; // 15 Minutes } private function loadCommitDiff() { $drequest = DiffusionRequest::newFromDictionary( array( 'user' => PhabricatorUser::getOmnipotentUser(), 'repository' => $this->repository, 'commit' => $this->commit->getCommitIdentifier(), )); $byte_limit = self::getEnormousByteLimit(); $raw = DiffusionQuery::callConduitWithDiffusionRequest( PhabricatorUser::getOmnipotentUser(), $drequest, 'diffusion.rawdiffquery', array( 'commit' => $this->commit->getCommitIdentifier(), 'timeout' => self::getEnormousTimeLimit(), 'byteLimit' => $byte_limit, 'linesOfContext' => 0, )); if (strlen($raw) >= $byte_limit) { throw new Exception( pht( 'The raw text of this change is enormous (larger than %d bytes). '. 'Herald can not process it.', $byte_limit)); } $parser = new ArcanistDiffParser(); $changes = $parser->parseDiff($raw); $diff = DifferentialDiff::newFromRawChanges($changes); return $diff; } private function getDiffContent($type) { if ($this->commitDiff === null) { try { $this->commitDiff = $this->loadCommitDiff(); } catch (Exception $ex) { $this->commitDiff = $ex; phlog($ex); } } if ($this->commitDiff instanceof Exception) { $ex = $this->commitDiff; $ex_class = get_class($ex); $ex_message = pht('Failed to load changes: %s', $ex->getMessage()); return array( '<'.$ex_class.'>' => $ex_message, ); } $changes = $this->commitDiff->getChangesets(); $result = array(); foreach ($changes as $change) { $lines = array(); foreach ($change->getHunks() as $hunk) { switch ($type) { case '-': $lines[] = $hunk->makeOldFile(); break; case '+': $lines[] = $hunk->makeNewFile(); break; case '*': $lines[] = $hunk->makeChanges(); break; default: throw new Exception("Unknown content selection '{$type}'!"); } } $result[$change->getFilename()] = implode("\n", $lines); } return $result; } public function getHeraldField($field) { $data = $this->commitData; switch ($field) { case self::FIELD_BODY: return $data->getCommitMessage(); case self::FIELD_AUTHOR: return $data->getCommitDetail('authorPHID'); case self::FIELD_COMMITTER: return $data->getCommitDetail('committerPHID'); case self::FIELD_REVIEWER: return $data->getCommitDetail('reviewerPHID'); case self::FIELD_DIFF_FILE: return $this->loadAffectedPaths(); case self::FIELD_REPOSITORY: return $this->repository->getPHID(); case self::FIELD_REPOSITORY_PROJECTS: return $this->repository->getProjectPHIDs(); case self::FIELD_DIFF_CONTENT: return $this->getDiffContent('*'); case self::FIELD_DIFF_ADDED_CONTENT: return $this->getDiffContent('+'); case self::FIELD_DIFF_REMOVED_CONTENT: return $this->getDiffContent('-'); case self::FIELD_DIFF_ENORMOUS: $this->getDiffContent('*'); return ($this->commitDiff instanceof Exception); case self::FIELD_AFFECTED_PACKAGE: $packages = $this->loadAffectedPackages(); return mpull($packages, 'getPHID'); case self::FIELD_AFFECTED_PACKAGE_OWNER: $packages = $this->loadAffectedPackages(); $owners = PhabricatorOwnersOwner::loadAllForPackages($packages); return mpull($owners, 'getUserPHID'); case self::FIELD_NEED_AUDIT_FOR_PACKAGE: return $this->loadAuditNeededPackage(); case self::FIELD_DIFFERENTIAL_REVISION: $revision = $this->loadDifferentialRevision(); if (!$revision) { return null; } return $revision->getID(); case self::FIELD_DIFFERENTIAL_ACCEPTED: $revision = $this->loadDifferentialRevision(); if (!$revision) { return null; } $status = $data->getCommitDetail( 'precommitRevisionStatus', $revision->getStatus()); switch ($status) { case ArcanistDifferentialRevisionStatus::ACCEPTED: case ArcanistDifferentialRevisionStatus::CLOSED: return $revision->getPHID(); } return null; case self::FIELD_DIFFERENTIAL_REVIEWERS: $revision = $this->loadDifferentialRevision(); if (!$revision) { return array(); } return $revision->getReviewers(); case self::FIELD_DIFFERENTIAL_CCS: $revision = $this->loadDifferentialRevision(); if (!$revision) { return array(); } return $revision->getCCPHIDs(); case self::FIELD_BRANCHES: $params = array( 'callsign' => $this->repository->getCallsign(), 'contains' => $this->commit->getCommitIdentifier(), ); $result = id(new ConduitCall('diffusion.branchquery', $params)) ->setUser(PhabricatorUser::getOmnipotentUser()) ->execute(); $refs = DiffusionRepositoryRef::loadAllFromDictionaries($result); return mpull($refs, 'getShortName'); case self::FIELD_REPOSITORY_AUTOCLOSE_BRANCH: return $this->repository->shouldAutocloseCommit($this->commit); } return parent::getHeraldField($field); } public function applyHeraldEffects(array $effects) { assert_instances_of($effects, 'HeraldEffect'); $result = array(); foreach ($effects as $effect) { $action = $effect->getAction(); switch ($action) { case self::ACTION_NOTHING: $result[] = new HeraldApplyTranscript( $effect, true, pht('Great success at doing nothing.')); break; case self::ACTION_EMAIL: foreach ($effect->getTarget() as $phid) { $this->emailPHIDs[$phid] = true; } $result[] = new HeraldApplyTranscript( $effect, true, pht('Added address to email targets.')); break; case self::ACTION_ADD_CC: foreach ($effect->getTarget() as $phid) { if (empty($this->addCCPHIDs[$phid])) { $this->addCCPHIDs[$phid] = array(); } $this->addCCPHIDs[$phid][] = $effect->getRuleID(); } $result[] = new HeraldApplyTranscript( $effect, true, pht('Added address to CC.')); break; case self::ACTION_AUDIT: foreach ($effect->getTarget() as $phid) { if (empty($this->auditMap[$phid])) { $this->auditMap[$phid] = array(); } $this->auditMap[$phid][] = $effect->getRuleID(); } $result[] = new HeraldApplyTranscript( $effect, true, pht('Triggered an audit.')); break; case self::ACTION_APPLY_BUILD_PLANS: foreach ($effect->getTarget() as $phid) { $this->buildPlans[] = $phid; } $result[] = new HeraldApplyTranscript( $effect, true, pht('Applied build plans.')); break; case self::ACTION_FLAG: $result[] = parent::applyFlagEffect( $effect, $this->commit->getPHID()); break; default: $custom_result = parent::handleCustomHeraldEffect($effect); if ($custom_result === null) { throw new Exception(pht( "No rules to handle action '%s'.", $action)); } $result[] = $custom_result; break; } } return $result; } } diff --git a/src/applications/herald/application/PhabricatorHeraldApplication.php b/src/applications/herald/application/PhabricatorHeraldApplication.php index 931a42cc71..23432a50ca 100644 --- a/src/applications/herald/application/PhabricatorHeraldApplication.php +++ b/src/applications/herald/application/PhabricatorHeraldApplication.php @@ -1,73 +1,73 @@ <?php final class PhabricatorHeraldApplication extends PhabricatorApplication { public function getBaseURI() { return '/herald/'; } public function getIconName() { return 'herald'; } public function getName() { return pht('Herald'); } public function getShortDescription() { return pht('Create Notification Rules'); } public function getTitleGlyph() { return "\xE2\x98\xBF"; } public function getHelpURI() { return PhabricatorEnv::getDoclink('Herald User Guide'); } public function getFlavorText() { return pht('Watch for danger!'); } public function getApplicationGroup() { return self::GROUP_UTILITIES; } public function getRemarkupRules() { return array( new HeraldRemarkupRule(), ); } public function getRoutes() { return array( '/herald/' => array( '(?:query/(?P<queryKey>[^/]+)/)?' => 'HeraldRuleListController', 'new/' => 'HeraldNewController', 'rule/(?P<id>[1-9]\d*)/' => 'HeraldRuleViewController', 'edit/(?:(?P<id>[1-9]\d*)/)?' => 'HeraldRuleController', 'disable/(?P<id>[1-9]\d*)/(?P<action>\w+)/' => 'HeraldDisableController', 'history/(?:(?P<id>[1-9]\d*)/)?' => 'HeraldRuleEditHistoryController', 'test/' => 'HeraldTestConsoleController', 'transcript/' => array( '' => 'HeraldTranscriptListController', '(?:query/(?P<queryKey>[^/]+)/)?' => 'HeraldTranscriptListController', '(?P<id>[1-9]\d*)/(?:(?P<filter>\w+)/)?' => 'HeraldTranscriptController', - ) - ) + ), + ), ); } protected function getCustomCapabilities() { return array( HeraldManageGlobalRulesCapability::CAPABILITY => array( 'caption' => pht('Global rules can bypass access controls.'), 'default' => PhabricatorPolicies::POLICY_ADMIN, ), ); } } diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index e103daeffe..dd33091dee 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -1,661 +1,663 @@ <?php final class HeraldRuleController extends HeraldController { private $id; private $filter; public function willProcessRequest(array $data) { $this->id = (int)idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $content_type_map = HeraldAdapter::getEnabledAdapterMap($user); $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); if ($this->id) { $id = $this->id; $rule = id(new HeraldRuleQuery()) ->setViewer($user) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$rule) { return new Aphront404Response(); } $cancel_uri = $this->getApplicationURI("rule/{$id}/"); } else { $rule = new HeraldRule(); $rule->setAuthorPHID($user->getPHID()); $rule->setMustMatchAll(1); $content_type = $request->getStr('content_type'); $rule->setContentType($content_type); $rule_type = $request->getStr('rule_type'); if (!isset($rule_type_map[$rule_type])) { $rule_type = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL; } $rule->setRuleType($rule_type); $adapter = HeraldAdapter::getAdapterForContentType( $rule->getContentType()); if (!$adapter->supportsRuleType($rule->getRuleType())) { throw new Exception( pht( "This rule's content type does not support the selected rule ". "type.")); } if ($rule->isObjectRule()) { $rule->setTriggerObjectPHID($request->getStr('targetPHID')); $object = id(new PhabricatorObjectQuery()) ->setViewer($user) ->withPHIDs(array($rule->getTriggerObjectPHID())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$object) { throw new Exception( pht('No valid object provided for object rule!')); } if (!$adapter->canTriggerOnObject($object)) { throw new Exception( pht('Object is of wrong type for adapter!')); } } $cancel_uri = $this->getApplicationURI(); } if ($rule->isGlobalRule()) { $this->requireApplicationCapability( HeraldManageGlobalRulesCapability::CAPABILITY); } $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); $local_version = id(new HeraldRule())->getConfigVersion(); if ($rule->getConfigVersion() > $local_version) { throw new Exception( pht( 'This rule was created with a newer version of Herald. You can not '. 'view or edit it in this older version. Upgrade your Phabricator '. 'deployment.')); } // Upgrade rule version to our version, since we might add newly-defined // conditions, etc. $rule->setConfigVersion($local_version); $rule_conditions = $rule->loadConditions(); $rule_actions = $rule->loadActions(); $rule->attachConditions($rule_conditions); $rule->attachActions($rule_actions); $e_name = true; $errors = array(); if ($request->isFormPost() && $request->getStr('save')) { list($e_name, $errors) = $this->saveRule($adapter, $rule, $request); if (!$errors) { $id = $rule->getID(); $uri = $this->getApplicationURI("rule/{$id}/"); return id(new AphrontRedirectResponse())->setURI($uri); } } $must_match_selector = $this->renderMustMatchSelector($rule); $repetition_selector = $this->renderRepetitionSelector($rule, $adapter); $handles = $this->loadHandlesForRule($rule); require_celerity_resource('herald-css'); $content_type_name = $content_type_map[$rule->getContentType()]; $rule_type_name = $rule_type_map[$rule->getRuleType()]; $form = id(new AphrontFormView()) ->setUser($user) ->setID('herald-rule-edit-form') ->addHiddenInput('content_type', $rule->getContentType()) ->addHiddenInput('rule_type', $rule->getRuleType()) ->addHiddenInput('save', 1) ->appendChild( // Build this explicitly (instead of using addHiddenInput()) // so we can add a sigil to it. javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'rule', 'sigil' => 'rule', ))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Rule Name')) ->setName('name') ->setError($e_name) ->setValue($rule->getName())); $trigger_object_control = false; if ($rule->isObjectRule()) { $trigger_object_control = id(new AphrontFormStaticControl()) ->setValue( pht( 'This rule triggers for %s.', $handles[$rule->getTriggerObjectPHID()]->renderLink())); } $form ->appendChild( id(new AphrontFormMarkupControl()) ->setValue(pht( 'This %s rule triggers for %s.', phutil_tag('strong', array(), $rule_type_name), phutil_tag('strong', array(), $content_type_name)))) ->appendChild($trigger_object_control) ->appendChild( id(new AphrontFormInsetView()) ->setTitle(pht('Conditions')) ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'create-condition', - 'mustcapture' => true + 'mustcapture' => true, ), pht('New Condition'))) ->setDescription( pht('When %s these conditions are met:', $must_match_selector)) ->setContent(javelin_tag( 'table', array( 'sigil' => 'rule-conditions', - 'class' => 'herald-condition-table' + 'class' => 'herald-condition-table', ), ''))) ->appendChild( id(new AphrontFormInsetView()) ->setTitle(pht('Action')) ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'create-action', 'mustcapture' => true, ), pht('New Action'))) ->setDescription(pht( 'Take these actions %s this rule matches:', $repetition_selector)) ->setContent(javelin_tag( 'table', array( 'sigil' => 'rule-actions', 'class' => 'herald-action-table', ), ''))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Rule')) ->addCancelButton($cancel_uri)); $this->setupEditorBehavior($rule, $handles, $adapter); $title = $rule->getID() ? pht('Edit Herald Rule') : pht('Create Herald Rule'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Edit Rule'), )); } private function saveRule(HeraldAdapter $adapter, $rule, $request) { $rule->setName($request->getStr('name')); $match_all = ($request->getStr('must_match') == 'all'); $rule->setMustMatchAll((int)$match_all); $repetition_policy_param = $request->getStr('repetition_policy'); $rule->setRepetitionPolicy( HeraldRepetitionPolicyConfig::toInt($repetition_policy_param)); $e_name = true; $errors = array(); if (!strlen($rule->getName())) { $e_name = pht('Required'); $errors[] = pht('Rule must have a name.'); } $data = json_decode($request->getStr('rule'), true); if (!is_array($data) || !$data['conditions'] || !$data['actions']) { throw new Exception('Failed to decode rule data.'); } $conditions = array(); foreach ($data['conditions'] as $condition) { if ($condition === null) { // We manage this as a sparse array on the client, so may receive // NULL if conditions have been removed. continue; } $obj = new HeraldCondition(); $obj->setFieldName($condition[0]); $obj->setFieldCondition($condition[1]); if (is_array($condition[2])) { $obj->setValue(array_keys($condition[2])); } else { $obj->setValue($condition[2]); } try { $adapter->willSaveCondition($obj); } catch (HeraldInvalidConditionException $ex) { $errors[] = $ex->getMessage(); } $conditions[] = $obj; } $actions = array(); foreach ($data['actions'] as $action) { if ($action === null) { // Sparse on the client; removals can give us NULLs. continue; } if (!isset($action[1])) { // Legitimate for any action which doesn't need a target, like // "Do nothing". $action[1] = null; } $obj = new HeraldAction(); $obj->setAction($action[0]); $obj->setTarget($action[1]); try { $adapter->willSaveAction($rule, $obj); } catch (HeraldInvalidActionException $ex) { $errors[] = $ex; } $actions[] = $obj; } $rule->attachConditions($conditions); $rule->attachActions($actions); if (!$errors) { $edit_action = $rule->getID() ? 'edit' : 'create'; $rule->openTransaction(); $rule->save(); $rule->saveConditions($conditions); $rule->saveActions($actions); $rule->logEdit($request->getUser()->getPHID(), $edit_action); $rule->saveTransaction(); } return array($e_name, $errors); } private function setupEditorBehavior( HeraldRule $rule, array $handles, HeraldAdapter $adapter) { $serial_conditions = array( array('default', 'default', ''), ); if ($rule->getConditions()) { $serial_conditions = array(); foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); switch ($condition->getFieldName()) { case HeraldAdapter::FIELD_TASK_PRIORITY: $value_map = array(); $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); foreach ($value as $priority) { $value_map[$priority] = idx($priority_map, $priority); } $value = $value_map; break; default: if (is_array($value)) { $value_map = array(); foreach ($value as $k => $fbid) { $value_map[$fbid] = $handles[$fbid]->getName(); } $value = $value_map; } break; } $serial_conditions[] = array( $condition->getFieldName(), $condition->getFieldCondition(), $value, ); } } $serial_actions = array( array('default', ''), ); if ($rule->getActions()) { $serial_actions = array(); foreach ($rule->getActions() as $action) { switch ($action->getAction()) { case HeraldAdapter::ACTION_FLAG: case HeraldAdapter::ACTION_BLOCK: $current_value = $action->getTarget(); break; default: if (is_array($action->getTarget())) { $target_map = array(); foreach ((array)$action->getTarget() as $fbid) { $target_map[$fbid] = $handles[$fbid]->getName(); } $current_value = $target_map; } else { $current_value = $action->getTarget(); } break; } $serial_actions[] = array( $action->getAction(), $current_value, ); } } $all_rules = $this->loadRulesThisRuleMayDependUpon($rule); $all_rules = mpull($all_rules, 'getName', 'getPHID'); asort($all_rules); $all_fields = $adapter->getFieldNameMap(); $all_conditions = $adapter->getConditionNameMap(); $all_actions = $adapter->getActionNameMap($rule->getRuleType()); $fields = $adapter->getFields(); $field_map = array_select_keys($all_fields, $fields); // Populate any fields which exist in the rule but which we don't know the // names of, so that saving a rule without touching anything doesn't change // it. foreach ($rule->getConditions() as $condition) { if (empty($field_map[$condition->getFieldName()])) { $field_map[$condition->getFieldName()] = pht('<Unknown Field>'); } } $actions = $adapter->getActions($rule->getRuleType()); $action_map = array_select_keys($all_actions, $actions); $config_info = array(); $config_info['fields'] = $field_map; $config_info['conditions'] = $all_conditions; $config_info['actions'] = $action_map; foreach ($config_info['fields'] as $field => $name) { $field_conditions = $adapter->getConditionsForField($field); $config_info['conditionMap'][$field] = $field_conditions; } foreach ($config_info['fields'] as $field => $fname) { foreach ($config_info['conditionMap'][$field] as $condition) { $value_type = $adapter->getValueTypeForFieldAndCondition( $field, $condition); $config_info['values'][$field][$condition] = $value_type; } } $config_info['rule_type'] = $rule->getRuleType(); foreach ($config_info['actions'] as $action => $name) { $config_info['targets'][$action] = $adapter->getValueTypeForAction( $action, $rule->getRuleType()); } $changeflag_options = PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions(); Javelin::initBehavior( 'herald-rule-editor', array( 'root' => 'herald-rule-edit-form', 'conditions' => (object)$serial_conditions, 'actions' => (object)$serial_actions, 'select' => array( HeraldAdapter::VALUE_CONTENT_SOURCE => array( 'options' => PhabricatorContentSource::getSourceNameMap(), 'default' => PhabricatorContentSource::SOURCE_WEB, ), HeraldAdapter::VALUE_FLAG_COLOR => array( 'options' => PhabricatorFlagColor::getColorNameMap(), 'default' => PhabricatorFlagColor::COLOR_BLUE, ), HeraldPreCommitRefAdapter::VALUE_REF_TYPE => array( 'options' => array( PhabricatorRepositoryPushLog::REFTYPE_BRANCH => pht('branch (git/hg)'), PhabricatorRepositoryPushLog::REFTYPE_TAG => pht('tag (git)'), PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK => pht('bookmark (hg)'), ), 'default' => PhabricatorRepositoryPushLog::REFTYPE_BRANCH, ), HeraldPreCommitRefAdapter::VALUE_REF_CHANGE => array( 'options' => $changeflag_options, 'default' => PhabricatorRepositoryPushLog::CHANGEFLAG_ADD, ), ), 'template' => $this->buildTokenizerTemplates($handles) + array( 'rules' => $all_rules, ), - 'author' => array($rule->getAuthorPHID() => - $handles[$rule->getAuthorPHID()]->getName()), + 'author' => array( + $rule->getAuthorPHID() => + $handles[$rule->getAuthorPHID()]->getName(), + ), 'info' => $config_info, )); } private function loadHandlesForRule($rule) { $phids = array(); foreach ($rule->getActions() as $action) { if (!is_array($action->getTarget())) { continue; } foreach ($action->getTarget() as $target) { $target = (array)$target; foreach ($target as $phid) { $phids[] = $phid; } } } foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); if (is_array($value)) { foreach ($value as $phid) { $phids[] = $phid; } } } $phids[] = $rule->getAuthorPHID(); if ($rule->isObjectRule()) { $phids[] = $rule->getTriggerObjectPHID(); } return $this->loadViewerHandles($phids); } /** * Render the selector for the "When (all of | any of) these conditions are * met:" element. */ private function renderMustMatchSelector($rule) { return AphrontFormSelectControl::renderSelectTag( $rule->getMustMatchAll() ? 'all' : 'any', array( 'all' => pht('all of'), 'any' => pht('any of'), ), array( 'name' => 'must_match', )); } /** * Render the selector for "Take these actions (every time | only the first * time) this rule matches..." element. */ private function renderRepetitionSelector($rule, HeraldAdapter $adapter) { $repetition_policy = HeraldRepetitionPolicyConfig::toString( $rule->getRepetitionPolicy()); $repetition_options = $adapter->getRepetitionOptions(); $repetition_names = HeraldRepetitionPolicyConfig::getMap(); $repetition_map = array_select_keys($repetition_names, $repetition_options); if (count($repetition_map) < 2) { return head($repetition_names); } else { return AphrontFormSelectControl::renderSelectTag( $repetition_policy, $repetition_map, array( 'name' => 'repetition_policy', )); } } protected function buildTokenizerTemplates(array $handles) { $template = new AphrontTokenizerTemplateView(); $template = $template->render(); $sources = array( 'repository' => new DiffusionRepositoryDatasource(), 'legaldocuments' => new LegalpadDocumentDatasource(), 'taskpriority' => new ManiphestTaskPriorityDatasource(), 'buildplan' => new HarbormasterBuildPlanDatasource(), 'arcanistprojects' => new DiffusionArcanistProjectDatasource(), 'package' => new PhabricatorOwnersPackageDatasource(), 'project' => new PhabricatorProjectDatasource(), 'user' => new PhabricatorPeopleDatasource(), 'email' => new PhabricatorMetaMTAMailableDatasource(), 'userorproject' => new PhabricatorProjectOrUserDatasource(), ); foreach ($sources as $key => $source) { $sources[$key] = array( 'uri' => $source->getDatasourceURI(), 'placeholder' => $source->getPlaceholderText(), ); } return array( 'source' => $sources, 'username' => $this->getRequest()->getUser()->getUserName(), 'icons' => mpull($handles, 'getTypeIcon', 'getPHID'), 'markup' => $template, ); } /** * Load rules for the "Another Herald rule..." condition dropdown, which * allows one rule to depend upon the success or failure of another rule. */ private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); // Any rule can depend on a global rule. $all_rules = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL)) ->withContentTypes(array($rule->getContentType())) ->execute(); if ($rule->isObjectRule()) { // Object rules may depend on other rules for the same object. $all_rules += id(new HeraldRuleQuery()) ->setViewer($viewer) ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT)) ->withContentTypes(array($rule->getContentType())) ->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID())) ->execute(); } if ($rule->isPersonalRule()) { // Personal rules may depend upon your other personal rules. $all_rules += id(new HeraldRuleQuery()) ->setViewer($viewer) ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL)) ->withContentTypes(array($rule->getContentType())) ->withAuthorPHIDs(array($rule->getAuthorPHID())) ->execute(); } // mark disabled rules as disabled since they are not useful as such; // don't filter though to keep edit cases sane / expected foreach ($all_rules as $current_rule) { if ($current_rule->getIsDisabled()) { $current_rule->makeEphemeral(); $current_rule->setName($rule->getName().' '.pht('(Disabled)')); } } // A rule can not depend upon itself. unset($all_rules[$rule->getID()]); return $all_rules; } } diff --git a/src/applications/herald/storage/__tests__/HeraldRuleTestCase.php b/src/applications/herald/storage/__tests__/HeraldRuleTestCase.php index 7b44247966..e0d7ae79d4 100644 --- a/src/applications/herald/storage/__tests__/HeraldRuleTestCase.php +++ b/src/applications/herald/storage/__tests__/HeraldRuleTestCase.php @@ -1,41 +1,41 @@ <?php final class HeraldRuleTestCase extends PhabricatorTestCase { public function testHeraldRuleExecutionOrder() { $rules = array( 1 => HeraldRuleTypeConfig::RULE_TYPE_GLOBAL, 2 => HeraldRuleTypeConfig::RULE_TYPE_GLOBAL, 3 => HeraldRuleTypeConfig::RULE_TYPE_OBJECT, 4 => HeraldRuleTypeConfig::RULE_TYPE_PERSONAL, 5 => HeraldRuleTypeConfig::RULE_TYPE_GLOBAL, 6 => HeraldRuleTypeConfig::RULE_TYPE_PERSONAL, ); foreach ($rules as $id => $type) { $rules[$id] = id(new HeraldRule()) ->setID($id) ->setRuleType($type); } shuffle($rules); $rules = msort($rules, 'getRuleExecutionOrderSortKey'); $this->assertEqual( array( // Personal 4, 6, // Object 3, // Global 1, 2, - 5 + 5, ), array_values(mpull($rules, 'getID'))); } } diff --git a/src/applications/home/controller/PhabricatorHomeMainController.php b/src/applications/home/controller/PhabricatorHomeMainController.php index 9fa032d566..5fa7684ffa 100644 --- a/src/applications/home/controller/PhabricatorHomeMainController.php +++ b/src/applications/home/controller/PhabricatorHomeMainController.php @@ -1,416 +1,416 @@ <?php final class PhabricatorHomeMainController extends PhabricatorHomeController { private $only; private $minipanels = array(); public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->only = idx($data, 'only'); } public function processRequest() { $user = $this->getRequest()->getUser(); $dashboard = PhabricatorDashboardInstall::getDashboard( $user, $user->getPHID(), get_class($this->getCurrentApplication())); if (!$dashboard) { $dashboard = PhabricatorDashboardInstall::getDashboard( $user, PhabricatorHomeApplication::DASHBOARD_DEFAULT, get_class($this->getCurrentApplication())); } if ($dashboard) { $content = id(new PhabricatorDashboardRenderingEngine()) ->setViewer($user) ->setDashboard($dashboard) ->renderDashboard(); } else { $project_query = new PhabricatorProjectQuery(); $project_query->setViewer($user); $project_query->withMemberPHIDs(array($user->getPHID())); $projects = $project_query->execute(); $content = $this->buildMainResponse($projects); } if (!$this->only) { $nav = $this->buildNav(); $nav->appendChild( array( $content, id(new PhabricatorGlobalUploadTargetView())->setUser($user), )); $content = $nav; } return $this->buildApplicationPage( $content, array( 'title' => 'Phabricator', )); } private function buildMainResponse(array $projects) { assert_instances_of($projects, 'PhabricatorProject'); $viewer = $this->getRequest()->getUser(); $has_maniphest = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorManiphestApplication', $viewer); $has_audit = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorAuditApplication', $viewer); $has_differential = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorDifferentialApplication', $viewer); if ($has_maniphest) { $unbreak_panel = $this->buildUnbreakNowPanel(); $triage_panel = $this->buildNeedsTriagePanel($projects); $tasks_panel = $this->buildTasksPanel(); } else { $unbreak_panel = null; $triage_panel = null; $tasks_panel = null; } if ($has_audit) { $audit_panel = $this->buildAuditPanel(); $commit_panel = $this->buildCommitPanel(); } else { $audit_panel = null; $commit_panel = null; } if (PhabricatorEnv::getEnvConfig('welcome.html') !== null) { $welcome_panel = $this->buildWelcomePanel(); } else { $welcome_panel = null; } if ($has_differential) { $revision_panel = $this->buildRevisionPanel(); } else { $revision_panel = null; } return array( $welcome_panel, $unbreak_panel, $triage_panel, $revision_panel, $tasks_panel, $audit_panel, $commit_panel, $this->minipanels, ); } private function buildUnbreakNowPanel() { $unbreak_now = PhabricatorEnv::getEnvConfig( 'maniphest.priorities.unbreak-now'); if (!$unbreak_now) { return null; } $user = $this->getRequest()->getUser(); $task_query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->withPriorities(array($unbreak_now)) ->setLimit(10); $tasks = $task_query->execute(); if (!$tasks) { return $this->renderMiniPanel( 'No "Unbreak Now!" Tasks', 'Nothing appears to be critically broken right now.'); } $href = urisprintf( '/maniphest/?statuses=%s&priorities=%s#R', implode(',', ManiphestTaskStatus::getOpenStatusConstants()), $unbreak_now); $title = pht('Unbreak Now!'); $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); return $panel; } private function buildNeedsTriagePanel(array $projects) { assert_instances_of($projects, 'PhabricatorProject'); $needs_triage = PhabricatorEnv::getEnvConfig( 'maniphest.priorities.needs-triage'); if (!$needs_triage) { return null; } $user = $this->getRequest()->getUser(); if (!$user->isLoggedIn()) { return null; } if ($projects) { $task_query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->withPriorities(array($needs_triage)) ->withAnyProjects(mpull($projects, 'getPHID')) ->setLimit(10); $tasks = $task_query->execute(); } else { $tasks = array(); } if (!$tasks) { return $this->renderMiniPanel( 'No "Needs Triage" Tasks', hsprintf( 'No tasks in <a href="/project/">projects you are a member of</a> '. 'need triage.')); } $title = pht('Needs Triage'); $href = urisprintf( '/maniphest/?statuses=%s&priorities=%s&userProjects=%s#R', implode(',', ManiphestTaskStatus::getOpenStatusConstants()), $needs_triage, $user->getPHID()); $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); return $panel; } private function buildRevisionPanel() { $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); $revision_query = id(new DifferentialRevisionQuery()) ->setViewer($user) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->withResponsibleUsers(array($user_phid)) ->needRelationships(true) ->needFlags(true) ->needDrafts(true); $revisions = $revision_query->execute(); list($blocking, $active, ) = DifferentialRevisionQuery::splitResponsible( $revisions, array($user_phid)); if (!$blocking && !$active) { return $this->renderMiniPanel( 'No Waiting Revisions', 'No revisions are waiting on you.'); } $title = pht('Revisions Waiting on You'); $href = '/differential'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $revision_view = id(new DifferentialRevisionListView()) ->setHighlightAge(true) ->setRevisions(array_merge($blocking, $active)) ->setUser($user); $phids = array_merge( array($user_phid), $revision_view->getRequiredHandlePHIDs()); $handles = $this->loadViewerHandles($phids); $revision_view->setHandles($handles); $list_view = $revision_view->render(); $list_view->setFlush(true); $panel->appendChild($list_view); $panel->setNoBackground(); return $panel; } private function buildWelcomePanel() { $panel = new AphrontPanelView(); $panel->appendChild( phutil_safe_html( PhabricatorEnv::getEnvConfig('welcome.html'))); $panel->setNoBackground(); return $panel; } private function buildTasksPanel() { $user = $this->getRequest()->getUser(); $user_phid = $user->getPHID(); $task_query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->setGroupBy(ManiphestTaskQuery::GROUP_PRIORITY) ->withOwners(array($user_phid)) ->setLimit(10); $tasks = $task_query->execute(); if (!$tasks) { return $this->renderMiniPanel( 'No Assigned Tasks', 'You have no assigned tasks.'); } $title = pht('Assigned Tasks'); $href = '/maniphest'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($this->buildTaskListView($tasks)); $panel->setNoBackground(); return $panel; } private function buildTaskListView(array $tasks) { assert_instances_of($tasks, 'ManiphestTask'); $user = $this->getRequest()->getUser(); $phids = array_merge( array_filter(mpull($tasks, 'getOwnerPHID')), array_mergev(mpull($tasks, 'getProjectPHIDs'))); $handles = $this->loadViewerHandles($phids); $view = new ManiphestTaskListView(); $view->setTasks($tasks); $view->setUser($user); $view->setHandles($handles); return $view; } private function renderSectionHeader($title, $href) { $header = phutil_tag( 'a', array( 'href' => $href, ), $title); return $header; } private function renderMiniPanel($title, $body) { $panel = new AphrontMiniPanelView(); $panel->appendChild( phutil_tag( 'p', array( ), array( phutil_tag('strong', array(), $title.': '), - $body + $body, ))); $this->minipanels[] = $panel; } public function buildAuditPanel() { $request = $this->getRequest(); $user = $request->getUser(); $phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser($user); $query = id(new DiffusionCommitQuery()) ->setViewer($user) ->withAuditorPHIDs($phids) ->withAuditStatus(DiffusionCommitQuery::AUDIT_STATUS_OPEN) ->withAuditAwaitingUser($user) ->needAuditRequests(true) ->needCommitData(true) ->setLimit(10); $commits = $query->execute(); if (!$commits) { return $this->renderMinipanel( 'No Audits', 'No commits are waiting for you to audit them.'); } $view = id(new PhabricatorAuditListView()) ->setCommits($commits) ->setUser($user); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $title = pht('Audits'); $href = '/audit/'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($view); $panel->setNoBackground(); return $panel; } public function buildCommitPanel() { $request = $this->getRequest(); $user = $request->getUser(); $phids = array($user->getPHID()); $query = id(new DiffusionCommitQuery()) ->setViewer($user) ->withAuthorPHIDs($phids) ->withAuditStatus(DiffusionCommitQuery::AUDIT_STATUS_CONCERN) ->needCommitData(true) ->needAuditRequests(true) ->setLimit(10); $commits = $query->execute(); if (!$commits) { return $this->renderMinipanel( 'No Problem Commits', 'No one has raised concerns with your commits.'); } $view = id(new PhabricatorAuditListView()) ->setCommits($commits) ->setUser($user); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); $title = pht('Problem Commits'); $href = '/audit/'; $panel = new AphrontPanelView(); $panel->setHeader($this->renderSectionHeader($title, $href)); $panel->appendChild($view); $panel->setNoBackground(); return $panel; } } diff --git a/src/applications/legalpad/application/PhabricatorLegalpadApplication.php b/src/applications/legalpad/application/PhabricatorLegalpadApplication.php index b5ef2792ce..d1fbb4d2f9 100644 --- a/src/applications/legalpad/application/PhabricatorLegalpadApplication.php +++ b/src/applications/legalpad/application/PhabricatorLegalpadApplication.php @@ -1,77 +1,78 @@ <?php final class PhabricatorLegalpadApplication extends PhabricatorApplication { public function getBaseURI() { return '/legalpad/'; } public function getName() { return pht('Legalpad'); } public function getShortDescription() { return pht('Agreements and Signatures'); } public function getIconName() { return 'legalpad'; } public function getTitleGlyph() { return "\xC2\xA9"; } public function getApplicationGroup() { return self::GROUP_UTILITIES; } public function getRemarkupRules() { return array( new LegalpadDocumentRemarkupRule(), ); } public function getHelpURI() { return PhabricatorEnv::getDoclink('Legalpad User Guide'); } public function getOverview() { return pht( '**Legalpad** is a simple application for tracking signatures and '. 'legal agreements. At the moment, it is primarily intended to help '. 'open source projects keep track of Contributor License Agreements.'); } public function getRoutes() { return array( '/L(?P<id>\d+)' => 'LegalpadDocumentSignController', '/legalpad/' => array( '' => 'LegalpadDocumentListController', '(?:query/(?P<queryKey>[^/]+)/)?' => 'LegalpadDocumentListController', 'create/' => 'LegalpadDocumentEditController', 'edit/(?P<id>\d+)/' => 'LegalpadDocumentEditController', 'comment/(?P<id>\d+)/' => 'LegalpadDocumentCommentController', 'view/(?P<id>\d+)/' => 'LegalpadDocumentManageController', 'done/' => 'LegalpadDocumentDoneController', 'verify/(?P<code>[^/]+)/' => 'LegalpadDocumentSignatureVerificationController', 'signatures/(?:(?P<id>\d+)/)?(?:query/(?P<queryKey>[^/]+)/)?' => 'LegalpadDocumentSignatureListController', 'addsignature/(?P<id>\d+)/' => 'LegalpadDocumentSignatureAddController', 'signature/(?P<id>\d+)/' => 'LegalpadDocumentSignatureViewController', 'document/' => array( 'preview/' => 'PhabricatorMarkupPreviewController', ), - )); + ), + ); } protected function getCustomCapabilities() { return array( LegalpadCreateDocumentsCapability::CAPABILITY => array(), LegalpadDefaultViewCapability::CAPABILITY => array(), LegalpadDefaultEditCapability::CAPABILITY => array(), ); } } diff --git a/src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php b/src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php index 76bf2bcca7..463df585c5 100644 --- a/src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php +++ b/src/applications/legalpad/config/PhabricatorLegalpadConfigOptions.php @@ -1,24 +1,24 @@ <?php final class PhabricatorLegalpadConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Legalpad'); } public function getDescription() { return pht('Configure Legalpad.'); } public function getOptions() { return array( $this->newOption( 'metamta.legalpad.subject-prefix', 'string', '[Legalpad]') - ->setDescription(pht('Subject prefix for Legalpad email.')) + ->setDescription(pht('Subject prefix for Legalpad email.')), ); } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentEditController.php b/src/applications/legalpad/controller/LegalpadDocumentEditController.php index 29fb4641c4..20002c4c77 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentEditController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentEditController.php @@ -1,228 +1,228 @@ <?php final class LegalpadDocumentEditController extends LegalpadController { private $id; public function willProcessRequest(array $data) { $this->id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if (!$this->id) { $is_create = true; $this->requireApplicationCapability( LegalpadCreateDocumentsCapability::CAPABILITY); $document = LegalpadDocument::initializeNewDocument($user); $body = id(new LegalpadDocumentBody()) ->setCreatorPHID($user->getPHID()); $document->attachDocumentBody($body); $document->setDocumentBodyPHID(PhabricatorPHIDConstants::PHID_VOID); } else { $is_create = false; $document = id(new LegalpadDocumentQuery()) ->setViewer($user) ->needDocumentBodies(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($this->id)) ->executeOne(); if (!$document) { return new Aphront404Response(); } } $e_title = true; $e_text = true; $title = $document->getDocumentBody()->getTitle(); $text = $document->getDocumentBody()->getText(); $v_signature_type = $document->getSignatureType(); $v_preamble = $document->getPreamble(); $errors = array(); $can_view = null; $can_edit = null; if ($request->isFormPost()) { $xactions = array(); $title = $request->getStr('title'); if (!strlen($title)) { $e_title = pht('Required'); $errors[] = pht('The document title may not be blank.'); } else { $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransactionType::TYPE_TITLE) ->setNewValue($title); } $text = $request->getStr('text'); if (!strlen($text)) { $e_text = pht('Required'); $errors[] = pht('The document may not be blank.'); } else { $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransactionType::TYPE_TEXT) ->setNewValue($text); } $can_view = $request->getStr('can_view'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($can_view); $can_edit = $request->getStr('can_edit'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($can_edit); if ($is_create) { $v_signature_type = $request->getStr('signatureType'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransactionType::TYPE_SIGNATURE_TYPE) ->setNewValue($v_signature_type); } $v_preamble = $request->getStr('preamble'); $xactions[] = id(new LegalpadTransaction()) ->setTransactionType(LegalpadTransactionType::TYPE_PREAMBLE) ->setNewValue($v_preamble); if (!$errors) { $editor = id(new LegalpadDocumentEditor()) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setActor($user); $xactions = $editor->applyTransactions($document, $xactions); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('view/'.$document->getID())); } } if ($errors) { // set these to what was specified in the form on post $document->setViewPolicy($can_view); $document->setEditPolicy($can_edit); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setID('document-title') ->setLabel(pht('Title')) ->setError($e_title) ->setValue($title) ->setName('title')); if ($is_create) { $form->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Who Should Sign?')) ->setName(pht('signatureType')) ->setValue($v_signature_type) ->setOptions(LegalpadDocument::getSignatureTypeMap())); } else { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Who Should Sign?')) ->setValue($document->getSignatureTypeName())); } $form ->appendChild( id(new PhabricatorRemarkupControl()) ->setID('preamble') ->setLabel(pht('Preamble')) ->setValue($v_preamble) ->setName('preamble') ->setCaption( pht('Optional help text for users signing this document.'))) ->appendChild( id(new PhabricatorRemarkupControl()) ->setID('document-text') ->setLabel(pht('Document Body')) ->setError($e_text) ->setValue($text) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setName('text')); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($document) ->execute(); $form ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($document) ->setPolicies($policies) ->setName('can_view')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($document) ->setPolicies($policies) ->setName('can_edit')); $crumbs = $this->buildApplicationCrumbs($this->buildSideNav()); $submit = new AphrontFormSubmitControl(); if ($is_create) { $submit->setValue(pht('Create Document')); $submit->addCancelButton($this->getApplicationURI()); $title = pht('Create Document'); $short = pht('Create'); } else { $submit->setValue(pht('Edit Document')); $submit->addCancelButton( $this->getApplicationURI('view/'.$document->getID())); $title = pht('Edit Document'); $short = pht('Edit'); $crumbs->addTextCrumb( $document->getMonogram(), $this->getApplicationURI('view/'.$document->getID())); } $form ->appendChild($submit); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $crumbs->addTextCrumb($short); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Document Preview')) ->setPreviewURI($this->getApplicationURI('document/preview/')) ->setControlID('document-text') ->setSkin('document'); return $this->buildApplicationPage( array( $crumbs, $form_box, - $preview + $preview, ), array( 'title' => $title, )); } } diff --git a/src/applications/macro/config/PhabricatorMacroConfigOptions.php b/src/applications/macro/config/PhabricatorMacroConfigOptions.php index 427405e840..1c21c0aa51 100644 --- a/src/applications/macro/config/PhabricatorMacroConfigOptions.php +++ b/src/applications/macro/config/PhabricatorMacroConfigOptions.php @@ -1,24 +1,24 @@ <?php final class PhabricatorMacroConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Macro'); } public function getDescription() { return pht('Configure Macro.'); } public function getOptions() { return array( $this->newOption('metamta.macro.reply-handler-domain', 'string', null) ->setDescription(pht( 'As {{metamta.maniphest.reply-handler-domain}}, but affects Macro.')), $this->newOption('metamta.macro.subject-prefix', 'string', '[Macro]') - ->setDescription(pht('Subject prefix for Macro email.')) + ->setDescription(pht('Subject prefix for Macro email.')), ); } } diff --git a/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php index ddd3b69a21..b6d933ae1e 100644 --- a/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php +++ b/src/applications/maniphest/conduit/ManiphestQueryStatusesConduitAPIMethod.php @@ -1,39 +1,39 @@ <?php final class ManiphestQueryStatusesConduitAPIMethod extends ManiphestConduitAPIMethod { public function getAPIMethodName() { return 'maniphest.querystatuses'; } public function getMethodDescription() { return 'Retrieve information about possible Maniphest Task status values.'; } public function defineParamTypes() { return array(); } public function defineReturnType() { return 'nonempty dict<string, wild>'; } public function defineErrorTypes() { return array(); } protected function execute(ConduitAPIRequest $request) { $results = array( 'defaultStatus' => ManiphestTaskStatus::getDefaultStatus(), 'defaultClosedStatus' => ManiphestTaskStatus::getDefaultClosedStatus(), 'duplicateStatus' => ManiphestTaskStatus::getDuplicateStatus(), 'openStatuses' => ManiphestTaskStatus::getOpenStatusConstants(), 'closedStatuses' => ManiphestTaskStatus::getClosedStatusConstants(), 'allStatuses' => array_keys(ManiphestTaskStatus::getTaskStatusMap()), - 'statusMap' => ManiphestTaskStatus::getTaskStatusMap() + 'statusMap' => ManiphestTaskStatus::getTaskStatusMap(), ); return $results; } } diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php index b91674fdab..dc41515f3a 100644 --- a/src/applications/maniphest/controller/ManiphestBatchEditController.php +++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php @@ -1,351 +1,351 @@ <?php final class ManiphestBatchEditController extends ManiphestController { public function processRequest() { $this->requireApplicationCapability( ManiphestBulkEditCapability::CAPABILITY); $request = $this->getRequest(); $user = $request->getUser(); $task_ids = $request->getArr('batch'); $tasks = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs($task_ids) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); $actions = $request->getStr('actions'); if ($actions) { $actions = json_decode($actions, true); } if ($request->isFormPost() && is_array($actions)) { foreach ($tasks as $task) { $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_EDIT); $field_list->readFieldsFromStorage($task); $xactions = $this->buildTransactions($actions, $task); if ($xactions) { // TODO: Set content source to "batch edit". $editor = id(new ManiphestTransactionEditor()) ->setActor($user) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($task, $xactions); } } $task_ids = implode(',', mpull($tasks, 'getID')); return id(new AphrontRedirectResponse()) ->setURI('/maniphest/?ids='.$task_ids); } $handles = ManiphestTaskListView::loadTaskHandles($user, $tasks); $list = new ManiphestTaskListView(); $list->setTasks($tasks); $list->setUser($user); $list->setHandles($handles); $template = new AphrontTokenizerTemplateView(); $template = $template->render(); $projects_source = new PhabricatorProjectDatasource(); $mailable_source = new PhabricatorMetaMTAMailableDatasource(); $owner_source = new PhabricatorTypeaheadOwnerDatasource(); require_celerity_resource('maniphest-batch-editor'); Javelin::initBehavior( 'maniphest-batch-editor', array( 'root' => 'maniphest-batch-edit-form', 'tokenizerTemplate' => $template, 'sources' => array( 'project' => array( 'src' => $projects_source->getDatasourceURI(), 'placeholder' => $projects_source->getPlaceholderText(), ), 'owner' => array( 'src' => $owner_source->getDatasourceURI(), 'placeholder' => $owner_source->getPlaceholderText(), 'limit' => 1, ), 'cc' => array( 'src' => $mailable_source->getDatasourceURI(), 'placeholder' => $mailable_source->getPlaceholderText(), - ) + ), ), 'input' => 'batch-form-actions', 'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(), 'statusMap' => ManiphestTaskStatus::getTaskStatusMap(), )); $form = new AphrontFormView(); $form->setUser($user); $form->setID('maniphest-batch-edit-form'); foreach ($tasks as $task) { $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'batch[]', 'value' => $task->getID(), ))); } $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'actions', 'id' => 'batch-form-actions', ))); $form->appendChild( phutil_tag('p', array(), pht('These tasks will be edited:'))); $form->appendChild($list); $form->appendChild( id(new AphrontFormInsetView()) ->setTitle('Actions') ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'add-action', 'mustcapture' => true, ), pht('Add Another Action'))) ->setContent(javelin_tag( 'table', array( 'sigil' => 'maniphest-batch-actions', 'class' => 'maniphest-batch-actions-table', ), ''))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Update Tasks')) ->addCancelButton('/maniphest/')); $title = pht('Batch Editor'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Batch Edit Tasks')) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, 'device' => false, )); } private function buildTransactions($actions, ManiphestTask $task) { $value_map = array(); $type_map = array( 'add_comment' => PhabricatorTransactions::TYPE_COMMENT, 'assign' => ManiphestTransaction::TYPE_OWNER, 'status' => ManiphestTransaction::TYPE_STATUS, 'priority' => ManiphestTransaction::TYPE_PRIORITY, 'add_project' => ManiphestTransaction::TYPE_PROJECTS, 'remove_project' => ManiphestTransaction::TYPE_PROJECTS, 'add_ccs' => ManiphestTransaction::TYPE_CCS, 'remove_ccs' => ManiphestTransaction::TYPE_CCS, ); $edge_edit_types = array( 'add_project' => true, 'remove_project' => true, 'add_ccs' => true, 'remove_ccs' => true, ); $xactions = array(); foreach ($actions as $action) { if (empty($type_map[$action['action']])) { throw new Exception("Unknown batch edit action '{$action}'!"); } $type = $type_map[$action['action']]; // Figure out the current value, possibly after modifications by other // batch actions of the same type. For example, if the user chooses to // "Add Comment" twice, we should add both comments. More notably, if the // user chooses "Remove Project..." and also "Add Project...", we should // avoid restoring the removed project in the second transaction. if (array_key_exists($type, $value_map)) { $current = $value_map[$type]; } else { switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: $current = null; break; case ManiphestTransaction::TYPE_OWNER: $current = $task->getOwnerPHID(); break; case ManiphestTransaction::TYPE_STATUS: $current = $task->getStatus(); break; case ManiphestTransaction::TYPE_PRIORITY: $current = $task->getPriority(); break; case ManiphestTransaction::TYPE_PROJECTS: $current = $task->getProjectPHIDs(); break; case ManiphestTransaction::TYPE_CCS: $current = $task->getCCPHIDs(); break; } } // Check if the value is meaningful / provided, and normalize it if // necessary. This discards, e.g., empty comments and empty owner // changes. $value = $action['value']; switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: if (!strlen($value)) { continue 2; } break; case ManiphestTransaction::TYPE_OWNER: if (empty($value)) { continue 2; } $value = head($value); if ($value === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) { $value = null; } break; case ManiphestTransaction::TYPE_PROJECTS: if (empty($value)) { continue 2; } break; case ManiphestTransaction::TYPE_CCS: if (empty($value)) { continue 2; } break; } // If the edit doesn't change anything, go to the next action. This // check is only valid for changes like "owner", "status", etc, not // for edge edits, because we should still apply an edit like // "Remove Projects: A, B" to a task with projects "A, B". if (empty($edge_edit_types[$action['action']])) { if ($value == $current) { continue; } } // Apply the value change; for most edits this is just replacement, but // some need to merge the current and edited values (add/remove project). switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: if (strlen($current)) { $value = $current."\n\n".$value; } break; case ManiphestTransaction::TYPE_PROJECTS: case ManiphestTransaction::TYPE_CCS: $remove_actions = array( 'remove_project' => true, 'remove_ccs' => true, ); $is_remove = isset($remove_actions[$action['action']]); $current = array_fill_keys($current, true); $value = array_fill_keys($value, true); $new = $current; $did_something = false; if ($is_remove) { foreach ($value as $phid => $ignored) { if (isset($new[$phid])) { unset($new[$phid]); $did_something = true; } } } else { foreach ($value as $phid => $ignored) { if (empty($new[$phid])) { $new[$phid] = true; $did_something = true; } } } if (!$did_something) { continue 2; } $value = array_keys($new); break; } $value_map[$type] = $value; } $template = new ManiphestTransaction(); foreach ($value_map as $type => $value) { $xaction = clone $template; $xaction->setTransactionType($type); switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: $xaction->attachComment( id(new ManiphestTransactionComment()) ->setContent($value)); break; case ManiphestTransaction::TYPE_PROJECTS: // TODO: Clean this mess up. $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xaction ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $project_type) ->setNewValue( array( '=' => array_fuse($value), )); break; default: $xaction->setNewValue($value); break; } $xactions[] = $xaction; } return $xactions; } } diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index 4f4befb456..fdf41722cd 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -1,776 +1,777 @@ <?php final class ManiphestReportController extends ManiphestController { private $view; public function willProcessRequest(array $data) { $this->view = idx($data, 'view'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($request->isFormPost()) { $uri = $request->getRequestURI(); $project = head($request->getArr('set_project')); $project = nonempty($project, null); $uri = $uri->alter('project', $project); $window = $request->getStr('set_window'); $uri = $uri->alter('window', $window); return id(new AphrontRedirectResponse())->setURI($uri); } $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI('/maniphest/report/')); $nav->addLabel(pht('Open Tasks')); $nav->addFilter('user', pht('By User')); $nav->addFilter('project', pht('By Project')); $nav->addLabel(pht('Burnup')); $nav->addFilter('burn', pht('Burnup Rate')); $this->view = $nav->selectFilter($this->view, 'user'); require_celerity_resource('maniphest-report-css'); switch ($this->view) { case 'burn': $core = $this->renderBurn(); break; case 'user': case 'project': $core = $this->renderOpenTasks(); break; default: return new Aphront404Response(); } $nav->appendChild($core); $nav->setCrumbs( $this->buildApplicationCrumbs() ->addTextCrumb(pht('Reports'))); return $this->buildApplicationPage( $nav, array( 'title' => pht('Maniphest Reports'), 'device' => false, )); } public function renderBurn() { $request = $this->getRequest(); $user = $request->getUser(); $handle = null; $project_phid = $request->getStr('project'); if ($project_phid) { $phids = array($project_phid); $handles = $this->loadViewerHandles($phids); $handle = $handles[$project_phid]; } $table = new ManiphestTransaction(); $conn = $table->establishConnection('r'); $joins = ''; if ($project_phid) { $joins = qsprintf( $conn, 'JOIN %T t ON x.objectPHID = t.phid JOIN %T p ON p.src = t.phid AND p.type = %d AND p.dst = %s', id(new ManiphestTask())->getTableName(), PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $project_phid); } $data = queryfx_all( $conn, 'SELECT x.oldValue, x.newValue, x.dateCreated FROM %T x %Q WHERE transactionType = %s ORDER BY x.dateCreated ASC', $table->getTableName(), $joins, ManiphestTransaction::TYPE_STATUS); $stats = array(); $day_buckets = array(); $open_tasks = array(); foreach ($data as $key => $row) { // NOTE: Hack to avoid json_decode(). $oldv = trim($row['oldValue'], '"'); $newv = trim($row['newValue'], '"'); if ($oldv == 'null') { $old_is_open = false; } else { $old_is_open = ManiphestTaskStatus::isOpenStatus($oldv); } $new_is_open = ManiphestTaskStatus::isOpenStatus($newv); $is_open = ($new_is_open && !$old_is_open); $is_close = ($old_is_open && !$new_is_open); $data[$key]['_is_open'] = $is_open; $data[$key]['_is_close'] = $is_close; if (!$is_open && !$is_close) { // This is either some kind of bogus event, or a resolution change // (e.g., resolved -> invalid). Just skip it. continue; } $day_bucket = phabricator_format_local_time( $row['dateCreated'], $user, 'Yz'); $day_buckets[$day_bucket] = $row['dateCreated']; if (empty($stats[$day_bucket])) { $stats[$day_bucket] = array( 'open' => 0, 'close' => 0, ); } $stats[$day_bucket][$is_close ? 'close' : 'open']++; } $template = array( 'open' => 0, 'close' => 0, ); $rows = array(); $rowc = array(); $last_month = null; $last_month_epoch = null; $last_week = null; $last_week_epoch = null; $week = null; $month = null; $last = last_key($stats) - 1; $period = $template; foreach ($stats as $bucket => $info) { $epoch = $day_buckets[$bucket]; $week_bucket = phabricator_format_local_time( $epoch, $user, 'YW'); if ($week_bucket != $last_week) { if ($week) { $rows[] = $this->formatBurnRow( 'Week of '.phabricator_date($last_week_epoch, $user), $week); $rowc[] = 'week'; } $week = $template; $last_week = $week_bucket; $last_week_epoch = $epoch; } $month_bucket = phabricator_format_local_time( $epoch, $user, 'Ym'); if ($month_bucket != $last_month) { if ($month) { $rows[] = $this->formatBurnRow( phabricator_format_local_time($last_month_epoch, $user, 'F, Y'), $month); $rowc[] = 'month'; } $month = $template; $last_month = $month_bucket; $last_month_epoch = $epoch; } $rows[] = $this->formatBurnRow(phabricator_date($epoch, $user), $info); $rowc[] = null; $week['open'] += $info['open']; $week['close'] += $info['close']; $month['open'] += $info['open']; $month['close'] += $info['close']; $period['open'] += $info['open']; $period['close'] += $info['close']; } if ($week) { $rows[] = $this->formatBurnRow( pht('Week To Date'), $week); $rowc[] = 'week'; } if ($month) { $rows[] = $this->formatBurnRow( pht('Month To Date'), $month); $rowc[] = 'month'; } $rows[] = $this->formatBurnRow( pht('All Time'), $period); $rowc[] = 'aggregate'; $rows = array_reverse($rows); $rowc = array_reverse($rowc); $table = new AphrontTableView($rows); $table->setRowClasses($rowc); $table->setHeaders( array( pht('Period'), pht('Opened'), pht('Closed'), pht('Change'), )); $table->setColumnClasses( array( 'right wide', 'n', 'n', 'n', )); if ($handle) { $inst = pht( 'NOTE: This table reflects tasks currently in '. 'the project. If a task was opened in the past but added to '. 'the project recently, it is counted on the day it was '. 'opened, not the day it was categorized. If a task was part '. 'of this project in the past but no longer is, it is not '. 'counted at all.'); $header = pht('Task Burn Rate for Project %s', $handle->renderLink()); $caption = phutil_tag('p', array(), $inst); } else { $header = pht('Task Burn Rate for All Tasks'); $caption = null; } if ($caption) { $caption = id(new AphrontErrorView()) ->appendChild($caption) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE); } $panel = new PHUIObjectBoxView(); $panel->setHeaderText($header); if ($caption) { $panel->setErrorView($caption); } $panel->appendChild($table); $tokens = array(); if ($handle) { $tokens = array($handle); } $filter = $this->renderReportFilters($tokens, $has_window = false); $id = celerity_generate_unique_node_id(); $chart = phutil_tag( 'div', array( 'id' => $id, 'style' => 'border: 1px solid #BFCFDA; '. 'background-color: #fff; '. 'margin: 8px 16px; '. 'height: 400px; ', ), ''); list($burn_x, $burn_y) = $this->buildSeries($data); require_celerity_resource('raphael-core'); require_celerity_resource('raphael-g'); require_celerity_resource('raphael-g-line'); Javelin::initBehavior('line-chart', array( 'hardpoint' => $id, 'x' => array( $burn_x, ), 'y' => array( $burn_y, ), 'xformat' => 'epoch', 'yformat' => 'int', )); return array($filter, $chart, $panel); } private function renderReportFilters(array $tokens, $has_window) { $request = $this->getRequest(); $user = $request->getUser(); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorProjectDatasource()) ->setLabel(pht('Project')) ->setLimit(1) ->setName('set_project') ->setValue($tokens)); if ($has_window) { list($window_str, $ignored, $window_error) = $this->getWindow(); $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Recently Means')) ->setName('set_window') ->setCaption( pht('Configure the cutoff for the "Recently Closed" column.')) ->setValue($window_str) ->setError($window_error)); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Filter By Project'))); $filter = new AphrontListFilterView(); $filter->appendChild($form); return $filter; } private function buildSeries(array $data) { $out = array(); $counter = 0; foreach ($data as $row) { $t = (int)$row['dateCreated']; if ($row['_is_close']) { --$counter; $out[$t] = $counter; } else if ($row['_is_open']) { ++$counter; $out[$t] = $counter; } } return array(array_keys($out), array_values($out)); } private function formatBurnRow($label, $info) { $delta = $info['open'] - $info['close']; $fmt = number_format($delta); if ($delta > 0) { $fmt = '+'.$fmt; $fmt = phutil_tag('span', array('class' => 'red'), $fmt); } else { $fmt = phutil_tag('span', array('class' => 'green'), $fmt); } return array( $label, number_format($info['open']), number_format($info['close']), - $fmt); + $fmt, + ); } public function renderOpenTasks() { $request = $this->getRequest(); $user = $request->getUser(); $query = id(new ManiphestTaskQuery()) ->setViewer($user) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()); $project_phid = $request->getStr('project'); $project_handle = null; if ($project_phid) { $phids = array($project_phid); $handles = $this->loadViewerHandles($phids); $project_handle = $handles[$project_phid]; $query->withAnyProjects($phids); } $tasks = $query->execute(); $recently_closed = $this->loadRecentlyClosedTasks(); $date = phabricator_date(time(), $user); switch ($this->view) { case 'user': $result = mgroup($tasks, 'getOwnerPHID'); $leftover = idx($result, '', array()); unset($result['']); $result_closed = mgroup($recently_closed, 'getOwnerPHID'); $leftover_closed = idx($result_closed, '', array()); unset($result_closed['']); $base_link = '/maniphest/?assigned='; $leftover_name = phutil_tag('em', array(), pht('(Up For Grabs)')); $col_header = pht('User'); $header = pht('Open Tasks by User and Priority (%s)', $date); break; case 'project': $result = array(); $leftover = array(); foreach ($tasks as $task) { $phids = $task->getProjectPHIDs(); if ($phids) { foreach ($phids as $project_phid) { $result[$project_phid][] = $task; } } else { $leftover[] = $task; } } $result_closed = array(); $leftover_closed = array(); foreach ($recently_closed as $task) { $phids = $task->getProjectPHIDs(); if ($phids) { foreach ($phids as $project_phid) { $result_closed[$project_phid][] = $task; } } else { $leftover_closed[] = $task; } } $base_link = '/maniphest/?allProjects='; $leftover_name = phutil_tag('em', array(), pht('(No Project)')); $col_header = pht('Project'); $header = pht('Open Tasks by Project and Priority (%s)', $date); break; } $phids = array_keys($result); $handles = $this->loadViewerHandles($phids); $handles = msort($handles, 'getName'); $order = $request->getStr('order', 'name'); list($order, $reverse) = AphrontTableView::parseSort($order); require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-tooltips', array()); $rows = array(); $pri_total = array(); foreach (array_merge($handles, array(null)) as $handle) { if ($handle) { if (($project_handle) && ($project_handle->getPHID() == $handle->getPHID())) { // If filtering by, e.g., "bugs", don't show a "bugs" group. continue; } $tasks = idx($result, $handle->getPHID(), array()); $name = phutil_tag( 'a', array( 'href' => $base_link.$handle->getPHID(), ), $handle->getName()); $closed = idx($result_closed, $handle->getPHID(), array()); } else { $tasks = $leftover; $name = $leftover_name; $closed = $leftover_closed; } $taskv = $tasks; $tasks = mgroup($tasks, 'getPriority'); $row = array(); $row[] = $name; $total = 0; foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $label) { $n = count(idx($tasks, $pri, array())); if ($n == 0) { $row[] = '-'; } else { $row[] = number_format($n); } $total += $n; } $row[] = number_format($total); list($link, $oldest_all) = $this->renderOldest($taskv); $row[] = $link; $normal_or_better = array(); foreach ($taskv as $id => $task) { // TODO: This is sort of a hard-code for the default "normal" status. // When reports are more powerful, this should be made more general. if ($task->getPriority() < 50) { continue; } $normal_or_better[$id] = $task; } list($link, $oldest_pri) = $this->renderOldest($normal_or_better); $row[] = $link; if ($closed) { $task_ids = implode(',', mpull($closed, 'getID')); $row[] = phutil_tag( 'a', array( 'href' => '/maniphest/?ids='.$task_ids, 'target' => '_blank', ), number_format(count($closed))); } else { $row[] = '-'; } switch ($order) { case 'total': $row['sort'] = $total; break; case 'oldest-all': $row['sort'] = $oldest_all; break; case 'oldest-pri': $row['sort'] = $oldest_pri; break; case 'closed': $row['sort'] = count($closed); break; case 'name': default: $row['sort'] = $handle ? $handle->getName() : '~'; break; } $rows[] = $row; } $rows = isort($rows, 'sort'); foreach ($rows as $k => $row) { unset($rows[$k]['sort']); } if ($reverse) { $rows = array_reverse($rows); } $cname = array($col_header); $cclass = array('pri right wide'); $pri_map = ManiphestTaskPriority::getShortNameMap(); foreach ($pri_map as $pri => $label) { $cname[] = $label; $cclass[] = 'n'; } $cname[] = 'Total'; $cclass[] = 'n'; $cname[] = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Oldest open task.'), 'size' => 200, ), ), pht('Oldest (All)')); $cclass[] = 'n'; $cname[] = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Oldest open task, excluding those with Low or '. 'Wishlist priority.'), 'size' => 200, ), ), pht('Oldest (Pri)')); $cclass[] = 'n'; list($ignored, $window_epoch) = $this->getWindow(); $edate = phabricator_datetime($window_epoch, $user); $cname[] = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Closed after %s', $edate), - 'size' => 260 + 'size' => 260, ), ), pht('Recently Closed')); $cclass[] = 'n'; $table = new AphrontTableView($rows); $table->setHeaders($cname); $table->setColumnClasses($cclass); $table->makeSortable( $request->getRequestURI(), 'order', $order, $reverse, array( 'name', null, null, null, null, null, null, 'total', 'oldest-all', 'oldest-pri', 'closed', )); $panel = new PHUIObjectBoxView(); $panel->setHeaderText($header); $panel->appendChild($table); $tokens = array(); if ($project_handle) { $tokens = array($project_handle); } $filter = $this->renderReportFilters($tokens, $has_window = true); return array($filter, $panel); } /** * Load all the tasks that have been recently closed. */ private function loadRecentlyClosedTasks() { list($ignored, $window_epoch) = $this->getWindow(); $table = new ManiphestTask(); $xtable = new ManiphestTransaction(); $conn_r = $table->establishConnection('r'); // TODO: Gross. This table is not meant to be queried like this. Build // real stats tables. $open_status_list = array(); foreach (ManiphestTaskStatus::getOpenStatusConstants() as $constant) { $open_status_list[] = json_encode((string)$constant); } $rows = queryfx_all( $conn_r, 'SELECT t.id FROM %T t JOIN %T x ON x.objectPHID = t.phid WHERE t.status NOT IN (%Ls) AND x.oldValue IN (null, %Ls) AND x.newValue NOT IN (%Ls) AND t.dateModified >= %d AND x.dateCreated >= %d', $table->getTableName(), $xtable->getTableName(), ManiphestTaskStatus::getOpenStatusConstants(), $open_status_list, $open_status_list, $window_epoch, $window_epoch); if (!$rows) { return array(); } $ids = ipull($rows, 'id'); return id(new ManiphestTaskQuery()) ->setViewer($this->getRequest()->getUser()) ->withIDs($ids) ->execute(); } /** * Parse the "Recently Means" filter into: * * - A string representation, like "12 AM 7 days ago" (default); * - a locale-aware epoch representation; and * - a possible error. */ private function getWindow() { $request = $this->getRequest(); $user = $request->getUser(); $window_str = $this->getRequest()->getStr('window', '12 AM 7 days ago'); $error = null; $window_epoch = null; // Do locale-aware parsing so that the user's timezone is assumed for // time windows like "3 PM", rather than assuming the server timezone. $window_epoch = PhabricatorTime::parseLocalTime($window_str, $user); if (!$window_epoch) { $error = 'Invalid'; $window_epoch = time() - (60 * 60 * 24 * 7); } // If the time ends up in the future, convert it to the corresponding time // and equal distance in the past. This is so users can type "6 days" (which // means "6 days from now") and get the behavior of "6 days ago", rather // than no results (because the window epoch is in the future). This might // be a little confusing because it casues "tomorrow" to mean "yesterday" // and "2022" (or whatever) to mean "ten years ago", but these inputs are // nonsense anyway. if ($window_epoch > time()) { $window_epoch = time() - ($window_epoch - time()); } return array($window_str, $window_epoch, $error); } private function renderOldest(array $tasks) { assert_instances_of($tasks, 'ManiphestTask'); $oldest = null; foreach ($tasks as $id => $task) { if (($oldest === null) || ($task->getDateCreated() < $tasks[$oldest]->getDateCreated())) { $oldest = $id; } } if ($oldest === null) { return array('-', 0); } $oldest = $tasks[$oldest]; $raw_age = (time() - $oldest->getDateCreated()); $age = number_format($raw_age / (24 * 60 * 60)).' d'; $link = javelin_tag( 'a', array( 'href' => '/T'.$oldest->getID(), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => 'T'.$oldest->getID().': '.$oldest->getTitle(), ), 'target' => '_blank', ), $age); return array($link, $raw_age); } } diff --git a/src/applications/maniphest/controller/ManiphestSubpriorityController.php b/src/applications/maniphest/controller/ManiphestSubpriorityController.php index 5deebdf8ef..5218b9a4b1 100644 --- a/src/applications/maniphest/controller/ManiphestSubpriorityController.php +++ b/src/applications/maniphest/controller/ManiphestSubpriorityController.php @@ -1,61 +1,63 @@ <?php final class ManiphestSubpriorityController extends ManiphestController { public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if (!$request->validateCSRF()) { return new Aphront403Response(); } $task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($request->getInt('task'))) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$task) { return new Aphront404Response(); } if ($request->getInt('after')) { $after_task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($request->getInt('after'))) ->executeOne(); if (!$after_task) { return new Aphront404Response(); } $after_pri = $after_task->getPriority(); $after_sub = $after_task->getSubpriority(); } else { $after_pri = $request->getInt('priority'); $after_sub = null; } $xactions = array(id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) ->setNewValue(array( 'newPriority' => $after_pri, 'newSubpriorityBase' => $after_sub, - 'direction' => '>'))); + 'direction' => '>', + )), + ); $editor = id(new ManiphestTransactionEditor()) ->setActor($user) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $editor->applyTransactions($task, $xactions); return id(new AphrontAjaxResponse())->setContent( array( 'tasks' => $this->renderSingleTask($task), )); } } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 1aae1198dd..985d3163fe 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -1,641 +1,641 @@ <?php final class ManiphestTaskDetailController extends ManiphestController { private $id; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $e_title = null; $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); $task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$task) { return new Aphront404Response(); } $workflow = $request->getStr('workflow'); $parent_task = null; if ($workflow && is_numeric($workflow)) { $parent_task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($workflow)) ->executeOne(); } $transactions = id(new ManiphestTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($task->getPHID())) ->needComments(true) ->execute(); $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_VIEW); $field_list ->setViewer($user) ->readFieldsFromStorage($task); $e_commit = ManiphestTaskHasCommitEdgeType::EDGECONST; $e_dep_on = PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK; $e_dep_by = PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK; $e_rev = ManiphestTaskHasRevisionEdgeType::EDGECONST; $e_mock = PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK; $phid = $task->getPHID(); $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($phid)) ->withEdgeTypes( array( $e_commit, $e_dep_on, $e_dep_by, $e_rev, $e_mock, )); $edges = idx($query->execute(), $phid); $phids = array_fill_keys($query->getDestinationPHIDs(), true); foreach ($task->getCCPHIDs() as $phid) { $phids[$phid] = true; } foreach ($task->getProjectPHIDs() as $phid) { $phids[$phid] = true; } if ($task->getOwnerPHID()) { $phids[$task->getOwnerPHID()] = true; } $phids[$task->getAuthorPHID()] = true; $attached = $task->getAttached(); foreach ($attached as $type => $list) { foreach ($list as $phid => $info) { $phids[$phid] = true; } } if ($parent_task) { $phids[$parent_task->getPHID()] = true; } $phids = array_keys($phids); $this->loadHandles($phids); $handles = $this->getLoadedHandles(); $context_bar = null; if ($parent_task) { $context_bar = new AphrontContextBarView(); $context_bar->addButton(phutil_tag( 'a', array( 'href' => '/maniphest/task/create/?parent='.$parent_task->getID(), 'class' => 'green button', ), pht('Create Another Subtask'))); $context_bar->appendChild(hsprintf( 'Created a subtask of <strong>%s</strong>', $this->getHandle($parent_task->getPHID())->renderLink())); } else if ($workflow == 'create') { $context_bar = new AphrontContextBarView(); $context_bar->addButton(phutil_tag('label', array(), 'Create Another')); $context_bar->addButton(phutil_tag( 'a', array( 'href' => '/maniphest/task/create/?template='.$task->getID(), 'class' => 'green button', ), pht('Similar Task'))); $context_bar->addButton(phutil_tag( 'a', array( 'href' => '/maniphest/task/create/', 'class' => 'green button', ), pht('Empty Task'))); $context_bar->appendChild(pht('New task created.')); } $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); $engine->addObject($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION); foreach ($transactions as $modern_xaction) { if ($modern_xaction->getComment()) { $engine->addObject( $modern_xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $resolution_types = ManiphestTaskStatus::getTaskStatusMap(); $transaction_types = array( PhabricatorTransactions::TYPE_COMMENT => pht('Comment'), ManiphestTransaction::TYPE_STATUS => pht('Change Status'), ManiphestTransaction::TYPE_OWNER => pht('Reassign / Claim'), ManiphestTransaction::TYPE_CCS => pht('Add CCs'), ManiphestTransaction::TYPE_PRIORITY => pht('Change Priority'), ManiphestTransaction::TYPE_PROJECTS => pht('Associate Projects'), ); // Remove actions the user doesn't have permission to take. $requires = array( ManiphestTransaction::TYPE_OWNER => ManiphestEditAssignCapability::CAPABILITY, ManiphestTransaction::TYPE_PRIORITY => ManiphestEditPriorityCapability::CAPABILITY, ManiphestTransaction::TYPE_PROJECTS => ManiphestEditProjectsCapability::CAPABILITY, ManiphestTransaction::TYPE_STATUS => ManiphestEditStatusCapability::CAPABILITY, ); foreach ($transaction_types as $type => $name) { if (isset($requires[$type])) { if (!$this->hasApplicationCapability($requires[$type])) { unset($transaction_types[$type]); } } } // Don't show an option to change to the current status, or to change to // the duplicate status explicitly. unset($resolution_types[$task->getStatus()]); unset($resolution_types[ManiphestTaskStatus::getDuplicateStatus()]); // Don't show owner/priority changes for closed tasks, as they don't make // much sense. if ($task->isClosed()) { unset($transaction_types[ManiphestTransaction::TYPE_PRIORITY]); unset($transaction_types[ManiphestTransaction::TYPE_OWNER]); } $default_claim = array( $user->getPHID() => $user->getUsername().' ('.$user->getRealName().')', ); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), $task->getPHID()); if ($draft) { $draft_text = $draft->getDraft(); } else { $draft_text = null; } $comment_form = new AphrontFormView(); $comment_form ->setUser($user) ->setWorkflow(true) ->setAction('/maniphest/transaction/save/') ->setEncType('multipart/form-data') ->addHiddenInput('taskID', $task->getID()) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Action')) ->setName('action') ->setOptions($transaction_types) ->setID('transaction-action')) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('resolution') ->setControlID('resolution') ->setControlStyle('display: none') ->setOptions($resolution_types)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Assign To')) ->setName('assign_to') ->setControlID('assign_to') ->setControlStyle('display: none') ->setID('assign-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('CCs')) ->setName('ccs') ->setControlID('ccs') ->setControlStyle('display: none') ->setID('cc-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Priority')) ->setName('priority') ->setOptions($priority_map) ->setControlID('priority') ->setControlStyle('display: none') ->setValue($task->getPriority())) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setControlID('projects') ->setControlStyle('display: none') ->setID('projects-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('File')) ->setName('file') ->setControlID('file') ->setControlStyle('display: none')) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Comments')) ->setName('comments') ->setValue($draft_text) ->setID('transaction-comments') ->setUser($user)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Submit'))); $control_map = array( ManiphestTransaction::TYPE_STATUS => 'resolution', ManiphestTransaction::TYPE_OWNER => 'assign_to', ManiphestTransaction::TYPE_CCS => 'ccs', ManiphestTransaction::TYPE_PRIORITY => 'priority', ManiphestTransaction::TYPE_PROJECTS => 'projects', ); $projects_source = new PhabricatorProjectDatasource(); $users_source = new PhabricatorPeopleDatasource(); $mailable_source = new PhabricatorMetaMTAMailableDatasource(); $tokenizer_map = array( ManiphestTransaction::TYPE_PROJECTS => array( 'id' => 'projects-tokenizer', 'src' => $projects_source->getDatasourceURI(), 'placeholder' => $projects_source->getPlaceholderText(), ), ManiphestTransaction::TYPE_OWNER => array( 'id' => 'assign-tokenizer', 'src' => $users_source->getDatasourceURI(), 'value' => $default_claim, 'limit' => 1, 'placeholder' => $users_source->getPlaceholderText(), ), ManiphestTransaction::TYPE_CCS => array( 'id' => 'cc-tokenizer', 'src' => $mailable_source->getDatasourceURI(), 'placeholder' => $mailable_source->getPlaceholderText(), ), ); // TODO: Initializing these behaviors for logged out users fatals things. if ($user->isLoggedIn()) { Javelin::initBehavior('maniphest-transaction-controls', array( 'select' => 'transaction-action', 'controlMap' => $control_map, 'tokenizers' => $tokenizer_map, )); Javelin::initBehavior('maniphest-transaction-preview', array( 'uri' => '/maniphest/transaction/preview/'.$task->getID().'/', 'preview' => 'transaction-preview', 'comments' => 'transaction-comments', 'action' => 'transaction-action', 'map' => $control_map, 'tokenizers' => $tokenizer_map, )); } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $comment_header = $is_serious ? pht('Add Comment') : pht('Weigh In'); $preview_panel = phutil_tag_div( 'aphront-panel-preview', phutil_tag( 'div', array('id' => 'transaction-preview'), phutil_tag_div( 'aphront-panel-preview-loading-text', pht('Loading preview...')))); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($task->getPHID()) ->setTransactions($transactions) ->setMarkupEngine($engine); $object_name = 'T'.$task->getID(); $actions = $this->buildActionView($task); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb($object_name, '/'.$object_name) ->setActionList($actions); $header = $this->buildHeaderView($task); $properties = $this->buildPropertyView( $task, $field_list, $edges, $actions); $description = $this->buildDescriptionView($task, $engine); if (!$user->isLoggedIn()) { // TODO: Eventually, everything should run through this. For now, we're // only using it to get a consistent "Login to Comment" button. $comment_box = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($user) ->setRequestURI($request->getRequestURI()); $preview_panel = null; } else { $comment_box = id(new PHUIObjectBoxView()) ->setFlush(true) ->setHeaderText($comment_header) ->appendChild($comment_form); $timeline->setQuoteTargetID('transaction-comments'); $timeline->setQuoteRef($object_name); } $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); if ($description) { $object_box->addPropertyList($description); } return $this->buildApplicationPage( array( $crumbs, $context_bar, $object_box, $timeline, $comment_box, $preview_panel, ), array( 'title' => 'T'.$task->getID().' '.$task->getTitle(), 'pageObjects' => array($task->getPHID()), )); } private function buildHeaderView(ManiphestTask $task) { $view = id(new PHUIHeaderView()) ->setHeader($task->getTitle()) ->setUser($this->getRequest()->getUser()) ->setPolicyObject($task); $status = $task->getStatus(); $status_name = ManiphestTaskStatus::renderFullDescription($status); $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name); return $view; } private function buildActionView(ManiphestTask $task) { $viewer = $this->getRequest()->getUser(); $viewer_phid = $viewer->getPHID(); $viewer_is_cc = in_array($viewer_phid, $task->getCCPHIDs()); $id = $task->getID(); $phid = $task->getPHID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $task, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($task) ->setObjectURI($this->getRequest()->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Task')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("/task/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($task->getOwnerPHID() === $viewer_phid) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Automatically Subscribed')) ->setDisabled(true) ->setIcon('fa-check-circle')); } else { $action = $viewer_is_cc ? 'rem' : 'add'; $name = $viewer_is_cc ? pht('Unsubscribe') : pht('Subscribe'); $icon = $viewer_is_cc ? 'fa-minus-circle' : 'fa-plus-circle'; $view->addAction( id(new PhabricatorActionView()) ->setName($name) ->setHref("/maniphest/subscribe/{$action}/{$id}/") ->setRenderAsForm(true) ->setUser($viewer) ->setIcon($icon)); } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Merge Duplicates In')) ->setHref("/search/attach/{$phid}/TASK/merge/") ->setWorkflow(true) ->setIcon('fa-compress') ->setDisabled(!$can_edit) ->setWorkflow(true)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Create Subtask')) ->setHref($this->getApplicationURI("/task/create/?parent={$id}")) ->setIcon('fa-level-down')); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Blocking Tasks')) ->setHref("/search/attach/{$phid}/TASK/blocks/") ->setWorkflow(true) ->setIcon('fa-link') ->setDisabled(!$can_edit) ->setWorkflow(true)); return $view; } private function buildPropertyView( ManiphestTask $task, PhabricatorCustomFieldList $field_list, array $edges, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($task) ->setActionList($actions); $view->addProperty( pht('Assigned To'), $task->getOwnerPHID() ? $this->getHandle($task->getOwnerPHID())->renderLink() : phutil_tag('em', array(), pht('None'))); $view->addProperty( pht('Priority'), ManiphestTaskPriority::getTaskPriorityName($task->getPriority())); $handles = $this->getLoadedHandles(); $cc_handles = array_select_keys($handles, $task->getCCPHIDs()); $subscriber_html = id(new SubscriptionListStringBuilder()) ->setObjectPHID($task->getPHID()) ->setHandles($cc_handles) ->buildPropertyString(); $view->addProperty(pht('Subscribers'), $subscriber_html); $view->addProperty( pht('Author'), $this->getHandle($task->getAuthorPHID())->renderLink()); $source = $task->getOriginalEmailSource(); if ($source) { $subject = '[T'.$task->getID().'] '.$task->getTitle(); $view->addProperty( pht('From Email'), phutil_tag( 'a', array( - 'href' => 'mailto:'.$source.'?subject='.$subject + 'href' => 'mailto:'.$source.'?subject='.$subject, ), $source)); } $edge_types = array( PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK => pht('Blocks'), PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK => pht('Blocked By'), ManiphestTaskHasRevisionEdgeType::EDGECONST => pht('Differential Revisions'), PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK => pht('Pholio Mocks'), ); $revisions_commits = array(); $handles = $this->getLoadedHandles(); $commit_phids = array_keys( $edges[ManiphestTaskHasCommitEdgeType::EDGECONST]); if ($commit_phids) { $commit_drev = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV; $drev_edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($commit_phids) ->withEdgeTypes(array($commit_drev)) ->execute(); foreach ($commit_phids as $phid) { $revisions_commits[$phid] = $handles[$phid]->renderLink(); $revision_phid = key($drev_edges[$phid][$commit_drev]); $revision_handle = idx($handles, $revision_phid); if ($revision_handle) { $task_drev = ManiphestTaskHasRevisionEdgeType::EDGECONST; unset($edges[$task_drev][$revision_phid]); $revisions_commits[$phid] = hsprintf( '%s / %s', $revision_handle->renderLink($revision_handle->getName()), $revisions_commits[$phid]); } } } foreach ($edge_types as $edge_type => $edge_name) { if ($edges[$edge_type]) { $view->addProperty( $edge_name, $this->renderHandlesForPHIDs(array_keys($edges[$edge_type]))); } } if ($revisions_commits) { $view->addProperty( pht('Commits'), phutil_implode_html(phutil_tag('br'), $revisions_commits)); } $attached = $task->getAttached(); if (!is_array($attached)) { $attached = array(); } $file_infos = idx($attached, PhabricatorFileFilePHIDType::TYPECONST); if ($file_infos) { $file_phids = array_keys($file_infos); // TODO: These should probably be handles or something; clean this up // as we sort out file attachments. $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $file_view = new PhabricatorFileLinkListView(); $file_view->setFiles($files); $view->addProperty( pht('Files'), $file_view->render()); } $view->invokeWillRenderEvent(); $field_list->appendFieldsToPropertyList( $task, $viewer, $view); return $view; } private function buildDescriptionView( ManiphestTask $task, PhabricatorMarkupEngine $engine) { $section = null; if (strlen($task->getDescription())) { $section = new PHUIPropertyListView(); $section->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $section->addTextContent( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION))); } return $section; } } diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php index c71712315a..abad3b7ad6 100644 --- a/src/applications/maniphest/controller/ManiphestTaskEditController.php +++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php @@ -1,753 +1,756 @@ <?php final class ManiphestTaskEditController extends ManiphestController { private $id; public function willProcessRequest(array $data) { $this->id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $response_type = $request->getStr('responseType', 'task'); $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER); $can_edit_assign = $this->hasApplicationCapability( ManiphestEditAssignCapability::CAPABILITY); $can_edit_policies = $this->hasApplicationCapability( ManiphestEditPoliciesCapability::CAPABILITY); $can_edit_priority = $this->hasApplicationCapability( ManiphestEditPriorityCapability::CAPABILITY); $can_edit_projects = $this->hasApplicationCapability( ManiphestEditProjectsCapability::CAPABILITY); $can_edit_status = $this->hasApplicationCapability( ManiphestEditStatusCapability::CAPABILITY); $parent_task = null; $template_id = null; if ($this->id) { $task = id(new ManiphestTaskQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($this->id)) ->executeOne(); if (!$task) { return new Aphront404Response(); } } else { $task = ManiphestTask::initializeNewTask($user); // We currently do not allow you to set the task status when creating // a new task, although now that statuses are custom it might make // sense. $can_edit_status = false; // These allow task creation with defaults. if (!$request->isFormPost()) { $task->setTitle($request->getStr('title')); if ($can_edit_projects) { $projects = $request->getStr('projects'); if ($projects) { $tokens = $request->getStrList('projects'); $type_project = PhabricatorProjectProjectPHIDType::TYPECONST; foreach ($tokens as $key => $token) { if (phid_get_type($token) == $type_project) { // If this is formatted like a PHID, leave it as-is. continue; } if (preg_match('/^#/', $token)) { // If this already has a "#", leave it as-is. continue; } // Add a "#" prefix. $tokens[$key] = '#'.$token; } $default_projects = id(new PhabricatorObjectQuery()) ->setViewer($user) ->withNames($tokens) ->execute(); $default_projects = mpull($default_projects, 'getPHID'); if ($default_projects) { $task->attachProjectPHIDs($default_projects); } } } if ($can_edit_priority) { $priority = $request->getInt('priority'); if ($priority !== null) { $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); if (isset($priority_map[$priority])) { $task->setPriority($priority); } } } $task->setDescription($request->getStr('description')); if ($can_edit_assign) { $assign = $request->getStr('assign'); if (strlen($assign)) { $assign_user = id(new PhabricatorPeopleQuery()) ->setViewer($user) ->withUsernames(array($assign)) ->executeOne(); if (!$assign_user) { $assign_user = id(new PhabricatorPeopleQuery()) ->setViewer($user) ->withPHIDs(array($assign)) ->executeOne(); } if ($assign_user) { $task->setOwnerPHID($assign_user->getPHID()); } } } } $template_id = $request->getInt('template'); // You can only have a parent task if you're creating a new task. $parent_id = $request->getInt('parent'); if ($parent_id) { $parent_task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($parent_id)) ->executeOne(); if (!$template_id) { $template_id = $parent_id; } } } $errors = array(); $e_title = true; $field_list = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_EDIT); $field_list->setViewer($user); $field_list->readFieldsFromStorage($task); $aux_fields = $field_list->getFields(); if ($request->isFormPost()) { $changes = array(); $new_title = $request->getStr('title'); $new_desc = $request->getStr('description'); $new_status = $request->getStr('status'); if (!$task->getID()) { $workflow = 'create'; } else { $workflow = ''; } $changes[ManiphestTransaction::TYPE_TITLE] = $new_title; $changes[ManiphestTransaction::TYPE_DESCRIPTION] = $new_desc; if ($can_edit_status) { $changes[ManiphestTransaction::TYPE_STATUS] = $new_status; } else if (!$task->getID()) { // Create an initial status transaction for the burndown chart. // TODO: We can probably remove this once Facts comes online. $changes[ManiphestTransaction::TYPE_STATUS] = $task->getStatus(); } $owner_tokenizer = $request->getArr('assigned_to'); $owner_phid = reset($owner_tokenizer); if (!strlen($new_title)) { $e_title = pht('Required'); $errors[] = pht('Title is required.'); } $old_values = array(); foreach ($aux_fields as $aux_arr_key => $aux_field) { // TODO: This should be buildFieldTransactionsFromRequest() once we // switch to ApplicationTransactions properly. $aux_old_value = $aux_field->getOldValueForApplicationTransactions(); $aux_field->readValueFromRequest($request); $aux_new_value = $aux_field->getNewValueForApplicationTransactions(); // TODO: We're faking a call to the ApplicaitonTransaction validation // logic here. We need valid objects to pass, but they aren't used // in a meaningful way. For now, build User objects. Once the Maniphest // objects exist, this will switch over automatically. This is a big // hack but shouldn't be long for this world. $placeholder_editor = new PhabricatorUserProfileEditor(); $field_errors = $aux_field->validateApplicationTransactions( $placeholder_editor, PhabricatorTransactions::TYPE_CUSTOMFIELD, array( id(new ManiphestTransaction()) ->setOldValue($aux_old_value) ->setNewValue($aux_new_value), )); foreach ($field_errors as $error) { $errors[] = $error->getMessage(); } $old_values[$aux_field->getFieldKey()] = $aux_old_value; } if ($errors) { $task->setTitle($new_title); $task->setDescription($new_desc); $task->setPriority($request->getInt('priority')); $task->setOwnerPHID($owner_phid); $task->setCCPHIDs($request->getArr('cc')); $task->attachProjectPHIDs($request->getArr('projects')); } else { if ($can_edit_priority) { $changes[ManiphestTransaction::TYPE_PRIORITY] = $request->getInt('priority'); } if ($can_edit_assign) { $changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid; } $changes[ManiphestTransaction::TYPE_CCS] = $request->getArr('cc'); if ($can_edit_projects) { $projects = $request->getArr('projects'); $changes[ManiphestTransaction::TYPE_PROJECTS] = $projects; $column_phid = $request->getStr('columnPHID'); // allow for putting a task in a project column at creation -only- if (!$task->getID() && $column_phid && $projects) { $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($user) ->withProjectPHIDs($projects) ->withPHIDs(array($column_phid)) ->executeOne(); if ($column) { $changes[ManiphestTransaction::TYPE_PROJECT_COLUMN] = array( 'new' => array( 'projectPHID' => $column->getProjectPHID(), - 'columnPHIDs' => array($column_phid)), + 'columnPHIDs' => array($column_phid), + ), 'old' => array( 'projectPHID' => $column->getProjectPHID(), - 'columnPHIDs' => array())); + 'columnPHIDs' => array(), + ), + ); } } } if ($can_edit_policies) { $changes[PhabricatorTransactions::TYPE_VIEW_POLICY] = $request->getStr('viewPolicy'); $changes[PhabricatorTransactions::TYPE_EDIT_POLICY] = $request->getStr('editPolicy'); } $template = new ManiphestTransaction(); $transactions = array(); foreach ($changes as $type => $value) { $transaction = clone $template; $transaction->setTransactionType($type); if ($type == ManiphestTransaction::TYPE_PROJECT_COLUMN) { $transaction->setNewValue($value['new']); $transaction->setOldValue($value['old']); } else if ($type == ManiphestTransaction::TYPE_PROJECTS) { // TODO: Gross. $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $transaction ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $project_type) ->setNewValue( array( '=' => array_fuse($value), )); } else { $transaction->setNewValue($value); } $transactions[] = $transaction; } if ($aux_fields) { foreach ($aux_fields as $aux_field) { $transaction = clone $template; $transaction->setTransactionType( PhabricatorTransactions::TYPE_CUSTOMFIELD); $aux_key = $aux_field->getFieldKey(); $transaction->setMetadataValue('customfield:key', $aux_key); $old = idx($old_values, $aux_key); $new = $aux_field->getNewValueForApplicationTransactions(); $transaction->setOldValue($old); $transaction->setNewValue($new); $transactions[] = $transaction; } } if ($transactions) { $is_new = !$task->getID(); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); $event->setUser($user); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); $task = $event->getValue('task'); $transactions = $event->getValue('transactions'); $editor = id(new ManiphestTransactionEditor()) ->setActor($user) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->applyTransactions($task, $transactions); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); $event->setUser($user); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); } if ($parent_task) { // TODO: This should be transactional now. id(new PhabricatorEdgeEditor()) ->addEdge( $parent_task->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK, $task->getPHID()) ->save(); $workflow = $parent_task->getID(); } if ($request->isAjax()) { switch ($response_type) { case 'card': $owner = null; if ($task->getOwnerPHID()) { $owner = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($task->getOwnerPHID())) ->executeOne(); } $tasks = id(new ProjectBoardTaskCard()) ->setViewer($user) ->setTask($task) ->setOwner($owner) ->setCanEdit(true) ->getItem(); $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($user) ->withPHIDs(array($request->getStr('columnPHID'))) ->executeOne(); if (!$column) { return new Aphront404Response(); } $positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($user) ->withColumns(array($column)) ->execute(); $task_phids = mpull($positions, 'getObjectPHID'); $column_tasks = id(new ManiphestTaskQuery()) ->setViewer($user) ->withPHIDs($task_phids) ->execute(); if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { // TODO: This is a little bit awkward, because PHP and JS use // slightly different sort order parameters to achieve the same // effect. It would be unify this a bit at some point. $sort_map = array(); foreach ($positions as $position) { $sort_map[$position->getObjectPHID()] = array( -$position->getSequence(), $position->getID(), ); } } else { $sort_map = mpull( $column_tasks, 'getPrioritySortVector', 'getPHID'); } $data = array( 'sortMap' => $sort_map, ); break; case 'task': default: $tasks = $this->renderSingleTask($task); $data = array(); break; } return id(new AphrontAjaxResponse())->setContent( array( 'tasks' => $tasks, 'data' => $data, )); } $redirect_uri = '/T'.$task->getID(); if ($workflow) { $redirect_uri .= '?workflow='.$workflow; } return id(new AphrontRedirectResponse()) ->setURI($redirect_uri); } } else { if (!$task->getID()) { $task->setCCPHIDs(array( $user->getPHID(), )); if ($template_id) { $template_task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($template_id)) ->executeOne(); if ($template_task) { $cc_phids = array_unique(array_merge( $template_task->getCCPHIDs(), array($user->getPHID()))); $task->setCCPHIDs($cc_phids); $task->attachProjectPHIDs($template_task->getProjectPHIDs()); $task->setOwnerPHID($template_task->getOwnerPHID()); $task->setPriority($template_task->getPriority()); $task->setViewPolicy($template_task->getViewPolicy()); $task->setEditPolicy($template_task->getEditPolicy()); $template_fields = PhabricatorCustomField::getObjectFields( $template_task, PhabricatorCustomField::ROLE_EDIT); $fields = $template_fields->getFields(); foreach ($fields as $key => $field) { if (!$field->shouldCopyWhenCreatingSimilarTask()) { unset($fields[$key]); } if (empty($aux_fields[$key])) { unset($fields[$key]); } } if ($fields) { id(new PhabricatorCustomFieldList($fields)) ->setViewer($user) ->readFieldsFromStorage($template_task); foreach ($fields as $key => $field) { $aux_fields[$key]->setValueFromStorage( $field->getValueForStorage()); } } } } } } $phids = array_merge( array($task->getOwnerPHID()), $task->getCCPHIDs(), $task->getProjectPHIDs()); if ($parent_task) { $phids[] = $parent_task->getPHID(); } $phids = array_filter($phids); $phids = array_unique($phids); $handles = $this->loadViewerHandles($phids); $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); } $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); if ($task->getOwnerPHID()) { $assigned_value = array($handles[$task->getOwnerPHID()]); } else { $assigned_value = array(); } if ($task->getCCPHIDs()) { $cc_value = array_select_keys($handles, $task->getCCPHIDs()); } else { $cc_value = array(); } if ($task->getProjectPHIDs()) { $projects_value = array_select_keys($handles, $task->getProjectPHIDs()); } else { $projects_value = array(); } $cancel_id = nonempty($task->getID(), $template_id); if ($cancel_id) { $cancel_uri = '/T'.$cancel_id; } else { $cancel_uri = '/maniphest/'; } if ($task->getID()) { $button_name = pht('Save Task'); $header_name = pht('Edit Task'); } else if ($parent_task) { $cancel_uri = '/T'.$parent_task->getID(); $button_name = pht('Create Task'); $header_name = pht('Create New Subtask'); } else { $button_name = pht('Create Task'); $header_name = pht('Create New Task'); } require_celerity_resource('maniphest-task-edit-css'); $project_tokenizer_id = celerity_generate_unique_node_id(); $form = new AphrontFormView(); $form ->setUser($user) ->addHiddenInput('template', $template_id) ->addHiddenInput('responseType', $response_type) ->addHiddenInput('order', $order) ->addHiddenInput('ungrippable', $request->getStr('ungrippable')) ->addHiddenInput('columnPHID', $request->getStr('columnPHID')); if ($parent_task) { $form ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Parent Task')) ->setValue($handles[$parent_task->getPHID()]->getFullName())) ->addHiddenInput('parent', $parent_task->getID()); } $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Title')) ->setName('title') ->setError($e_title) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setValue($task->getTitle())); if ($can_edit_status) { // See T4819. $status_map = ManiphestTaskStatus::getTaskStatusMap(); $dup_status = ManiphestTaskStatus::getDuplicateStatus(); if ($task->getStatus() != $dup_status) { unset($status_map[$dup_status]); } $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('status') ->setValue($task->getStatus()) ->setOptions($status_map)); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($task) ->execute(); if ($can_edit_assign) { $form->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Assigned To')) ->setName('assigned_to') ->setValue($assigned_value) ->setUser($user) ->setDatasource(new PhabricatorPeopleDatasource()) ->setLimit(1)); } $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('CC')) ->setName('cc') ->setValue($cc_value) ->setUser($user) ->setDatasource(new PhabricatorMetaMTAMailableDatasource())); if ($can_edit_priority) { $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Priority')) ->setName('priority') ->setOptions($priority_map) ->setValue($task->getPriority())); } if ($can_edit_policies) { $form ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($task) ->setPolicies($policies) ->setName('viewPolicy')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($task) ->setPolicies($policies) ->setName('editPolicy')); } if ($can_edit_projects) { $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($projects_value) ->setID($project_tokenizer_id) ->setCaption( javelin_tag( 'a', array( 'href' => '/project/create/', 'mustcapture' => true, 'sigil' => 'project-create', ), pht('Create New Project'))) ->setDatasource(new PhabricatorProjectDatasource())); } $field_list->appendFieldsToForm($form); require_celerity_resource('aphront-error-view-css'); Javelin::initBehavior('project-create', array( 'tokenizerID' => $project_tokenizer_id, )); $description_control = new PhabricatorRemarkupControl(); // "Upsell" creating tasks via email in create flows if the instance is // configured for this awesomeness. $email_create = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.public-create-email'); if (!$task->getID() && $email_create) { $email_hint = pht( 'You can also create tasks by sending an email to: %s', phutil_tag('tt', array(), $email_create)); $description_control->setCaption($email_hint); } $description_control ->setLabel(pht('Description')) ->setName('description') ->setID('description-textarea') ->setValue($task->getDescription()) ->setUser($user); $form ->appendChild($description_control); if ($request->isAjax()) { $dialog = id(new AphrontDialogView()) ->setUser($user) ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle($header_name) ->appendChild( array( $error_view, $form->buildLayoutView(), )) ->addCancelButton($cancel_uri) ->addSubmitButton($button_name); return id(new AphrontDialogResponse())->setDialog($dialog); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($button_name)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($header_name) ->setFormErrors($errors) ->setForm($form); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Description Preview')) ->setControlID('description-textarea') ->setPreviewURI($this->getApplicationURI('task/descriptionpreview/')); if ($task->getID()) { $page_objects = array($task->getPHID()); } else { $page_objects = array(); } $crumbs = $this->buildApplicationCrumbs(); if ($task->getID()) { $crumbs->addTextCrumb('T'.$task->getID(), '/T'.$task->getID()); } $crumbs->addTextCrumb($header_name); return $this->buildApplicationPage( array( $crumbs, $form_box, $preview, ), array( 'title' => $header_name, 'pageObjects' => $page_objects, )); } } diff --git a/src/applications/maniphest/view/ManiphestTaskResultListView.php b/src/applications/maniphest/view/ManiphestTaskResultListView.php index a1eb40e2be..091317d860 100644 --- a/src/applications/maniphest/view/ManiphestTaskResultListView.php +++ b/src/applications/maniphest/view/ManiphestTaskResultListView.php @@ -1,284 +1,284 @@ <?php final class ManiphestTaskResultListView extends ManiphestView { private $tasks; private $savedQuery; private $canEditPriority; private $canBatchEdit; private $showBatchControls; public function setSavedQuery(PhabricatorSavedQuery $query) { $this->savedQuery = $query; return $this; } public function setTasks(array $tasks) { $this->tasks = $tasks; return $this; } public function setCanEditPriority($can_edit_priority) { $this->canEditPriority = $can_edit_priority; return $this; } public function setCanBatchEdit($can_batch_edit) { $this->canBatchEdit = $can_batch_edit; return $this; } public function setShowBatchControls($show_batch_controls) { $this->showBatchControls = $show_batch_controls; return $this; } public function render() { $viewer = $this->getUser(); $tasks = $this->tasks; $query = $this->savedQuery; // If we didn't match anything, just pick up the default empty state. if (!$tasks) { return id(new PHUIObjectItemListView()) ->setUser($viewer); } $group_parameter = nonempty($query->getParameter('group'), 'priority'); $order_parameter = nonempty($query->getParameter('order'), 'priority'); $handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); $groups = $this->groupTasks( $tasks, $group_parameter, $handles); $can_edit_priority = $this->canEditPriority; $can_drag = ($order_parameter == 'priority') && ($can_edit_priority) && ($group_parameter == 'none' || $group_parameter == 'priority'); if (!$viewer->isLoggedIn()) { // TODO: (T603) Eventually, we conceivably need to make each task // draggable individually, since the user may be able to edit some but // not others. $can_drag = false; } $result = array(); $lists = array(); foreach ($groups as $group => $list) { $task_list = new ManiphestTaskListView(); $task_list->setShowBatchControls($this->showBatchControls); if ($can_drag) { $task_list->setShowSubpriorityControls(true); } $task_list->setUser($viewer); $task_list->setTasks($list); $task_list->setHandles($handles); $header = javelin_tag( 'h1', array( 'class' => 'maniphest-task-group-header', 'sigil' => 'task-group', 'meta' => array( 'priority' => head($list)->getPriority(), ), ), pht('%s (%s)', $group, new PhutilNumber(count($list)))); $lists[] = phutil_tag( 'div', array( - 'class' => 'maniphest-task-group' + 'class' => 'maniphest-task-group', ), array( $header, $task_list, )); } if ($can_drag) { Javelin::initBehavior( 'maniphest-subpriority-editor', array( 'uri' => '/maniphest/subpriority/', )); } return phutil_tag( 'div', array( 'class' => 'maniphest-list-container', ), array( $lists, $this->showBatchControls ? $this->renderBatchEditor($query) : null, )); } private function groupTasks(array $tasks, $group, array $handles) { assert_instances_of($tasks, 'ManiphestTask'); assert_instances_of($handles, 'PhabricatorObjectHandle'); $groups = $this->getTaskGrouping($tasks, $group); $results = array(); foreach ($groups as $label_key => $tasks) { $label = $this->getTaskLabelName($group, $label_key, $handles); $results[$label][] = $tasks; } foreach ($results as $label => $task_groups) { $results[$label] = array_mergev($task_groups); } return $results; } private function getTaskGrouping(array $tasks, $group) { switch ($group) { case 'priority': return mgroup($tasks, 'getPriority'); case 'status': return mgroup($tasks, 'getStatus'); case 'assigned': return mgroup($tasks, 'getOwnerPHID'); case 'project': return mgroup($tasks, 'getGroupByProjectPHID'); default: return array(pht('Tasks') => $tasks); } } private function getTaskLabelName($group, $label_key, array $handles) { switch ($group) { case 'priority': return ManiphestTaskPriority::getTaskPriorityName($label_key); case 'status': return ManiphestTaskStatus::getTaskStatusFullName($label_key); case 'assigned': if ($label_key) { return $handles[$label_key]->getFullName(); } else { return pht('(Not Assigned)'); } case 'project': if ($label_key) { return $handles[$label_key]->getFullName(); } else { // This may mean "No Projects", or it may mean the query has project // constraints but the task is only in constrained projects (in this // case, we don't show the group because it would always have all // of the tasks). Since distinguishing between these two cases is // messy and the UI is reasonably clear, label generically. return pht('(Ungrouped)'); } default: return pht('Tasks'); } } private function renderBatchEditor(PhabricatorSavedQuery $saved_query) { $user = $this->getUser(); if (!$this->canBatchEdit) { return null; } if (!$user->isLoggedIn()) { // Don't show the batch editor or excel export for logged-out users. // Technically we //could// let them export, but ehh. return null; } Javelin::initBehavior( 'maniphest-batch-selector', array( 'selectAll' => 'batch-select-all', 'selectNone' => 'batch-select-none', 'submit' => 'batch-select-submit', 'status' => 'batch-select-status-cell', 'idContainer' => 'batch-select-id-container', 'formID' => 'batch-select-form', )); $select_all = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'class' => 'grey button', 'id' => 'batch-select-all', ), pht('Select All')); $select_none = javelin_tag( 'a', array( 'href' => '#', 'mustcapture' => true, 'class' => 'grey button', 'id' => 'batch-select-none', ), pht('Clear Selection')); $submit = phutil_tag( 'button', array( 'id' => 'batch-select-submit', 'disabled' => 'disabled', 'class' => 'disabled', ), pht("Batch Edit Selected \xC2\xBB")); $export = javelin_tag( 'a', array( 'href' => '/maniphest/export/'.$saved_query->getQueryKey().'/', 'class' => 'grey button', ), pht('Export to Excel')); $hidden = phutil_tag( 'div', array( 'id' => 'batch-select-id-container', ), ''); $editor = hsprintf( '<div class="maniphest-batch-editor">'. '<div class="batch-editor-header">%s</div>'. '<table class="maniphest-batch-editor-layout">'. '<tr>'. '<td>%s%s</td>'. '<td>%s</td>'. '<td id="batch-select-status-cell">%s</td>'. '<td class="batch-select-submit-cell">%s%s</td>'. '</tr>'. '</table>'. '</div>', pht('Batch Task Editor'), $select_all, $select_none, $export, '', $submit, $hidden); $editor = phabricator_form( $user, array( 'method' => 'POST', 'action' => '/maniphest/batch/', 'id' => 'batch-select-form', ), $editor); return $editor; } } diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php index 92166b6dac..9f62049444 100644 --- a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php @@ -1,110 +1,110 @@ <?php /** * Mail adapter that doesn't actually send any email, for writing unit tests * against. */ final class PhabricatorMailImplementationTestAdapter extends PhabricatorMailImplementationAdapter { private $guts = array(); private $config; public function __construct(array $config = array()) { $this->config = $config; } public function setFrom($email, $name = '') { $this->guts['from'] = $email; $this->guts['from-name'] = $name; return $this; } public function addReplyTo($email, $name = '') { if (empty($this->guts['reply-to'])) { $this->guts['reply-to'] = array(); } $this->guts['reply-to'][] = array( 'email' => $email, 'name' => $name, ); return $this; } public function addTos(array $emails) { foreach ($emails as $email) { $this->guts['tos'][] = $email; } return $this; } public function addCCs(array $emails) { foreach ($emails as $email) { $this->guts['ccs'][] = $email; } return $this; } public function addAttachment($data, $filename, $mimetype) { $this->guts['attachments'][] = array( 'data' => $data, 'filename' => $filename, - 'mimetype' => $mimetype + 'mimetype' => $mimetype, ); return $this; } public function addHeader($header_name, $header_value) { $this->guts['headers'][] = array($header_name, $header_value); return $this; } public function setBody($body) { $this->guts['body'] = $body; return $this; } public function setHTMLBody($html_body) { $this->guts['html-body'] = $html_body; return $this; } public function setSubject($subject) { $this->guts['subject'] = $subject; return $this; } public function supportsMessageIDHeader() { return $this->config['supportsMessageIDHeader']; } public function send() { if (!empty($this->guts['fail-permanently'])) { throw new PhabricatorMetaMTAPermanentFailureException( 'Unit Test (Permanent)'); } if (!empty($this->guts['fail-temporarily'])) { throw new Exception( 'Unit Test (Temporary)'); } $this->guts['did-send'] = true; return true; } public function getGuts() { return $this->guts; } public function setFailPermanently($fail) { $this->guts['fail-permanently'] = $fail; return $this; } public function setFailTemporarily($fail) { $this->guts['fail-temporarily'] = $fail; return $this; } } diff --git a/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php b/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php index 7ee2e02485..f138ba243a 100644 --- a/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php +++ b/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php @@ -1,128 +1,129 @@ <?php final class PhabricatorMetaMTAEmailBodyParser { /** * Mails can have bodies such as * * !claim * * taking this task * * Or * * !assign epriestley * * please, take this task I took; its hard * * This function parses such an email body and returns a dictionary * containing a clean body text (e.g. "taking this task"), a $command * (e.g. !claim, !assign) and a $command_value (e.g. "epriestley" in the * !assign example.) * * @return dict */ public function parseBody($body) { $body = $this->stripTextBody($body); $lines = explode("\n", trim($body)); $first_line = head($lines); $command = null; $command_value = null; $matches = null; if (preg_match('/^!(\w+)\s*(\S+)?/', $first_line, $matches)) { $lines = array_slice($lines, 1); $body = implode("\n", $lines); $body = trim($body); $command = $matches[1]; $command_value = idx($matches, 2); } return array( 'body' => $body, 'command' => $command, - 'command_value' => $command_value); + 'command_value' => $command_value, + ); } public function stripTextBody($body) { return trim($this->stripSignature($this->stripQuotedText($body))); } private function stripQuotedText($body) { // Look for "On <date>, <user> wrote:". This may be split across multiple // lines. We need to be careful not to remove all of a message like this: // // On which day do you want to meet? // // On <date>, <user> wrote: // > Let's set up a meeting. $start = null; $lines = phutil_split_lines($body); foreach ($lines as $key => $line) { if (preg_match('/^\s*>?\s*On\b/', $line)) { $start = $key; } if ($start !== null) { if (preg_match('/\bwrote:/', $line)) { $lines = array_slice($lines, 0, $start); $body = implode('', $lines); break; } } } // Outlook english $body = preg_replace( '/^\s*(> )?-----Original Message-----.*?/imsU', '', $body); // Outlook danish $body = preg_replace( '/^\s*(> )?-----Oprindelig Meddelelse-----.*?/imsU', '', $body); // See example in T3217. $body = preg_replace( '/^________________________________________\s+From:.*?/imsU', '', $body); return rtrim($body); } private function stripSignature($body) { // Quasi-"standard" delimiter, for lols see: // https://bugzilla.mozilla.org/show_bug.cgi?id=58406 $body = preg_replace( '/^-- +$.*/sm', '', $body); // Mailbox seems to make an attempt to comply with the "standard" but // omits the leading newline and uses an em dash? $body = preg_replace( "/\s*\xE2\x80\x94 \nSent from Mailbox\s*\z/su", '', $body); // HTC Mail application (mobile) $body = preg_replace( '/^\s*^Sent from my HTC smartphone.*/sm', '', $body); // Apple iPhone $body = preg_replace( '/^\s*^Sent from my iPhone\s*$.*/sm', '', $body); return rtrim($body); } } diff --git a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php index 9f5b7be099..e52b5c52c0 100644 --- a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php +++ b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php @@ -1,183 +1,184 @@ <?php /** * Render the body of an application email by building it up section-by-section. * * @task compose Composition * @task render Rendering */ final class PhabricatorMetaMTAMailBody { private $sections = array(); private $htmlSections = array(); private $attachments = array(); /* -( Composition )-------------------------------------------------------- */ /** * Add a raw block of text to the email. This will be rendered as-is. * * @param string Block of text. * @return this * @task compose */ public function addRawSection($text) { if (strlen($text)) { $text = rtrim($text); $this->sections[] = $text; $this->htmlSections[] = phutil_escape_html_newlines( phutil_tag('div', array(), $text)); } return $this; } public function addRawPlaintextSection($text) { if (strlen($text)) { $text = rtrim($text); $this->sections[] = $text; } return $this; } public function addRawHTMLSection($html) { $this->htmlSections[] = phutil_safe_html($html); return $this; } /** * Add a block of text with a section header. This is rendered like this: * * HEADER * Text is indented. * * @param string Header text. * @param string Section text. * @return this * @task compose */ public function addTextSection($header, $section) { if ($section instanceof PhabricatorMetaMTAMailSection) { $plaintext = $section->getPlaintext(); $html = $section->getHTML(); } else { $plaintext = $section; $html = phutil_escape_html_newlines(phutil_tag('div', array(), $section)); } $this->addPlaintextSection($header, $plaintext); $this->addHTMLSection($header, $html); return $this; } public function addPlaintextSection($header, $text) { $this->sections[] = $header."\n".$this->indent($text); return $this; } public function addHTMLSection($header, $html_fragment) { $this->htmlSections[] = array( phutil_tag('div', array('style' => 'font-weight:800;'), $header), - $html_fragment); + $html_fragment, + ); return $this; } /** * Add a Herald section with a rule management URI and a transcript URI. * * @param string URI to rule transcripts. * @return this * @task compose */ public function addHeraldSection($xscript_uri) { if (!PhabricatorEnv::getEnvConfig('metamta.herald.show-hints')) { return $this; } $this->addTextSection( pht('WHY DID I GET THIS EMAIL?'), PhabricatorEnv::getProductionURI($xscript_uri)); return $this; } /** * Add a section with reply handler instructions. * * @param string Reply handler instructions. * @return this * @task compose */ public function addReplySection($instructions) { if (!PhabricatorEnv::getEnvConfig('metamta.reply.show-hints')) { return $this; } if (!strlen($instructions)) { return $this; } $this->addTextSection(pht('REPLY HANDLER ACTIONS'), $instructions); return $this; } /** * Add an attachment. * * @param PhabricatorMetaMTAAttachment Attachment. * @return this * @task compose */ public function addAttachment(PhabricatorMetaMTAAttachment $attachment) { $this->attachments[] = $attachment; return $this; } /* -( Rendering )---------------------------------------------------------- */ /** * Render the email body. * * @return string Rendered body. * @task render */ public function render() { return implode("\n\n", $this->sections)."\n"; } public function renderHTML() { $br = phutil_tag('br'); $body = phutil_implode_html($br, $this->htmlSections); return (string)hsprintf('%s', array($body, $br)); } /** * Retrieve attachments. * * @return list<PhabricatorMetaMTAAttachment> Attachments. * @task render */ public function getAttachments() { return $this->attachments; } /** * Indent a block of text for rendering under a section heading. * * @param string Text to indent. * @return string Indented text. * @task render */ private function indent($text) { return rtrim(" ".str_replace("\n", "\n ", $text)); } } diff --git a/src/applications/notification/controller/PhabricatorNotificationPanelController.php b/src/applications/notification/controller/PhabricatorNotificationPanelController.php index 71fb663bfe..850bfe7392 100644 --- a/src/applications/notification/controller/PhabricatorNotificationPanelController.php +++ b/src/applications/notification/controller/PhabricatorNotificationPanelController.php @@ -1,93 +1,93 @@ <?php final class PhabricatorNotificationPanelController extends PhabricatorNotificationController { public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $query = id(new PhabricatorNotificationQuery()) ->setViewer($user) ->withUserPHIDs(array($user->getPHID())) ->setLimit(15); $stories = $query->execute(); $clear_ui_class = 'phabricator-notification-clear-all'; $clear_uri = id(new PhutilURI('/notification/clear/')); if ($stories) { $builder = new PhabricatorNotificationBuilder($stories); $notifications_view = $builder->buildView(); $content = $notifications_view->render(); $clear_uri->setQueryParam( 'chronoKey', head($stories)->getChronologicalKey()); } else { $content = phutil_tag_div( 'phabricator-notification no-notifications', pht('You have no notifications.')); $clear_ui_class .= ' disabled'; } $clear_ui = javelin_tag( 'a', array( 'sigil' => 'workflow', 'href' => (string) $clear_uri, 'class' => $clear_ui_class, ), pht('Mark All Read')); $notifications_link = phutil_tag( 'a', array( 'href' => '/notification/', ), pht('Notifications')); if (PhabricatorEnv::getEnvConfig('notification.enabled')) { $connection_status = new PhabricatorNotificationStatusView(); } else { $connection_status = phutil_tag( 'a', array( 'href' => PhabricatorEnv::getDoclink( 'Notifications User Guide: Setup and Configuration'), ), pht('Notification Server not enabled.')); } $connection_ui = phutil_tag( 'div', array( - 'class' => 'phabricator-notification-footer' + 'class' => 'phabricator-notification-footer', ), $connection_status); $header = phutil_tag( 'div', array( 'class' => 'phabricator-notification-header', ), array( $notifications_link, $clear_ui, )); $content = hsprintf( '%s%s%s', $header, $content, $connection_ui); $unread_count = id(new PhabricatorFeedStoryNotification()) ->countUnread($user); $json = array( 'content' => $content, 'number' => (int)$unread_count, ); return id(new AphrontAjaxResponse())->setContent($json); } } diff --git a/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php b/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php index 070c822737..d9589e7149 100644 --- a/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php +++ b/src/applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php @@ -1,96 +1,96 @@ <?php final class PhabricatorOAuthServerTestCase extends PhabricatorTestCase { public function testValidateRedirectURI() { static $map = array( 'http://www.google.com' => true, 'http://www.google.com/' => true, 'http://www.google.com/auth' => true, 'www.google.com' => false, - 'http://www.google.com/auth#invalid' => false + 'http://www.google.com/auth#invalid' => false, ); $server = new PhabricatorOAuthServer(); foreach ($map as $input => $expected) { $uri = new PhutilURI($input); $result = $server->validateRedirectURI($uri); $this->assertEqual( $expected, $result, "Validation of redirect URI '{$input}'"); } } public function testValidateSecondaryRedirectURI() { $server = new PhabricatorOAuthServer(); $primary_uri = new PhutilURI('http://www.google.com'); static $test_domain_map = array( 'http://www.google.com' => true, 'http://www.google.com/' => true, 'http://www.google.com/auth' => true, 'http://www.google.com/?auth' => true, 'www.google.com' => false, 'http://www.google.com/auth#invalid' => false, - 'http://www.example.com' => false + 'http://www.example.com' => false, ); foreach ($test_domain_map as $input => $expected) { $uri = new PhutilURI($input); $this->assertEqual( $expected, $server->validateSecondaryRedirectURI($uri, $primary_uri), "Validation of redirect URI '{$input}' ". "relative to '{$primary_uri}'"); } $primary_uri = new PhutilURI('http://www.google.com/?auth'); static $test_query_map = array( 'http://www.google.com' => false, 'http://www.google.com/' => false, 'http://www.google.com/auth' => false, 'http://www.google.com/?auth' => true, 'http://www.google.com/?auth&stuff' => true, 'http://www.google.com/?stuff' => false, ); foreach ($test_query_map as $input => $expected) { $uri = new PhutilURI($input); $this->assertEqual( $expected, $server->validateSecondaryRedirectURI($uri, $primary_uri), "Validation of secondary redirect URI '{$input}' ". "relative to '{$primary_uri}'"); } $primary_uri = new PhutilURI('https://secure.example.com/'); $tests = array( 'https://secure.example.com/' => true, 'http://secure.example.com/' => false, ); foreach ($tests as $input => $expected) { $uri = new PhutilURI($input); $this->assertEqual( $expected, $server->validateSecondaryRedirectURI($uri, $primary_uri), "Validation (https): {$input}"); } $primary_uri = new PhutilURI('http://example.com/?z=2&y=3'); $tests = array( 'http://example.com?z=2&y=3' => true, 'http://example.com?y=3&z=2' => true, 'http://example.com?y=3&z=2&x=1' => true, 'http://example.com?y=2&z=3' => false, 'http://example.com?y&x' => false, 'http://example.com?z=2&x=3' => false, ); foreach ($tests as $input => $expected) { $uri = new PhutilURI($input); $this->assertEqual( $expected, $server->validateSecondaryRedirectURI($uri, $primary_uri), "Validation (params): {$input}"); } } } diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php index 11330447e6..48fe6ae98c 100644 --- a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php +++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php @@ -1,266 +1,266 @@ <?php final class PhabricatorOAuthServerAuthController extends PhabricatorAuthController { public function shouldRequireLogin() { return true; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $server = new PhabricatorOAuthServer(); $client_phid = $request->getStr('client_id'); $scope = $request->getStr('scope', array()); $redirect_uri = $request->getStr('redirect_uri'); $response_type = $request->getStr('response_type'); // state is an opaque value the client sent us for their own purposes // we just need to send it right back to them in the response! $state = $request->getStr('state'); if (!$client_phid) { return $this->buildErrorResponse( 'invalid_request', pht('Malformed Request'), pht( 'Required parameter %s was not present in the request.', phutil_tag('strong', array(), 'client_id'))); } $server->setUser($viewer); $is_authorized = false; $authorization = null; $uri = null; $name = null; // one giant try / catch around all the exciting database stuff so we // can return a 'server_error' response if something goes wrong! try { $client = id(new PhabricatorOAuthServerClient()) ->loadOneWhere('phid = %s', $client_phid); if (!$client) { return $this->buildErrorResponse( 'invalid_request', pht('Invalid Client Application'), pht( 'Request parameter %s does not specify a valid client application.', phutil_tag('strong', array(), 'client_id'))); } $name = $client->getName(); $server->setClient($client); if ($redirect_uri) { $client_uri = new PhutilURI($client->getRedirectURI()); $redirect_uri = new PhutilURI($redirect_uri); if (!($server->validateSecondaryRedirectURI($redirect_uri, $client_uri))) { return $this->buildErrorResponse( 'invalid_request', pht('Invalid Redirect URI'), pht( 'Request parameter %s specifies an invalid redirect URI. '. 'The redirect URI must be a fully-qualified domain with no '. 'fragments, and must have the same domain and at least '. 'the same query parameters as the redirect URI the client '. 'registered.', phutil_tag('strong', array(), 'redirect_uri'))); } $uri = $redirect_uri; } else { $uri = new PhutilURI($client->getRedirectURI()); } if (empty($response_type)) { return $this->buildErrorResponse( 'invalid_request', pht('Invalid Response Type'), pht( 'Required request parameter %s is missing.', phutil_tag('strong', array(), 'response_type'))); } if ($response_type != 'code') { return $this->buildErrorResponse( 'unsupported_response_type', pht('Unsupported Response Type'), pht( 'Request parameter %s specifies an unsupported response type. '. 'Valid response types are: %s.', phutil_tag('strong', array(), 'response_type'), implode(', ', array('code')))); } if ($scope) { if (!PhabricatorOAuthServerScope::validateScopesList($scope)) { return $this->buildErrorResponse( 'invalid_scope', pht('Invalid Scope'), pht( 'Request parameter %s specifies an unsupported scope.', phutil_tag('strong', array(), 'scope'))); } $scope = PhabricatorOAuthServerScope::scopesListToDict($scope); } // NOTE: We're always requiring a confirmation dialog to redirect. // Partly this is a general defense against redirect attacks, and // partly this shakes off anchors in the URI (which are not shaken // by 302'ing). $auth_info = $server->userHasAuthorizedClient($scope); list($is_authorized, $authorization) = $auth_info; if ($request->isFormPost()) { // TODO: We should probably validate this more? It feels a little funky. $scope = PhabricatorOAuthServerScope::getScopesFromRequest($request); if ($authorization) { $authorization->setScope($scope)->save(); } else { $authorization = $server->authorizeClient($scope); } $is_authorized = true; } } catch (Exception $e) { return $this->buildErrorResponse( 'server_error', pht('Server Error'), pht( 'The authorization server encountered an unexpected condition '. 'which prevented it from fulfilling the request.')); } // When we reach this part of the controller, we can be in two states: // // 1. The user has not authorized the application yet. We want to // give them an "Authorize this application?" dialog. // 2. The user has authorized the application. We want to give them // a "Confirm Login" dialog. if ($is_authorized) { // The second case is simpler, so handle it first. The user either // authorized the application previously, or has just authorized the // application. Show them a confirm dialog with a normal link back to // the application. This shakes anchors from the URI. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $auth_code = $server->generateAuthorizationCode($uri); unset($unguarded); $full_uri = $this->addQueryParams( $uri, array( 'code' => $auth_code->getCode(), 'scope' => $authorization->getScopeString(), 'state' => $state, )); // TODO: It would be nice to give the user more options here, like // reviewing permissions, canceling the authorization, or aborting // the workflow. $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Authenticate: %s', $name)) ->appendParagraph( pht( 'This application ("%s") is authorized to use your Phabricator '. 'credentials. Continue to complete the authentication workflow.', phutil_tag('strong', array(), $name))) ->addCancelButton((string)$full_uri, pht('Continue to Application')); return id(new AphrontDialogResponse())->setDialog($dialog); } // Here, we're confirming authorization for the application. if ($scope) { if ($authorization) { $desired_scopes = array_merge($scope, $authorization->getScope()); } else { $desired_scopes = $scope; } if (!PhabricatorOAuthServerScope::validateScopesDict($desired_scopes)) { return $this->buildErrorResponse( 'invalid_scope', pht('Invalid Scope'), pht('The requested scope is invalid, unknown, or malformed.')); } } else { $desired_scopes = array( PhabricatorOAuthServerScope::SCOPE_WHOAMI => 1, - PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS => 1 + PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS => 1, ); } $form = id(new AphrontFormView()) ->addHiddenInput('client_id', $client_phid) ->addHiddenInput('redirect_uri', $redirect_uri) ->addHiddenInput('response_type', $response_type) ->addHiddenInput('state', $state) ->addHiddenInput('scope', $request->getStr('scope')) ->setUser($viewer) ->appendChild( PhabricatorOAuthServerScope::getCheckboxControl($desired_scopes)); $cancel_msg = pht('The user declined to authorize this application.'); $cancel_uri = $this->addQueryParams( $uri, array( 'error' => 'access_denied', 'error_description' => $cancel_msg, )); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Authorize "%s"?', $name)) ->setSubmitURI($request->getRequestURI()->getPath()) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendParagraph( pht( 'Do you want to authorize the external application "%s" to '. 'access your Phabricator account data?', phutil_tag('strong', array(), $name))) ->appendChild($form->buildLayoutView()) ->addSubmitButton(pht('Authorize Access')) ->addCancelButton((string)$cancel_uri, pht('Do Not Authorize')); return id(new AphrontDialogResponse())->setDialog($dialog); } private function buildErrorResponse($code, $title, $message) { $viewer = $this->getRequest()->getUser(); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('OAuth: %s', $title)) ->appendParagraph($message) ->appendParagraph( pht('OAuth Error Code: %s', phutil_tag('tt', array(), $code))) ->addCancelButton('/', pht('Alas!')); return id(new AphrontDialogResponse())->setDialog($dialog); } private function addQueryParams(PhutilURI $uri, array $params) { $full_uri = clone $uri; foreach ($params as $key => $value) { if (strlen($value)) { $full_uri->setQueryParam($key, $value); } } return $full_uri; } } diff --git a/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php b/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php index 57b59e92ad..f0908f83e7 100644 --- a/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php +++ b/src/applications/owners/conduit/OwnersQueryConduitAPIMethod.php @@ -1,157 +1,157 @@ <?php final class OwnersQueryConduitAPIMethod extends OwnersConduitAPIMethod { public function getAPIMethodName() { return 'owners.query'; } public function getMethodDescription() { return 'Query for packages by one of the following: repository/path, '. 'packages with a given user or project owner, or packages affiliated '. 'with a user (owned by either the user or a project they are a member '. 'of.) You should only provide at most one search query.'; } public function defineParamTypes() { return array( 'userOwner' => 'optional string', 'projectOwner' => 'optional string', 'userAffiliated' => 'optional string', 'repositoryCallsign' => 'optional string', 'path' => 'optional string', ); } public function defineReturnType() { return 'dict<phid -> dict of package info>'; } public function defineErrorTypes() { return array( 'ERR-INVALID-USAGE' => 'Provide one of a single owner phid (user/project), a single '. 'affiliated user phid (user), or a repository/path.', 'ERR-INVALID-PARAMETER' => 'parameter should be a phid', 'ERR_REP_NOT_FOUND' => 'The repository callsign is not recognized', ); } protected static function queryAll() { return id(new PhabricatorOwnersPackage())->loadAll(); } protected static function queryByOwner($owner) { $is_valid_phid = phid_get_type($owner) == PhabricatorPeopleUserPHIDType::TYPECONST || phid_get_type($owner) == PhabricatorProjectProjectPHIDType::TYPECONST; if (!$is_valid_phid) { throw id(new ConduitException('ERR-INVALID-PARAMETER')) ->setErrorDescription( 'Expected user/project PHID for owner, got '.$owner); } $owners = id(new PhabricatorOwnersOwner())->loadAllWhere( 'userPHID = %s', $owner); $package_ids = mpull($owners, 'getPackageID'); $packages = array(); foreach ($package_ids as $id) { $packages[] = id(new PhabricatorOwnersPackage())->load($id); } return $packages; } private static function queryByPath( PhabricatorUser $viewer, $repo_callsign, $path) { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withCallsigns(array($repo_callsign)) ->executeOne(); if (!$repository) { throw id(new ConduitException('ERR_REP_NOT_FOUND')) ->setErrorDescription( 'Repository callsign '.$repo_callsign.' not recognized'); } if ($path == null) { return PhabricatorOwnersPackage::loadPackagesForRepository($repository); } else { return PhabricatorOwnersPackage::loadOwningPackages( $repository, $path); } } public static function buildPackageInformationDictionaries($packages) { assert_instances_of($packages, 'PhabricatorOwnersPackage'); $result = array(); foreach ($packages as $package) { $p_owners = $package->loadOwners(); $p_paths = $package->loadPaths(); $owners = array_values(mpull($p_owners, 'getUserPHID')); $paths = array(); foreach ($p_paths as $p) { $paths[] = array($p->getRepositoryPHID(), $p->getPath()); } $result[$package->getPHID()] = array( 'phid' => $package->getPHID(), 'name' => $package->getName(), 'description' => $package->getDescription(), 'primaryOwner' => $package->getPrimaryOwnerPHID(), 'owners' => $owners, - 'paths' => $paths + 'paths' => $paths, ); } return $result; } protected function execute(ConduitAPIRequest $request) { $is_owner_query = ($request->getValue('userOwner') || $request->getValue('projectOwner')) ? 1 : 0; $is_affiliated_query = $request->getValue('userAffiliated') ? 1 : 0; $repo = $request->getValue('repositoryCallsign'); $path = $request->getValue('path'); $is_path_query = $repo ? 1 : 0; if ($is_owner_query + $is_path_query + $is_affiliated_query === 0) { // if no search terms are provided, return everything $packages = self::queryAll(); } else if ($is_owner_query + $is_path_query + $is_affiliated_query > 1) { // otherwise, exactly one of these should be provided throw new ConduitException('ERR-INVALID-USAGE'); } if ($is_affiliated_query) { $query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($request->getUser()); $query->withOwnerPHIDs(array($request->getValue('userAffiliated'))); $packages = $query->execute(); } else if ($is_owner_query) { $owner = nonempty( $request->getValue('userOwner'), $request->getValue('projectOwner')); $packages = self::queryByOwner($owner); } else if ($is_path_query) { $packages = self::queryByPath($request->getUser(), $repo, $path); } return self::buildPackageInformationDictionaries($packages); } } diff --git a/src/applications/owners/config/PhabricatorOwnersConfigOptions.php b/src/applications/owners/config/PhabricatorOwnersConfigOptions.php index 27333211e0..7360ed4cb6 100644 --- a/src/applications/owners/config/PhabricatorOwnersConfigOptions.php +++ b/src/applications/owners/config/PhabricatorOwnersConfigOptions.php @@ -1,27 +1,27 @@ <?php final class PhabricatorOwnersConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Owners'); } public function getDescription() { return pht('Configure Owners.'); } public function getOptions() { return array( $this->newOption( 'metamta.package.reply-handler', 'class', 'OwnersPackageReplyHandler') ->setBaseClass('PhabricatorMailReplyHandler') ->setDescription(pht('Reply handler for owners mail.')), $this->newOption('metamta.package.subject-prefix', 'string', '[Package]') - ->setDescription(pht('Subject prefix for Owners email.')) + ->setDescription(pht('Subject prefix for Owners email.')), ); } } diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php index 0af49d57d9..698df2b1be 100644 --- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php +++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php @@ -1,236 +1,236 @@ <?php final class PhabricatorOwnersDetailController extends PhabricatorOwnersController { private $id; private $package; public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $package = id(new PhabricatorOwnersPackage())->load($this->id); if (!$package) { return new Aphront404Response(); } $this->package = $package; $paths = $package->loadPaths(); $owners = $package->loadOwners(); $repository_phids = array(); foreach ($paths as $path) { $repository_phids[$path->getRepositoryPHID()] = true; } if ($repository_phids) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($user) ->withPHIDs(array_keys($repository_phids)) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); } else { $repositories = array(); } $phids = array(); foreach ($owners as $owner) { $phids[$owner->getUserPHID()] = true; } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); $rows = array(); $rows[] = array(pht('Name'), $package->getName()); $rows[] = array(pht('Description'), $package->getDescription()); $primary_owner = null; $primary_phid = $package->getPrimaryOwnerPHID(); if ($primary_phid && isset($handles[$primary_phid])) { $primary_owner = phutil_tag( 'strong', array(), $handles[$primary_phid]->renderLink()); } $rows[] = array(pht('Primary Owner'), $primary_owner); $owner_links = array(); foreach ($owners as $owner) { $owner_links[] = $handles[$owner->getUserPHID()]->renderLink(); } $owner_links = phutil_implode_html(phutil_tag('br'), $owner_links); $rows[] = array(pht('Owners'), $owner_links); $rows[] = array( pht('Auditing'), $package->getAuditingEnabled() ? pht('Enabled') : pht('Disabled'), ); $path_links = array(); foreach ($paths as $path) { $repo = idx($repositories, $path->getRepositoryPHID()); if (!$repo) { continue; } $href = DiffusionRequest::generateDiffusionURI( array( 'callsign' => $repo->getCallsign(), 'branch' => $repo->getDefaultBranch(), 'path' => $path->getPath(), - 'action' => 'browse' + 'action' => 'browse', )); $repo_name = phutil_tag('strong', array(), $repo->getName()); $path_link = phutil_tag( 'a', array( 'href' => (string) $href, ), $path->getPath()); $path_links[] = hsprintf( '%s %s %s', ($path->getExcluded() ? "\xE2\x80\x93" : '+'), $repo_name, $path_link); } $path_links = phutil_implode_html(phutil_tag('br'), $path_links); $rows[] = array(pht('Paths'), $path_links); $table = new AphrontTableView($rows); $table->setColumnClasses( array( 'header', 'wide', )); $panel = new AphrontPanelView(); $panel->setNoBackground(); $panel->setHeader( pht('Package Details for "%s"', $package->getName())); $panel->addButton( javelin_tag( 'a', array( 'href' => '/owners/delete/'.$package->getID().'/', 'class' => 'button grey', 'sigil' => 'workflow', ), pht('Delete Package'))); $panel->addButton( phutil_tag( 'a', array( 'href' => '/owners/edit/'.$package->getID().'/', 'class' => 'button', ), pht('Edit Package'))); $panel->appendChild($table); $key = 'package/'.$package->getID(); $this->setSideNavFilter($key); $commit_views = array(); $commit_uri = id(new PhutilURI('/audit/view/packagecommits/')) ->setQueryParams( array( 'phid' => $package->getPHID(), )); $attention_commits = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->withAuditorPHIDs(array($package->getPHID())) ->withAuditStatus(DiffusionCommitQuery::AUDIT_STATUS_CONCERN) ->needCommitData(true) ->setLimit(10) ->execute(); if ($attention_commits) { $view = id(new PhabricatorAuditListView()) ->setUser($user) ->setCommits($attention_commits); $commit_views[] = array( 'view' => $view, 'header' => pht('Commits in this Package that Need Attention'), 'button' => phutil_tag( 'a', array( 'href' => $commit_uri->alter('status', 'open'), 'class' => 'button grey', ), pht('View All Problem Commits')), ); } $all_commits = id(new DiffusionCommitQuery()) ->setViewer($request->getUser()) ->withAuditorPHIDs(array($package->getPHID())) ->needCommitData(true) ->setLimit(100) ->execute(); $view = id(new PhabricatorAuditListView()) ->setUser($user) ->setCommits($all_commits) ->setNoDataString(pht('No commits in this package.')); $commit_views[] = array( 'view' => $view, 'header' => pht('Recent Commits in Package'), 'button' => phutil_tag( 'a', array( 'href' => $commit_uri, 'class' => 'button grey', ), pht('View All Package Commits')), ); $phids = array(); foreach ($commit_views as $commit_view) { $phids[] = $commit_view['view']->getRequiredHandlePHIDs(); } $phids = array_mergev($phids); $handles = $this->loadViewerHandles($phids); $commit_panels = array(); foreach ($commit_views as $commit_view) { $commit_panel = new AphrontPanelView(); $commit_panel->setNoBackground(); $commit_panel->setHeader($commit_view['header']); if (isset($commit_view['button'])) { $commit_panel->addButton($commit_view['button']); } $commit_view['view']->setHandles($handles); $commit_panel->appendChild($commit_view['view']); $commit_panels[] = $commit_panel; } $nav = $this->buildSideNavView(); $nav->appendChild($panel); $nav->appendChild($commit_panels); return $this->buildApplicationPage( array( $nav, ), array( 'title' => pht('Package %s', $package->getName()), )); } protected function getExtraPackageViews(AphrontSideNavFilterView $view) { $package = $this->package; $view->addFilter('package/'.$package->getID(), pht('Details')); } } diff --git a/src/applications/owners/controller/PhabricatorOwnersListController.php b/src/applications/owners/controller/PhabricatorOwnersListController.php index 6f35bef0a5..70a8fcdbb5 100644 --- a/src/applications/owners/controller/PhabricatorOwnersListController.php +++ b/src/applications/owners/controller/PhabricatorOwnersListController.php @@ -1,346 +1,346 @@ <?php final class PhabricatorOwnersListController extends PhabricatorOwnersController { protected $view; public function willProcessRequest(array $data) { $this->view = idx($data, 'view', 'owned'); $this->setSideNavFilter('view/'.$this->view); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $package = new PhabricatorOwnersPackage(); $owner = new PhabricatorOwnersOwner(); $path = new PhabricatorOwnersPath(); $repository_phid = ''; if ($request->getStr('repository') != '') { $repository_phid = id(new PhabricatorRepositoryQuery()) ->setViewer($user) ->withCallsigns(array($request->getStr('repository'))) ->executeOne() ->getPHID(); } switch ($this->view) { case 'search': $packages = array(); $conn_r = $package->establishConnection('r'); $where = array('1 = 1'); $join = array(); $having = ''; if ($request->getStr('name')) { $where[] = qsprintf( $conn_r, 'p.name LIKE %~', $request->getStr('name')); } if ($repository_phid || $request->getStr('path')) { $join[] = qsprintf( $conn_r, 'JOIN %T path ON path.packageID = p.id', $path->getTableName()); if ($repository_phid) { $where[] = qsprintf( $conn_r, 'path.repositoryPHID = %s', $repository_phid); } if ($request->getStr('path')) { $where[] = qsprintf( $conn_r, '(path.path LIKE %~ AND NOT path.excluded) OR %s LIKE CONCAT(REPLACE(path.path, %s, %s), %s)', $request->getStr('path'), $request->getStr('path'), '_', '\_', '%'); $having = 'HAVING MAX(path.excluded) = 0'; } } if ($request->getArr('owner')) { $join[] = qsprintf( $conn_r, 'JOIN %T o ON o.packageID = p.id', $owner->getTableName()); $where[] = qsprintf( $conn_r, 'o.userPHID IN (%Ls)', $request->getArr('owner')); } $data = queryfx_all( $conn_r, 'SELECT p.* FROM %T p %Q WHERE %Q GROUP BY p.id %Q', $package->getTableName(), implode(' ', $join), '('.implode(') AND (', $where).')', $having); $packages = $package->loadAllFromArray($data); $header = pht('Search Results'); $nodata = pht('No packages match your query.'); break; case 'owned': $data = queryfx_all( $package->establishConnection('r'), 'SELECT p.* FROM %T p JOIN %T o ON p.id = o.packageID WHERE o.userPHID = %s GROUP BY p.id', $package->getTableName(), $owner->getTableName(), $user->getPHID()); $packages = $package->loadAllFromArray($data); $header = pht('Owned Packages'); $nodata = pht('No owned packages'); break; case 'projects': $projects = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withMemberPHIDs(array($user->getPHID())) ->withStatus(PhabricatorProjectQuery::STATUS_ANY) ->execute(); $owner_phids = mpull($projects, 'getPHID'); if ($owner_phids) { $data = queryfx_all( $package->establishConnection('r'), 'SELECT p.* FROM %T p JOIN %T o ON p.id = o.packageID WHERE o.userPHID IN (%Ls) GROUP BY p.id', $package->getTableName(), $owner->getTableName(), $owner_phids); } else { $data = array(); } $packages = $package->loadAllFromArray($data); $header = pht('Owned Packages'); $nodata = pht('No owned packages'); break; case 'all': $packages = $package->loadAll(); $header = pht('All Packages'); $nodata = pht('There are no defined packages.'); break; } $content = $this->renderPackageTable( $packages, $header, $nodata); $filter = new AphrontListFilterView(); $owners_search_value = array(); if ($request->getArr('owner')) { $phids = $request->getArr('owner'); $phid = reset($phids); $handles = $this->loadViewerHandles(array($phid)); $owners_search_value = array($handles[$phid]); } $callsigns = array('' => pht('(Any Repository)')); $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($user) ->setOrder(PhabricatorRepositoryQuery::ORDER_CALLSIGN) ->execute(); foreach ($repositories as $repository) { $callsigns[$repository->getCallsign()] = $repository->getCallsign().': '.$repository->getName(); } $form = id(new AphrontFormView()) ->setUser($user) ->setAction('/owners/view/search/') ->setMethod('GET') ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($request->getStr('name'))) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorProjectOrUserDatasource()) ->setLimit(1) ->setName('owner') ->setLabel(pht('Owner')) ->setValue($owners_search_value)) ->appendChild( id(new AphrontFormSelectControl()) ->setName('repository') ->setLabel(pht('Repository')) ->setOptions($callsigns) ->setValue($request->getStr('repository'))) ->appendChild( id(new AphrontFormTextControl()) ->setName('path') ->setLabel(pht('Path')) ->setValue($request->getStr('path'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Search for Packages'))); $filter->appendChild($form); $nav = $this->buildSideNavView(); $nav->appendChild($filter); $nav->appendChild($content); return $this->buildApplicationPage( array( $nav, ), array( 'title' => pht('Package Index'), )); } private function renderPackageTable(array $packages, $header, $nodata) { assert_instances_of($packages, 'PhabricatorOwnersPackage'); if ($packages) { $package_ids = mpull($packages, 'getID'); $owners = id(new PhabricatorOwnersOwner())->loadAllWhere( 'packageID IN (%Ld)', $package_ids); $paths = id(new PhabricatorOwnersPath())->loadAllWhere( 'packageID in (%Ld)', $package_ids); $phids = array(); foreach ($owners as $owner) { $phids[$owner->getUserPHID()] = true; } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); $repository_phids = array(); foreach ($paths as $path) { $repository_phids[$path->getRepositoryPHID()] = true; } if ($repository_phids) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getRequest()->getUser()) ->withPHIDs(array_keys($repository_phids)) ->execute(); } else { $repositories = array(); } $repositories = mpull($repositories, null, 'getPHID'); $owners = mgroup($owners, 'getPackageID'); $paths = mgroup($paths, 'getPackageID'); } else { $handles = array(); $repositories = array(); $owners = array(); $paths = array(); } $rows = array(); foreach ($packages as $package) { $pkg_owners = idx($owners, $package->getID(), array()); foreach ($pkg_owners as $key => $owner) { $pkg_owners[$key] = $handles[$owner->getUserPHID()]->renderLink(); if ($owner->getUserPHID() == $package->getPrimaryOwnerPHID()) { $pkg_owners[$key] = phutil_tag('strong', array(), $pkg_owners[$key]); } } $pkg_owners = phutil_implode_html(phutil_tag('br'), $pkg_owners); $pkg_paths = idx($paths, $package->getID(), array()); foreach ($pkg_paths as $key => $path) { $repo = idx($repositories, $path->getRepositoryPHID()); if ($repo) { $href = DiffusionRequest::generateDiffusionURI( array( 'callsign' => $repo->getCallsign(), 'branch' => $repo->getDefaultBranch(), 'path' => $path->getPath(), 'action' => 'browse', )); $pkg_paths[$key] = hsprintf( '%s %s%s', ($path->getExcluded() ? "\xE2\x80\x93" : '+'), phutil_tag('strong', array(), $repo->getName()), phutil_tag( 'a', array( 'href' => (string) $href, ), $path->getPath())); } else { $pkg_paths[$key] = $path->getPath(); } } $pkg_paths = phutil_implode_html(phutil_tag('br'), $pkg_paths); $rows[] = array( phutil_tag( 'a', array( 'href' => '/owners/package/'.$package->getID().'/', ), $package->getName()), $pkg_owners, $pkg_paths, phutil_tag( 'a', array( 'href' => '/audit/view/packagecommits/?phid='.$package->getPHID(), ), - pht('Related Commits')) + pht('Related Commits')), ); } $table = new AphrontTableView($rows); $table->setHeaders( array( pht('Name'), pht('Owners'), pht('Paths'), pht('Related Commits'), )); $table->setColumnClasses( array( 'pri', '', 'wide wrap', 'narrow', )); $panel = new AphrontPanelView(); $panel->setHeader($header); $panel->appendChild($table); $panel->setNoBackground(); return $panel; } protected function getExtraPackageViews(AphrontSideNavFilterView $view) { if ($this->view == 'search') { $view->addFilter('view/search', pht('Search Results')); } } } diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index bc19ecdc45..fa667d2dae 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -1,439 +1,440 @@ <?php final class PhabricatorOwnersPackage extends PhabricatorOwnersDAO implements PhabricatorPolicyInterface { protected $name; protected $originalName; protected $auditingEnabled; protected $description; protected $primaryOwnerPHID; private $unsavedOwners; private $unsavedPaths; private $actorPHID; private $oldPrimaryOwnerPHID; private $oldAuditingEnabled; public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } public function getConfiguration() { return array( // This information is better available from the history table. self::CONFIG_TIMESTAMPS => false, self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'originalName' => 'text255', 'description' => 'text', 'primaryOwnerPHID' => 'phid?', 'auditingEnabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'name' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID('OPKG'); } public function attachUnsavedOwners(array $owners) { $this->unsavedOwners = $owners; return $this; } public function attachUnsavedPaths(array $paths) { $this->unsavedPaths = $paths; return $this; } public function attachActorPHID($actor_phid) { $this->actorPHID = $actor_phid; return $this; } public function getActorPHID() { return $this->actorPHID; } public function attachOldPrimaryOwnerPHID($old_primary) { $this->oldPrimaryOwnerPHID = $old_primary; return $this; } public function getOldPrimaryOwnerPHID() { return $this->oldPrimaryOwnerPHID; } public function attachOldAuditingEnabled($auditing_enabled) { $this->oldAuditingEnabled = $auditing_enabled; return $this; } public function getOldAuditingEnabled() { return $this->oldAuditingEnabled; } public function setName($name) { $this->name = $name; if (!$this->getID()) { $this->originalName = $name; } return $this; } public function loadOwners() { if (!$this->getID()) { return array(); } return id(new PhabricatorOwnersOwner())->loadAllWhere( 'packageID = %d', $this->getID()); } public function loadPaths() { if (!$this->getID()) { return array(); } return id(new PhabricatorOwnersPath())->loadAllWhere( 'packageID = %d', $this->getID()); } public static function loadAffectedPackages( PhabricatorRepository $repository, array $paths) { if (!$paths) { return array(); } return self::loadPackagesForPaths($repository, $paths); } public static function loadOwningPackages($repository, $path) { if (empty($path)) { return array(); } return self::loadPackagesForPaths($repository, array($path), 1); } private static function loadPackagesForPaths( PhabricatorRepository $repository, array $paths, $limit = 0) { $fragments = array(); foreach ($paths as $path) { foreach (self::splitPath($path) as $fragment) { $fragments[$fragment][$path] = true; } } $package = new PhabricatorOwnersPackage(); $path = new PhabricatorOwnersPath(); $conn = $package->establishConnection('r'); $repository_clause = qsprintf( $conn, 'AND p.repositoryPHID = %s', $repository->getPHID()); // NOTE: The list of $paths may be very large if we're coming from // the OwnersWorker and processing, e.g., an SVN commit which created a new // branch. Break it apart so that it will fit within 'max_allowed_packet', // and then merge results in PHP. $rows = array(); foreach (array_chunk(array_keys($fragments), 128) as $chunk) { $rows[] = queryfx_all( $conn, 'SELECT pkg.id, p.excluded, p.path FROM %T pkg JOIN %T p ON p.packageID = pkg.id WHERE p.path IN (%Ls) %Q', $package->getTableName(), $path->getTableName(), $chunk, $repository_clause); } $rows = array_mergev($rows); $ids = self::findLongestPathsPerPackage($rows, $fragments); if (!$ids) { return array(); } arsort($ids); if ($limit) { $ids = array_slice($ids, 0, $limit, $preserve_keys = true); } $ids = array_keys($ids); $packages = $package->loadAllWhere('id in (%Ld)', $ids); $packages = array_select_keys($packages, $ids); return $packages; } public static function loadPackagesForRepository($repository) { $package = new PhabricatorOwnersPackage(); $ids = ipull( queryfx_all( $package->establishConnection('r'), 'SELECT DISTINCT packageID FROM %T WHERE repositoryPHID = %s', id(new PhabricatorOwnersPath())->getTableName(), $repository->getPHID()), 'packageID'); return $package->loadAllWhere('id in (%Ld)', $ids); } public static function findLongestPathsPerPackage(array $rows, array $paths) { $ids = array(); foreach (igroup($rows, 'id') as $id => $package_paths) { $relevant_paths = array_select_keys( $paths, ipull($package_paths, 'path')); // For every package, remove all excluded paths. $remove = array(); foreach ($package_paths as $package_path) { if ($package_path['excluded']) { $remove += idx($relevant_paths, $package_path['path'], array()); unset($relevant_paths[$package_path['path']]); } } if ($remove) { foreach ($relevant_paths as $fragment => $fragment_paths) { $relevant_paths[$fragment] = array_diff_key($fragment_paths, $remove); } } $relevant_paths = array_filter($relevant_paths); if ($relevant_paths) { $ids[$id] = max(array_map('strlen', array_keys($relevant_paths))); } } return $ids; } private function getActor() { // TODO: This should be cleaner, but we'd likely need to move the whole // thing to an Editor (T603). return PhabricatorUser::getOmnipotentUser(); } public function save() { if ($this->getID()) { $is_new = false; } else { $is_new = true; } $this->openTransaction(); $ret = parent::save(); $add_owners = array(); $remove_owners = array(); $all_owners = array(); if ($this->unsavedOwners) { $new_owners = array_fill_keys($this->unsavedOwners, true); $cur_owners = array(); foreach ($this->loadOwners() as $owner) { if (empty($new_owners[$owner->getUserPHID()])) { $remove_owners[$owner->getUserPHID()] = true; $owner->delete(); continue; } $cur_owners[$owner->getUserPHID()] = true; } $add_owners = array_diff_key($new_owners, $cur_owners); $all_owners = array_merge( array($this->getPrimaryOwnerPHID() => true), $new_owners, $remove_owners); foreach ($add_owners as $phid => $ignored) { $owner = new PhabricatorOwnersOwner(); $owner->setPackageID($this->getID()); $owner->setUserPHID($phid); $owner->save(); } unset($this->unsavedOwners); } $add_paths = array(); $remove_paths = array(); $touched_repos = array(); if ($this->unsavedPaths) { $new_paths = igroup($this->unsavedPaths, 'repositoryPHID', 'path'); $cur_paths = $this->loadPaths(); foreach ($cur_paths as $key => $path) { $repository_phid = $path->getRepositoryPHID(); $new_path = head(idx( idx($new_paths, $repository_phid, array()), $path->getPath(), array())); $excluded = $path->getExcluded(); if (!$new_path || idx($new_path, 'excluded') != $excluded) { $touched_repos[$repository_phid] = true; $remove_paths[$repository_phid][$path->getPath()] = $excluded; $path->delete(); unset($cur_paths[$key]); } } $cur_paths = mgroup($cur_paths, 'getRepositoryPHID', 'getPath'); foreach ($new_paths as $repository_phid => $paths) { // TODO: (T603) Thread policy stuff in here. // get repository object for path validation $repository = id(new PhabricatorRepository())->loadOneWhere( 'phid = %s', $repository_phid); if (!$repository) { continue; } foreach ($paths as $path => $dicts) { $path = ltrim($path, '/'); // build query to validate path $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $this->getActor(), 'repository' => $repository, 'path' => $path, )); $results = DiffusionBrowseResultSet::newFromConduit( DiffusionQuery::callConduitWithDiffusionRequest( $this->getActor(), $drequest, 'diffusion.browsequery', array( 'commit' => $drequest->getCommit(), 'path' => $path, - 'needValidityOnly' => true))); + 'needValidityOnly' => true, + ))); $valid = $results->isValidResults(); $is_directory = true; if (!$valid) { switch ($results->getReasonForEmptyResultSet()) { case DiffusionBrowseResultSet::REASON_IS_FILE: $valid = true; $is_directory = false; break; case DiffusionBrowseResultSet::REASON_IS_EMPTY: $valid = true; break; } } if ($is_directory && substr($path, -1) != '/') { $path .= '/'; } if (substr($path, 0, 1) != '/') { $path = '/'.$path; } if (empty($cur_paths[$repository_phid][$path]) && $valid) { $touched_repos[$repository_phid] = true; $excluded = idx(reset($dicts), 'excluded', 0); $add_paths[$repository_phid][$path] = $excluded; $obj = new PhabricatorOwnersPath(); $obj->setPackageID($this->getID()); $obj->setRepositoryPHID($repository_phid); $obj->setPath($path); $obj->setExcluded($excluded); $obj->save(); } } } unset($this->unsavedPaths); } $this->saveTransaction(); if ($is_new) { $mail = new PackageCreateMail($this); } else { $mail = new PackageModifyMail( $this, array_keys($add_owners), array_keys($remove_owners), array_keys($all_owners), array_keys($touched_repos), $add_paths, $remove_paths); } $mail->setActor($this->getActor()); $mail->send(); return $ret; } public function delete() { $mails = id(new PackageDeleteMail($this)) ->setActor($this->getActor()) ->prepareMails(); $this->openTransaction(); foreach ($this->loadOwners() as $owner) { $owner->delete(); } foreach ($this->loadPaths() as $path) { $path->delete(); } $ret = parent::delete(); $this->saveTransaction(); foreach ($mails as $mail) { $mail->saveAndSend(); } return $ret; } private static function splitPath($path) { $result = array('/'); $trailing_slash = preg_match('@/$@', $path) ? '/' : ''; $path = trim($path, '/'); $parts = explode('/', $path); while (count($parts)) { $result[] = '/'.implode('/', $parts).$trailing_slash; $trailing_slash = '/'; array_pop($parts); } return $result; } } diff --git a/src/applications/passphrase/application/PhabricatorPassphraseApplication.php b/src/applications/passphrase/application/PhabricatorPassphraseApplication.php index 3deadcbd7f..41fd1fe8f5 100644 --- a/src/applications/passphrase/application/PhabricatorPassphraseApplication.php +++ b/src/applications/passphrase/application/PhabricatorPassphraseApplication.php @@ -1,59 +1,60 @@ <?php final class PhabricatorPassphraseApplication extends PhabricatorApplication { public function getName() { return pht('Passphrase'); } public function getBaseURI() { return '/passphrase/'; } public function getShortDescription() { return pht('Credential Store'); } public function getIconName() { return 'passphrase'; } public function getTitleGlyph() { return "\xE2\x97\x88"; } public function getFlavorText() { return pht('Put your secrets in a lockbox.'); } public function getApplicationGroup() { return self::GROUP_UTILITIES; } public function canUninstall() { return false; } public function getRoutes() { return array( '/K(?P<id>\d+)' => 'PassphraseCredentialViewController', '/passphrase/' => array( '(?:query/(?P<queryKey>[^/]+)/)?' => 'PassphraseCredentialListController', 'create/' => 'PassphraseCredentialCreateController', 'edit/(?:(?P<id>\d+)/)?' => 'PassphraseCredentialEditController', 'destroy/(?P<id>\d+)/' => 'PassphraseCredentialDestroyController', 'reveal/(?P<id>\d+)/' => 'PassphraseCredentialRevealController', 'public/(?P<id>\d+)/' => 'PassphraseCredentialPublicController', 'lock/(?P<id>\d+)/' => 'PassphraseCredentialLockController', 'conduit/(?P<id>\d+)/' => 'PassphraseCredentialConduitController', - )); + ), + ); } public function getRemarkupRules() { return array( new PassphraseRemarkupRule(), ); } } diff --git a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php index f9667c3081..e82dea7408 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php @@ -1,110 +1,111 @@ <?php final class PassphraseCredentialRevealController extends PassphraseController { private $id; public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $credential = id(new PassphraseCredentialQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->needSecrets(true) ->executeOne(); if (!$credential) { return new Aphront404Response(); } $view_uri = '/K'.$credential->getID(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $view_uri); $is_locked = $credential->getIsLocked(); if ($is_locked) { return $this->newDialog() ->setUser($viewer) ->setTitle(pht('Credential is locked')) ->appendChild( pht( 'This credential can not be shown, because it is locked.')) ->addCancelButton($view_uri); } if ($request->isFormPost()) { $secret = $credential->getSecret(); if (!$secret) { $body = pht('This credential has no associated secret.'); } else if (!strlen($secret->openEnvelope())) { $body = pht('This credential has an empty secret.'); } else { $body = id(new PHUIFormLayoutView()) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Plaintext')) ->setReadOnly(true) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setValue($secret->openEnvelope())); } // NOTE: Disable workflow on the cancel button to reload the page so // the viewer can see that their view was logged. $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Credential Secret (%s)', $credential->getMonogram())) ->appendChild($body) ->setDisableWorkflowOnCancel(true) ->addCancelButton($view_uri, pht('Done')); $type_secret = PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET; $xactions = array(id(new PassphraseCredentialTransaction()) ->setTransactionType($type_secret) - ->setNewValue(true)); + ->setNewValue(true), + ); $editor = id(new PassphraseCredentialTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->applyTransactions($credential, $xactions); return id(new AphrontDialogResponse())->setDialog($dialog); } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if ($is_serious) { $body = pht( 'The secret associated with this credential will be shown in plain '. 'text on your screen.'); } else { $body = pht( 'The secret associated with this credential will be shown in plain '. 'text on your screen. Before continuing, wrap your arms around '. 'your monitor to create a human shield, keeping it safe from '. 'prying eyes. Protect company secrets!'); } return $this->newDialog() ->setUser($viewer) ->setTitle(pht('Really show secret?')) ->appendChild($body) ->addSubmitButton(pht('Show Secret')) ->addCancelButton($view_uri); } } diff --git a/src/applications/paste/config/PhabricatorPasteConfigOptions.php b/src/applications/paste/config/PhabricatorPasteConfigOptions.php index cc001e12ed..c58f5b1394 100644 --- a/src/applications/paste/config/PhabricatorPasteConfigOptions.php +++ b/src/applications/paste/config/PhabricatorPasteConfigOptions.php @@ -1,29 +1,29 @@ <?php final class PhabricatorPasteConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Paste'); } public function getDescription() { return pht('Configure Paste.'); } public function getOptions() { return array( $this->newOption( 'metamta.paste.public-create-email', 'string', null) ->setDescription(pht('Allow creating pastes via email.')), $this->newOption( 'metamta.paste.subject-prefix', 'string', '[Paste]') - ->setDescription(pht('Subject prefix for Paste email.')) + ->setDescription(pht('Subject prefix for Paste email.')), ); } } diff --git a/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php b/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php index 2e7061e279..744a98b621 100644 --- a/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php +++ b/src/applications/paste/lipsum/PhabricatorPasteTestDataGenerator.php @@ -1,92 +1,93 @@ <?php final class PhabricatorPasteTestDataGenerator extends PhabricatorTestDataGenerator { // Better Support for this in the future public $supportedLanguages = array( 'Java' => 'java', - 'PHP' => 'php'); + 'PHP' => 'php', + ); public function generate() { $authorphid = $this->loadPhabrictorUserPHID(); $language = $this->generateLanguage(); $content = $this->generateContent($language); $title = $this->generateTitle($language); $paste_file = PhabricatorFile::newFromFileData( $content, array( 'name' => $title, 'mime-type' => 'text/plain; charset=utf-8', 'authorPHID' => $authorphid, )); $policy = $this->generatePolicy(); $filephid = $paste_file->getPHID(); $parentphid = $this->loadPhabrictorPastePHID(); $paste = id(new PhabricatorPaste()) ->setParentPHID($parentphid) ->setAuthorPHID($authorphid) ->setTitle($title) ->setLanguage($language) ->setViewPolicy($policy) ->setFilePHID($filephid) ->save(); return $paste; } private function loadPhabrictorPastePHID() { $random = rand(0, 1); if ($random == 1) { $paste = id($this->loadOneRandom('PhabricatorPaste')); if ($paste) { return $paste->getPHID(); } } return null; } public function generateTitle($language = null) { $taskgen = new PhutilLipsumContextFreeGrammar(); // Remove Punctuation $title = preg_replace('/[^a-zA-Z 0-9]+/', '', $taskgen->generate()); // Capitalize First Letters $title = ucwords($title); // Remove Spaces $title = preg_replace('/\s+/', '', $title); if ($language == null || !in_array($language, array_keys($this->supportedLanguages))) { return $title.'.txt'; } else { return $title.'.'.$this->supportedLanguages[$language]; } } public function generateLanguage() { $supplemented_lang = $this->supportedLanguages; $supplemented_lang['lipsum'] = 'txt'; return array_rand($supplemented_lang); } public function generateContent($language = null) { if ($language == null || !in_array($language, array_keys($this->supportedLanguages))) { return id(new PhutilLipsumContextFreeGrammar()) ->generateSeveral(rand(30, 40)); } else { $cfg_class = 'Phutil'.$language.'CodeSnippetContextFreeGrammar'; return newv($cfg_class, array())->generate(); } } public function generatePolicy() { // Make sure 4/5th of all generated Pastes are viewable to all switch (rand(0, 4)) { case 0: return PhabricatorPolicies::POLICY_PUBLIC; case 1: return PhabricatorPolicies::POLICY_NOONE; default: return PhabricatorPolicies::POLICY_USER; } } } diff --git a/src/applications/paste/view/PasteEmbedView.php b/src/applications/paste/view/PasteEmbedView.php index 7c429a2792..e86c923794 100644 --- a/src/applications/paste/view/PasteEmbedView.php +++ b/src/applications/paste/view/PasteEmbedView.php @@ -1,70 +1,70 @@ <?php final class PasteEmbedView extends AphrontView { private $paste; private $handle; private $highlights = array(); private $lines = 30; public function setPaste(PhabricatorPaste $paste) { $this->paste = $paste; return $this; } public function setHandle(PhabricatorObjectHandle $handle) { $this->handle = $handle; return $this; } public function setHighlights(array $highlights) { $this->highlights = $highlights; return $this; } public function setLines($lines) { $this->lines = $lines; } public function render() { if (!$this->paste) { throw new Exception('Call setPaste() before render()!'); } $lines = phutil_split_lines($this->paste->getContent()); require_celerity_resource('paste-css'); $link = phutil_tag( 'a', array( - 'href' => '/P'.$this->paste->getID() + 'href' => '/P'.$this->paste->getID(), ), $this->handle->getFullName()); $head = phutil_tag( 'div', array( - 'class' => 'paste-embed-head' + 'class' => 'paste-embed-head', ), $link); $body_attributes = array('class' => 'paste-embed-body'); if ($this->lines != null) { $body_attributes['style'] = 'max-height: '.$this->lines * (1.15).'em;'; } $body = phutil_tag( 'div', $body_attributes, id(new PhabricatorSourceCodeView()) ->setLines($lines) ->setHighlights($this->highlights) ->disableHighlightOnClick()); return phutil_tag( 'div', array('class' => 'paste-embed'), array($head, $body)); } } diff --git a/src/applications/people/conduit/UserFindConduitAPIMethod.php b/src/applications/people/conduit/UserFindConduitAPIMethod.php index 056c898773..afb6d0c1c8 100644 --- a/src/applications/people/conduit/UserFindConduitAPIMethod.php +++ b/src/applications/people/conduit/UserFindConduitAPIMethod.php @@ -1,45 +1,45 @@ <?php final class UserFindConduitAPIMethod extends UserConduitAPIMethod { public function getAPIMethodName() { return 'user.find'; } public function getMethodStatus() { return self::METHOD_STATUS_DEPRECATED; } public function getMethodStatusDescription() { return pht('Obsoleted by "user.query".'); } public function getMethodDescription() { return pht('Lookup PHIDs by username. Obsoleted by "user.query".'); } public function defineParamTypes() { return array( - 'aliases' => 'required list<string>' + 'aliases' => 'required list<string>', ); } public function defineReturnType() { return 'nonempty dict<string, phid>'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $users = id(new PhabricatorPeopleQuery()) ->setViewer($request->getUser()) ->withUsernames($request->getValue('aliases', array())) ->execute(); return mpull($users, 'getPHID', 'getUsername'); } } diff --git a/src/applications/people/controller/PhabricatorPeopleCalendarController.php b/src/applications/people/controller/PhabricatorPeopleCalendarController.php index b71588bb46..8f49408bad 100644 --- a/src/applications/people/controller/PhabricatorPeopleCalendarController.php +++ b/src/applications/people/controller/PhabricatorPeopleCalendarController.php @@ -1,91 +1,92 @@ <?php final class PhabricatorPeopleCalendarController extends PhabricatorPeopleController { private $username; public function shouldRequireAdmin() { return false; } public function willProcessRequest(array $data) { $this->username = idx($data, 'username'); } public function processRequest() { $viewer = $this->getRequest()->getUser(); $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withUsernames(array($this->username)) ->needProfileImage(true) ->executeOne(); if (!$user) { return new Aphront404Response(); } $picture = $user->loadProfileImageURI(); $now = time(); $request = $this->getRequest(); $year_d = phabricator_format_local_time($now, $user, 'Y'); $year = $request->getInt('year', $year_d); $month_d = phabricator_format_local_time($now, $user, 'm'); $month = $request->getInt('month', $month_d); $day = phabricator_format_local_time($now, $user, 'j'); $holidays = id(new PhabricatorCalendarHoliday())->loadAllWhere( 'day BETWEEN %s AND %s', "{$year}-{$month}-01", "{$year}-{$month}-31"); $statuses = id(new PhabricatorCalendarEventQuery()) ->setViewer($user) ->withInvitedPHIDs(array($user->getPHID())) ->withDateRange( strtotime("{$year}-{$month}-01"), strtotime("{$year}-{$month}-01 next month")) ->execute(); if ($month == $month_d && $year == $year_d) { $month_view = new PHUICalendarMonthView($month, $year, $day); } else { $month_view = new PHUICalendarMonthView($month, $year); } $month_view->setBrowseURI($request->getRequestURI()); $month_view->setUser($user); $month_view->setHolidays($holidays); $month_view->setImage($picture); $phids = mpull($statuses, 'getUserPHID'); $handles = $this->loadViewerHandles($phids); foreach ($statuses as $status) { $event = new AphrontCalendarEventView(); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $event->setUserPHID($status->getUserPHID()); $event->setName($status->getHumanStatus()); $event->setDescription($status->getDescription()); $event->setEventID($status->getID()); $month_view->addEvent($event); } $date = new DateTime("{$year}-{$month}-01"); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( $user->getUsername(), '/p/'.$user->getUsername().'/'); $crumbs->addTextCrumb($date->format('F Y')); return $this->buildApplicationPage( + array( + $crumbs, + $month_view, + ), array( - $crumbs, - $month_view), - array( - 'title' => pht('Calendar'), - )); + 'title' => pht('Calendar'), + )); } } diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php index 4a4524d8aa..ad4fc6e32b 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -1,288 +1,288 @@ <?php final class PhabricatorPeopleProfileController extends PhabricatorPeopleController { private $username; public function shouldRequireAdmin() { return false; } public function willProcessRequest(array $data) { $this->username = idx($data, 'username'); } public function processRequest() { $viewer = $this->getRequest()->getUser(); $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withUsernames(array($this->username)) ->needProfileImage(true) ->executeOne(); if (!$user) { return new Aphront404Response(); } require_celerity_resource('phabricator-profile-css'); $profile = $user->loadUserProfile(); $username = phutil_escape_uri($user->getUserName()); $picture = $user->loadProfileImageURI(); $header = id(new PHUIHeaderView()) ->setHeader($user->getFullName()) ->setSubheader($profile->getTitle()) ->setImage($picture); $actions = id(new PhabricatorActionListView()) ->setObject($user) ->setObjectURI($this->getRequest()->getRequestURI()) ->setUser($viewer); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $user, PhabricatorPolicyCapability::CAN_EDIT); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Profile')) ->setHref($this->getApplicationURI('editprofile/'.$user->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-picture-o') ->setName(pht('Edit Profile Picture')) ->setHref($this->getApplicationURI('picture/'.$user->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($viewer->getIsAdmin()) { $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-wrench') ->setName(pht('Edit Settings')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) ->setHref('/settings/'.$user->getID().'/')); if ($user->getIsAdmin()) { $empower_icon = 'fa-arrow-circle-o-down'; $empower_name = pht('Remove Administrator'); } else { $empower_icon = 'fa-arrow-circle-o-up'; $empower_name = pht('Make Administrator'); } $actions->addAction( id(new PhabricatorActionView()) ->setIcon($empower_icon) ->setName($empower_name) ->setDisabled(($user->getPHID() == $viewer->getPHID())) ->setWorkflow(true) ->setHref($this->getApplicationURI('empower/'.$user->getID().'/'))); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-tag') ->setName(pht('Change Username')) ->setWorkflow(true) ->setHref($this->getApplicationURI('rename/'.$user->getID().'/'))); if ($user->getIsDisabled()) { $disable_icon = 'fa-check-circle-o'; $disable_name = pht('Enable User'); } else { $disable_icon = 'fa-ban'; $disable_name = pht('Disable User'); } $actions->addAction( id(new PhabricatorActionView()) ->setIcon($disable_icon) ->setName($disable_name) ->setDisabled(($user->getPHID() == $viewer->getPHID())) ->setWorkflow(true) ->setHref($this->getApplicationURI('disable/'.$user->getID().'/'))); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-times') ->setName(pht('Delete User')) ->setDisabled(($user->getPHID() == $viewer->getPHID())) ->setWorkflow(true) ->setHref($this->getApplicationURI('delete/'.$user->getID().'/'))); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-envelope') ->setName(pht('Send Welcome Email')) ->setWorkflow(true) ->setHref($this->getApplicationURI('welcome/'.$user->getID().'/'))); } $properties = $this->buildPropertyView($user, $actions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($user->getUsername()); $crumbs->setActionList($actions); $feed = $this->renderUserFeed($user); $cal_class = 'PhabricatorCalendarApplication'; $classes = array(); $classes[] = 'profile-activity-view'; if (PhabricatorApplication::isClassInstalledForViewer($cal_class, $user)) { $calendar = $this->renderUserCalendar($user); $classes[] = 'profile-has-calendar'; $classes[] = 'grouped'; } else { $calendar = null; } $activity = phutil_tag( 'div', array( 'class' => implode($classes, ' '), ), array( $calendar, - $feed + $feed, )); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $activity, ), array( 'title' => $user->getUsername(), )); } private function buildPropertyView( PhabricatorUser $user, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($user) ->setActionList($actions); $field_list = PhabricatorCustomField::getObjectFields( $user, PhabricatorCustomField::ROLE_VIEW); $field_list->appendFieldsToPropertyList($user, $viewer, $view); return $view; } private function renderUserFeed(PhabricatorUser $user) { $viewer = $this->getRequest()->getUser(); $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs( array( $user->getPHID(), )); $query->setLimit(100); $query->setViewer($viewer); $stories = $query->execute(); $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($viewer); $builder->setShowHovercards(true); $view = $builder->buildView(); return phutil_tag_div( 'profile-feed', $view->render()); } private function renderUserCalendar(PhabricatorUser $user) { $viewer = $this->getRequest()->getUser(); $epochs = CalendarTimeUtil::getCalendarEventEpochs( $viewer, 'today', 7); $start_epoch = $epochs['start_epoch']; $end_epoch = $epochs['end_epoch']; $statuses = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withInvitedPHIDs(array($user->getPHID())) ->withDateRange($start_epoch, $end_epoch) ->execute(); $timestamps = CalendarTimeUtil::getCalendarWeekTimestamps( $viewer); $today = $timestamps['today']; $epoch_stamps = $timestamps['epoch_stamps']; $events = array(); foreach ($epoch_stamps as $day) { $epoch_start = $day->format('U'); $next_day = clone $day; $next_day->modify('+1 day'); $epoch_end = $next_day->format('U'); foreach ($statuses as $status) { if ($status->getDateTo() < $epoch_start) { continue; } if ($status->getDateFrom() >= $epoch_end) { continue; } $event = new AphrontCalendarEventView(); $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); $status_text = $status->getHumanStatus(); $event->setUserPHID($status->getUserPHID()); $event->setName($status_text); $event->setDescription($status->getDescription()); $event->setEventID($status->getID()); $events[$epoch_start][] = $event; } } $week = array(); foreach ($epoch_stamps as $day) { $epoch = $day->format('U'); $headertext = phabricator_format_local_time($epoch, $user, 'l, M d'); $list = new PHUICalendarListView(); $list->setUser($viewer); $list->showBlankState(true); if (isset($events[$epoch])) { foreach ($events[$epoch] as $event) { $list->addEvent($event); } } $header = phutil_tag( 'a', array( - 'href' => $this->getRequest()->getRequestURI().'calendar/' + 'href' => $this->getRequest()->getRequestURI().'calendar/', ), $headertext); $calendar = new PHUICalendarWidgetView(); $calendar->setHeader($header); $calendar->setCalendarList($list); $week[] = $calendar; } return phutil_tag_div( 'profile-calendar', $week); } } diff --git a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php index a36b2b4696..190d3f1c62 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php @@ -1,299 +1,300 @@ <?php final class PhabricatorPeopleProfilePictureController extends PhabricatorPeopleController { private $id; public function shouldRequireAdmin() { return false; } public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$user) { return new Aphront404Response(); } $profile_uri = '/p/'.$user->getUsername().'/'; $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_file = true; $errors = array(); if ($request->isFormPost()) { $phid = $request->getStr('phid'); $is_default = false; if ($phid == PhabricatorPHIDConstants::PHID_VOID) { $phid = null; $is_default = true; } else if ($phid) { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); } else { if ($request->getFileExists('picture')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['picture'], array( 'authorPHID' => $viewer->getPHID(), 'canCDN' => true, )); } else { $e_file = pht('Required'); $errors[] = pht( 'You must choose a file when uploading a new profile picture.'); } } if (!$errors && !$is_default) { if (!$file->isTransformableImage()) { $e_file = pht('Not Supported'); $errors[] = pht( 'This server only supports these image formats: %s.', implode(', ', $supported_formats)); } else { $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); } } if (!$errors) { if ($is_default) { $user->setProfileImagePHID(null); } else { $user->setProfileImagePHID($xformed->getPHID()); $xformed->attachToObject($user->getPHID()); } $user->save(); return id(new AphrontRedirectResponse())->setURI($profile_uri); } } $title = pht('Edit Profile Picture'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($user->getUsername(), $profile_uri); $crumbs->addTextCrumb($title); $form = id(new PHUIFormLayoutView()) ->setUser($viewer); $default_image = PhabricatorFile::loadBuiltin($viewer, 'profile.png'); $images = array(); $current = $user->getProfileImagePHID(); $has_current = false; if ($current) { $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($current)) ->execute(); if ($files) { $file = head($files); if ($file->isTransformableImage()) { $has_current = true; $images[$current] = array( 'uri' => $file->getBestURI(), 'tip' => pht('Current Picture'), ); } } } // Try to add external account images for any associated external accounts. $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($user->getPHID())) ->needImages(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); foreach ($accounts as $account) { $file = $account->getProfileImageFile(); if ($account->getProfileImagePHID() != $file->getPHID()) { // This is a default image, just skip it. continue; } $provider = PhabricatorAuthProvider::getEnabledProviderByKey( $account->getProviderKey()); if ($provider) { $tip = pht('Picture From %s', $provider->getProviderName()); } else { $tip = pht('Picture From External Account'); } if ($file->isTransformableImage()) { $images[$file->getPHID()] = array( 'uri' => $file->getBestURI(), 'tip' => $tip, ); } } // Try to add Gravatar images for any email addresses associated with the // account. if (PhabricatorEnv::getEnvConfig('security.allow-outbound-http')) { $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s ORDER BY address', $user->getPHID()); $futures = array(); foreach ($emails as $email_object) { $email = $email_object->getAddress(); $hash = md5(strtolower(trim($email))); $uri = id(new PhutilURI("https://secure.gravatar.com/avatar/{$hash}")) ->setQueryParams( array( 'size' => 200, 'default' => '404', 'rating' => 'x', )); $futures[$email] = new HTTPSFuture($uri); } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); foreach (Futures($futures) as $email => $future) { try { list($body) = $future->resolvex(); $file = PhabricatorFile::newFromFileData( $body, array( 'name' => 'profile-gravatar', 'ttl' => (60 * 60 * 4), )); if ($file->isTransformableImage()) { $images[$file->getPHID()] = array( 'uri' => $file->getBestURI(), 'tip' => pht('Gravatar for %s', $email), ); } } catch (Exception $ex) { // Just continue. } } unset($unguarded); } $images[PhabricatorPHIDConstants::PHID_VOID] = array( 'uri' => $default_image->getBestURI(), 'tip' => pht('Default Picture'), ); require_celerity_resource('people-profile-css'); Javelin::initBehavior('phabricator-tooltips', array()); $buttons = array(); foreach ($images as $phid => $spec) { $button = javelin_tag( 'button', array( 'class' => 'grey profile-image-button', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $spec['tip'], 'size' => 300, ), ), phutil_tag( 'img', array( 'height' => 50, 'width' => 50, 'src' => $spec['uri'], ))); $button = array( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'phid', 'value' => $phid, )), - $button); + $button, + ); $button = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', ), $button); $buttons[] = $button; } if ($has_current) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Current Picture')) ->setValue(array_shift($buttons))); } $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Use Picture')) ->setValue($buttons)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $upload_form = id(new AphrontFormView()) ->setUser($viewer) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormFileControl()) ->setName('picture') ->setLabel(pht('Upload Picture')) ->setError($e_file) ->setCaption( pht('Supported formats: %s', implode(', ', $supported_formats)))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($profile_uri) ->setValue(pht('Upload Picture'))); $upload_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New Picture')) ->setForm($upload_form); return $this->buildApplicationPage( array( $crumbs, $form_box, $upload_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 44bf1e05a2..6d0a681942 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1,930 +1,931 @@ <?php /** * @task factors Multi-Factor Authentication */ final class PhabricatorUser extends PhabricatorUserDAO implements PhutilPerson, PhabricatorPolicyInterface, PhabricatorCustomFieldInterface, PhabricatorDestructibleInterface { const SESSION_TABLE = 'phabricator_session'; const NAMETOKEN_TABLE = 'user_nametoken'; const MAXIMUM_USERNAME_LENGTH = 64; protected $userName; protected $realName; protected $sex; protected $translation; protected $passwordSalt; protected $passwordHash; protected $profileImagePHID; protected $timezoneIdentifier = ''; protected $consoleEnabled = 0; protected $consoleVisible = 0; protected $consoleTab = ''; protected $conduitCertificate; protected $isSystemAgent = 0; protected $isAdmin = 0; protected $isDisabled = 0; protected $isEmailVerified = 0; protected $isApproved = 0; protected $isEnrolledInMultiFactor = 0; protected $accountSecret; private $profileImage = self::ATTACHABLE; private $profile = null; private $status = self::ATTACHABLE; private $preferences = null; private $omnipotent = false; private $customFields = self::ATTACHABLE; private $alternateCSRFString = self::ATTACHABLE; private $session = self::ATTACHABLE; protected function readField($field) { switch ($field) { case 'timezoneIdentifier': // If the user hasn't set one, guess the server's time. return nonempty( $this->timezoneIdentifier, date_default_timezone_get()); // Make sure these return booleans. case 'isAdmin': return (bool)$this->isAdmin; case 'isDisabled': return (bool)$this->isDisabled; case 'isSystemAgent': return (bool)$this->isSystemAgent; case 'isEmailVerified': return (bool)$this->isEmailVerified; case 'isApproved': return (bool)$this->isApproved; default: return parent::readField($field); } } /** * Is this a live account which has passed required approvals? Returns true * if this is an enabled, verified (if required), approved (if required) * account, and false otherwise. * * @return bool True if this is a standard, usable account. */ public function isUserActivated() { if ($this->getIsDisabled()) { return false; } if (!$this->getIsApproved()) { return false; } if (PhabricatorUserEmail::isEmailVerificationRequired()) { if (!$this->getIsEmailVerified()) { return false; } } return true; } /** * Returns `true` if this is a standard user who is logged in. Returns `false` * for logged out, anonymous, or external users. * * @return bool `true` if the user is a standard user who is logged in with * a normal session. */ public function getIsStandardUser() { $type_user = PhabricatorPeopleUserPHIDType::TYPECONST; return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'userName' => 'text64', 'realName' => 'text128', 'sex' => 'text4?', 'translation' => 'text64?', 'passwordSalt' => 'text32?', 'passwordHash' => 'text128?', 'profileImagePHID' => 'phid?', 'consoleEnabled' => 'bool', 'consoleVisible' => 'bool', 'consoleTab' => 'text64', 'conduitCertificate' => 'text255', 'isSystemAgent' => 'bool', 'isDisabled' => 'bool', 'isAdmin' => 'bool', 'timezoneIdentifier' => 'text255', 'isEmailVerified' => 'uint32', 'isApproved' => 'uint32', 'accountSecret' => 'bytes64', 'isEnrolledInMultiFactor' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'userName' => array( 'columns' => array('userName'), 'unique' => true, ), 'realName' => array( 'columns' => array('realName'), ), 'key_approved' => array( 'columns' => array('isApproved'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPeopleUserPHIDType::TYPECONST); } public function setPassword(PhutilOpaqueEnvelope $envelope) { if (!$this->getPHID()) { throw new Exception( 'You can not set a password for an unsaved user because their PHID '. 'is a salt component in the password hash.'); } if (!strlen($envelope->openEnvelope())) { $this->setPasswordHash(''); } else { $this->setPasswordSalt(md5(Filesystem::readRandomBytes(32))); $hash = $this->hashPassword($envelope); $this->setPasswordHash($hash->openEnvelope()); } return $this; } // To satisfy PhutilPerson. public function getSex() { return $this->sex; } public function getMonogram() { return '@'.$this->getUsername(); } public function getTranslation() { try { if ($this->translation && class_exists($this->translation) && is_subclass_of($this->translation, 'PhabricatorTranslation')) { return $this->translation; } } catch (PhutilMissingSymbolException $ex) { return null; } return null; } public function isLoggedIn() { return !($this->getPHID() === null); } public function save() { if (!$this->getConduitCertificate()) { $this->setConduitCertificate($this->generateConduitCertificate()); } if (!strlen($this->getAccountSecret())) { $this->setAccountSecret(Filesystem::readRandomCharacters(64)); } $result = parent::save(); if ($this->profile) { $this->profile->save(); } $this->updateNameTokens(); id(new PhabricatorSearchIndexer()) ->queueDocumentForIndexing($this->getPHID()); return $result; } public function attachSession(PhabricatorAuthSession $session) { $this->session = $session; return $this; } public function getSession() { return $this->assertAttached($this->session); } public function hasSession() { return ($this->session !== self::ATTACHABLE); } private function generateConduitCertificate() { return Filesystem::readRandomCharacters(255); } public function comparePassword(PhutilOpaqueEnvelope $envelope) { if (!strlen($envelope->openEnvelope())) { return false; } if (!strlen($this->getPasswordHash())) { return false; } return PhabricatorPasswordHasher::comparePassword( $this->getPasswordHashInput($envelope), new PhutilOpaqueEnvelope($this->getPasswordHash())); } private function getPasswordHashInput(PhutilOpaqueEnvelope $password) { $input = $this->getUsername(). $password->openEnvelope(). $this->getPHID(). $this->getPasswordSalt(); return new PhutilOpaqueEnvelope($input); } private function hashPassword(PhutilOpaqueEnvelope $password) { $hasher = PhabricatorPasswordHasher::getBestHasher(); $input_envelope = $this->getPasswordHashInput($password); return $hasher->getPasswordHashForStorage($input_envelope); } const CSRF_CYCLE_FREQUENCY = 3600; const CSRF_SALT_LENGTH = 8; const CSRF_TOKEN_LENGTH = 16; const CSRF_BREACH_PREFIX = 'B@'; const EMAIL_CYCLE_FREQUENCY = 86400; const EMAIL_TOKEN_LENGTH = 24; private function getRawCSRFToken($offset = 0) { return $this->generateToken( time() + (self::CSRF_CYCLE_FREQUENCY * $offset), self::CSRF_CYCLE_FREQUENCY, PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), self::CSRF_TOKEN_LENGTH); } /** * @phutil-external-symbol class PhabricatorStartup */ public function getCSRFToken() { $salt = PhabricatorStartup::getGlobal('csrf.salt'); if (!$salt) { $salt = Filesystem::readRandomCharacters(self::CSRF_SALT_LENGTH); PhabricatorStartup::setGlobal('csrf.salt', $salt); } // Generate a token hash to mitigate BREACH attacks against SSL. See // discussion in T3684. $token = $this->getRawCSRFToken(); $hash = PhabricatorHash::digest($token, $salt); return 'B@'.$salt.substr($hash, 0, self::CSRF_TOKEN_LENGTH); } public function validateCSRFToken($token) { $salt = null; $version = 'plain'; // This is a BREACH-mitigating token. See T3684. $breach_prefix = self::CSRF_BREACH_PREFIX; $breach_prelen = strlen($breach_prefix); if (!strncmp($token, $breach_prefix, $breach_prelen)) { $version = 'breach'; $salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH); $token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH); } // When the user posts a form, we check that it contains a valid CSRF token. // Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept // either the current token, the next token (users can submit a "future" // token if you have two web frontends that have some clock skew) or any of // the last 6 tokens. This means that pages are valid for up to 7 hours. // There is also some Javascript which periodically refreshes the CSRF // tokens on each page, so theoretically pages should be valid indefinitely. // However, this code may fail to run (if the user loses their internet // connection, or there's a JS problem, or they don't have JS enabled). // Choosing the size of the window in which we accept old CSRF tokens is // an issue of balancing concerns between security and usability. We could // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to // attacks using captured CSRF tokens, but it's also more likely that real // users will be affected by this, e.g. if they close their laptop for an // hour, open it back up, and try to submit a form before the CSRF refresh // can kick in. Since the user experience of submitting a form with expired // CSRF is often quite bad (you basically lose data, or it's a big pain to // recover at least) and I believe we gain little additional protection // by keeping the window very short (the overwhelming value here is in // preventing blind attacks, and most attacks which can capture CSRF tokens // can also just capture authentication information [sniffing networks] // or act as the user [xss]) the 7 hour default seems like a reasonable // balance. Other major platforms have much longer CSRF token lifetimes, // like Rails (session duration) and Django (forever), which suggests this // is a reasonable analysis. $csrf_window = 6; for ($ii = -$csrf_window; $ii <= 1; $ii++) { $valid = $this->getRawCSRFToken($ii); switch ($version) { // TODO: We can remove this after the BREACH version has been in the // wild for a while. case 'plain': if ($token == $valid) { return true; } break; case 'breach': $digest = PhabricatorHash::digest($valid, $salt); if (substr($digest, 0, self::CSRF_TOKEN_LENGTH) == $token) { return true; } break; default: throw new Exception('Unknown CSRF token format!'); } } return false; } private function generateToken($epoch, $frequency, $key, $len) { if ($this->getPHID()) { $vec = $this->getPHID().$this->getAccountSecret(); } else { $vec = $this->getAlternateCSRFString(); } if ($this->hasSession()) { $vec = $vec.$this->getSession()->getSessionKey(); } $time_block = floor($epoch / $frequency); $vec = $vec.$key.$time_block; return substr(PhabricatorHash::digest($vec), 0, $len); } public function attachUserProfile(PhabricatorUserProfile $profile) { $this->profile = $profile; return $this; } public function loadUserProfile() { if ($this->profile) { return $this->profile; } $profile_dao = new PhabricatorUserProfile(); $this->profile = $profile_dao->loadOneWhere('userPHID = %s', $this->getPHID()); if (!$this->profile) { $profile_dao->setUserPHID($this->getPHID()); $this->profile = $profile_dao; } return $this->profile; } public function loadPrimaryEmailAddress() { $email = $this->loadPrimaryEmail(); if (!$email) { throw new Exception('User has no primary email address!'); } return $email->getAddress(); } public function loadPrimaryEmail() { return $this->loadOneRelative( new PhabricatorUserEmail(), 'userPHID', 'getPHID', '(isPrimary = 1)'); } public function loadPreferences() { if ($this->preferences) { return $this->preferences; } $preferences = null; if ($this->getPHID()) { $preferences = id(new PhabricatorUserPreferences())->loadOneWhere( 'userPHID = %s', $this->getPHID()); } if (!$preferences) { $preferences = new PhabricatorUserPreferences(); $preferences->setUserPHID($this->getPHID()); $default_dict = array( PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph', PhabricatorUserPreferences::PREFERENCE_EDITOR => '', PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '', - PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0); + PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0, + ); $preferences->setPreferences($default_dict); } $this->preferences = $preferences; return $preferences; } public function loadEditorLink($path, $line, $callsign) { $editor = $this->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_EDITOR); if (is_array($path)) { $multiedit = $this->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_MULTIEDIT); switch ($multiedit) { case '': $path = implode(' ', $path); break; case 'disable': return null; } } if (!strlen($editor)) { return null; } $uri = strtr($editor, array( '%%' => '%', '%f' => phutil_escape_uri($path), '%l' => phutil_escape_uri($line), '%r' => phutil_escape_uri($callsign), )); // The resulting URI must have an allowed protocol. Otherwise, we'll return // a link to an error page explaining the misconfiguration. $ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri); if (!$ok) { return '/help/editorprotocol/'; } return (string)$uri; } public function getAlternateCSRFString() { return $this->assertAttached($this->alternateCSRFString); } public function attachAlternateCSRFString($string) { $this->alternateCSRFString = $string; return $this; } /** * Populate the nametoken table, which used to fetch typeahead results. When * a user types "linc", we want to match "Abraham Lincoln" from on-demand * typeahead sources. To do this, we need a separate table of name fragments. */ public function updateNameTokens() { $table = self::NAMETOKEN_TABLE; $conn_w = $this->establishConnection('w'); $tokens = PhabricatorTypeaheadDatasource::tokenizeString( $this->getUserName().' '.$this->getRealName()); $sql = array(); foreach ($tokens as $token) { $sql[] = qsprintf( $conn_w, '(%d, %s)', $this->getID(), $token); } queryfx( $conn_w, 'DELETE FROM %T WHERE userID = %d', $table, $this->getID()); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T (userID, token) VALUES %Q', $table, implode(', ', $sql)); } } public function sendWelcomeEmail(PhabricatorUser $admin) { $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $user_username = $this->getUserName(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $base_uri = PhabricatorEnv::getProductionURI('/'); $engine = new PhabricatorAuthSessionEngine(); $uri = $engine->getOneTimeLoginURI( $this, $this->loadPrimaryEmail(), PhabricatorAuthSessionEngine::ONETIME_WELCOME); $body = <<<EOBODY Welcome to Phabricator! {$admin_username} ({$admin_realname}) has created an account for you. Username: {$user_username} To login to Phabricator, follow this link and set a password: {$uri} After you have set a password, you can login in the future by going here: {$base_uri} EOBODY; if (!$is_serious) { $body .= <<<EOBODY Love, Phabricator EOBODY; } $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($this->getPHID())) ->setForceDelivery(true) ->setSubject('[Phabricator] Welcome to Phabricator') ->setBody($body) ->saveAndSend(); } public function sendUsernameChangeEmail( PhabricatorUser $admin, $old_username) { $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $new_username = $this->getUserName(); $password_instructions = null; if (PhabricatorPasswordAuthProvider::getPasswordProvider()) { $engine = new PhabricatorAuthSessionEngine(); $uri = $engine->getOneTimeLoginURI( $this, null, PhabricatorAuthSessionEngine::ONETIME_USERNAME); $password_instructions = <<<EOTXT If you use a password to login, you'll need to reset it before you can login again. You can reset your password by following this link: {$uri} And, of course, you'll need to use your new username to login from now on. If you use OAuth to login, nothing should change. EOTXT; } $body = <<<EOBODY {$admin_username} ({$admin_realname}) has changed your Phabricator username. Old Username: {$old_username} New Username: {$new_username} {$password_instructions} EOBODY; $mail = id(new PhabricatorMetaMTAMail()) ->addTos(array($this->getPHID())) ->setForceDelivery(true) ->setSubject('[Phabricator] Username Changed') ->setBody($body) ->saveAndSend(); } public static function describeValidUsername() { return pht( 'Usernames must contain only numbers, letters, period, underscore and '. 'hyphen, and can not end with a period. They must have no more than %d '. 'characters.', new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH)); } public static function validateUsername($username) { // NOTE: If you update this, make sure to update: // // - Remarkup rule for @mentions. // - Routing rule for "/p/username/". // - Unit tests, obviously. // - describeValidUsername() method, above. if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) { return false; } return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username); } public static function getDefaultProfileImageURI() { return celerity_get_resource_uri('/rsrc/image/avatar.png'); } public function attachStatus(PhabricatorCalendarEvent $status) { $this->status = $status; return $this; } public function getStatus() { return $this->assertAttached($this->status); } public function hasStatus() { return $this->status !== self::ATTACHABLE; } public function attachProfileImageURI($uri) { $this->profileImage = $uri; return $this; } public function getProfileImageURI() { return $this->assertAttached($this->profileImage); } public function loadProfileImageURI() { if ($this->profileImage && ($this->profileImage !== self::ATTACHABLE)) { return $this->profileImage; } $src_phid = $this->getProfileImagePHID(); if ($src_phid) { // TODO: (T603) Can we get rid of this entirely and move it to // PeopleQuery with attach/attachable? $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); if ($file) { $this->profileImage = $file->getBestURI(); return $this->profileImage; } } $this->profileImage = self::getDefaultProfileImageURI(); return $this->profileImage; } public function getFullName() { if (strlen($this->getRealName())) { return $this->getUsername().' ('.$this->getRealName().')'; } else { return $this->getUsername(); } } public function __toString() { return $this->getUsername(); } public static function loadOneWithEmailAddress($address) { $email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $address); if (!$email) { return null; } return id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $email->getUserPHID()); } /* -( Multi-Factor Authentication )---------------------------------------- */ /** * Update the flag storing this user's enrollment in multi-factor auth. * * With certain settings, we need to check if a user has MFA on every page, * so we cache MFA enrollment on the user object for performance. Calling this * method synchronizes the cache by examining enrollment records. After * updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if * the user is enrolled. * * This method should be called after any changes are made to a given user's * multi-factor configuration. * * @return void * @task factors */ public function updateMultiFactorEnrollment() { $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 'userPHID = %s', $this->getPHID()); $enrolled = count($factors) ? 1 : 0; if ($enrolled !== $this->isEnrolledInMultiFactor) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $this->establishConnection('w'), 'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d', $this->getTableName(), $enrolled, $this->getID()); unset($unguarded); $this->isEnrolledInMultiFactor = $enrolled; } } /** * Check if the user is enrolled in multi-factor authentication. * * Enrolled users have one or more multi-factor authentication sources * attached to their account. For performance, this value is cached. You * can use @{method:updateMultiFactorEnrollment} to update the cache. * * @return bool True if the user is enrolled. * @task factors */ public function getIsEnrolledInMultiFactor() { return $this->isEnrolledInMultiFactor; } /* -( Omnipotence )-------------------------------------------------------- */ /** * Returns true if this user is omnipotent. Omnipotent users bypass all policy * checks. * * @return bool True if the user bypasses policy checks. */ public function isOmnipotent() { return $this->omnipotent; } /** * Get an omnipotent user object for use in contexts where there is no acting * user, notably daemons. * * @return PhabricatorUser An omnipotent user. */ public static function getOmnipotentUser() { static $user = null; if (!$user) { $user = new PhabricatorUser(); $user->omnipotent = true; $user->makeEphemeral(); } return $user; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::POLICY_PUBLIC; case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getIsSystemAgent()) { return PhabricatorPolicies::POLICY_ADMIN; } else { return PhabricatorPolicies::POLICY_NOONE; } } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getPHID() && ($viewer->getPHID() === $this->getPHID()); } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: return pht('Only you can edit your information.'); default: return null; } } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('user.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorUserCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $externals = id(new PhabricatorExternalAccount())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($externals as $external) { $external->delete(); } $prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($prefs as $pref) { $pref->delete(); } $profiles = id(new PhabricatorUserProfile())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($profiles as $profile) { $profile->delete(); } $keys = id(new PhabricatorUserSSHKey())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($keys as $key) { $key->delete(); } $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($emails as $email) { $email->delete(); } $sessions = id(new PhabricatorAuthSession())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($sessions as $session) { $session->delete(); } $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 'userPHID = %s', $this->getPHID()); foreach ($factors as $factor) { $factor->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/phame/application/PhabricatorPhameApplication.php b/src/applications/phame/application/PhabricatorPhameApplication.php index 326c45e54e..156bb20309 100644 --- a/src/applications/phame/application/PhabricatorPhameApplication.php +++ b/src/applications/phame/application/PhabricatorPhameApplication.php @@ -1,67 +1,67 @@ <?php final class PhabricatorPhameApplication extends PhabricatorApplication { public function getName() { return pht('Phame'); } public function getBaseURI() { return '/phame/'; } public function getIconName() { return 'phame'; } public function getShortDescription() { return 'Blog'; } public function getTitleGlyph() { return "\xe2\x9c\xa9"; } public function getHelpURI() { return PhabricatorEnv::getDoclink('Phame User Guide'); } public function isPrototype() { return true; } public function getRoutes() { return array( '/phame/' => array( '' => 'PhamePostListController', 'r/(?P<id>\d+)/(?P<hash>[^/]+)/(?P<name>.*)' => 'PhameResourceController', 'live/(?P<id>[^/]+)/(?P<more>.*)' => 'PhameBlogLiveController', 'post/' => array( '(?:(?P<filter>draft|all)/)?' => 'PhamePostListController', 'blogger/(?P<bloggername>[\w\.-_]+)/' => 'PhamePostListController', 'delete/(?P<id>[^/]+)/' => 'PhamePostDeleteController', 'edit/(?:(?P<id>[^/]+)/)?' => 'PhamePostEditController', 'view/(?P<id>\d+)/' => 'PhamePostViewController', 'publish/(?P<id>\d+)/' => 'PhamePostPublishController', 'unpublish/(?P<id>\d+)/' => 'PhamePostUnpublishController', 'notlive/(?P<id>\d+)/' => 'PhamePostNotLiveController', 'preview/' => 'PhamePostPreviewController', 'framed/(?P<id>\d+)/' => 'PhamePostFramedController', 'new/' => 'PhamePostNewController', - 'move/(?P<id>\d+)/' => 'PhamePostNewController' + 'move/(?P<id>\d+)/' => 'PhamePostNewController', ), 'blog/' => array( '(?:(?P<filter>user|all)/)?' => 'PhameBlogListController', 'delete/(?P<id>[^/]+)/' => 'PhameBlogDeleteController', 'edit/(?P<id>[^/]+)/' => 'PhameBlogEditController', 'view/(?P<id>[^/]+)/' => 'PhameBlogViewController', 'feed/(?P<id>[^/]+)/' => 'PhameBlogFeedController', 'new/' => 'PhameBlogEditController', ), ), ); } } diff --git a/src/applications/phame/controller/blog/PhameBlogEditController.php b/src/applications/phame/controller/blog/PhameBlogEditController.php index 4eb91bb098..e8ec34d19c 100644 --- a/src/applications/phame/controller/blog/PhameBlogEditController.php +++ b/src/applications/phame/controller/blog/PhameBlogEditController.php @@ -1,193 +1,193 @@ <?php final class PhameBlogEditController extends PhameController { private $id; public function willProcessRequest(array $data) { $this->id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( - PhabricatorPolicyCapability::CAN_EDIT + PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$blog) { return new Aphront404Response(); } $submit_button = pht('Save Changes'); $page_title = pht('Edit Blog'); $cancel_uri = $this->getApplicationURI('blog/view/'.$blog->getID().'/'); } else { $blog = id(new PhameBlog()) ->setCreatorPHID($user->getPHID()); $blog->setViewPolicy(PhabricatorPolicies::POLICY_USER); $blog->setEditPolicy(PhabricatorPolicies::POLICY_USER); $blog->setJoinPolicy(PhabricatorPolicies::POLICY_USER); $submit_button = pht('Create Blog'); $page_title = pht('Create Blog'); $cancel_uri = $this->getApplicationURI(); } $e_name = true; $e_custom_domain = null; $errors = array(); if ($request->isFormPost()) { $name = $request->getStr('name'); $description = $request->getStr('description'); $custom_domain = $request->getStr('custom_domain'); $skin = $request->getStr('skin'); if (empty($name)) { $errors[] = pht('You must give the blog a name.'); $e_name = pht('Required'); } else { $e_name = null; } $blog->setName($name); $blog->setDescription($description); $blog->setDomain(nonempty($custom_domain, null)); $blog->setSkin($skin); $blog->setViewPolicy($request->getStr('can_view')); $blog->setEditPolicy($request->getStr('can_edit')); $blog->setJoinPolicy($request->getStr('can_join')); if (!empty($custom_domain)) { list($error_label, $error_text) = $blog->validateCustomDomain($custom_domain); if ($error_label) { $errors[] = $error_text; $e_custom_domain = $error_label; } if ($blog->getViewPolicy() != PhabricatorPolicies::POLICY_PUBLIC) { $errors[] = pht( 'For custom domains to work, the blog must have a view policy of '. 'public.'); // Prefer earlier labels for the multiple error scenario. if (!$e_custom_domain) { $e_custom_domain = pht('Invalid Policy'); } } } // Don't let users remove their ability to edit blogs. PhabricatorPolicyFilter::mustRetainCapability( $user, $blog, PhabricatorPolicyCapability::CAN_EDIT); if (!$errors) { try { $blog->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('blog/view/'.$blog->getID().'/')); } catch (AphrontDuplicateKeyQueryException $ex) { $errors[] = pht('Domain must be unique.'); $e_custom_domain = pht('Not Unique'); } } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($blog) ->execute(); $skins = PhameSkinSpecification::loadAllSkinSpecifications(); $skins = mpull($skins, 'getName'); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($blog->getName()) ->setID('blog-name') ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Description')) ->setName('description') ->setValue($blog->getDescription()) ->setID('blog-description') ->setUser($user) ->setDisableMacros(true)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($blog) ->setPolicies($policies) ->setName('can_view')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($blog) ->setPolicies($policies) ->setName('can_edit')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_JOIN) ->setPolicyObject($blog) ->setPolicies($policies) ->setName('can_join')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Custom Domain')) ->setName('custom_domain') ->setValue($blog->getDomain()) ->setCaption( pht('Must include at least one dot (.), e.g. blog.example.com')) ->setError($e_custom_domain)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Skin')) ->setName('skin') ->setValue($blog->getSkin()) ->setOptions($skins)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($page_title, $this->getApplicationURI('blog/new')); $nav = $this->renderSideNavFilterView(); $nav->selectFilter($this->id ? null : 'blog/new'); $nav->appendChild( array( $crumbs, $form_box, )); return $this->buildApplicationPage( $nav, array( 'title' => $page_title, )); } } diff --git a/src/applications/phame/controller/blog/PhameBlogFeedController.php b/src/applications/phame/controller/blog/PhameBlogFeedController.php index d239650638..68b197a17b 100644 --- a/src/applications/phame/controller/blog/PhameBlogFeedController.php +++ b/src/applications/phame/controller/blog/PhameBlogFeedController.php @@ -1,104 +1,104 @@ <?php final class PhameBlogFeedController extends PhameController { private $id; public function shouldRequireLogin() { return false; } public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$blog) { return new Aphront404Response(); } $posts = id(new PhamePostQuery()) ->setViewer($user) ->withBlogPHIDs(array($blog->getPHID())) ->withVisibility(PhamePost::VISIBILITY_PUBLISHED) ->execute(); $blog_uri = PhabricatorEnv::getProductionURI( $this->getApplicationURI('blog/feed/'.$blog->getID().'/')); $content = array(); $content[] = phutil_tag('title', array(), $blog->getName()); $content[] = phutil_tag('id', array(), $blog_uri); $content[] = phutil_tag('link', array( 'rel' => 'self', 'type' => 'application/atom+xml', - 'href' => $blog_uri + 'href' => $blog_uri, )); $updated = $blog->getDateModified(); if ($posts) { $updated = max($updated, max(mpull($posts, 'getDateModified'))); } $content[] = phutil_tag('updated', array(), date('c', $updated)); $description = $blog->getDescription(); if ($description != '') { $content[] = phutil_tag('subtitle', array(), $description); } $engine = id(new PhabricatorMarkupEngine())->setViewer($user); foreach ($posts as $post) { $engine->addObject($post, PhamePost::MARKUP_FIELD_BODY); } $engine->process(); $blogger_phids = mpull($posts, 'getBloggerPHID'); $bloggers = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs($blogger_phids) ->execute(); foreach ($posts as $post) { $content[] = hsprintf('<entry>'); $content[] = phutil_tag('title', array(), $post->getTitle()); $content[] = phutil_tag('link', array('href' => $post->getViewURI())); $content[] = phutil_tag('id', array(), PhabricatorEnv::getProductionURI( '/phame/post/view/'.$post->getID().'/')); $content[] = hsprintf( '<author><name>%s</name></author>', $bloggers[$post->getBloggerPHID()]->getFullName()); $content[] = phutil_tag( 'updated', array(), date('c', $post->getDateModified())); $content[] = hsprintf( '<content type="xhtml">'. '<div xmlns="http://www.w3.org/1999/xhtml">%s</div>'. '</content>', $engine->getOutput($post, PhamePost::MARKUP_FIELD_BODY)); $content[] = hsprintf('</entry>'); } $content = phutil_tag( 'feed', array('xmlns' => 'http://www.w3.org/2005/Atom'), $content); return id(new AphrontFileResponse()) ->setMimeType('application/xml') ->setContent($content); } } diff --git a/src/applications/phame/controller/blog/PhameBlogViewController.php b/src/applications/phame/controller/blog/PhameBlogViewController.php index c99a38edad..c5f347b6bf 100644 --- a/src/applications/phame/controller/blog/PhameBlogViewController.php +++ b/src/applications/phame/controller/blog/PhameBlogViewController.php @@ -1,190 +1,190 @@ <?php final class PhameBlogViewController extends PhameController { private $id; public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$blog) { return new Aphront404Response(); } $pager = id(new AphrontCursorPagerView()) ->readFromRequest($request); $posts = id(new PhamePostQuery()) ->setViewer($user) ->withBlogPHIDs(array($blog->getPHID())) ->executeWithCursorPager($pager); $nav = $this->renderSideNavFilterView(null); $header = id(new PHUIHeaderView()) ->setHeader($blog->getName()) ->setUser($user) ->setPolicyObject($blog); $handle_phids = array_merge( mpull($posts, 'getBloggerPHID'), mpull($posts, 'getBlogPHID')); $this->loadHandles($handle_phids); $actions = $this->renderActions($blog, $user); $properties = $this->renderProperties($blog, $user, $actions); $post_list = $this->renderPostList( $posts, $user, pht('This blog has no visible posts.')); require_celerity_resource('phame-css'); $post_list = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_LARGE) ->addClass('phame-post-list') ->appendChild($post_list); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($blog->getName(), $this->getApplicationURI()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $nav->appendChild( array( $crumbs, $object_box, $post_list, )); return $this->buildApplicationPage( $nav, array( 'title' => $blog->getName(), )); } private function renderProperties( PhameBlog $blog, PhabricatorUser $user, PhabricatorActionListView $actions) { require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-tooltips'); $properties = new PHUIPropertyListView(); $properties->setActionList($actions); $properties->addProperty( pht('Skin'), $blog->getSkin()); $properties->addProperty( pht('Domain'), $blog->getDomain()); $feed_uri = PhabricatorEnv::getProductionURI( $this->getApplicationURI('blog/feed/'.$blog->getID().'/')); $properties->addProperty( pht('Atom URI'), javelin_tag('a', array( 'href' => $feed_uri, 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Atom URI does not support custom domains.'), 'size' => 320, - ) + ), ), $feed_uri)); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $user, $blog); $properties->addProperty( pht('Editable By'), $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); $properties->addProperty( pht('Joinable By'), $descriptions[PhabricatorPolicyCapability::CAN_JOIN]); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user) ->addObject($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION) ->process(); $properties->addTextContent( phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $engine->getOutput($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION))); return $properties; } private function renderActions(PhameBlog $blog, PhabricatorUser $user) { $actions = id(new PhabricatorActionListView()) ->setObject($blog) ->setObjectURI($this->getRequest()->getRequestURI()) ->setUser($user); $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $blog, PhabricatorPolicyCapability::CAN_EDIT); $can_join = PhabricatorPolicyFilter::hasCapability( $user, $blog, PhabricatorPolicyCapability::CAN_JOIN); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-plus') ->setHref($this->getApplicationURI('post/edit/?blog='.$blog->getID())) ->setName(pht('Write Post')) ->setDisabled(!$can_join) ->setWorkflow(!$can_join)); $actions->addAction( id(new PhabricatorActionView()) ->setUser($user) ->setIcon('fa-globe') ->setHref($blog->getLiveURI()) ->setName(pht('View Live'))); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI('blog/edit/'.$blog->getID().'/')) ->setName('Edit Blog') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-times') ->setHref($this->getApplicationURI('blog/delete/'.$blog->getID().'/')) ->setName('Delete Blog') ->setDisabled(!$can_edit) ->setWorkflow(true)); return $actions; } } diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php index 4dea551414..033b98d7e2 100644 --- a/src/applications/phame/storage/PhameBlog.php +++ b/src/applications/phame/storage/PhameBlog.php @@ -1,303 +1,313 @@ <?php final class PhameBlog extends PhameDAO implements PhabricatorPolicyInterface, PhabricatorMarkupInterface { const MARKUP_FIELD_DESCRIPTION = 'markup:description'; const SKIN_DEFAULT = 'oblivious'; protected $name; protected $description; protected $domain; protected $configData; protected $creatorPHID; protected $viewPolicy; protected $editPolicy; protected $joinPolicy; private $bloggerPHIDs = self::ATTACHABLE; private $bloggers = self::ATTACHABLE; static private $requestBlog; public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'configData' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text64', 'description' => 'text', 'domain' => 'text128?', // T6203/NULLABILITY // These policies should always be non-null. 'joinPolicy' => 'policy?', 'editPolicy' => 'policy?', 'viewPolicy' => 'policy?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'domain' => array( 'columns' => array('domain'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPhameBlogPHIDType::TYPECONST); } public function getSkinRenderer(AphrontRequest $request) { $spec = PhameSkinSpecification::loadOneSkinSpecification( $this->getSkin()); if (!$spec) { $spec = PhameSkinSpecification::loadOneSkinSpecification( self::SKIN_DEFAULT); } if (!$spec) { throw new Exception( 'This blog has an invalid skin, and the default skin failed to '. 'load.'); } $skin = newv($spec->getSkinClass(), array($request)); $skin->setSpecification($spec); return $skin; } /** * Makes sure a given custom blog uri is properly configured in DNS * to point at this Phabricator instance. If there is an error in * the configuration, return a string describing the error and how * to fix it. If there is no error, return an empty string. * * @return string */ public function validateCustomDomain($custom_domain) { $example_domain = 'blog.example.com'; $label = pht('Invalid'); // note this "uri" should be pretty busted given the desired input // so just use it to test if there's a protocol specified $uri = new PhutilURI($custom_domain); if ($uri->getProtocol()) { - return array($label, + return array( + $label, pht( 'The custom domain should not include a protocol. Just provide '. 'the bare domain name (for example, "%s").', - $example_domain)); + $example_domain), + ); } if ($uri->getPort()) { - return array($label, + return array( + $label, pht( 'The custom domain should not include a port number. Just provide '. 'the bare domain name (for example, "%s").', - $example_domain)); + $example_domain), + ); } if (strpos($custom_domain, '/') !== false) { - return array($label, + return array( + $label, pht( 'The custom domain should not specify a path (hosting a Phame '. 'blog at a path is currently not supported). Instead, just provide '. 'the bare domain name (for example, "%s").', - $example_domain)); + $example_domain), + ); } if (strpos($custom_domain, '.') === false) { - return array($label, + return array( + $label, pht( 'The custom domain should contain at least one dot (.) because '. 'some browsers fail to set cookies on domains without a dot. '. 'Instead, use a normal looking domain name like "%s".', - $example_domain)); + $example_domain), + ); } if (!PhabricatorEnv::getEnvConfig('policy.allow-public')) { $href = PhabricatorEnv::getProductionURI( '/config/edit/policy.allow-public/'); - return array(pht('Fix Configuration'), + return array( + pht('Fix Configuration'), pht( 'For custom domains to work, this Phabricator instance must be '. 'configured to allow the public access policy. Configure this '. 'setting %s, or ask an administrator to configure this setting. '. 'The domain can be specified later once this setting has been '. 'changed.', phutil_tag( 'a', array('href' => $href), - pht('here')))); + pht('here'))), + ); } return null; } public function getBloggerPHIDs() { return $this->assertAttached($this->bloggerPHIDs); } public function attachBloggers(array $bloggers) { assert_instances_of($bloggers, 'PhabricatorObjectHandle'); $this->bloggers = $bloggers; return $this; } public function getBloggers() { return $this->assertAttached($this->bloggers); } public function getSkin() { $config = coalesce($this->getConfigData(), array()); return idx($config, 'skin', self::SKIN_DEFAULT); } public function setSkin($skin) { $config = coalesce($this->getConfigData(), array()); $config['skin'] = $skin; return $this->setConfigData($config); } static public function getSkinOptionsForSelect() { $classes = id(new PhutilSymbolLoader()) ->setAncestorClass('PhameBlogSkin') ->setType('class') ->setConcreteOnly(true) ->selectSymbolsWithoutLoading(); return ipull($classes, 'name', 'name'); } public static function setRequestBlog(PhameBlog $blog) { self::$requestBlog = $blog; } public static function getRequestBlog() { return self::$requestBlog; } public function getLiveURI(PhamePost $post = null) { if ($this->getDomain()) { $base = new PhutilURI('http://'.$this->getDomain().'/'); } else { $base = '/phame/live/'.$this->getID().'/'; $base = PhabricatorEnv::getURI($base); } if ($post) { $base .= '/post/'.$post->getPhameTitle(); } return $base; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, PhabricatorPolicyCapability::CAN_JOIN, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); case PhabricatorPolicyCapability::CAN_JOIN: return $this->getJoinPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { $can_edit = PhabricatorPolicyCapability::CAN_EDIT; $can_join = PhabricatorPolicyCapability::CAN_JOIN; switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: // Users who can edit or post to a blog can always view it. if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) { return true; } if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_join)) { return true; } break; case PhabricatorPolicyCapability::CAN_JOIN: // Users who can edit a blog can always post to it. if (PhabricatorPolicyFilter::hasCapability($user, $this, $can_edit)) { return true; } break; } return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht( 'Users who can edit or post on a blog can always view it.'); case PhabricatorPolicyCapability::CAN_JOIN: return pht( 'Users who can edit a blog can always post on it.'); } return null; } /* -( PhabricatorMarkupInterface Implementation )-------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); return $this->getPHID().':'.$field.':'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhameMarkupEngine(); } public function getMarkupText($field) { return $this->getDescription(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getPHID(); } } diff --git a/src/applications/phame/view/PhamePostView.php b/src/applications/phame/view/PhamePostView.php index 5f60b26f18..236ecc2b39 100644 --- a/src/applications/phame/view/PhamePostView.php +++ b/src/applications/phame/view/PhamePostView.php @@ -1,240 +1,240 @@ <?php final class PhamePostView extends AphrontView { private $post; private $author; private $body; private $skin; private $summary; public function setSkin(PhameBlogSkin $skin) { $this->skin = $skin; return $this; } public function getSkin() { return $this->skin; } public function setAuthor(PhabricatorObjectHandle $author) { $this->author = $author; return $this; } public function getAuthor() { return $this->author; } public function setPost(PhamePost $post) { $this->post = $post; return $this; } public function getPost() { return $this->post; } public function setBody($body) { $this->body = $body; return $this; } public function getBody() { return $this->body; } public function setSummary($summary) { $this->summary = $summary; return $this; } public function getSummary() { return $this->summary; } public function renderTitle() { $href = $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()); return phutil_tag( 'h2', array( 'class' => 'phame-post-title', ), phutil_tag( 'a', array( 'href' => $href, ), $this->getPost()->getTitle())); } public function renderDatePublished() { return phutil_tag( 'div', array( 'class' => 'phame-post-date', ), pht( 'Published on %s by %s', phabricator_datetime( $this->getPost()->getDatePublished(), $this->getUser()), $this->getAuthor()->getName())); } public function renderBody() { return phutil_tag( 'div', array( 'class' => 'phame-post-body', ), $this->getBody()); } public function renderSummary() { return phutil_tag( 'div', array( 'class' => 'phame-post-body', ), $this->getSummary()); } public function renderComments() { $post = $this->getPost(); switch ($post->getCommentsWidget()) { case 'facebook': $comments = $this->renderFacebookComments(); break; case 'disqus': $comments = $this->renderDisqusComments(); break; case 'none': default: $comments = null; break; } return $comments; } public function render() { return phutil_tag( 'div', array( 'class' => 'phame-post', ), array( $this->renderTitle(), $this->renderDatePublished(), $this->renderBody(), $this->renderComments(), )); } public function renderWithSummary() { return phutil_tag( 'div', array( 'class' => 'phame-post', ), array( $this->renderTitle(), $this->renderDatePublished(), $this->renderSummary(), )); } private function renderFacebookComments() { $fb_id = PhabricatorFacebookAuthProvider::getFacebookApplicationID(); if (!$fb_id) { return null; } $fb_root = phutil_tag('div', array( 'id' => 'fb-root', ), ''); $c_uri = '//connect.facebook.net/en_US/all.js#xfbml=1&appId='.$fb_id; $fb_js = CelerityStaticResourceResponse::renderInlineScript( jsprintf( '(function(d, s, id) {'. ' var js, fjs = d.getElementsByTagName(s)[0];'. ' if (d.getElementById(id)) return;'. ' js = d.createElement(s); js.id = id;'. ' js.src = %s;'. ' fjs.parentNode.insertBefore(js, fjs);'. '}(document, \'script\', \'facebook-jssdk\'));', $c_uri)); $uri = $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()); $fb_comments = phutil_tag('div', array( 'class' => 'fb-comments', 'data-href' => $uri, 'data-num-posts' => 5, ), ''); return phutil_tag( 'div', array( 'class' => 'phame-comments-facebook', ), array( $fb_root, $fb_js, $fb_comments, )); } private function renderDisqusComments() { $disqus_shortname = PhabricatorEnv::getEnvConfig('disqus.shortname'); if (!$disqus_shortname) { return null; } $post = $this->getPost(); $disqus_thread = phutil_tag('div', array( - 'id' => 'disqus_thread' + 'id' => 'disqus_thread', )); // protip - try some var disqus_developer = 1; action to test locally $disqus_js = CelerityStaticResourceResponse::renderInlineScript( jsprintf( ' var disqus_shortname = %s;'. ' var disqus_identifier = %s;'. ' var disqus_url = %s;'. ' var disqus_title = %s;'. '(function() {'. ' var dsq = document.createElement("script");'. ' dsq.type = "text/javascript";'. ' dsq.async = true;'. ' dsq.src = "//" + disqus_shortname + ".disqus.com/embed.js";'. '(document.getElementsByTagName("head")[0] ||'. ' document.getElementsByTagName("body")[0]).appendChild(dsq);'. '})();', $disqus_shortname, $post->getPHID(), $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()), $post->getTitle())); return phutil_tag( 'div', array( 'class' => 'phame-comments-disqus', ), array( $disqus_thread, $disqus_js, )); } } diff --git a/src/applications/phlux/storage/PhluxVariable.php b/src/applications/phlux/storage/PhluxVariable.php index 23a0ffcc82..a6f27761c9 100644 --- a/src/applications/phlux/storage/PhluxVariable.php +++ b/src/applications/phlux/storage/PhluxVariable.php @@ -1,63 +1,63 @@ <?php final class PhluxVariable extends PhluxDAO implements PhabricatorFlaggableInterface, PhabricatorPolicyInterface { protected $variableKey; protected $variableValue; protected $viewPolicy; protected $editPolicy; public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( - 'variableValue' => self::SERIALIZATION_JSON + 'variableValue' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'variableKey' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_key' => array( 'columns' => array('variableKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PhluxVariablePHIDType::TYPECONST); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->viewPolicy; case PhabricatorPolicyCapability::CAN_EDIT: return $this->editPolicy; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/pholio/application/PhabricatorPholioApplication.php b/src/applications/pholio/application/PhabricatorPholioApplication.php index 8a6d411dc0..88ebf8aaa1 100644 --- a/src/applications/pholio/application/PhabricatorPholioApplication.php +++ b/src/applications/pholio/application/PhabricatorPholioApplication.php @@ -1,80 +1,80 @@ <?php final class PhabricatorPholioApplication extends PhabricatorApplication { public function getName() { return pht('Pholio'); } public function getBaseURI() { return '/pholio/'; } public function getShortDescription() { return pht('Review Mocks and Design'); } public function getIconName() { return 'pholio'; } public function getTitleGlyph() { return "\xE2\x9D\xA6"; } public function getFlavorText() { return pht('Things before they were cool.'); } public function getEventListeners() { return array( new PholioActionMenuEventListener(), ); } public function getRemarkupRules() { return array( new PholioRemarkupRule(), ); } public function getRoutes() { return array( '/M(?P<id>[1-9]\d*)(?:/(?P<imageID>\d+)/)?' => 'PholioMockViewController', '/pholio/' => array( '(?:query/(?P<queryKey>[^/]+)/)?' => 'PholioMockListController', 'new/' => 'PholioMockEditController', 'edit/(?P<id>\d+)/' => 'PholioMockEditController', 'comment/(?P<id>\d+)/' => 'PholioMockCommentController', 'inline/' => array( '(?:(?P<id>\d+)/)?' => 'PholioInlineController', 'list/(?P<id>\d+)/' => 'PholioInlineListController', - 'thumb/(?P<imageid>\d+)/' => 'PholioInlineThumbController' + 'thumb/(?P<imageid>\d+)/' => 'PholioInlineThumbController', ), 'image/' => array( 'upload/' => 'PholioImageUploadController', ), ), ); } public function getQuickCreateItems(PhabricatorUser $viewer) { $items = array(); $item = id(new PHUIListItemView()) ->setName(pht('Pholio Mock')) ->setIcon('fa-picture-o') ->setHref($this->getBaseURI().'new/'); $items[] = $item; return $items; } protected function getCustomCapabilities() { return array( PholioDefaultViewCapability::CAPABILITY => array(), PholioDefaultEditCapability::CAPABILITY => array(), ); } } diff --git a/src/applications/pholio/config/PhabricatorPholioConfigOptions.php b/src/applications/pholio/config/PhabricatorPholioConfigOptions.php index 67cd29d853..5eb1b38ae6 100644 --- a/src/applications/pholio/config/PhabricatorPholioConfigOptions.php +++ b/src/applications/pholio/config/PhabricatorPholioConfigOptions.php @@ -1,26 +1,26 @@ <?php final class PhabricatorPholioConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Pholio'); } public function getDescription() { return pht('Configure Pholio.'); } public function getOptions() { return array( $this->newOption('metamta.pholio.reply-handler-domain', 'string', null) ->setDescription( pht( 'Like {{metamta.maniphest.reply-handler-domain}}, but affects '. 'Pholio.')), $this->newOption('metamta.pholio.subject-prefix', 'string', '[Pholio]') - ->setDescription(pht('Subject prefix for Pholio email.')) + ->setDescription(pht('Subject prefix for Pholio email.')), ); } } diff --git a/src/applications/pholio/query/PholioMockSearchEngine.php b/src/applications/pholio/query/PholioMockSearchEngine.php index ac7d5c971f..7656cdfc49 100644 --- a/src/applications/pholio/query/PholioMockSearchEngine.php +++ b/src/applications/pholio/query/PholioMockSearchEngine.php @@ -1,146 +1,147 @@ <?php final class PholioMockSearchEngine extends PhabricatorApplicationSearchEngine { public function getResultTypeDescription() { return pht('Pholio Mocks'); } public function getApplicationClassName() { return 'PhabricatorPholioApplication'; } public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); $saved->setParameter( 'authorPHIDs', $this->readUsersFromRequest($request, 'authors')); $saved->setParameter( 'statuses', $request->getStrList('status')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PholioMockQuery()) ->needCoverFiles(true) ->needImages(true) ->needTokenCounts(true) ->withAuthorPHIDs($saved->getParameter('authorPHIDs', array())) ->withStatuses($saved->getParameter('statuses', array())); return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved_query) { $phids = $saved_query->getParameter('authorPHIDs', array()); $author_handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->withPHIDs($phids) ->execute(); $statuses = array( '' => pht('Any Status'), 'closed' => pht('Closed'), - 'open' => pht('Open')); + 'open' => pht('Open'), + ); $status = $saved_query->getParameter('statuses', array()); $status = head($status); $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('authors') ->setLabel(pht('Authors')) ->setValue($author_handles)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('status') ->setOptions($statuses) ->setValue($status)); } protected function getURI($path) { return '/pholio/'.$path; } public function getBuiltinQueryNames() { $names = array( 'open' => pht('Open Mocks'), 'all' => pht('All Mocks'), ); if ($this->requireViewer()->isLoggedIn()) { $names['authored'] = pht('Authored'); } return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'open': return $query->setParameter( 'statuses', array('open')); case 'all': return $query; case 'authored': return $query->setParameter( 'authorPHIDs', array($this->requireViewer()->getPHID())); } return parent::buildSavedQueryFromBuiltin($query_key); } protected function getRequiredHandlePHIDsForResultList( array $mocks, PhabricatorSavedQuery $query) { return mpull($mocks, 'getAuthorPHID'); } protected function renderResultList( array $mocks, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($mocks, 'PholioMock'); $viewer = $this->requireViewer(); $board = new PHUIPinboardView(); foreach ($mocks as $mock) { $header = 'M'.$mock->getID().' '.$mock->getName(); $item = id(new PHUIPinboardItemView()) ->setHeader($header) ->setURI('/M'.$mock->getID()) ->setImageURI($mock->getCoverFile()->getThumb280x210URI()) ->setImageSize(280, 210) ->setDisabled($mock->isClosed()) ->addIconCount('fa-picture-o', count($mock->getImages())) ->addIconCount('fa-trophy', $mock->getTokenCount()); if ($mock->getAuthorPHID()) { $author_handle = $handles[$mock->getAuthorPHID()]; $datetime = phabricator_date($mock->getDateCreated(), $viewer); $item->appendChild( pht('By %s on %s', $author_handle->renderLink(), $datetime)); } $board->addItem($item); } return $board; } } diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index 79d1f65a58..bde3a86a36 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -1,193 +1,193 @@ <?php final class PholioMockImagesView extends AphrontView { private $mock; private $imageID; private $requestURI; private $commentFormID; public function setCommentFormID($comment_form_id) { $this->commentFormID = $comment_form_id; return $this; } public function getCommentFormID() { return $this->commentFormID; } public function setRequestURI(PhutilURI $request_uri) { $this->requestURI = $request_uri; return $this; } public function getRequestURI() { return $this->requestURI; } public function setImageID($image_id) { $this->imageID = $image_id; return $this; } public function getImageID() { return $this->imageID; } public function setMock(PholioMock $mock) { $this->mock = $mock; return $this; } public function render() { if (!$this->mock) { throw new Exception('Call setMock() before render()!'); } $mock = $this->mock; require_celerity_resource('javelin-behavior-pholio-mock-view'); $images = array(); $panel_id = celerity_generate_unique_node_id(); $viewport_id = celerity_generate_unique_node_id(); $ids = mpull($mock->getImages(), 'getID'); if ($this->imageID && isset($ids[$this->imageID])) { $selected_id = $this->imageID; } else { $selected_id = head_key($ids); } // TODO: We could maybe do a better job with tailoring this, which is the // image shown on the review stage. $nonimage_uri = celerity_get_resource_uri( 'rsrc/image/icon/fatcow/thumbnails/default.p100.png'); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($this->getUser()); foreach ($mock->getAllImages() as $image) { $engine->addObject($image, 'default'); } $engine->process(); $current_set = 0; foreach ($mock->getAllImages() as $image) { $file = $image->getFile(); $metadata = $file->getMetadata(); $x = idx($metadata, PhabricatorFile::METADATA_IMAGE_WIDTH); $y = idx($metadata, PhabricatorFile::METADATA_IMAGE_HEIGHT); $is_obs = (bool)$image->getIsObsolete(); if (!$is_obs) { $current_set++; } $history_uri = '/pholio/image/history/'.$image->getID().'/'; $images[] = array( 'id' => $image->getID(), 'fullURI' => $file->getBestURI(), 'stageURI' => ($file->isViewableImage() ? $file->getBestURI() : $nonimage_uri), 'pageURI' => $this->getImagePageURI($image, $mock), 'downloadURI' => $file->getDownloadURI(), 'historyURI' => $history_uri, 'width' => $x, 'height' => $y, 'title' => $image->getName(), 'descriptionMarkup' => $engine->getOutput($image, 'default'), 'isObsolete' => (bool)$image->getIsObsolete(), 'isImage' => $file->isViewableImage(), 'isViewable' => $file->isViewableInBrowser(), ); } $navsequence = array(); foreach ($mock->getImages() as $image) { $navsequence[] = $image->getID(); } $full_icon = array( javelin_tag('span', array('aural' => true), pht('View Raw File')), id(new PHUIIconView())->setIconFont('fa-file-image-o'), ); $download_icon = array( javelin_tag('span', array('aural' => true), pht('Download File')), id(new PHUIIconView())->setIconFont('fa-download'), ); $login_uri = id(new PhutilURI('/login/')) ->setQueryParam('next', (string) $this->getRequestURI()); $config = array( 'mockID' => $mock->getID(), 'panelID' => $panel_id, 'viewportID' => $viewport_id, 'commentFormID' => $this->getCommentFormID(), 'images' => $images, 'selectedID' => $selected_id, 'loggedIn' => $this->getUser()->isLoggedIn(), 'logInLink' => (string) $login_uri, 'navsequence' => $navsequence, 'fullIcon' => hsprintf('%s', $full_icon), 'downloadIcon' => hsprintf('%s', $download_icon), 'currentSetSize' => $current_set, ); Javelin::initBehavior('pholio-mock-view', $config); $mockview = ''; $mock_wrapper = javelin_tag( 'div', array( 'id' => $viewport_id, 'sigil' => 'mock-viewport', - 'class' => 'pholio-mock-image-viewport' + 'class' => 'pholio-mock-image-viewport', ), ''); $image_header = javelin_tag( 'div', array( 'id' => 'mock-image-header', 'class' => 'pholio-mock-image-header', ), ''); $mock_wrapper = javelin_tag( 'div', array( 'id' => $panel_id, 'sigil' => 'mock-panel touchable', 'class' => 'pholio-mock-image-panel', ), array( $image_header, $mock_wrapper, )); $inline_comments_holder = javelin_tag( 'div', array( 'id' => 'mock-image-description', 'sigil' => 'mock-image-description', - 'class' => 'mock-image-description' + 'class' => 'mock-image-description', ), ''); $mockview[] = phutil_tag( 'div', array( 'class' => 'pholio-mock-image-container', - 'id' => 'pholio-mock-image-container' + 'id' => 'pholio-mock-image-container', ), array($mock_wrapper, $inline_comments_holder)); return $mockview; } private function getImagePageURI(PholioImage $image, PholioMock $mock) { $uri = '/M'.$mock->getID().'/'.$image->getID().'/'; return $uri; } } diff --git a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php index 57dd2d6817..0e9726616c 100644 --- a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php @@ -1,214 +1,214 @@ <?php final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider { public function isEnabled() { return $this->getWePayClientID() && $this->getWePayClientSecret() && $this->getWePayAccessToken() && $this->getWePayAccountID(); } public function getProviderType() { return 'wepay'; } public function getProviderDomain() { return 'wepay.com'; } public function getPaymentMethodDescription() { return pht('Credit Card or Bank Account'); } public function getPaymentMethodIcon() { return celerity_get_resource_uri('/rsrc/image/phortune/wepay.png'); } public function getPaymentMethodProviderDescription() { return 'WePay'; } public function canHandlePaymentMethod(PhortunePaymentMethod $method) { $type = $method->getMetadataValue('type'); return ($type == 'wepay'); } protected function executeCharge( PhortunePaymentMethod $payment_method, PhortuneCharge $charge) { throw new Exception('!'); } private function getWePayClientID() { return PhabricatorEnv::getEnvConfig('phortune.wepay.client-id'); } private function getWePayClientSecret() { return PhabricatorEnv::getEnvConfig('phortune.wepay.client-secret'); } private function getWePayAccessToken() { return PhabricatorEnv::getEnvConfig('phortune.wepay.access-token'); } private function getWePayAccountID() { return PhabricatorEnv::getEnvConfig('phortune.wepay.account-id'); } /* -( One-Time Payments )-------------------------------------------------- */ public function canProcessOneTimePayments() { return true; } /* -( Controllers )-------------------------------------------------------- */ public function canRespondToControllerAction($action) { switch ($action) { case 'checkout': case 'charge': case 'cancel': return true; } return parent::canRespondToControllerAction(); } /** * @phutil-external-symbol class WePay */ public function processControllerRequest( PhortuneProviderController $controller, AphrontRequest $request) { $viewer = $request->getUser(); $cart = $controller->loadCart($request->getInt('cartID')); if (!$cart) { return new Aphront404Response(); } $root = dirname(phutil_get_library_root('phabricator')); require_once $root.'/externals/wepay/wepay.php'; WePay::useStaging( $this->getWePayClientID(), $this->getWePayClientSecret()); $wepay = new WePay($this->getWePayAccessToken()); $charge = $controller->loadActiveCharge($cart); switch ($controller->getAction()) { case 'checkout': if ($charge) { throw new Exception(pht('Cart is already charging!')); } break; case 'charge': case 'cancel': if (!$charge) { throw new Exception(pht('Cart is not charging yet!')); } break; } switch ($controller->getAction()) { case 'checkout': $return_uri = $this->getControllerURI( 'charge', array( 'cartID' => $cart->getID(), )); $cancel_uri = $this->getControllerURI( 'cancel', array( 'cartID' => $cart->getID(), )); $price = $cart->getTotalPriceAsCurrency(); $params = array( 'account_id' => $this->getWePayAccountID(), 'short_description' => 'Services', // TODO 'type' => 'SERVICE', 'amount' => $price->formatBareValue(), 'long_description' => 'Services', // TODO 'reference_id' => $cart->getPHID(), 'app_fee' => 0, 'fee_payer' => 'Payee', 'redirect_uri' => $return_uri, 'fallback_uri' => $cancel_uri, // NOTE: If we don't `auto_capture`, we might get a result back in // either an "authorized" or a "reserved" state. We can't capture // an "authorized" result, so just autocapture. 'auto_capture' => true, 'require_shipping' => 0, 'shipping_fee' => 0, 'charge_tax' => 0, 'mode' => 'regular', - 'funding_sources' => 'bank,cc' + 'funding_sources' => 'bank,cc', ); $charge = $cart->willApplyCharge($viewer, $this); $result = $wepay->request('checkout/create', $params); $cart->setMetadataValue('provider.checkoutURI', $result->checkout_uri); $cart->save(); $charge->setMetadataValue('wepay.checkoutID', $result->checkout_id); $charge->save(); $uri = new PhutilURI($result->checkout_uri); return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI($uri); case 'charge': $checkout_id = $request->getInt('checkout_id'); $params = array( 'checkout_id' => $checkout_id, ); $checkout = $wepay->request('checkout', $params); if ($checkout->reference_id != $cart->getPHID()) { throw new Exception( pht('Checkout reference ID does not match cart PHID!')); } switch ($checkout->state) { case 'authorized': case 'reserved': case 'captured': break; default: throw new Exception( pht( 'Checkout is in bad state "%s"!', $result->state)); } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $cart->didApplyCharge($charge); unset($unguarded); return id(new AphrontRedirectResponse()) ->setURI($cart->getDoneURI()); case 'cancel': // TODO: I don't know how it's possible to cancel out of a WePay // charge workflow. throw new Exception( pht('How did you get here? WePay has no cancel flow in its UI...?')); break; } throw new Exception( pht('Unsupported action "%s".', $controller->getAction())); } } diff --git a/src/applications/phortune/view/PhortuneCreditCardForm.php b/src/applications/phortune/view/PhortuneCreditCardForm.php index f2280c6fc3..335123e04e 100644 --- a/src/applications/phortune/view/PhortuneCreditCardForm.php +++ b/src/applications/phortune/view/PhortuneCreditCardForm.php @@ -1,107 +1,107 @@ <?php final class PhortuneCreditCardForm { private $formID; private $scripts = array(); private $user; private $errors = array(); private $cardNumberError; private $cardCVCError; private $cardExpirationError; public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function setErrors(array $errors) { $this->errors = $errors; return $this; } public function addScript($script_uri) { $this->scripts[] = $script_uri; return $this; } public function getFormID() { if (!$this->formID) { $this->formID = celerity_generate_unique_node_id(); } return $this->formID; } public function buildForm() { $form_id = $this->getFormID(); require_celerity_resource('phortune-credit-card-form-css'); require_celerity_resource('phortune-credit-card-form'); require_celerity_resource('aphront-tooltip-css'); Javelin::initBehavior('phabricator-tooltips'); $form = new AphrontFormView(); foreach ($this->scripts as $script) { $form->appendChild( phutil_tag( 'script', array( 'type' => 'text/javascript', 'src' => $script, ))); } $errors = $this->errors; $e_number = isset($errors[PhortuneErrCode::ERR_CC_INVALID_NUMBER]) ? pht('Invalid') : true; $e_cvc = isset($errors[PhortuneErrCode::ERR_CC_INVALID_CVC]) ? pht('Invalid') : true; $e_expiry = isset($errors[PhortuneErrCode::ERR_CC_INVALID_EXPIRY]) ? pht('Invalid') : null; $form ->setID($form_id) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('') ->setValue( javelin_tag( 'div', array( 'class' => 'credit-card-logos', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => 'We support Visa, Mastercard, American Express, '. 'Discover, JCB, and Diners Club.', 'size' => 440, - ) + ), )))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Card Number') ->setDisableAutocomplete(true) ->setSigil('number-input') ->setError($e_number)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('CVC') ->setDisableAutocomplete(true) ->setSigil('cvc-input') ->setError($e_cvc)) ->appendChild( id(new PhortuneMonthYearExpiryControl()) ->setLabel('Expiration') ->setUser($this->user) ->setError($e_expiry)); return $form; } } diff --git a/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php b/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php index a800d37609..65ec7731f1 100644 --- a/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php +++ b/src/applications/phpast/controller/PhabricatorXHPASTViewFrameController.php @@ -1,27 +1,28 @@ <?php final class PhabricatorXHPASTViewFrameController extends PhabricatorXHPASTViewController { private $id; public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $id = $this->id; return $this->buildStandardPageResponse( phutil_tag( 'iframe', array( 'src' => '/xhpast/frameset/'.$id.'/', 'frameborder' => '0', 'style' => 'width: 100%; height: 800px;', - '')), + '', + )), array( 'title' => 'XHPAST View', )); } } diff --git a/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php b/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php index efa49e3e77..186b8ed4c8 100644 --- a/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php +++ b/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php @@ -1,186 +1,189 @@ <?php final class PhragmentGetPatchConduitAPIMethod extends PhragmentConduitAPIMethod { public function getAPIMethodName() { return 'phragment.getpatch'; } public function getMethodStatus() { return self::METHOD_STATUS_UNSTABLE; } public function getMethodDescription() { return pht('Retrieve the patches to apply for a given set of files.'); } public function defineParamTypes() { return array( 'path' => 'required string', 'state' => 'required dict<string, string>', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR_BAD_FRAGMENT' => 'No such fragment exists', ); } protected function execute(ConduitAPIRequest $request) { $path = $request->getValue('path'); $state = $request->getValue('state'); // The state is an array mapping file paths to hashes. $patches = array(); // We need to get all of the mappings (like phragment.getstate) first // so that we can detect deletions and creations of files. $fragment = id(new PhragmentFragmentQuery()) ->setViewer($request->getUser()) ->withPaths(array($path)) ->executeOne(); if ($fragment === null) { throw new ConduitException('ERR_BAD_FRAGMENT'); } $mappings = $fragment->getFragmentMappings( $request->getUser(), $fragment->getPath()); $file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID'); $files = id(new PhabricatorFileQuery()) ->setViewer($request->getUser()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); // Scan all of the files that the caller currently has and iterate // over that. foreach ($state as $path => $hash) { // If $mappings[$path] exists, then the user has the file and it's // also a fragment. if (array_key_exists($path, $mappings)) { $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID(); if ($file_phid !== null) { // If the file PHID is present, then we need to check the // hashes to see if they are the same. $hash_caller = strtolower($state[$path]); $hash_current = $files[$file_phid]->getContentHash(); if ($hash_caller === $hash_current) { // The user's version is identical to our version, so // there is no update needed. } else { // The hash differs, and the user needs to update. $patches[] = array( 'path' => $path, 'fileOld' => null, 'fileNew' => $files[$file_phid], 'hashOld' => $hash_caller, 'hashNew' => $hash_current, - 'patchURI' => null); + 'patchURI' => null, + ); } } else { // We have a record of this as a file, but there is no file // attached to the latest version, so we consider this to be // a deletion. $patches[] = array( 'path' => $path, 'fileOld' => null, 'fileNew' => null, 'hashOld' => $hash_caller, 'hashNew' => PhragmentPatchUtil::EMPTY_HASH, - 'patchURI' => null); + 'patchURI' => null, + ); } } else { // If $mappings[$path] does not exist, then the user has a file, // and we have absolutely no record of it what-so-ever (we haven't // even recorded a deletion). Assuming most applications will store // some form of data near their own files, this is probably a data // file relevant for the application that is not versioned, so we // don't tell the client to do anything with it. } } // Check the remaining files that we know about but the caller has // not reported. foreach ($mappings as $path => $child) { if (array_key_exists($path, $state)) { // We have already evaluated this above. } else { $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID(); if ($file_phid !== null) { // If the file PHID is present, then this is a new file that // we know about, but the caller does not. We need to tell // the caller to create the file. $hash_current = $files[$file_phid]->getContentHash(); $patches[] = array( 'path' => $path, 'fileOld' => null, 'fileNew' => $files[$file_phid], 'hashOld' => PhragmentPatchUtil::EMPTY_HASH, 'hashNew' => $hash_current, - 'patchURI' => null); + 'patchURI' => null, + ); } else { // We have a record of deleting this file, and the caller hasn't // reported it, so they've probably deleted it in a previous // update. } } } // Before we can calculate patches, we need to resolve the old versions // of files so we can draw diffs on them. $hashes = array(); foreach ($patches as $patch) { if ($patch['hashOld'] !== PhragmentPatchUtil::EMPTY_HASH) { $hashes[] = $patch['hashOld']; } } $old_files = array(); if (count($hashes) !== 0) { $old_files = id(new PhabricatorFileQuery()) ->setViewer($request->getUser()) ->withContentHashes($hashes) ->execute(); } $old_files = mpull($old_files, null, 'getContentHash'); foreach ($patches as $key => $patch) { if ($patch['hashOld'] !== PhragmentPatchUtil::EMPTY_HASH) { if (array_key_exists($patch['hashOld'], $old_files)) { $patches[$key]['fileOld'] = $old_files[$patch['hashOld']]; } else { // We either can't see or can't read the old file. $patches[$key]['hashOld'] = PhragmentPatchUtil::EMPTY_HASH; $patches[$key]['fileOld'] = null; } } } // Now run through all of the patch entries, calculate the patches // and return the results. foreach ($patches as $key => $patch) { $data = PhragmentPatchUtil::calculatePatch( $patches[$key]['fileOld'], $patches[$key]['fileNew']); unset($patches[$key]['fileOld']); unset($patches[$key]['fileNew']); $file = PhabricatorFile::buildFromFileDataOrHash( $data, array( 'name' => 'patch.dmp', 'ttl' => time() + 60 * 60 * 24, )); $patches[$key]['patchURI'] = $file->getDownloadURI(); } return $patches; } } diff --git a/src/applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php b/src/applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php index 0ecb5f4fd2..f4aafd3105 100644 --- a/src/applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php +++ b/src/applications/phragment/conduit/PhragmentQueryFragmentsConduitAPIMethod.php @@ -1,84 +1,85 @@ <?php final class PhragmentQueryFragmentsConduitAPIMethod extends PhragmentConduitAPIMethod { public function getAPIMethodName() { return 'phragment.queryfragments'; } public function getMethodStatus() { return self::METHOD_STATUS_UNSTABLE; } public function getMethodDescription() { return pht('Query fragments based on their paths.'); } public function defineParamTypes() { return array( 'paths' => 'required list<string>', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR_BAD_FRAGMENT' => 'No such fragment exists', ); } protected function execute(ConduitAPIRequest $request) { $paths = $request->getValue('paths'); $fragments = id(new PhragmentFragmentQuery()) ->setViewer($request->getUser()) ->withPaths($paths) ->execute(); $fragments = mpull($fragments, null, 'getPath'); foreach ($paths as $path) { if (!array_key_exists($path, $fragments)) { throw new ConduitException('ERR_BAD_FRAGMENT'); } } $results = array(); foreach ($fragments as $path => $fragment) { $mappings = $fragment->getFragmentMappings( $request->getUser(), $fragment->getPath()); $file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID'); $files = id(new PhabricatorFileQuery()) ->setViewer($request->getUser()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); $result = array(); foreach ($mappings as $cpath => $child) { $file_phid = $child->getLatestVersion()->getFilePHID(); if (!isset($files[$file_phid])) { // Skip any files we don't have permission to access. continue; } $file = $files[$file_phid]; $cpath = substr($child->getPath(), strlen($fragment->getPath()) + 1); $result[] = array( 'phid' => $child->getPHID(), 'phidVersion' => $child->getLatestVersionPHID(), 'path' => $cpath, 'hash' => $file->getContentHash(), 'version' => $child->getLatestVersion()->getSequence(), - 'uri' => $file->getViewURI()); + 'uri' => $file->getViewURI(), + ); } $results[$path] = $result; } return $results; } } diff --git a/src/applications/phragment/controller/PhragmentBrowseController.php b/src/applications/phragment/controller/PhragmentBrowseController.php index f04c16d787..d41ff4442a 100644 --- a/src/applications/phragment/controller/PhragmentBrowseController.php +++ b/src/applications/phragment/controller/PhragmentBrowseController.php @@ -1,97 +1,98 @@ <?php final class PhragmentBrowseController extends PhragmentController { private $dblob; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->dblob = idx($data, 'dblob', ''); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } $current = nonempty(last($parents), null); $path = ''; if ($current !== null) { $path = $current->getPath(); } $crumbs = $this->buildApplicationCrumbsWithPath($parents); if ($this->hasApplicationCapability( PhragmentCanCreateCapability::CAPABILITY)) { $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Fragment')) ->setHref($this->getApplicationURI('/create/'.$path)) ->setIcon('fa-plus-square')); } $current_box = $this->createCurrentFragmentView($current, false); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); $fragments = null; if ($current === null) { // Find all root fragments. $fragments = id(new PhragmentFragmentQuery()) ->setViewer($this->getRequest()->getUser()) ->needLatestVersion(true) ->withDepths(array(1)) ->execute(); } else { // Find all child fragments. $fragments = id(new PhragmentFragmentQuery()) ->setViewer($this->getRequest()->getUser()) ->needLatestVersion(true) ->withLeadingPath($current->getPath().'/') ->withDepths(array($current->getDepth() + 1)) ->execute(); } foreach ($fragments as $fragment) { $item = id(new PHUIObjectItemView()); $item->setHeader($fragment->getName()); $item->setHref($fragment->getURI()); if (!$fragment->isDirectory()) { $item->addAttribute(pht( 'Last Updated %s', phabricator_datetime( $fragment->getLatestVersion()->getDateCreated(), $viewer))); $item->addAttribute(pht( 'Latest Version %s', $fragment->getLatestVersion()->getSequence())); if ($fragment->isDeleted()) { $item->setDisabled(true); $item->addAttribute(pht('Deleted')); } } else { $item->addAttribute('Directory'); } $list->addItem($item); } return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), $current_box, - $list), + $list, + ), array( 'title' => pht('Browse Fragments'), )); } } diff --git a/src/applications/phragment/controller/PhragmentCreateController.php b/src/applications/phragment/controller/PhragmentCreateController.php index 3125e8631d..9f2543a59d 100644 --- a/src/applications/phragment/controller/PhragmentCreateController.php +++ b/src/applications/phragment/controller/PhragmentCreateController.php @@ -1,131 +1,132 @@ <?php final class PhragmentCreateController extends PhragmentController { private $dblob; public function willProcessRequest(array $data) { $this->dblob = idx($data, 'dblob', ''); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parent = null; $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } if (count($parents) !== 0) { $parent = idx($parents, count($parents) - 1, null); } $parent_path = ''; if ($parent !== null) { $parent_path = $parent->getPath(); } $parent_path = trim($parent_path, '/'); $fragment = id(new PhragmentFragment()); $error_view = null; if ($request->isFormPost()) { $errors = array(); $v_name = $request->getStr('name'); $v_fileid = $request->getInt('fileID'); $v_viewpolicy = $request->getStr('viewPolicy'); $v_editpolicy = $request->getStr('editPolicy'); if (strpos($v_name, '/') !== false) { $errors[] = pht('The fragment name can not contain \'/\'.'); } $file = id(new PhabricatorFile())->load($v_fileid); if ($file === null) { $errors[] = pht('The specified file doesn\'t exist.'); } if (!count($errors)) { $depth = 1; if ($parent !== null) { $depth = $parent->getDepth() + 1; } PhragmentFragment::createFromFile( $viewer, $file, trim($parent_path.'/'.$v_name, '/'), $v_viewpolicy, $v_editpolicy); return id(new AphrontRedirectResponse()) ->setURI('/phragment/browse/'.trim($parent_path.'/'.$v_name, '/')); } else { $error_view = id(new AphrontErrorView()) ->setErrors($errors) ->setTitle(pht('Errors while creating fragment')); } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($fragment) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Parent Path')) ->setDisabled(true) ->setValue('/'.trim($parent_path.'/', '/'))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('File ID')) ->setName('fileID')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setName('viewPolicy') ->setPolicyObject($fragment) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($viewer) ->setName('editPolicy') ->setPolicyObject($fragment) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Create Fragment')) ->addCancelButton( $this->getApplicationURI('browse/'.$parent_path))); $crumbs = $this->buildApplicationCrumbsWithPath($parents); $crumbs->addTextCrumb(pht('Create Fragment')); $box = id(new PHUIObjectBoxView()) ->setHeaderText('Create Fragment') ->setValidationException(null) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), - $box), + $box, + ), array( 'title' => pht('Create Fragment'), )); } } diff --git a/src/applications/phragment/controller/PhragmentHistoryController.php b/src/applications/phragment/controller/PhragmentHistoryController.php index c3102d938d..3df7095c9d 100644 --- a/src/applications/phragment/controller/PhragmentHistoryController.php +++ b/src/applications/phragment/controller/PhragmentHistoryController.php @@ -1,111 +1,112 @@ <?php final class PhragmentHistoryController extends PhragmentController { private $dblob; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->dblob = idx($data, 'dblob', ''); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } $current = idx($parents, count($parents) - 1, null); $path = $current->getPath(); $crumbs = $this->buildApplicationCrumbsWithPath($parents); if ($this->hasApplicationCapability( PhragmentCanCreateCapability::CAPABILITY)) { $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Fragment')) ->setHref($this->getApplicationURI('/create/'.$path)) ->setIcon('fa-plus-square')); } $current_box = $this->createCurrentFragmentView($current, true); $versions = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($current->getPHID())) ->execute(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); $file_phids = mpull($versions, 'getFilePHID'); $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $current, PhabricatorPolicyCapability::CAN_EDIT); $first = true; foreach ($versions as $version) { $item = id(new PHUIObjectItemView()); $item->setHeader('Version '.$version->getSequence()); $item->setHref($version->getURI()); $item->addAttribute(phabricator_datetime( $version->getDateCreated(), $viewer)); if ($version->getFilePHID() === null) { $item->setDisabled(true); $item->addAttribute('Deletion'); } if (!$first && $can_edit) { $item->addAction(id(new PHUIListItemView()) ->setIcon('fa-refresh') ->setRenderNameAsTooltip(true) ->setWorkflow(true) ->setName(pht('Revert to Here')) ->setHref($this->getApplicationURI( 'revert/'.$version->getID().'/'.$current->getPath()))); } $disabled = !isset($files[$version->getFilePHID()]); $action = id(new PHUIListItemView()) ->setIcon('fa-download') ->setDisabled($disabled || !$this->isCorrectlyConfigured()) ->setRenderNameAsTooltip(true) ->setName(pht('Download')); if (!$disabled && $this->isCorrectlyConfigured()) { $action->setHref($files[$version->getFilePHID()] ->getDownloadURI($version->getURI())); } $item->addAction($action); $list->addItem($item); $first = false; } return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), $current_box, - $list), + $list, + ), array( 'title' => pht('Fragment History'), )); } } diff --git a/src/applications/phragment/controller/PhragmentPolicyController.php b/src/applications/phragment/controller/PhragmentPolicyController.php index 756c83e906..700c3edb5b 100644 --- a/src/applications/phragment/controller/PhragmentPolicyController.php +++ b/src/applications/phragment/controller/PhragmentPolicyController.php @@ -1,108 +1,109 @@ <?php final class PhragmentPolicyController extends PhragmentController { private $dblob; public function willProcessRequest(array $data) { $this->dblob = idx($data, 'dblob', ''); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } $fragment = idx($parents, count($parents) - 1, null); $error_view = null; if ($request->isFormPost()) { $errors = array(); $v_view_policy = $request->getStr('viewPolicy'); $v_edit_policy = $request->getStr('editPolicy'); $v_replace_children = $request->getBool('replacePoliciesOnChildren'); $fragment->setViewPolicy($v_view_policy); $fragment->setEditPolicy($v_edit_policy); $fragment->save(); if ($v_replace_children) { // If you can edit a fragment, you can forcibly set the policies // on child fragments, regardless of whether you can see them or not. $children = id(new PhragmentFragmentQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withLeadingPath($fragment->getPath().'/') ->execute(); $children_phids = mpull($children, 'getPHID'); $fragment->openTransaction(); foreach ($children as $child) { $child->setViewPolicy($v_view_policy); $child->setEditPolicy($v_edit_policy); $child->save(); } $fragment->saveTransaction(); } return id(new AphrontRedirectResponse()) ->setURI('/phragment/browse/'.$fragment->getPath()); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($fragment) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($fragment) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($fragment) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicies($policies)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'replacePoliciesOnChildren', 'true', pht( 'Replace policies on child fragments with '. 'the policies above.'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Fragment Policies')) ->addCancelButton( $this->getApplicationURI('browse/'.$fragment->getPath()))); $crumbs = $this->buildApplicationCrumbsWithPath($parents); $crumbs->addTextCrumb(pht('Edit Fragment Policies')); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edit Fragment Policies: %s', $fragment->getPath())) ->setValidationException(null) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), - $box), + $box, + ), array( 'title' => pht('Edit Fragment Policies'), )); } } diff --git a/src/applications/phragment/controller/PhragmentSnapshotCreateController.php b/src/applications/phragment/controller/PhragmentSnapshotCreateController.php index b86be02e30..2cdea8528a 100644 --- a/src/applications/phragment/controller/PhragmentSnapshotCreateController.php +++ b/src/applications/phragment/controller/PhragmentSnapshotCreateController.php @@ -1,168 +1,173 @@ <?php final class PhragmentSnapshotCreateController extends PhragmentController { private $dblob; public function willProcessRequest(array $data) { $this->dblob = idx($data, 'dblob', ''); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } $fragment = nonempty(last($parents), null); if ($fragment === null) { return new Aphront404Response(); } PhabricatorPolicyFilter::requireCapability( $viewer, $fragment, PhabricatorPolicyCapability::CAN_EDIT); $children = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->needLatestVersion(true) ->withLeadingPath($fragment->getPath().'/') ->execute(); $errors = array(); if ($request->isFormPost()) { $v_name = $request->getStr('name'); if (strlen($v_name) === 0) { $errors[] = pht('You must specify a name.'); } if (strpos($v_name, '/') !== false) { $errors[] = pht('Snapshot names can not contain "/".'); } if (!count($errors)) { $snapshot = null; try { // Create the snapshot. $snapshot = id(new PhragmentSnapshot()) ->setPrimaryFragmentPHID($fragment->getPHID()) ->setName($v_name) ->save(); } catch (AphrontDuplicateKeyQueryException $e) { $errors[] = pht('A snapshot with this name already exists.'); } if (!count($errors)) { // Add the primary fragment. id(new PhragmentSnapshotChild()) ->setSnapshotPHID($snapshot->getPHID()) ->setFragmentPHID($fragment->getPHID()) ->setFragmentVersionPHID($fragment->getLatestVersionPHID()) ->save(); // Add all of the child fragments. foreach ($children as $child) { id(new PhragmentSnapshotChild()) ->setSnapshotPHID($snapshot->getPHID()) ->setFragmentPHID($child->getPHID()) ->setFragmentVersionPHID($child->getLatestVersionPHID()) ->save(); } return id(new AphrontRedirectResponse()) ->setURI('/phragment/snapshot/view/'.$snapshot->getID()); } } } $fragment_sequence = '-'; if ($fragment->getLatestVersion() !== null) { $fragment_sequence = $fragment->getLatestVersion()->getSequence(); } $rows = array(); $rows[] = phutil_tag( 'tr', array(), array( phutil_tag('th', array(), 'Fragment'), - phutil_tag('th', array(), 'Version'))); + phutil_tag('th', array(), 'Version'), + )); $rows[] = phutil_tag( 'tr', array(), array( phutil_tag('td', array(), $fragment->getPath()), - phutil_tag('td', array(), $fragment_sequence))); + phutil_tag('td', array(), $fragment_sequence), + )); foreach ($children as $child) { $sequence = '-'; if ($child->getLatestVersion() !== null) { $sequence = $child->getLatestVersion()->getSequence(); } $rows[] = phutil_tag( 'tr', array(), array( phutil_tag('td', array(), $child->getPath()), - phutil_tag('td', array(), $sequence))); + phutil_tag('td', array(), $sequence), + )); } $table = phutil_tag( 'table', array('class' => 'remarkup-table'), $rows); $container = phutil_tag( 'div', array('class' => 'phabricator-remarkup'), array( phutil_tag( 'p', array(), pht( 'The snapshot will contain the following fragments at '. 'the specified versions: ')), - $table)); + $table, + )); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Fragment Path')) ->setDisabled(true) ->setValue('/'.$fragment->getPath())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Snapshot Name')) ->setName('name')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Create Snapshot')) ->addCancelButton( $this->getApplicationURI('browse/'.$fragment->getPath()))) ->appendChild( id(new PHUIFormDividerControl())) ->appendInstructions($container); $crumbs = $this->buildApplicationCrumbsWithPath($parents); $crumbs->addTextCrumb(pht('Create Snapshot')); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create Snapshot of %s', $fragment->getName())) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), - $box), + $box, + ), array( 'title' => pht('Create Fragment'), )); } } diff --git a/src/applications/phragment/controller/PhragmentSnapshotDeleteController.php b/src/applications/phragment/controller/PhragmentSnapshotDeleteController.php index d9958195b7..26591d4aef 100644 --- a/src/applications/phragment/controller/PhragmentSnapshotDeleteController.php +++ b/src/applications/phragment/controller/PhragmentSnapshotDeleteController.php @@ -1,53 +1,54 @@ <?php final class PhragmentSnapshotDeleteController extends PhragmentController { private $id; public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $snapshot = id(new PhragmentSnapshotQuery()) ->setViewer($viewer) ->requireCapabilities(array( PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT)) + PhabricatorPolicyCapability::CAN_EDIT, + )) ->withIDs(array($this->id)) ->executeOne(); if ($snapshot === null) { return new Aphront404Response(); } if ($request->isDialogFormPost()) { $fragment_uri = $snapshot->getPrimaryFragment()->getURI(); $snapshot->delete(); return id(new AphrontRedirectResponse()) ->setURI($fragment_uri); } return $this->createDialog(); } function createDialog() { $request = $this->getRequest(); $viewer = $request->getUser(); $dialog = id(new AphrontDialogView()) ->setTitle(pht('Really delete this snapshot?')) ->setUser($request->getUser()) ->addSubmitButton(pht('Delete')) ->addCancelButton(pht('Cancel')) ->appendParagraph(pht( 'Deleting this snapshot is a permanent operation. You can not '. 'recover the state of the snapshot.')); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phragment/controller/PhragmentSnapshotPromoteController.php b/src/applications/phragment/controller/PhragmentSnapshotPromoteController.php index fa0b40d8c5..5d45842ebe 100644 --- a/src/applications/phragment/controller/PhragmentSnapshotPromoteController.php +++ b/src/applications/phragment/controller/PhragmentSnapshotPromoteController.php @@ -1,192 +1,195 @@ <?php final class PhragmentSnapshotPromoteController extends PhragmentController { private $dblob; private $id; private $targetSnapshot; private $targetFragment; private $snapshots; private $options; public function willProcessRequest(array $data) { $this->dblob = idx($data, 'dblob', null); $this->id = idx($data, 'id', null); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); // When the user is promoting a snapshot to the latest version, the // identifier is a fragment path. if ($this->dblob !== null) { $this->targetFragment = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->requireCapabilities(array( PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT)) + PhabricatorPolicyCapability::CAN_EDIT, + )) ->withPaths(array($this->dblob)) ->executeOne(); if ($this->targetFragment === null) { return new Aphront404Response(); } $this->snapshots = id(new PhragmentSnapshotQuery()) ->setViewer($viewer) ->withPrimaryFragmentPHIDs(array($this->targetFragment->getPHID())) ->execute(); } // When the user is promoting a snapshot to another snapshot, the // identifier is another snapshot ID. if ($this->id !== null) { $this->targetSnapshot = id(new PhragmentSnapshotQuery()) ->setViewer($viewer) ->requireCapabilities(array( PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT)) + PhabricatorPolicyCapability::CAN_EDIT, + )) ->withIDs(array($this->id)) ->executeOne(); if ($this->targetSnapshot === null) { return new Aphront404Response(); } $this->snapshots = id(new PhragmentSnapshotQuery()) ->setViewer($viewer) ->withPrimaryFragmentPHIDs(array( - $this->targetSnapshot->getPrimaryFragmentPHID())) + $this->targetSnapshot->getPrimaryFragmentPHID(), + )) ->execute(); } // If there's no identifier, just 404. if ($this->snapshots === null) { return new Aphront404Response(); } // Work out what options the user has. $this->options = mpull( $this->snapshots, 'getName', 'getID'); if ($this->id !== null) { unset($this->options[$this->id]); } // If there's no options, show a dialog telling the // user there are no snapshots to promote. if (count($this->options) === 0) { return id(new AphrontDialogResponse())->setDialog( id(new AphrontDialogView()) ->setTitle(pht('No snapshots to promote')) ->appendParagraph(pht( 'There are no snapshots available to promote.')) ->setUser($request->getUser()) ->addCancelButton(pht('Cancel'))); } // Handle snapshot promotion. if ($request->isDialogFormPost()) { $snapshot = id(new PhragmentSnapshotQuery()) ->setViewer($viewer) ->withIDs(array($request->getStr('snapshot'))) ->executeOne(); if ($snapshot === null) { return new Aphront404Response(); } $snapshot->openTransaction(); // Delete all existing child entries. $children = id(new PhragmentSnapshotChildQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withSnapshotPHIDs(array($snapshot->getPHID())) ->execute(); foreach ($children as $child) { $child->delete(); } if ($this->id === null) { // The user is promoting the snapshot to the latest version. $children = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->needLatestVersion(true) ->withLeadingPath($this->targetFragment->getPath().'/') ->execute(); // Add the primary fragment. id(new PhragmentSnapshotChild()) ->setSnapshotPHID($snapshot->getPHID()) ->setFragmentPHID($this->targetFragment->getPHID()) ->setFragmentVersionPHID( $this->targetFragment->getLatestVersionPHID()) ->save(); // Add all of the child fragments. foreach ($children as $child) { id(new PhragmentSnapshotChild()) ->setSnapshotPHID($snapshot->getPHID()) ->setFragmentPHID($child->getPHID()) ->setFragmentVersionPHID($child->getLatestVersionPHID()) ->save(); } } else { // The user is promoting the snapshot to another snapshot. We just // copy the other snapshot's child entries and change the snapshot // PHID to make it identical. $children = id(new PhragmentSnapshotChildQuery()) ->setViewer($viewer) ->withSnapshotPHIDs(array($this->targetSnapshot->getPHID())) ->execute(); foreach ($children as $child) { id(new PhragmentSnapshotChild()) ->setSnapshotPHID($snapshot->getPHID()) ->setFragmentPHID($child->getFragmentPHID()) ->setFragmentVersionPHID($child->getFragmentVersionPHID()) ->save(); } } $snapshot->saveTransaction(); if ($this->id === null) { return id(new AphrontRedirectResponse()) ->setURI($this->targetFragment->getURI()); } else { return id(new AphrontRedirectResponse()) ->setURI($this->targetSnapshot->getURI()); } } return $this->createDialog(); } function createDialog() { $request = $this->getRequest(); $viewer = $request->getUser(); $dialog = id(new AphrontDialogView()) ->setTitle(pht('Promote which snapshot?')) ->setUser($request->getUser()) ->addSubmitButton(pht('Promote')) ->addCancelButton(pht('Cancel')); if ($this->id === null) { // The user is promoting a snapshot to the latest version. $dialog->appendParagraph(pht( 'Select the snapshot you want to promote to the latest version:')); } else { // The user is promoting a snapshot to another snapshot. $dialog->appendParagraph(pht( "Select the snapshot you want to promote to '%s':", $this->targetSnapshot->getName())); } $dialog->appendChild( id(new AphrontFormSelectControl()) ->setUser($viewer) ->setName('snapshot') ->setOptions($this->options)); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phragment/controller/PhragmentSnapshotViewController.php b/src/applications/phragment/controller/PhragmentSnapshotViewController.php index 4ea8ba79bb..006ba448c7 100644 --- a/src/applications/phragment/controller/PhragmentSnapshotViewController.php +++ b/src/applications/phragment/controller/PhragmentSnapshotViewController.php @@ -1,154 +1,155 @@ <?php final class PhragmentSnapshotViewController extends PhragmentController { private $id; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->id = idx($data, 'id', ''); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $snapshot = id(new PhragmentSnapshotQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if ($snapshot === null) { return new Aphront404Response(); } $box = $this->createSnapshotView($snapshot); $fragment = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->withPHIDs(array($snapshot->getPrimaryFragmentPHID())) ->executeOne(); if ($fragment === null) { return new Aphront404Response(); } $parents = $this->loadParentFragments($fragment->getPath()); if ($parents === null) { return new Aphront404Response(); } $crumbs = $this->buildApplicationCrumbsWithPath($parents); $crumbs->addTextCrumb(pht('"%s" Snapshot', $snapshot->getName())); $children = id(new PhragmentSnapshotChildQuery()) ->setViewer($viewer) ->needFragments(true) ->needFragmentVersions(true) ->withSnapshotPHIDs(array($snapshot->getPHID())) ->execute(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($children as $child) { $item = id(new PHUIObjectItemView()) ->setHeader($child->getFragment()->getPath()); if ($child->getFragmentVersion() !== null) { $item ->setHref($child->getFragmentVersion()->getURI()) ->addAttribute(pht( 'Version %s', $child->getFragmentVersion()->getSequence())); } else { $item ->setHref($child->getFragment()->getURI()) ->addAttribute(pht('Directory')); } $list->addItem($item); } return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), $box, - $list), + $list, + ), array( 'title' => pht('View Snapshot'), )); } protected function createSnapshotView($snapshot) { if ($snapshot === null) { return null; } $viewer = $this->getRequest()->getUser(); $phids = array(); $phids[] = $snapshot->getPrimaryFragmentPHID(); $this->loadHandles($phids); $header = id(new PHUIHeaderView()) ->setHeader(pht('"%s" Snapshot', $snapshot->getName())) ->setPolicyObject($snapshot) ->setUser($viewer); $zip_uri = $this->getApplicationURI( 'zip@'.$snapshot->getName(). '/'.$snapshot->getPrimaryFragment()->getPath()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $snapshot, PhabricatorPolicyCapability::CAN_EDIT); $actions = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($snapshot) ->setObjectURI($snapshot->getURI()); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Download Snapshot as ZIP')) ->setHref($this->isCorrectlyConfigured() ? $zip_uri : null) ->setDisabled(!$this->isCorrectlyConfigured()) ->setIcon('fa-floppy-o')); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete Snapshot')) ->setHref($this->getApplicationURI( 'snapshot/delete/'.$snapshot->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(true) ->setIcon('fa-times')); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Promote Another Snapshot to Here')) ->setHref($this->getApplicationURI( 'snapshot/promote/'.$snapshot->getID().'/')) ->setDisabled(!$can_edit) ->setWorkflow(true) ->setIcon('fa-arrow-up')); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($snapshot) ->setActionList($actions); $properties->addProperty( pht('Name'), $snapshot->getName()); $properties->addProperty( pht('Fragment'), $this->renderHandlesForPHIDs(array($snapshot->getPrimaryFragmentPHID()))); return id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); } } diff --git a/src/applications/phragment/controller/PhragmentUpdateController.php b/src/applications/phragment/controller/PhragmentUpdateController.php index 289db1ad72..0c70442312 100644 --- a/src/applications/phragment/controller/PhragmentUpdateController.php +++ b/src/applications/phragment/controller/PhragmentUpdateController.php @@ -1,82 +1,83 @@ <?php final class PhragmentUpdateController extends PhragmentController { private $dblob; public function willProcessRequest(array $data) { $this->dblob = idx($data, 'dblob', ''); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } $fragment = idx($parents, count($parents) - 1, null); $error_view = null; if ($request->isFormPost()) { $errors = array(); $v_fileid = $request->getInt('fileID'); $file = id(new PhabricatorFile())->load($v_fileid); if ($file === null) { $errors[] = pht('The specified file doesn\'t exist.'); } if (!count($errors)) { // If the file is a ZIP archive (has application/zip mimetype) // then we extract the zip and apply versions for each of the // individual fragments, creating and deleting files as needed. if ($file->getMimeType() === 'application/zip') { $fragment->updateFromZIP($viewer, $file); } else { $fragment->updateFromFile($viewer, $file); } return id(new AphrontRedirectResponse()) ->setURI('/phragment/browse/'.$fragment->getPath()); } else { $error_view = id(new AphrontErrorView()) ->setErrors($errors) ->setTitle(pht('Errors while updating fragment')); } } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('File ID')) ->setName('fileID')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Update Fragment')) ->addCancelButton( $this->getApplicationURI('browse/'.$fragment->getPath()))); $crumbs = $this->buildApplicationCrumbsWithPath($parents); $crumbs->addTextCrumb(pht('Update Fragment')); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Update Fragment: %s', $fragment->getPath())) ->setValidationException(null) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), - $box), + $box, + ), array( 'title' => pht('Update Fragment'), )); } } diff --git a/src/applications/phragment/controller/PhragmentVersionController.php b/src/applications/phragment/controller/PhragmentVersionController.php index 55ed3d67ee..ceebd3fb96 100644 --- a/src/applications/phragment/controller/PhragmentVersionController.php +++ b/src/applications/phragment/controller/PhragmentVersionController.php @@ -1,136 +1,137 @@ <?php final class PhragmentVersionController extends PhragmentController { private $id; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->id = idx($data, 'id', 0); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $version = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if ($version === null) { return new Aphront404Response(); } $parents = $this->loadParentFragments($version->getFragment()->getPath()); if ($parents === null) { return new Aphront404Response(); } $current = idx($parents, count($parents) - 1, null); $crumbs = $this->buildApplicationCrumbsWithPath($parents); $crumbs->addTextCrumb(pht('View Version %d', $version->getSequence())); $phids = array(); $phids[] = $version->getFilePHID(); $this->loadHandles($phids); $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($version->getFilePHID())) ->executeOne(); if ($file !== null) { $file_uri = $file->getDownloadURI(); } $header = id(new PHUIHeaderView()) ->setHeader(pht( '%s at version %d', $version->getFragment()->getName(), $version->getSequence())) ->setPolicyObject($version) ->setUser($viewer); $actions = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($version) ->setObjectURI($version->getURI()); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Download Version')) ->setDisabled($file === null || !$this->isCorrectlyConfigured()) ->setHref($this->isCorrectlyConfigured() ? $file_uri : null) ->setIcon('fa-download')); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($version) ->setActionList($actions); $properties->addProperty( pht('File'), $this->renderHandlesForPHIDs(array($version->getFilePHID()))); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), $box, - $this->renderPreviousVersionList($version)), + $this->renderPreviousVersionList($version), + ), array( 'title' => pht('View Version'), )); } private function renderPreviousVersionList( PhragmentFragmentVersion $version) { $request = $this->getRequest(); $viewer = $request->getUser(); $previous_versions = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($version->getFragmentPHID())) ->withSequenceBefore($version->getSequence()) ->execute(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($previous_versions as $previous_version) { $item = id(new PHUIObjectItemView()); $item->setHeader('Version '.$previous_version->getSequence()); $item->setHref($previous_version->getURI()); $item->addAttribute(phabricator_datetime( $previous_version->getDateCreated(), $viewer)); $patch_uri = $this->getApplicationURI( 'patch/'.$previous_version->getID().'/'.$version->getID()); $item->addAction(id(new PHUIListItemView()) ->setIcon('fa-file-o') ->setName(pht('Get Patch')) ->setHref($this->isCorrectlyConfigured() ? $patch_uri : null) ->setDisabled(!$this->isCorrectlyConfigured())); $list->addItem($item); } $item = id(new PHUIObjectItemView()); $item->setHeader('Prior to Version 0'); $item->addAttribute('Prior to any content (empty file)'); $item->addAction(id(new PHUIListItemView()) ->setIcon('fa-file-o') ->setName(pht('Get Patch')) ->setHref($this->getApplicationURI( 'patch/x/'.$version->getID()))); $list->addItem($item); return $list; } } diff --git a/src/applications/phragment/storage/PhragmentFragmentVersion.php b/src/applications/phragment/storage/PhragmentFragmentVersion.php index 9085598314..c80faad7fe 100644 --- a/src/applications/phragment/storage/PhragmentFragmentVersion.php +++ b/src/applications/phragment/storage/PhragmentFragmentVersion.php @@ -1,72 +1,72 @@ <?php final class PhragmentFragmentVersion extends PhragmentDAO implements PhabricatorPolicyInterface { protected $sequence; protected $fragmentPHID; protected $filePHID; private $fragment = self::ATTACHABLE; private $file = self::ATTACHABLE; public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'sequence' => 'uint32', 'filePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_version' => array( 'columns' => array('fragmentPHID', 'sequence'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhragmentFragmentVersionPHIDType::TYPECONST); } public function getURI() { return '/phragment/version/'.$this->getID().'/'; } public function getFragment() { return $this->assertAttached($this->fragment); } public function attachFragment(PhragmentFragment $fragment) { return $this->fragment = $fragment; } public function getFile() { return $this->assertAttached($this->file); } public function attachFile(PhabricatorFile $file) { return $this->file = $file; } public function getCapabilities() { return array( - PhabricatorPolicyCapability::CAN_VIEW + PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getFragment()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getFragment()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return $this->getFragment()->describeAutomaticCapability($capability); } } diff --git a/src/applications/phragment/storage/PhragmentSnapshotChild.php b/src/applications/phragment/storage/PhragmentSnapshotChild.php index 502ddc2a82..85a604b2f0 100644 --- a/src/applications/phragment/storage/PhragmentSnapshotChild.php +++ b/src/applications/phragment/storage/PhragmentSnapshotChild.php @@ -1,82 +1,82 @@ <?php final class PhragmentSnapshotChild extends PhragmentDAO implements PhabricatorPolicyInterface { protected $snapshotPHID; protected $fragmentPHID; protected $fragmentVersionPHID; private $snapshot = self::ATTACHABLE; private $fragment = self::ATTACHABLE; private $fragmentVersion = self::ATTACHABLE; public function getConfiguration() { return array( self::CONFIG_COLUMN_SCHEMA => array( 'fragmentVersionPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_child' => array( 'columns' => array( 'snapshotPHID', 'fragmentPHID', 'fragmentVersionPHID', ), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getSnapshot() { return $this->assertAttached($this->snapshot); } public function attachSnapshot(PhragmentSnapshot $snapshot) { return $this->snapshot = $snapshot; } public function getFragment() { return $this->assertAttached($this->fragment); } public function attachFragment(PhragmentFragment $fragment) { return $this->fragment = $fragment; } public function getFragmentVersion() { if ($this->fragmentVersionPHID === null) { return null; } return $this->assertAttached($this->fragmentVersion); } public function attachFragmentVersion(PhragmentFragmentVersion $version) { return $this->fragmentVersion = $version; } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( - PhabricatorPolicyCapability::CAN_VIEW + PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getSnapshot()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getSnapshot() ->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return $this->getSnapshot() ->describeAutomaticCapability($capability); } } diff --git a/src/applications/phrequent/application/PhabricatorPhrequentApplication.php b/src/applications/phrequent/application/PhabricatorPhrequentApplication.php index ba82157ff3..2d54ddfa09 100644 --- a/src/applications/phrequent/application/PhabricatorPhrequentApplication.php +++ b/src/applications/phrequent/application/PhabricatorPhrequentApplication.php @@ -1,65 +1,65 @@ <?php final class PhabricatorPhrequentApplication extends PhabricatorApplication { public function getName() { return pht('Phrequent'); } public function getShortDescription() { return pht('Track Time Spent'); } public function getBaseURI() { return '/phrequent/'; } public function isPrototype() { return true; } public function getIconName() { return 'phrequent'; } public function getApplicationGroup() { return self::GROUP_UTILITIES; } public function getApplicationOrder() { return 0.110; } public function getEventListeners() { return array( new PhrequentUIEventListener(), ); } public function getRoutes() { return array( '/phrequent/' => array( '(?:query/(?P<queryKey>[^/]+)/)?' => 'PhrequentListController', 'track/(?P<verb>[a-z]+)/(?P<phid>[^/]+)/' - => 'PhrequentTrackController' + => 'PhrequentTrackController', ), ); } public function loadStatus(PhabricatorUser $user) { $status = array(); // Show number of objects that are currently // being tracked for a user. $count = PhrequentUserTimeQuery::getUserTotalObjectsTracked($user); $type = PhabricatorApplicationStatusView::TYPE_NEEDS_ATTENTION; $status[] = id(new PhabricatorApplicationStatusView()) ->setType($type) ->setText(pht('%d Object(s) Tracked', $count)) ->setCount($count); return $status; } } diff --git a/src/applications/phrequent/conduit/PhrequentPopConduitAPIMethod.php b/src/applications/phrequent/conduit/PhrequentPopConduitAPIMethod.php index 764b1d75ee..467d822d9f 100644 --- a/src/applications/phrequent/conduit/PhrequentPopConduitAPIMethod.php +++ b/src/applications/phrequent/conduit/PhrequentPopConduitAPIMethod.php @@ -1,52 +1,52 @@ <?php final class PhrequentPopConduitAPIMethod extends PhrequentConduitAPIMethod { public function getAPIMethodName() { return 'phrequent.pop'; } public function getMethodDescription() { return pht('Stop tracking time on an object by popping it from the stack.'); } public function getMethodStatus() { return self::METHOD_STATUS_UNSTABLE; } public function defineParamTypes() { return array( 'objectPHID' => 'phid', 'stopTime' => 'int', - 'note' => 'string' + 'note' => 'string', ); } public function defineReturnType() { return 'phid'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $user = $request->getUser(); $object_phid = $request->getValue('objectPHID'); $timestamp = $request->getValue('stopTime'); $note = $request->getValue('note'); if ($timestamp === null) { $timestamp = time(); } $editor = new PhrequentTrackingEditor(); if (!$object_phid) { return $editor->stopTrackingTop($user, $timestamp, $note); } else { return $editor->stopTracking($user, $object_phid, $timestamp, $note); } } } diff --git a/src/applications/phrequent/conduit/PhrequentPushConduitAPIMethod.php b/src/applications/phrequent/conduit/PhrequentPushConduitAPIMethod.php index cfcf4a9227..d890f8e87c 100644 --- a/src/applications/phrequent/conduit/PhrequentPushConduitAPIMethod.php +++ b/src/applications/phrequent/conduit/PhrequentPushConduitAPIMethod.php @@ -1,47 +1,47 @@ <?php final class PhrequentPushConduitAPIMethod extends PhrequentConduitAPIMethod { public function getAPIMethodName() { return 'phrequent.push'; } public function getMethodDescription() { return pht( 'Start tracking time on an object by '. 'pushing it on the tracking stack.'); } public function getMethodStatus() { return self::METHOD_STATUS_UNSTABLE; } public function defineParamTypes() { return array( 'objectPHID' => 'required phid', - 'startTime' => 'int' + 'startTime' => 'int', ); } public function defineReturnType() { return 'phid'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $user = $request->getUser(); $object_phid = $request->getValue('objectPHID'); $timestamp = $request->getValue('startTime'); if ($timestamp === null) { $timestamp = time(); } $editor = new PhrequentTrackingEditor(); return $editor->startTracking($user, $object_phid, $timestamp); } } diff --git a/src/applications/phrequent/query/PhrequentUserTimeQuery.php b/src/applications/phrequent/query/PhrequentUserTimeQuery.php index 2bb4674ddd..a27e8aca01 100644 --- a/src/applications/phrequent/query/PhrequentUserTimeQuery.php +++ b/src/applications/phrequent/query/PhrequentUserTimeQuery.php @@ -1,330 +1,332 @@ <?php final class PhrequentUserTimeQuery extends PhabricatorCursorPagedPolicyAwareQuery { const ORDER_ID_ASC = 0; const ORDER_ID_DESC = 1; const ORDER_STARTED_ASC = 2; const ORDER_STARTED_DESC = 3; const ORDER_ENDED_ASC = 4; const ORDER_ENDED_DESC = 5; const ORDER_DURATION_ASC = 6; const ORDER_DURATION_DESC = 7; const ENDED_YES = 0; const ENDED_NO = 1; const ENDED_ALL = 2; private $userPHIDs; private $objectPHIDs; private $order = self::ORDER_ID_ASC; private $ended = self::ENDED_ALL; private $needPreemptingEvents; public function withUserPHIDs($user_phids) { $this->userPHIDs = $user_phids; return $this; } public function withObjectPHIDs($object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function withEnded($ended) { $this->ended = $ended; return $this; } public function setOrder($order) { $this->order = $order; return $this; } public function needPreemptingEvents($need_events) { $this->needPreemptingEvents = $need_events; return $this; } private function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->userPHIDs) { $where[] = qsprintf( $conn, 'userPHID IN (%Ls)', $this->userPHIDs); } if ($this->objectPHIDs) { $where[] = qsprintf( $conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } switch ($this->ended) { case self::ENDED_ALL: break; case self::ENDED_YES: $where[] = qsprintf( $conn, 'dateEnded IS NOT NULL'); break; case self::ENDED_NO: $where[] = qsprintf( $conn, 'dateEnded IS NULL'); break; default: throw new Exception("Unknown ended '{$this->ended}'!"); } $where[] = $this->buildPagingClause($conn); return $this->formatWhereClause($where); } protected function getPagingColumn() { switch ($this->order) { case self::ORDER_ID_ASC: case self::ORDER_ID_DESC: return 'id'; case self::ORDER_STARTED_ASC: case self::ORDER_STARTED_DESC: return 'dateStarted'; case self::ORDER_ENDED_ASC: case self::ORDER_ENDED_DESC: return 'dateEnded'; case self::ORDER_DURATION_ASC: case self::ORDER_DURATION_DESC: return 'COALESCE(dateEnded, UNIX_TIMESTAMP()) - dateStarted'; default: throw new Exception("Unknown order '{$this->order}'!"); } } protected function getPagingValue($result) { switch ($this->order) { case self::ORDER_ID_ASC: case self::ORDER_ID_DESC: return $result->getID(); case self::ORDER_STARTED_ASC: case self::ORDER_STARTED_DESC: return $result->getDateStarted(); case self::ORDER_ENDED_ASC: case self::ORDER_ENDED_DESC: return $result->getDateEnded(); case self::ORDER_DURATION_ASC: case self::ORDER_DURATION_DESC: return ($result->getDateEnded() || time()) - $result->getDateStarted(); default: throw new Exception("Unknown order '{$this->order}'!"); } } protected function getReversePaging() { switch ($this->order) { case self::ORDER_ID_ASC: case self::ORDER_STARTED_ASC: case self::ORDER_ENDED_ASC: case self::ORDER_DURATION_ASC: return true; case self::ORDER_ID_DESC: case self::ORDER_STARTED_DESC: case self::ORDER_ENDED_DESC: case self::ORDER_DURATION_DESC: return false; default: throw new Exception("Unknown order '{$this->order}'!"); } } protected function loadPage() { $usertime = new PhrequentUserTime(); $conn = $usertime->establishConnection('r'); $data = queryfx_all( $conn, 'SELECT usertime.* FROM %T usertime %Q %Q %Q', $usertime->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $usertime->loadAllFromArray($data); } protected function didFilterPage(array $page) { if ($this->needPreemptingEvents) { $usertime = new PhrequentUserTime(); $conn_r = $usertime->establishConnection('r'); $preempt = array(); foreach ($page as $event) { $preempt[] = qsprintf( $conn_r, '(userPHID = %s AND (dateStarted BETWEEN %d AND %d) AND (dateEnded IS NULL OR dateEnded > %d))', $event->getUserPHID(), $event->getDateStarted(), nonempty($event->getDateEnded(), PhabricatorTime::getNow()), $event->getDateStarted()); } $preempting_events = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE %Q ORDER BY dateStarted ASC, id ASC', $usertime->getTableName(), implode(' OR ', $preempt)); $preempting_events = $usertime->loadAllFromArray($preempting_events); $preempting_events = mgroup($preempting_events, 'getUserPHID'); foreach ($page as $event) { $e_start = $event->getDateStarted(); $e_end = $event->getDateEnded(); $select = array(); $user_events = idx($preempting_events, $event->getUserPHID(), array()); foreach ($user_events as $u_event) { if ($u_event->getID() == $event->getID()) { // Don't allow an event to preempt itself. continue; } $u_start = $u_event->getDateStarted(); $u_end = $u_event->getDateEnded(); if ($u_start < $e_start) { // This event started before our event started, so it's not // preempting us. continue; } if ($u_start == $e_start) { if ($u_event->getID() < $event->getID()) { // This event started at the same time as our event started, // but has a lower ID, so it's not preempting us. continue; } } if (($e_end !== null) && ($u_start > $e_end)) { // Our event has ended, and this event started after it ended. continue; } if (($u_end !== null) && ($u_end < $e_start)) { // This event ended before our event began. continue; } $select[] = $u_event; } $event->attachPreemptingEvents($select); } } return $page; } /* -( Helper Functions ) --------------------------------------------------- */ public static function getEndedSearchOptions() { return array( self::ENDED_ALL => pht('All'), self::ENDED_NO => pht('No'), - self::ENDED_YES => pht('Yes')); + self::ENDED_YES => pht('Yes'), + ); } public static function getOrderSearchOptions() { return array( self::ORDER_STARTED_ASC => pht('by furthest start date'), self::ORDER_STARTED_DESC => pht('by nearest start date'), self::ORDER_ENDED_ASC => pht('by furthest end date'), self::ORDER_ENDED_DESC => pht('by nearest end date'), self::ORDER_DURATION_ASC => pht('by smallest duration'), - self::ORDER_DURATION_DESC => pht('by largest duration')); + self::ORDER_DURATION_DESC => pht('by largest duration'), + ); } public static function getUserTotalObjectsTracked( PhabricatorUser $user) { $usertime_dao = new PhrequentUserTime(); $conn = $usertime_dao->establishConnection('r'); $count = queryfx_one( $conn, 'SELECT COUNT(usertime.id) N FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.dateEnded IS NULL', $usertime_dao->getTableName(), $user->getPHID()); return $count['N']; } public static function isUserTrackingObject( PhabricatorUser $user, $phid) { $usertime_dao = new PhrequentUserTime(); $conn = $usertime_dao->establishConnection('r'); $count = queryfx_one( $conn, 'SELECT COUNT(usertime.id) N FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.objectPHID = %s '. 'AND usertime.dateEnded IS NULL', $usertime_dao->getTableName(), $user->getPHID(), $phid); return $count['N'] > 0; } public static function getUserTimeSpentOnObject( PhabricatorUser $user, $phid) { $usertime_dao = new PhrequentUserTime(); $conn = $usertime_dao->establishConnection('r'); // First calculate all the time spent where the // usertime blocks have ended. $sum_ended = queryfx_one( $conn, 'SELECT SUM(usertime.dateEnded - usertime.dateStarted) N '. 'FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.objectPHID = %s '. 'AND usertime.dateEnded IS NOT NULL', $usertime_dao->getTableName(), $user->getPHID(), $phid); // Now calculate the time spent where the usertime // blocks have not yet ended. $sum_not_ended = queryfx_one( $conn, 'SELECT SUM(UNIX_TIMESTAMP() - usertime.dateStarted) N '. 'FROM %T usertime '. 'WHERE usertime.userPHID = %s '. 'AND usertime.objectPHID = %s '. 'AND usertime.dateEnded IS NULL', $usertime_dao->getTableName(), $user->getPHID(), $phid); return $sum_ended['N'] + $sum_not_ended['N']; } public function getQueryApplicationClass() { return 'PhabricatorPhrequentApplication'; } } diff --git a/src/applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php b/src/applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php index b2e062a8e8..da0357f10b 100644 --- a/src/applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php +++ b/src/applications/phrequent/storage/__tests__/PhrequentTimeBlockTestCase.php @@ -1,329 +1,329 @@ <?php final class PhrequentTimeBlockTestCase extends PhabricatorTestCase { public function testMergeTimeRanges() { // Overlapping ranges. $input = array( array(50, 150), array(100, 175), ); $expect = array( array(50, 175), ); $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input)); // Identical ranges. $input = array( array(1, 1), array(1, 1), array(1, 1), ); $expect = array( array(1, 1), ); $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input)); // Range which is a strict subset of another range. $input = array( array(2, 7), array(1, 10), ); $expect = array( array(1, 10), ); $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input)); // These are discontinuous and should not be merged. $input = array( array(5, 6), array(7, 8), ); $expect = array( array(5, 6), array(7, 8), ); $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input)); // These overlap only on an edge, but should merge. $input = array( array(5, 6), array(6, 7), ); $expect = array( array(5, 7), ); $this->assertEqual($expect, PhrequentTimeBlock::mergeTimeRanges($input)); } public function testPreemptingEvents() { // Roughly, this is: got into work, started T1, had a meeting from 10-11, // left the office around 2 to meet a client at a coffee shop, worked on // T1 again for about 40 minutes, had the meeting from 3-3:30, finished up // on T1, headed back to the office, got another hour of work in, and // headed home. $event = $this->newEvent('T1', 900, 1700); $event->attachPreemptingEvents( array( $this->newEvent('meeting', 1000, 1100), $this->newEvent('offsite', 1400, 1600), $this->newEvent('T1', 1420, 1580), $this->newEvent('offsite meeting', 1500, 1550), )); $block = new PhrequentTimeBlock(array($event)); $ranges = $block->getObjectTimeRanges(); $ranges = $this->reduceRanges($ranges); $this->assertEqual( array( 'T1' => array( array(900, 1000), // Before morning meeting. array(1100, 1400), // After morning meeting. array(1420, 1500), // Coffee, before client meeting. array(1550, 1580), // Coffee, after client meeting. array(1600, 1700), // After returning from off site. ), ), $ranges); $event = $this->newEvent('T2', 100, 300); $event->attachPreemptingEvents( array( $this->newEvent('meeting', 200, null), )); $block = new PhrequentTimeBlock(array($event)); $ranges = $block->getObjectTimeRanges(); $ranges = $this->reduceRanges($ranges); $this->assertEqual( array( 'T2' => array( array(100, 200), ), ), $ranges); } public function testTimelineSort() { $e1 = $this->newEvent('X1', 1, 1)->setID(1); $in = array( array( 'event' => $e1, 'at' => 1, 'type' => 'start', ), array( 'event' => $e1, 'at' => 1, 'type' => 'end', ), ); usort($in, array('PhrequentTimeBlock', 'sortTimeline')); $this->assertEqual( array( 'start', 'end', ), ipull($in, 'type')); } public function testInstantaneousEvent() { $event = $this->newEvent('T1', 8, 8); $event->attachPreemptingEvents(array()); $block = new PhrequentTimeBlock(array($event)); $ranges = $block->getObjectTimeRanges(); $ranges = $this->reduceRanges($ranges); $this->assertEqual( array( 'T1' => array( array(8, 8), ), ), $ranges); } public function testPopAcrossStrata() { $event = $this->newEvent('T1', 1, 1000); $event->attachPreemptingEvents( array( $this->newEvent('T2', 100, 300), $this->newEvent('T1', 200, 400), $this->newEvent('T3', 250, 275), )); $block = new PhrequentTimeBlock(array($event)); $ranges = $block->getObjectTimeRanges(); $ranges = $this->reduceRanges($ranges); $this->assertEqual( array( 'T1' => array( array(1, 100), array(200, 250), array(275, 1000), ), ), $ranges); } public function testEndDeeperStratum() { $event = $this->newEvent('T1', 1, 1000); $event->attachPreemptingEvents( array( $this->newEvent('T2', 100, 900), $this->newEvent('T1', 200, 400), $this->newEvent('T3', 300, 800), $this->newEvent('T1', 350, 600), $this->newEvent('T4', 380, 390), )); $block = new PhrequentTimeBlock(array($event)); $ranges = $block->getObjectTimeRanges(); $ranges = $this->reduceRanges($ranges); $this->assertEqual( array( 'T1' => array( array(1, 100), array(200, 300), array(350, 380), array(390, 600), array(900, 1000), ), ), $ranges); } public function testOngoing() { $event = $this->newEvent('T1', 1, null); $event->attachPreemptingEvents(array()); $block = new PhrequentTimeBlock(array($event)); $ranges = $block->getObjectTimeRanges(); $ranges = $this->reduceRanges($ranges); $this->assertEqual( array( 'T1' => array( array(1, null), ), ), $ranges); } public function testOngoingInterrupted() { $event = $this->newEvent('T1', 1, null); $event->attachPreemptingEvents( array( $this->newEvent('T2', 100, 900), )); $block = new PhrequentTimeBlock(array($event)); $ranges = $block->getObjectTimeRanges(); $ranges = $this->reduceRanges($ranges); $this->assertEqual( array( 'T1' => array( array(1, 100), - array(900, null) + array(900, null), ), ), $ranges); } public function testOngoingPreempted() { $event = $this->newEvent('T1', 1, null); $event->attachPreemptingEvents( array( $this->newEvent('T2', 100, null), )); $block = new PhrequentTimeBlock(array($event)); $ranges = $block->getObjectTimeRanges(); $ranges = $this->reduceRanges($ranges); $this->assertEqual( array( 'T1' => array( array(1, 100), ), ), $ranges); } public function testSumTimeSlices() { // This block has multiple closed slices. $block = new PhrequentTimeBlock( array( $this->newEvent('T1', 3456, 4456)->attachPreemptingEvents(array()), $this->newEvent('T1', 8000, 9000)->attachPreemptingEvents(array()), )); $this->assertEqual( 2000, $block->getTimeSpentOnObject('T1', 10000)); // This block has an open slice. $block = new PhrequentTimeBlock( array( $this->newEvent('T1', 3456, 4456)->attachPreemptingEvents(array()), $this->newEvent('T1', 8000, null)->attachPreemptingEvents(array()), )); $this->assertEqual( 3000, $block->getTimeSpentOnObject('T1', 10000)); } private function newEvent($object_phid, $start_time, $end_time) { static $id = 0; return id(new PhrequentUserTime()) ->setID(++$id) ->setObjectPHID($object_phid) ->setDateStarted($start_time) ->setDateEnded($end_time); } private function reduceRanges(array $ranges) { $results = array(); foreach ($ranges as $phid => $slices) { $results[$phid] = $slices->getRanges(); } return $results; } } diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index d39bfc18bc..2ace1a281c 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -1,499 +1,501 @@ <?php final class PhrictionDocumentController extends PhrictionController { private $slug; public function willProcessRequest(array $data) { $this->slug = $data['slug']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $slug = PhabricatorSlug::normalize($this->slug); if ($slug != $this->slug) { $uri = PhrictionDocument::getSlugURI($slug); // Canonicalize pages to their one true URI. return id(new AphrontRedirectResponse())->setURI($uri); } require_celerity_resource('phriction-document-css'); $document = id(new PhrictionDocumentQuery()) ->setViewer($user) ->withSlugs(array($slug)) ->executeOne(); $version_note = null; $core_content = ''; $move_notice = ''; $properties = null; if (!$document) { $document = new PhrictionDocument(); if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withPhrictionSlugs(array( - PhrictionDocument::getProjectSlugIdentifier($slug))) + PhrictionDocument::getProjectSlugIdentifier($slug), + )) ->executeOne(); if (!$project) { return new Aphront404Response(); } } $create_uri = '/phriction/edit/?slug='.$slug; $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NODATA); $notice->setTitle(pht('No content here!')); $notice->appendChild( pht( 'No document found at %s. You can <strong>'. '<a href="%s">create a new document here</a></strong>.', phutil_tag('tt', array(), $slug), $create_uri)); $core_content = $notice; $page_title = pht('Page Not Found'); } else { $version = $request->getInt('v'); if ($version) { $content = id(new PhrictionContent())->loadOneWhere( 'documentID = %d AND version = %d', $document->getID(), $version); if (!$content) { return new Aphront404Response(); } if ($content->getID() != $document->getContentID()) { $vdate = phabricator_datetime($content->getDateCreated(), $user); $version_note = new AphrontErrorView(); $version_note->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $version_note->appendChild( pht('You are viewing an older version of this document, as it '. 'appeared on %s.', $vdate)); } } else { $content = id(new PhrictionContent())->load($document->getContentID()); } $page_title = $content->getTitle(); $properties = $this ->buildPropertyListView($document, $content, $slug); $doc_status = $document->getStatus(); $current_status = $content->getChangeType(); if ($current_status == PhrictionChangeType::CHANGE_EDIT || $current_status == PhrictionChangeType::CHANGE_MOVE_HERE) { $core_content = $content->renderContent($user); } else if ($current_status == PhrictionChangeType::CHANGE_DELETE) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('Document Deleted')); $notice->appendChild( pht('This document has been deleted. You can edit it to put new '. 'content here, or use history to revert to an earlier version.')); $core_content = $notice->render(); } else if ($current_status == PhrictionChangeType::CHANGE_STUB) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('Empty Document')); $notice->appendChild( pht('This document is empty. You can edit it to put some proper '. 'content here.')); $core_content = $notice->render(); } else if ($current_status == PhrictionChangeType::CHANGE_MOVE_AWAY) { $new_doc_id = $content->getChangeRef(); $slug_uri = null; // If the new document exists and the viewer can see it, provide a link // to it. Otherwise, render a generic message. $new_docs = id(new PhrictionDocumentQuery()) ->setViewer($user) ->withIDs(array($new_doc_id)) ->execute(); if ($new_docs) { $new_doc = head($new_docs); $slug_uri = PhrictionDocument::getSlugURI($new_doc->getSlug()); } $notice = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE); if ($slug_uri) { $notice->appendChild( phutil_tag( 'p', array(), pht( 'This document has been moved to %s. You can edit it to put '. 'new content here, or use history to revert to an earlier '. 'version.', phutil_tag('a', array('href' => $slug_uri), $slug_uri)))); } else { $notice->appendChild( phutil_tag( 'p', array(), pht( 'This document has been moved. You can edit it to put new '. 'contne here, or use history to revert to an earlier '. 'version.'))); } $core_content = $notice->render(); } else { throw new Exception("Unknown document status '{$doc_status}'!"); } $move_notice = null; if ($current_status == PhrictionChangeType::CHANGE_MOVE_HERE) { $from_doc_id = $content->getChangeRef(); $slug_uri = null; // If the old document exists and is visible, provide a link to it. $from_docs = id(new PhrictionDocumentQuery()) ->setViewer($user) ->withIDs(array($from_doc_id)) ->execute(); if ($from_docs) { $from_doc = head($from_docs); $slug_uri = PhrictionDocument::getSlugURI($from_doc->getSlug()); } $move_notice = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE); if ($slug_uri) { $move_notice->appendChild( pht( 'This document was moved from %s.', phutil_tag('a', array('href' => $slug_uri), $slug_uri))); } else { // Render this for consistency, even though it's a bit silly. $move_notice->appendChild( pht('This document was moved from elsewhere.')); } } } $children = $this->renderDocumentChildren($slug); $actions = $this->buildActionView($user, $document); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); $crumb_views = $this->renderBreadcrumbs($slug); foreach ($crumb_views as $view) { $crumbs->addCrumb($view); } $header = id(new PHUIHeaderView()) ->setHeader($page_title); $prop_list = null; if ($properties) { $prop_list = new PHUIPropertyGroupView(); $prop_list->addPropertyList($properties); } $page_content = id(new PHUIDocumentView()) ->setOffset(true) ->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS) ->setHeader($header) ->appendChild( array( $actions, $prop_list, $version_note, $move_notice, $core_content, )); $core_page = phutil_tag( 'div', array( - 'class' => 'phriction-offset' + 'class' => 'phriction-offset', ), array( $page_content, $children, )); return $this->buildApplicationPage( array( $crumbs->render(), $core_page, ), array( 'pageObjects' => array($document->getPHID()), 'title' => $page_title, )); } private function buildPropertyListView( PhrictionDocument $document, PhrictionContent $content, $slug) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($document); $project_phid = null; if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withPhrictionSlugs(array( - PhrictionDocument::getProjectSlugIdentifier($slug))) + PhrictionDocument::getProjectSlugIdentifier($slug), + )) ->executeOne(); if ($project) { $project_phid = $project->getPHID(); } } $phids = array_filter( array( $content->getAuthorPHID(), $project_phid, )); $this->loadHandles($phids); $project_info = null; if ($project_phid) { $view->addProperty( pht('Project Info'), $this->getHandle($project_phid)->renderLink()); } $view->addProperty( pht('Last Author'), $this->getHandle($content->getAuthorPHID())->renderLink()); $age = time() - $content->getDateCreated(); $age = floor($age / (60 * 60 * 24)); if ($age < 1) { $when = pht('Today'); } else if ($age == 1) { $when = pht('Yesterday'); } else { $when = pht('%d Days Ago', $age); } $view->addProperty(pht('Last Updated'), $when); return $view; } private function buildActionView( PhabricatorUser $user, PhrictionDocument $document) { $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $document, PhabricatorPolicyCapability::CAN_EDIT); $slug = PhabricatorSlug::normalize($this->slug); $action_view = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($document); if (!$document->getID()) { return $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Create This Document')) ->setIcon('fa-plus-square') ->setHref('/phriction/edit/?slug='.$slug)); } $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Document')) ->setIcon('fa-pencil') ->setHref('/phriction/edit/'.$document->getID().'/')); if ($document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) { $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Move Document')) ->setIcon('fa-arrows') ->setHref('/phriction/move/'.$document->getID().'/') ->setWorkflow(true)); $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete Document')) ->setIcon('fa-times') ->setHref('/phriction/delete/'.$document->getID().'/') ->setWorkflow(true)); } return $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setIcon('fa-list') ->setHref(PhrictionDocument::getSlugURI($slug, 'history'))); } private function renderDocumentChildren($slug) { $document_dao = new PhrictionDocument(); $content_dao = new PhrictionContent(); $conn = $document_dao->establishConnection('r'); $limit = 250; $d_child = PhabricatorSlug::getDepth($slug) + 1; $d_grandchild = PhabricatorSlug::getDepth($slug) + 2; // Select children and grandchildren. $children = queryfx_all( $conn, 'SELECT d.slug, d.depth, c.title FROM %T d JOIN %T c ON d.contentID = c.id WHERE d.slug LIKE %> AND d.depth IN (%d, %d) AND d.status IN (%Ld) ORDER BY d.depth, c.title LIMIT %d', $document_dao->getTableName(), $content_dao->getTableName(), ($slug == '/' ? '' : $slug), $d_child, $d_grandchild, array( PhrictionDocumentStatus::STATUS_EXISTS, PhrictionDocumentStatus::STATUS_STUB, ), $limit); if (!$children) { return; } // We're going to render in one of three modes to try to accommodate // different information scales: // // - If we found fewer than $limit rows, we know we have all the children // and grandchildren and there aren't all that many. We can just render // everything. // - If we found $limit rows but the results included some grandchildren, // we just throw them out and render only the children, as we know we // have them all. // - If we found $limit rows and the results have no grandchildren, we // have a ton of children. Render them and then let the user know that // this is not an exhaustive list. if (count($children) == $limit) { $more_children = true; foreach ($children as $child) { if ($child['depth'] == $d_grandchild) { $more_children = false; } } $show_grandchildren = false; } else { $show_grandchildren = true; $more_children = false; } $grandchildren = array(); foreach ($children as $key => $child) { if ($child['depth'] == $d_child) { continue; } else { unset($children[$key]); if ($show_grandchildren) { $ancestors = PhabricatorSlug::getAncestry($child['slug']); $grandchildren[end($ancestors)][] = $child; } } } // Fill in any missing children. $known_slugs = ipull($children, null, 'slug'); foreach ($grandchildren as $slug => $ignored) { if (empty($known_slugs[$slug])) { $children[] = array( 'slug' => $slug, 'depth' => $d_child, 'title' => PhabricatorSlug::getDefaultTitle($slug), 'empty' => true, ); } } $children = isort($children, 'title'); $list = array(); foreach ($children as $child) { $list[] = hsprintf('<li>'); $list[] = $this->renderChildDocumentLink($child); $grand = idx($grandchildren, $child['slug'], array()); if ($grand) { $list[] = hsprintf('<ul>'); foreach ($grand as $grandchild) { $list[] = hsprintf('<li>'); $list[] = $this->renderChildDocumentLink($grandchild); $list[] = hsprintf('</li>'); } $list[] = hsprintf('</ul>'); } $list[] = hsprintf('</li>'); } if ($more_children) { $list[] = phutil_tag('li', array(), pht('More...')); } $content = array( phutil_tag( 'div', array( 'class' => 'phriction-children-header '. 'sprite-gradient gradient-lightblue-header', ), pht('Document Hierarchy')), phutil_tag( 'div', array( 'class' => 'phriction-children', ), phutil_tag('ul', array(), $list)), ); return id(new PHUIDocumentView()) ->setOffset(true) ->appendChild($content); } private function renderChildDocumentLink(array $info) { $title = nonempty($info['title'], pht('(Untitled Document)')); $item = phutil_tag( 'a', array( 'href' => PhrictionDocument::getSlugURI($info['slug']), ), $title); if (isset($info['empty'])) { $item = phutil_tag('em', array(), $item); } return $item; } protected function getDocumentSlug() { return $this->slug; } } diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php index c6d6f0e3c4..b5475e7940 100644 --- a/src/applications/phriction/controller/PhrictionEditController.php +++ b/src/applications/phriction/controller/PhrictionEditController.php @@ -1,299 +1,300 @@ <?php final class PhrictionEditController extends PhrictionController { private $id; public function willProcessRequest(array $data) { $this->id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $current_version = null; if ($this->id) { $document = id(new PhrictionDocumentQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needContent(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$document) { return new Aphront404Response(); } $current_version = $document->getContent()->getVersion(); $revert = $request->getInt('revert'); if ($revert) { $content = id(new PhrictionContent())->loadOneWhere( 'documentID = %d AND version = %d', $document->getID(), $revert); if (!$content) { return new Aphront404Response(); } } else { $content = $document->getContent(); } } else { $slug = $request->getStr('slug'); $slug = PhabricatorSlug::normalize($slug); if (!$slug) { return new Aphront404Response(); } $document = id(new PhrictionDocumentQuery()) ->setViewer($user) ->withSlugs(array($slug)) ->needContent(true) ->executeOne(); if ($document) { $content = $document->getContent(); $current_version = $content->getVersion(); } else { if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withPhrictionSlugs(array( - PhrictionDocument::getProjectSlugIdentifier($slug))) + PhrictionDocument::getProjectSlugIdentifier($slug), + )) ->executeOne(); if (!$project) { return new Aphront404Response(); } } $document = new PhrictionDocument(); $document->setSlug($slug); $content = new PhrictionContent(); $content->setSlug($slug); $default_title = PhabricatorSlug::getDefaultTitle($slug); $content->setTitle($default_title); } } if ($request->getBool('nodraft')) { $draft = null; $draft_key = null; } else { if ($document->getPHID()) { $draft_key = $document->getPHID().':'.$content->getVersion(); } else { $draft_key = 'phriction:'.$content->getSlug(); } $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), $draft_key); } require_celerity_resource('phriction-document-css'); $e_title = true; $notes = null; $errors = array(); if ($request->isFormPost()) { $overwrite = $request->getBool('overwrite'); if (!$overwrite) { $edit_version = $request->getStr('contentVersion'); if ($edit_version != $current_version) { $dialog = $this->newDialog() ->setTitle(pht('Edit Conflict!')) ->appendParagraph( pht( 'Another user made changes to this document after you began '. 'editing it. Do you want to overwrite their changes?')) ->appendParagraph( pht( 'If you choose to overwrite their changes, you should review '. 'the document edit history to see what you overwrote, and '. 'then make another edit to merge the changes if necessary.')) ->addSubmitButton(pht('Overwrite Changes')) ->addCancelButton($request->getRequestURI()); $dialog->addHiddenInput('overwrite', 'true'); foreach ($request->getPassthroughRequestData() as $key => $value) { $dialog->addHiddenInput($key, $value); } return $dialog; } } $title = $request->getStr('title'); $notes = $request->getStr('description'); if (!strlen($title)) { $e_title = pht('Required'); $errors[] = pht('Document title is required.'); } else { $e_title = null; } if ($document->getID()) { if ($content->getTitle() == $title && $content->getContent() == $request->getStr('content')) { $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setTitle(pht('No Edits')); $dialog->appendChild(phutil_tag('p', array(), pht( 'You did not make any changes to the document.'))); $dialog->addCancelButton($request->getRequestURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } } else if (!strlen($request->getStr('content'))) { // We trigger this only for new pages. For existing pages, deleting // all the content counts as deleting the page. $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setTitle(pht('Empty Page')); $dialog->appendChild(phutil_tag('p', array(), pht( 'You can not create an empty document.'))); $dialog->addCancelButton($request->getRequestURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } if (!count($errors)) { $editor = id(PhrictionDocumentEditor::newForSlug($document->getSlug())) ->setActor($user) ->setTitle($title) ->setContent($request->getStr('content')) ->setDescription($notes); $editor->save(); if ($draft) { $draft->delete(); } $uri = PhrictionDocument::getSlugURI($document->getSlug()); return id(new AphrontRedirectResponse())->setURI($uri); } } if ($document->getID()) { $panel_header = pht('Edit Phriction Document'); $submit_button = pht('Save Changes'); } else { $panel_header = pht('Create New Phriction Document'); $submit_button = pht('Create Document'); } $uri = $document->getSlug(); $uri = PhrictionDocument::getSlugURI($uri); $uri = PhabricatorEnv::getProductionURI($uri); $cancel_uri = PhrictionDocument::getSlugURI($document->getSlug()); if ($draft && strlen($draft->getDraft()) && ($draft->getDraft() != $content->getContent())) { $content_text = $draft->getDraft(); $discard = phutil_tag( 'a', array( 'href' => $request->getRequestURI()->alter('nodraft', true), ), pht('discard this draft')); $draft_note = new AphrontErrorView(); $draft_note->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $draft_note->setTitle('Recovered Draft'); $draft_note->appendChild(hsprintf( '<p>Showing a saved draft of your edits, you can %s.</p>', $discard)); } else { $content_text = $content->getContent(); $draft_note = null; } $form = id(new AphrontFormView()) ->setUser($user) ->setWorkflow(true) ->setAction($request->getRequestURI()->getPath()) ->addHiddenInput('slug', $document->getSlug()) ->addHiddenInput('nodraft', $request->getBool('nodraft')) ->addHiddenInput('contentVersion', $current_version) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setValue($content->getTitle()) ->setError($e_title) ->setName('title')) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('URI')) ->setValue($uri)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Content')) ->setValue($content_text) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setName('content') ->setID('document-textarea') ->setUser($user)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Edit Notes')) ->setValue($notes) ->setError(null) ->setName('description')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edit Document')) ->setFormErrors($errors) ->setForm($form); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Document Preview')) ->setPreviewURI('/phriction/preview/') ->setControlID('document-textarea') ->setSkin('document'); $crumbs = $this->buildApplicationCrumbs(); if ($document->getID()) { $crumbs->addTextCrumb( $content->getTitle(), PhrictionDocument::getSlugURI($document->getSlug())); $crumbs->addTextCrumb(pht('Edit')); } else { $crumbs->addTextCrumb(pht('Create')); } return $this->buildApplicationPage( array( $crumbs, $draft_note, $form_box, $preview, ), array( 'title' => pht('Edit Document'), )); } } diff --git a/src/applications/phriction/controller/PhrictionNewController.php b/src/applications/phriction/controller/PhrictionNewController.php index fa122c489e..333600287c 100644 --- a/src/applications/phriction/controller/PhrictionNewController.php +++ b/src/applications/phriction/controller/PhrictionNewController.php @@ -1,82 +1,83 @@ <?php final class PhrictionNewController extends PhrictionController { public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $slug = PhabricatorSlug::normalize($request->getStr('slug')); if ($request->isFormPost()) { $document = id(new PhrictionDocumentQuery()) ->setViewer($user) ->withSlugs(array($slug)) ->executeOne(); $prompt = $request->getStr('prompt', 'no'); $document_exists = $document && $document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS; if ($document_exists && $prompt == 'no') { $dialog = new AphrontDialogView(); $dialog->setSubmitURI('/phriction/new/') ->setTitle(pht('Edit Existing Document?')) ->setUser($user) ->appendChild(pht( 'The document %s already exists. Do you want to edit it instead?', phutil_tag('tt', array(), $slug))) ->addHiddenInput('slug', $slug) ->addHiddenInput('prompt', 'yes') ->addCancelButton('/w/') ->addSubmitButton(pht('Edit Document')); return id(new AphrontDialogResponse())->setDialog($dialog); } else if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withPhrictionSlugs(array( - PhrictionDocument::getProjectSlugIdentifier($slug))) + PhrictionDocument::getProjectSlugIdentifier($slug), + )) ->executeOne(); if (!$project) { $dialog = new AphrontDialogView(); $dialog->setSubmitURI('/w/') ->setTitle(pht('Oops!')) ->setUser($user) ->appendChild(pht( 'You cannot create wiki pages under "projects/", because they are reserved as project pages. Create a new project with this name first.')) ->addCancelButton('/w/', 'Okay'); return id(new AphrontDialogResponse())->setDialog($dialog); } } $uri = '/phriction/edit/?slug='.$slug; return id(new AphrontRedirectResponse()) ->setURI($uri); } if ($slug == '/') { $slug = ''; } $view = id(new PHUIFormLayoutView()) ->appendChild(id(new AphrontFormTextControl()) ->setLabel('/w/') ->setValue($slug) ->setName('slug')); $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('New Document')) ->setSubmitURI('/phriction/new/') ->appendChild(phutil_tag('p', array(), pht('Create a new document at'))) ->appendChild($view) ->addSubmitButton(pht('Create')) ->addCancelButton('/w/'); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phriction/editor/PhrictionDocumentEditor.php b/src/applications/phriction/editor/PhrictionDocumentEditor.php index 89321b3c95..e3f900844e 100644 --- a/src/applications/phriction/editor/PhrictionDocumentEditor.php +++ b/src/applications/phriction/editor/PhrictionDocumentEditor.php @@ -1,378 +1,379 @@ <?php /** * Create or update Phriction documents. */ final class PhrictionDocumentEditor extends PhabricatorEditor { private $document; private $content; private $newTitle; private $newContent; private $description; // For the Feed Story when moving documents private $fromDocumentPHID; private function __construct() { // <restricted> } public static function newForSlug($slug) { $slug = PhabricatorSlug::normalize($slug); // TODO: Get rid of this. $document = id(new PhrictionDocument())->loadOneWhere( 'slug = %s', $slug); $content = null; if ($document) { $content = id(new PhrictionContent())->load($document->getContentID()); } else { $document = new PhrictionDocument(); $document->setSlug($slug); } if (!$content) { $default_title = PhabricatorSlug::getDefaultTitle($slug); $content = new PhrictionContent(); $content->setSlug($slug); $content->setTitle($default_title); $content->setContent(''); } $obj = new PhrictionDocumentEditor(); $obj->document = $document; $obj->content = $content; return $obj; } public function setTitle($title) { $this->newTitle = $title; return $this; } public function setContent($content) { $this->newContent = $content; return $this; } public function setDescription($description) { $this->description = $description; return $this; } public function getDocument() { return $this->document; } public function moveAway($new_doc_id) { return $this->execute( PhrictionChangeType::CHANGE_MOVE_AWAY, true, $new_doc_id); } public function moveHere($old_doc_id, $old_doc_phid) { $this->fromDocumentPHID = $old_doc_phid; return $this->execute( PhrictionChangeType::CHANGE_MOVE_HERE, false, $old_doc_id); } private function execute( $change_type, $del_new_content = true, $doc_ref = null) { $actor = $this->requireActor(); $document = $this->document; $content = $this->content; $new_content = $this->buildContentTemplate($document, $content); $new_content->setChangeType($change_type); if ($del_new_content) { $new_content->setContent(''); } if ($doc_ref) { $new_content->setChangeRef($doc_ref); } return $this->updateDocument($document, $content, $new_content); } public function delete() { return $this->execute(PhrictionChangeType::CHANGE_DELETE, true); } private function stub() { return $this->execute(PhrictionChangeType::CHANGE_STUB, true); } public function save() { $actor = $this->requireActor(); if ($this->newContent === '') { // If this is an edit which deletes all the content, just treat it as // a delete. NOTE: null means "don't change the content", not "delete // the page"! Thus the strict type check. return $this->delete(); } $document = $this->document; $content = $this->content; $new_content = $this->buildContentTemplate($document, $content); return $this->updateDocument($document, $content, $new_content); } private function buildContentTemplate( PhrictionDocument $document, PhrictionContent $content) { $new_content = new PhrictionContent(); $new_content->setSlug($document->getSlug()); $new_content->setAuthorPHID($this->getActor()->getPHID()); $new_content->setChangeType(PhrictionChangeType::CHANGE_EDIT); $new_content->setTitle( coalesce( $this->newTitle, $content->getTitle())); $new_content->setContent( coalesce( $this->newContent, $content->getContent())); if (strlen($this->description)) { $new_content->setDescription($this->description); } return $new_content; } private function updateDocument($document, $content, $new_content) { $is_new = false; if (!$document->getID()) { $is_new = true; } $new_content->setVersion($content->getVersion() + 1); $change_type = $new_content->getChangeType(); switch ($change_type) { case PhrictionChangeType::CHANGE_EDIT: $doc_status = PhrictionDocumentStatus::STATUS_EXISTS; $feed_action = $is_new ? PhrictionActionConstants::ACTION_CREATE : PhrictionActionConstants::ACTION_EDIT; break; case PhrictionChangeType::CHANGE_DELETE: $doc_status = PhrictionDocumentStatus::STATUS_DELETED; $feed_action = PhrictionActionConstants::ACTION_DELETE; if ($is_new) { throw new Exception( "You can not delete a document which doesn't exist yet!"); } break; case PhrictionChangeType::CHANGE_STUB: $doc_status = PhrictionDocumentStatus::STATUS_STUB; $feed_action = null; break; case PhrictionChangeType::CHANGE_MOVE_AWAY: $doc_status = PhrictionDocumentStatus::STATUS_MOVED; $feed_action = null; break; case PhrictionChangeType::CHANGE_MOVE_HERE: $doc_status = PhrictionDocumentStatus::STATUS_EXISTS; $feed_action = PhrictionActionConstants::ACTION_MOVE_HERE; break; default: throw new Exception( "Unsupported content change type '{$change_type}'!"); } $document->setStatus($doc_status); // TODO: This should be transactional. if ($is_new) { $document->save(); } $new_content->setDocumentID($document->getID()); $new_content->save(); $document->setContentID($new_content->getID()); $document->save(); $document->attachContent($new_content); id(new PhabricatorSearchIndexer()) ->queueDocumentForIndexing($document->getPHID()); // Stub out empty parent documents if they don't exist $ancestral_slugs = PhabricatorSlug::getAncestry($document->getSlug()); if ($ancestral_slugs) { $ancestors = id(new PhrictionDocument())->loadAllWhere( 'slug IN (%Ls)', $ancestral_slugs); $ancestors = mpull($ancestors, null, 'getSlug'); foreach ($ancestral_slugs as $slug) { // We check for change type to prevent near-infinite recursion if (!isset($ancestors[$slug]) && $new_content->getChangeType() != PhrictionChangeType::CHANGE_STUB) { id(PhrictionDocumentEditor::newForSlug($slug)) ->setActor($this->getActor()) ->setTitle(PhabricatorSlug::getDefaultTitle($slug)) ->setContent('') ->setDescription(pht('Empty Parent Document')) ->stub(); } } } $project_phid = null; $slug = $document->getSlug(); if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($this->requireActor()) ->withPhrictionSlugs(array( - PhrictionDocument::getProjectSlugIdentifier($slug))) + PhrictionDocument::getProjectSlugIdentifier($slug), + )) ->executeOne(); if ($project) { $project_phid = $project->getPHID(); } } $related_phids = array( $document->getPHID(), $this->getActor()->getPHID(), ); if ($project_phid) { $related_phids[] = $project_phid; } if ($this->fromDocumentPHID) { $related_phids[] = $this->fromDocumentPHID; } if ($feed_action) { $content_str = id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(140) ->truncateString($new_content->getContent()); id(new PhabricatorFeedStoryPublisher()) ->setRelatedPHIDs($related_phids) ->setStoryAuthorPHID($this->getActor()->getPHID()) ->setStoryTime(time()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_PHRICTION) ->setStoryData( array( 'phid' => $document->getPHID(), 'action' => $feed_action, 'content' => $content_str, 'project' => $project_phid, 'movedFromPHID' => $this->fromDocumentPHID, )) ->publish(); } // TODO: Migrate to ApplicationTransactions fast, so we get rid of this code $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $document->getPHID()); $this->sendMailToSubscribers($subscribers, $content); return $this; } private function getChangeTypeDescription($const, $title) { $map = array( PhrictionChangeType::CHANGE_EDIT => pht('Phriction Document %s was edited.', $title), PhrictionChangeType::CHANGE_DELETE => pht('Phriction Document %s was deleted.', $title), PhrictionChangeType::CHANGE_MOVE_HERE => pht('Phriction Document %s was moved here.', $title), PhrictionChangeType::CHANGE_MOVE_AWAY => pht('Phriction Document %s was moved away.', $title), PhrictionChangeType::CHANGE_STUB => pht('Phriction Document %s was created through child.', $title), ); return idx($map, $const, pht('Something magical occurred.')); } private function sendMailToSubscribers(array $subscribers, $old_content) { if (!$subscribers) { return; } $author_phid = $this->getActor()->getPHID(); $document = $this->document; $content = $document->getContent(); $slug_uri = PhrictionDocument::getSlugURI($document->getSlug()); $diff_uri = new PhutilURI('/phriction/diff/'.$document->getID().'/'); $prod_uri = PhabricatorEnv::getProductionURI(''); $vs_head = $diff_uri ->alter('l', $old_content->getVersion()) ->alter('r', $content->getVersion()); $old_title = $old_content->getTitle(); $title = $content->getTitle(); $name = $this->getChangeTypeDescription($content->getChangeType(), $title); $action = PhrictionChangeType::getChangeTypeLabel( $content->getChangeType()); $body = array($name); // Content may have changed, you never know if ($content->getChangeType() == PhrictionChangeType::CHANGE_EDIT) { if ($old_title != $title) { $body[] = pht('Title was changed from "%s" to "%s"', $old_title, $title); } $body[] = pht("Link to new version:\n%s", $prod_uri.$slug_uri.'?v='.$content->getVersion()); $body[] = pht("Link to diff:\n%s", $prod_uri.$vs_head); } else if ($content->getChangeType() == PhrictionChangeType::CHANGE_MOVE_AWAY) { $target_document = id(new PhrictionDocument()) ->load($content->getChangeRef()); $slug_uri = PhrictionDocument::getSlugURI($target_document->getSlug()); $body[] = pht("Link to destination document:\n%s", $prod_uri.$slug_uri); } $body = implode("\n\n", $body); $subject_prefix = $this->getMailSubjectPrefix(); $mail = new PhabricatorMetaMTAMail(); $mail->setSubject($name) ->setSubjectPrefix($subject_prefix) ->setVarySubjectPrefix('['.$action.']') ->addHeader('Thread-Topic', $name) ->setFrom($author_phid) ->addTos($subscribers) ->setBody($body) ->setRelatedPHID($document->getPHID()) ->setIsBulk(true); $mail->saveAndSend(); } /* --( For less copy-pasting when switching to ApplicationTransactions )--- */ protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.phriction.subject-prefix'); } } diff --git a/src/applications/policy/config/PhabricatorPolicyConfigOptions.php b/src/applications/policy/config/PhabricatorPolicyConfigOptions.php index 03328a6c4d..5637f9dcdf 100644 --- a/src/applications/policy/config/PhabricatorPolicyConfigOptions.php +++ b/src/applications/policy/config/PhabricatorPolicyConfigOptions.php @@ -1,44 +1,45 @@ <?php final class PhabricatorPolicyConfigOptions extends PhabricatorApplicationConfigOptions { public function getName() { return pht('Policy'); } public function getDescription() { return pht('Options relating to object visibility.'); } public function getOptions() { return array( $this->newOption('policy.allow-public', 'bool', false) ->setBoolOptions( array( pht('Allow Public Visibility'), - pht('Require Login'))) + pht('Require Login'), + )) ->setSummary(pht('Allow users to set object visibility to public.')) ->setDescription( pht( "Phabricator allows you to set the visibility of objects (like ". "repositories and tasks) to 'Public', which means **anyone ". "on the internet can see them, without needing to log in or ". "have an account**.". "\n\n". "This is intended for open source projects. Many installs will ". "never want to make anything public, so this policy is disabled ". "by default. You can enable it here, which will let you set the ". "policy for objects to 'Public'.". "\n\n". "Enabling this setting will immediately open up some features, ". "like the user directory. Anyone on the internet will be able to ". "access these features.". "\n\n". "With this setting disabled, the 'Public' policy is not ". "available, and the most open policy is 'All Users' (which means ". "users must have accounts and be logged in to view things).")), ); } } diff --git a/src/applications/policy/controller/PhabricatorPolicyEditController.php b/src/applications/policy/controller/PhabricatorPolicyEditController.php index 3560f4beac..d1b0d6dc14 100644 --- a/src/applications/policy/controller/PhabricatorPolicyEditController.php +++ b/src/applications/policy/controller/PhabricatorPolicyEditController.php @@ -1,219 +1,219 @@ <?php final class PhabricatorPolicyEditController extends PhabricatorPolicyController { private $phid; public function willProcessRequest(array $data) { $this->phid = idx($data, 'phid'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $action_options = array( PhabricatorPolicy::ACTION_ALLOW => pht('Allow'), PhabricatorPolicy::ACTION_DENY => pht('Deny'), ); $rules = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorPolicyRule') ->loadObjects(); $rules = msort($rules, 'getRuleOrder'); $default_rule = array( 'action' => head_key($action_options), 'rule' => head_key($rules), 'value' => null, ); if ($this->phid) { $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->withPHIDs(array($this->phid)) ->execute(); if (!$policies) { return new Aphront404Response(); } $policy = head($policies); } else { $policy = id(new PhabricatorPolicy()) ->setRules(array($default_rule)) ->setDefaultAction(PhabricatorPolicy::ACTION_DENY); } $root_id = celerity_generate_unique_node_id(); $default_action = $policy->getDefaultAction(); $rule_data = $policy->getRules(); $errors = array(); if ($request->isFormPost()) { $data = $request->getStr('rules'); $data = @json_decode($data, true); if (!is_array($data)) { throw new Exception('Failed to JSON decode rule data!'); } $rule_data = array(); foreach ($data as $rule) { $action = idx($rule, 'action'); switch ($action) { case 'allow': case 'deny': break; default: throw new Exception("Invalid action '{$action}'!"); } $rule_class = idx($rule, 'rule'); if (empty($rules[$rule_class])) { throw new Exception("Invalid rule class '{$rule_class}'!"); } $rule_obj = $rules[$rule_class]; $value = $rule_obj->getValueForStorage(idx($rule, 'value')); $rule_data[] = array( 'action' => $action, 'rule' => $rule_class, 'value' => $value, ); } // Filter out nonsense rules, like a "users" rule without any users // actually specified. $valid_rules = array(); foreach ($rule_data as $rule) { $rule_class = $rule['rule']; if ($rules[$rule_class]->ruleHasEffect($rule['value'])) { $valid_rules[] = $rule; } } if (!$valid_rules) { $errors[] = pht('None of these policy rules have any effect.'); } // NOTE: Policies are immutable once created, and we always create a new // policy here. If we didn't, we would need to lock this endpoint down, // as users could otherwise just go edit the policies of objects with // custom policies. if (!$errors) { $new_policy = new PhabricatorPolicy(); $new_policy->setRules($valid_rules); $new_policy->setDefaultAction($request->getStr('default')); $new_policy->save(); $data = array( 'phid' => $new_policy->getPHID(), 'info' => array( 'name' => $new_policy->getName(), 'full' => $new_policy->getName(), 'icon' => $new_policy->getIcon(), ), ); return id(new AphrontAjaxResponse())->setContent($data); } } // Convert rule values to display format (for example, expanding PHIDs // into tokens). foreach ($rule_data as $key => $rule) { $rule_data[$key]['value'] = $rules[$rule['rule']]->getValueForDisplay( $viewer, $rule['value']); } $default_select = AphrontFormSelectControl::renderSelectTag( $default_action, $action_options, array( 'name' => 'default', )); if ($errors) { $errors = id(new AphrontErrorView()) ->setErrors($errors); } $form = id(new PHUIFormLayoutView()) ->appendChild($errors) ->appendChild( javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'rules', 'sigil' => 'rules', ))) ->appendChild( id(new AphrontFormInsetView()) ->setTitle(pht('Rules')) ->setRightButton( javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'create-rule', - 'mustcapture' => true + 'mustcapture' => true, ), pht('New Rule'))) ->setDescription( pht('These rules are processed in order.')) ->setContent(javelin_tag( 'table', array( 'sigil' => 'rules', - 'class' => 'policy-rules-table' + 'class' => 'policy-rules-table', ), ''))) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('If No Rules Match')) ->setValue(pht( '%s all other users.', $default_select))); $form = phutil_tag( 'div', array( 'id' => $root_id, ), $form); $rule_options = mpull($rules, 'getRuleDescription'); $type_map = mpull($rules, 'getValueControlType'); $templates = mpull($rules, 'getValueControlTemplate'); require_celerity_resource('policy-edit-css'); Javelin::initBehavior( 'policy-rule-editor', array( 'rootID' => $root_id, 'actions' => $action_options, 'rules' => $rule_options, 'types' => $type_map, 'templates' => $templates, 'data' => $rule_data, 'defaultRule' => $default_rule, )); $dialog = id(new AphrontDialogView()) ->setWidth(AphrontDialogView::WIDTH_FULL) ->setUser($viewer) ->setTitle(pht('Edit Policy')) ->appendChild($form) ->addSubmitButton(pht('Save Policy')) ->addCancelButton('#'); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php index 30d521911c..ed3245f3c7 100644 --- a/src/applications/policy/storage/PhabricatorPolicy.php +++ b/src/applications/policy/storage/PhabricatorPolicy.php @@ -1,364 +1,364 @@ <?php final class PhabricatorPolicy extends PhabricatorPolicyDAO implements PhabricatorPolicyInterface { const ACTION_ALLOW = 'allow'; const ACTION_DENY = 'deny'; private $name; private $shortName; private $type; private $href; private $workflow; private $icon; protected $rules = array(); protected $defaultAction = self::ACTION_DENY; private $ruleObjects = self::ATTACHABLE; public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'rules' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'defaultAction' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPolicyPHIDTypePolicy::TYPECONST); } public static function newFromPolicyAndHandle( $policy_identifier, PhabricatorObjectHandle $handle = null) { $is_global = PhabricatorPolicyQuery::isGlobalPolicy($policy_identifier); if ($is_global) { return PhabricatorPolicyQuery::getGlobalPolicy($policy_identifier); } if (!$handle) { throw new Exception( "Policy identifier is an object PHID ('{$policy_identifier}'), but no ". "object handle was provided. A handle must be provided for object ". "policies."); } $handle_phid = $handle->getPHID(); if ($policy_identifier != $handle_phid) { throw new Exception( "Policy identifier is an object PHID ('{$policy_identifier}'), but ". "the provided handle has a different PHID ('{$handle_phid}'). The ". "handle must correspond to the policy identifier."); } $policy = id(new PhabricatorPolicy()) ->setPHID($policy_identifier) ->setHref($handle->getURI()); $phid_type = phid_get_type($policy_identifier); switch ($phid_type) { case PhabricatorProjectProjectPHIDType::TYPECONST: $policy->setType(PhabricatorPolicyType::TYPE_PROJECT); $policy->setName($handle->getName()); break; case PhabricatorPeopleUserPHIDType::TYPECONST: $policy->setType(PhabricatorPolicyType::TYPE_USER); $policy->setName($handle->getFullName()); break; case PhabricatorPolicyPHIDTypePolicy::TYPECONST: // TODO: This creates a weird handle-based version of a rule policy. // It behaves correctly, but can't be applied since it doesn't have // any rules. It is used to render transactions, and might need some // cleanup. break; default: $policy->setType(PhabricatorPolicyType::TYPE_MASKED); $policy->setName($handle->getFullName()); break; } $policy->makeEphemeral(); return $policy; } public function setType($type) { $this->type = $type; return $this; } public function getType() { if (!$this->type) { return PhabricatorPolicyType::TYPE_CUSTOM; } return $this->type; } public function setName($name) { $this->name = $name; return $this; } public function getName() { if (!$this->name) { return pht('Custom Policy'); } return $this->name; } public function setShortName($short_name) { $this->shortName = $short_name; return $this; } public function getShortName() { if ($this->shortName) { return $this->shortName; } return $this->getName(); } public function setHref($href) { $this->href = $href; return $this; } public function getHref() { return $this->href; } public function setWorkflow($workflow) { $this->workflow = $workflow; return $this; } public function getWorkflow() { return $this->workflow; } public function getIcon() { switch ($this->getType()) { case PhabricatorPolicyType::TYPE_GLOBAL: static $map = array( PhabricatorPolicies::POLICY_PUBLIC => 'fa-globe', PhabricatorPolicies::POLICY_USER => 'fa-users', PhabricatorPolicies::POLICY_ADMIN => 'fa-eye', PhabricatorPolicies::POLICY_NOONE => 'fa-ban', ); return idx($map, $this->getPHID(), 'fa-question-circle'); case PhabricatorPolicyType::TYPE_USER: return 'fa-user'; case PhabricatorPolicyType::TYPE_PROJECT: return 'fa-briefcase'; case PhabricatorPolicyType::TYPE_CUSTOM: case PhabricatorPolicyType::TYPE_MASKED: return 'fa-certificate'; default: return 'fa-question-circle'; } } public function getSortKey() { return sprintf( '%02d%s', PhabricatorPolicyType::getPolicyTypeOrder($this->getType()), $this->getSortName()); } private function getSortName() { if ($this->getType() == PhabricatorPolicyType::TYPE_GLOBAL) { static $map = array( PhabricatorPolicies::POLICY_PUBLIC => 0, PhabricatorPolicies::POLICY_USER => 1, PhabricatorPolicies::POLICY_ADMIN => 2, PhabricatorPolicies::POLICY_NOONE => 3, ); return idx($map, $this->getPHID()); } return $this->getName(); } public static function getPolicyExplanation( PhabricatorUser $viewer, $policy) { switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: return pht('This object is public.'); case PhabricatorPolicies::POLICY_USER: return pht('Logged in users can take this action.'); case PhabricatorPolicies::POLICY_ADMIN: return pht('Administrators can take this action.'); case PhabricatorPolicies::POLICY_NOONE: return pht('By default, no one can take this action.'); default: $handle = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array($policy)) ->executeOne(); $type = phid_get_type($policy); if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) { return pht( 'Members of the project "%s" can take this action.', $handle->getFullName()); } else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) { return pht( '%s can take this action.', $handle->getFullName()); } else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) { return pht( 'This object has a custom policy controlling who can take this '. 'action.'); } else { return pht( 'This object has an unknown or invalid policy setting ("%s").', $policy); } } } public function getFullName() { switch ($this->getType()) { case PhabricatorPolicyType::TYPE_PROJECT: return pht('Project: %s', $this->getName()); case PhabricatorPolicyType::TYPE_MASKED: return pht('Other: %s', $this->getName()); default: return $this->getName(); } } - public function renderDescription($icon=false) { + public function renderDescription($icon = false) { $img = null; if ($icon) { $img = id(new PHUIIconView()) ->setIconFont($this->getIcon()); } if ($this->getHref()) { $desc = javelin_tag( 'a', array( 'href' => $this->getHref(), 'class' => 'policy-link', 'sigil' => $this->getWorkflow() ? 'workflow' : null, ), array( $img, $this->getName(), )); } else { if ($img) { $desc = array($img, $this->getName()); } else { $desc = $this->getName(); } } switch ($this->getType()) { case PhabricatorPolicyType::TYPE_PROJECT: return pht('%s (Project)', $desc); case PhabricatorPolicyType::TYPE_CUSTOM: return $desc; case PhabricatorPolicyType::TYPE_MASKED: return pht( '%s (You do not have permission to view policy details.)', $desc); default: return $desc; } } /** * Return a list of custom rule classes (concrete subclasses of * @{class:PhabricatorPolicyRule}) this policy uses. * * @return list<string> List of class names. */ public function getCustomRuleClasses() { $classes = array(); foreach ($this->getRules() as $rule) { $class = idx($rule, 'rule'); try { if (class_exists($class)) { $classes[$class] = $class; } } catch (Exception $ex) { continue; } } return array_keys($classes); } /** * Return a list of all values used by a given rule class to implement this * policy. This is used to bulk load data (like project memberships) in order * to apply policy filters efficiently. * * @param string Policy rule classname. * @return list<wild> List of values used in this policy. */ public function getCustomRuleValues($rule_class) { $values = array(); foreach ($this->getRules() as $rule) { if ($rule['rule'] == $rule_class) { $values[] = $rule['value']; } } return $values; } public function attachRuleObjects(array $objects) { $this->ruleObjects = $objects; return $this; } public function getRuleObjects() { return $this->assertAttached($this->ruleObjects); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // NOTE: We implement policies only so we can comply with the interface. // The actual query skips them, as enforcing policies on policies seems // perilous and isn't currently required by the application. return PhabricatorPolicies::POLICY_PUBLIC; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index 504f2fb777..2c046b50e2 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -1,415 +1,415 @@ <?php final class PonderQuestionViewController extends PonderController { private $questionID; public function willProcessRequest(array $data) { $this->questionID = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $question = id(new PonderQuestionQuery()) ->setViewer($user) ->withIDs(array($this->questionID)) ->needAnswers(true) ->needViewerVotes(true) ->executeOne(); if (!$question) { return new Aphront404Response(); } $question->attachVotes($user->getPHID()); $question_xactions = $this->buildQuestionTransactions($question); $answers = $this->buildAnswers($question->getAnswers()); $authors = mpull($question->getAnswers(), null, 'getAuthorPHID'); if (isset($authors[$user->getPHID()])) { $answer_add_panel = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NODATA) ->appendChild( pht( 'You have already answered this question. You can not answer '. 'twice, but you can edit your existing answer.')); } else { $answer_add_panel = new PonderAddAnswerView(); $answer_add_panel ->setQuestion($question) ->setUser($user) ->setActionURI('/ponder/answer/add/'); } $header = id(new PHUIHeaderView()) ->setHeader($question->getTitle()); $actions = $this->buildActionListView($question); $properties = $this->buildPropertyListView($question, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->setActionList($actions); $crumbs->addTextCrumb('Q'.$this->questionID, '/Q'.$this->questionID); return $this->buildApplicationPage( array( $crumbs, $object_box, $question_xactions, $answers, - $answer_add_panel + $answer_add_panel, ), array( 'title' => 'Q'.$question->getID().' '.$question->getTitle(), 'pageObjects' => array_merge( array($question->getPHID()), mpull($question->getAnswers(), 'getPHID')), )); } private function buildActionListView(PonderQuestion $question) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $question->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $question, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($request->getUser()) ->setObject($question) ->setObjectURI($request->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Question')) ->setHref($this->getApplicationURI("/question/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $name = pht('Close Question'); $icon = 'fa-times'; $href = 'close'; } else { $name = pht('Reopen Question'); $icon = 'fa-check-circle-o'; $href = 'open'; } $view->addAction( id(new PhabricatorActionView()) ->setName($name) ->setIcon($icon) ->setRenderAsForm($can_edit) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit) ->setHref($this->getApplicationURI("/question/{$href}/{$id}/"))); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-list') ->setName(pht('View History')) ->setHref($this->getApplicationURI("/question/history/{$id}/"))); return $view; } private function buildPropertyListView( PonderQuestion $question, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($question) ->setActionList($actions); $this->loadHandles(array($question->getAuthorPHID())); $view->addProperty( pht('Status'), PonderQuestionStatus::getQuestionStatusFullName($question->getStatus())); $view->addProperty( pht('Author'), $this->getHandle($question->getAuthorPHID())->renderLink()); $view->addProperty( pht('Created'), phabricator_datetime($question->getDateCreated(), $viewer)); $view->invokeWillRenderEvent(); $votable = id(new PonderVotableView()) ->setPHID($question->getPHID()) ->setURI($this->getApplicationURI('vote/')) ->setCount($question->getVoteCount()) ->setVote($question->getUserVote()); $view->addSectionHeader(pht('Question')); $view->addTextContent( array( $votable, phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), PhabricatorMarkupEngine::renderOneObject( $question, $question->getMarkupField(), $viewer)), )); return $view; } private function buildQuestionTransactions(PonderQuestion $question) { $viewer = $this->getRequest()->getUser(); $id = $question->getID(); $xactions = id(new PonderQuestionTransactionQuery()) ->setViewer($viewer) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)) ->withObjectPHIDs(array($question->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $timeline = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($question->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $add_comment = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($question->getPHID()) ->setShowPreview(false) ->setHeaderText(pht('Question Comment')) ->setAction($this->getApplicationURI("/question/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); return $this->wrapComments( count($xactions), array( $timeline, $add_comment, )); } private function buildAnswers(array $answers) { $request = $this->getRequest(); $viewer = $request->getUser(); $out = array(); $phids = mpull($answers, 'getAuthorPHID'); $this->loadHandles($phids); $xactions = id(new PonderAnswerTransactionQuery()) ->setViewer($viewer) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)) ->withObjectPHIDs(mpull($answers, 'getPHID')) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($xactions as $xaction) { if ($xaction->getComment()) { $engine->addObject( $xaction->getComment(), PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); } } $engine->process(); $xaction_groups = mgroup($xactions, 'getObjectPHID'); foreach ($answers as $answer) { $author_phid = $answer->getAuthorPHID(); $xactions = idx($xaction_groups, $answer->getPHID(), array()); $id = $answer->getID(); $out[] = phutil_tag('br'); $out[] = phutil_tag('br'); $out[] = id(new PhabricatorAnchorView()) ->setAnchorName("A$id"); $header = id(new PHUIHeaderView()) ->setHeader($this->getHandle($author_phid)->getFullName()); $actions = $this->buildAnswerActions($answer); $properties = $this->buildAnswerProperties($answer, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $out[] = $object_box; $details = array(); $details[] = id(new PhabricatorApplicationTransactionView()) ->setUser($viewer) ->setObjectPHID($answer->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $form = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($answer->getPHID()) ->setShowPreview(false) ->setHeaderText(pht('Answer Comment')) ->setAction($this->getApplicationURI("/answer/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); $details[] = $form; $out[] = $this->wrapComments( count($xactions), $details); } $out[] = phutil_tag('br'); $out[] = phutil_tag('br'); return $out; } private function buildAnswerActions(PonderAnswer $answer) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $answer->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $answer, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PhabricatorActionListView()) ->setUser($request->getUser()) ->setObject($answer) ->setObjectURI($request->getRequestURI()); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Answer')) ->setHref($this->getApplicationURI("/answer/edit/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-list') ->setName(pht('View History')) ->setHref($this->getApplicationURI("/answer/history/{$id}/"))); return $view; } private function buildAnswerProperties( PonderAnswer $answer, PhabricatorActionListView $actions) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($answer) ->setActionList($actions); $view->addProperty( pht('Created'), phabricator_datetime($answer->getDateCreated(), $viewer)); $view->invokeWillRenderEvent(); $votable = id(new PonderVotableView()) ->setPHID($answer->getPHID()) ->setURI($this->getApplicationURI('vote/')) ->setCount($answer->getVoteCount()) ->setVote($answer->getUserVote()); $view->addSectionHeader(pht('Answer')); $view->addTextContent( array( $votable, phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), PhabricatorMarkupEngine::renderOneObject( $answer, $answer->getMarkupField(), $viewer)), )); return $view; } private function wrapComments($n, $stuff) { if ($n == 0) { $text = pht('Add a Comment'); } else { $text = pht('Show %s Comments', new PhutilNumber($n)); } $show_id = celerity_generate_unique_node_id(); $hide_id = celerity_generate_unique_node_id(); Javelin::initBehavior('phabricator-reveal-content'); require_celerity_resource('ponder-comment-table-css'); $show = phutil_tag( 'div', array( 'id' => $show_id, 'class' => 'ponder-show-comments', ), javelin_tag( 'a', array( 'href' => '#', 'sigil' => 'reveal-content', 'meta' => array( 'showIDs' => array($hide_id), 'hideIDs' => array($show_id), ), ), $text)); $hide = phutil_tag( 'div', array( 'id' => $hide_id, 'style' => 'display: none', ), $stuff); return array($show, $hide); } } diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php index 4db9118867..5caa52ad78 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -1,277 +1,277 @@ <?php final class PonderQuestion extends PonderDAO implements PhabricatorMarkupInterface, PonderVotableInterface, PhabricatorSubscribableInterface, PhabricatorFlaggableInterface, PhabricatorPolicyInterface, PhabricatorTokenReceiverInterface, PhabricatorProjectInterface, PhabricatorDestructibleInterface { const MARKUP_FIELD_CONTENT = 'markup:content'; protected $title; protected $phid; protected $authorPHID; protected $status; protected $content; protected $contentSource; protected $voteCount; protected $answerCount; protected $heat; protected $mailKey; private $answers; private $vote; private $comments; public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'voteCount' => 'sint32', 'status' => 'uint32', 'content' => 'text', 'heat' => 'double', 'answerCount' => 'uint32', 'mailKey' => 'bytes20', // T6203/NULLABILITY // This should always exist. 'contentSource' => 'text?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'heat' => array( 'columns' => array('heat'), ), 'status' => array( 'columns' => array('status'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(PonderQuestionPHIDType::TYPECONST); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function attachVotes($user_phid) { $qa_phids = mpull($this->answers, 'getPHID') + array($this->getPHID()); $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($user_phid)) ->withDestinationPHIDs($qa_phids) ->withEdgeTypes( array( PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION, - PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER + PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER, )) ->needEdgeData(true) ->execute(); $question_edge = $edges[$user_phid][PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION]; $answer_edges = $edges[$user_phid][PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_ANSWER]; $edges = null; $this->setUserVote(idx($question_edge, $this->getPHID())); foreach ($this->answers as $answer) { $answer->setUserVote(idx($answer_edges, $answer->getPHID())); } } public function setUserVote($vote) { $this->vote = $vote['data']; if (!$this->vote) { $this->vote = PonderVote::VOTE_NONE; } return $this; } public function attachUserVote($user_phid, $vote) { $this->vote = $vote; return $this; } public function getUserVote() { return $this->vote; } public function setComments($comments) { $this->comments = $comments; return $this; } public function getComments() { return $this->comments; } public function attachAnswers(array $answers) { assert_instances_of($answers, 'PonderAnswer'); $this->answers = $answers; return $this; } public function getAnswers() { return $this->answers; } public function getMarkupField() { return self::MARKUP_FIELD_CONTENT; } // Markup interface public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "ponder:Q{$id}:{$field}:{$hash}"; } public function getMarkupText($field) { return $this->getContent(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::getEngine(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } // votable interface public function getUserVoteEdgeType() { return PhabricatorEdgeConfig::TYPE_VOTING_USER_HAS_QUESTION; } public function getVotablePHID() { return $this->getPHID(); } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getOriginalTitle() { // TODO: Make this actually save/return the original title. return $this->getTitle(); } public function getFullTitle() { $id = $this->getID(); $title = $this->getTitle(); return "Q{$id}: {$title}"; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { $policy = PhabricatorPolicies::POLICY_NOONE; switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $policy = PhabricatorPolicies::POLICY_USER; break; } return $policy; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { return pht( 'The user who asked a question can always view and edit it.'); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getAuthorPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $answers = id(new PonderAnswer())->loadAllWhere( 'questionID = %d', $this->getID()); foreach ($answers as $answer) { $engine->destroyObject($answer); } $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 48c1d86b38..c8380998a4 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -1,662 +1,662 @@ <?php final class PhabricatorProjectBoardViewController extends PhabricatorProjectBoardController { private $id; private $slug; private $handles; private $queryKey; private $filter; private $sortKey; private $showHidden; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->id = idx($data, 'id'); $this->slug = idx($data, 'slug'); $this->queryKey = idx($data, 'queryKey'); $this->filter = (bool)idx($data, 'filter'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $show_hidden = $request->getBool('hidden'); $this->showHidden = $show_hidden; $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->needImages(true); if ($this->slug) { $project->withSlugs(array($this->slug)); } else { $project->withIDs(array($this->id)); } $project = $project->executeOne(); if (!$project) { return new Aphront404Response(); } $this->setProject($project); $this->id = $project->getID(); $sort_key = $request->getStr('order'); switch ($sort_key) { case PhabricatorProjectColumn::ORDER_NATURAL: case PhabricatorProjectColumn::ORDER_PRIORITY: break; default: $sort_key = PhabricatorProjectColumn::DEFAULT_ORDER; break; } $this->sortKey = $sort_key; $column_query = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())); if (!$show_hidden) { $column_query->withStatuses( array(PhabricatorProjectColumn::STATUS_ACTIVE)); } $columns = $column_query->execute(); $columns = mpull($columns, null, 'getSequence'); if (empty($columns[0])) { switch ($request->getStr('initialize-type')) { case 'backlog-only': $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $column = PhabricatorProjectColumn::initializeNewColumn($viewer) ->setSequence(0) ->setProperty('isDefault', true) ->setProjectPHID($project->getPHID()) ->save(); $column->attachProject($project); $columns[0] = $column; unset($unguarded); break; case 'import': return id(new AphrontRedirectResponse()) ->setURI( $this->getApplicationURI('board/'.$project->getID().'/import/')); break; default: return $this->initializeWorkboardDialog($project); break; } } ksort($columns); $board_uri = $this->getApplicationURI('board/'.$project->getID().'/'); $engine = id(new ManiphestTaskSearchEngine()) ->setViewer($viewer) ->setBaseURI($board_uri) ->setIsBoardView(true); if ($request->isFormPost()) { $saved = $engine->buildSavedQueryFromRequest($request); $engine->saveQuery($saved); return id(new AphrontRedirectResponse())->setURI( $this->getURIWithState( $engine->getQueryResultsPageURI($saved->getQueryKey()))); } $query_key = $this->queryKey; if (!$query_key) { $query_key = 'open'; } $this->queryKey = $query_key; $custom_query = null; if ($engine->isBuiltinQuery($query_key)) { $saved = $engine->buildSavedQueryFromBuiltin($query_key); } else { $saved = id(new PhabricatorSavedQueryQuery()) ->setViewer($viewer) ->withQueryKeys(array($query_key)) ->executeOne(); if (!$saved) { return new Aphront404Response(); } $custom_query = $saved; } if ($this->filter) { $filter_form = id(new AphrontFormView()) ->setUser($viewer); $engine->buildSearchForm($filter_form, $saved); return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FULL) ->setTitle(pht('Advanced Filter')) ->appendChild($filter_form->buildLayoutView()) ->setSubmitURI($board_uri) ->addSubmitButton(pht('Apply Filter')) ->addCancelButton($board_uri); } $task_query = $engine->buildQueryFromSavedQuery($saved); $tasks = $task_query ->addWithAllProjects(array($project->getPHID())) ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) ->setViewer($viewer) ->execute(); $tasks = mpull($tasks, null, 'getPHID'); if ($tasks) { $positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($viewer) ->withObjectPHIDs(mpull($tasks, 'getPHID')) ->withColumns($columns) ->execute(); $positions = mpull($positions, null, 'getObjectPHID'); } else { $positions = array(); } $task_map = array(); foreach ($tasks as $task) { $task_phid = $task->getPHID(); if (empty($positions[$task_phid])) { // This shouldn't normally be possible because we create positions on // demand, but we might have raced as an object was removed from the // board. Just drop the task if we don't have a position for it. continue; } $position = $positions[$task_phid]; $task_map[$position->getColumnPHID()][] = $task_phid; } // If we're showing the board in "natural" order, sort columns by their // column positions. if ($this->sortKey == PhabricatorProjectColumn::ORDER_NATURAL) { foreach ($task_map as $column_phid => $task_phids) { $order = array(); foreach ($task_phids as $task_phid) { if (isset($positions[$task_phid])) { $order[$task_phid] = $positions[$task_phid]->getOrderingKey(); } else { $order[$task_phid] = 0; } } asort($order); $task_map[$column_phid] = array_keys($order); } } $task_can_edit_map = id(new PhabricatorPolicyFilter()) ->setViewer($viewer) ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) ->apply($tasks); $board_id = celerity_generate_unique_node_id(); $board = id(new PHUIWorkboardView()) ->setUser($viewer) ->setID($board_id); $this->initBehavior( 'project-boards', array( 'boardID' => $board_id, 'projectPHID' => $project->getPHID(), 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'createURI' => '/maniphest/task/create/', 'order' => $this->sortKey, )); $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); foreach ($columns as $column) { $task_phids = idx($task_map, $column->getPHID(), array()); $column_tasks = array_select_keys($tasks, $task_phids); $panel = id(new PHUIWorkpanelView()) ->setHeader($column->getDisplayName()) ->addSigil('workpanel'); $header_icon = $column->getHeaderIcon(); if ($header_icon) { $panel->setHeaderIcon($header_icon); } if ($column->isHidden()) { $panel->addClass('project-panel-hidden'); } $column_menu = $this->buildColumnMenu($project, $column); $panel->addHeaderAction($column_menu); $tag_id = celerity_generate_unique_node_id(); $tag_content_id = celerity_generate_unique_node_id(); $count_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setShade(PHUITagView::COLOR_BLUE) ->setID($tag_id) ->setName(phutil_tag('span', array('id' => $tag_content_id), '-')) ->setStyle('display: none'); $panel->setHeaderTag($count_tag); $cards = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setFlush(true) ->setAllowEmptyList(true) ->addSigil('project-column') ->setMetadata( array( 'columnPHID' => $column->getPHID(), 'countTagID' => $tag_id, 'countTagContentID' => $tag_content_id, 'pointLimit' => $column->getPointLimit(), )); foreach ($column_tasks as $task) { $owner = null; if ($task->getOwnerPHID()) { $owner = $this->handles[$task->getOwnerPHID()]; } $can_edit = idx($task_can_edit_map, $task->getPHID(), false); $cards->addItem(id(new ProjectBoardTaskCard()) ->setViewer($viewer) ->setTask($task) ->setOwner($owner) ->setCanEdit($can_edit) ->getItem()); } $panel->setCards($cards); $board->addPanel($panel); } Javelin::initBehavior( 'boards-dropdown', array()); $sort_menu = $this->buildSortMenu( $viewer, $sort_key); $filter_menu = $this->buildFilterMenu( $viewer, $custom_query, $engine, $query_key); $manage_menu = $this->buildManageMenu($project, $show_hidden); $header_link = phutil_tag( 'a', array( - 'href' => $this->getApplicationURI('view/'.$project->getID().'/') + 'href' => $this->getApplicationURI('view/'.$project->getID().'/'), ), $project->getName()); $header = id(new PHUIHeaderView()) ->setHeader($header_link) ->setUser($viewer) ->setNoBackground(true) ->setImage($project->getProfileImageURI()) ->setImageURL($this->getApplicationURI('view/'.$project->getID().'/')) ->addActionLink($sort_menu) ->addActionLink($filter_menu) ->addActionLink($manage_menu) ->setPolicyObject($project); $board_box = id(new PHUIBoxView()) ->appendChild($board) ->addClass('project-board-wrapper'); return $this->buildApplicationPage( array( $header, $board_box, ), array( 'title' => pht('%s Board', $project->getName()), 'showFooter' => false, )); } private function buildSortMenu( PhabricatorUser $viewer, $sort_key) { $sort_icon = id(new PHUIIconView()) ->setIconFont('fa-sort-amount-asc bluegrey'); $named = array( PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'), PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'), ); $base_uri = $this->getURIWithState(); $items = array(); foreach ($named as $key => $name) { $is_selected = ($key == $sort_key); if ($is_selected) { $active_order = $name; } $item = id(new PhabricatorActionView()) ->setIcon('fa-sort-amount-asc') ->setSelected($is_selected) ->setName($name); $uri = $base_uri->alter('order', $key); $item->setHref($uri); $items[] = $item; } $sort_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($items as $item) { $sort_menu->addAction($item); } $sort_button = id(new PHUIButtonView()) ->setText(pht('Sort: %s', $active_order)) ->setIcon($sort_icon) ->setTag('a') ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( array( 'items' => hsprintf('%s', $sort_menu), )); return $sort_button; } private function buildFilterMenu( PhabricatorUser $viewer, $custom_query, PhabricatorApplicationSearchEngine $engine, $query_key) { $filter_icon = id(new PHUIIconView()) ->setIconFont('fa-search-plus bluegrey'); $named = array( 'open' => pht('Open Tasks'), 'all' => pht('All Tasks'), ); if ($viewer->isLoggedIn()) { $named['assigned'] = pht('Assigned to Me'); } if ($custom_query) { $named[$custom_query->getQueryKey()] = pht('Custom Filter'); } $items = array(); foreach ($named as $key => $name) { $is_selected = ($key == $query_key); if ($is_selected) { $active_filter = $name; } $is_custom = false; if ($custom_query) { $is_custom = ($key == $custom_query->getQueryKey()); } $item = id(new PhabricatorActionView()) ->setIcon('fa-search') ->setSelected($is_selected) ->setName($name); if ($is_custom) { $uri = $this->getApplicationURI( 'board/'.$this->id.'/filter/query/'.$key.'/'); $item->setWorkflow(true); } else { $uri = $engine->getQueryResultsPageURI($key); } $uri = $this->getURIWithState($uri); $item->setHref($uri); $items[] = $item; } $items[] = id(new PhabricatorActionView()) ->setIcon('fa-cog') ->setHref($this->getApplicationURI('board/'.$this->id.'/filter/')) ->setWorkflow(true) ->setName(pht('Advanced Filter...')); $filter_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($items as $item) { $filter_menu->addAction($item); } $filter_button = id(new PHUIButtonView()) ->setText(pht('Filter: %s', $active_filter)) ->setIcon($filter_icon) ->setTag('a') ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( array( 'items' => hsprintf('%s', $filter_menu), )); return $filter_button; } private function buildManageMenu( PhabricatorProject $project, $show_hidden) { $request = $this->getRequest(); $viewer = $request->getUser(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $manage_icon = id(new PHUIIconView()) ->setIconFont('fa-cog bluegrey'); $manage_items = array(); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-plus') ->setName(pht('Add Column')) ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-exchange') ->setName(pht('Reorder Columns')) ->setHref($this->getApplicationURI('board/'.$this->id.'/reorder/')) ->setDisabled(!$can_edit) ->setWorkflow(true); if ($show_hidden) { $hidden_uri = $this->getURIWithState() ->setQueryParam('hidden', null); $hidden_icon = 'fa-eye-slash'; $hidden_text = pht('Hide Hidden Columns'); } else { $hidden_uri = $this->getURIWithState() ->setQueryParam('hidden', 'true'); $hidden_icon = 'fa-eye'; $hidden_text = pht('Show Hidden Columns'); } $manage_items[] = id(new PhabricatorActionView()) ->setIcon($hidden_icon) ->setName($hidden_text) ->setHref($hidden_uri); $manage_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($manage_items as $item) { $manage_menu->addAction($item); } $manage_button = id(new PHUIButtonView()) ->setText(pht('Manage Board')) ->setIcon($manage_icon) ->setTag('a') ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( array( 'items' => hsprintf('%s', $manage_menu), )); return $manage_button; } private function buildColumnMenu( PhabricatorProject $project, PhabricatorProjectColumn $column) { $request = $this->getRequest(); $viewer = $request->getUser(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $column_items = array(); $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-plus') ->setName(pht('Create Task...')) ->setHref('/maniphest/task/create/') ->addSigil('column-add-task') ->setMetadata( array( 'columnPHID' => $column->getPHID(), )) ->setDisabled(!$can_edit); $edit_uri = $this->getApplicationURI( 'board/'.$this->id.'/column/'.$column->getID().'/'); $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Column')) ->setHref($edit_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit); $can_hide = ($can_edit && !$column->isDefaultColumn()); $hide_uri = 'board/'.$this->id.'/hide/'.$column->getID().'/'; $hide_uri = $this->getApplicationURI($hide_uri); $hide_uri = $this->getURIWithState($hide_uri); if (!$column->isHidden()) { $column_items[] = id(new PhabricatorActionView()) ->setName(pht('Hide Column')) ->setIcon('fa-eye-slash') ->setHref($hide_uri) ->setDisabled(!$can_hide) ->setWorkflow(true); } else { $column_items[] = id(new PhabricatorActionView()) ->setName(pht('Show Column')) ->setIcon('fa-eye') ->setHref($hide_uri) ->setDisabled(!$can_hide) ->setWorkflow(true); } $column_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($column_items as $item) { $column_menu->addAction($item); } $column_button = id(new PHUIIconView()) ->setIconFont('fa-caret-down') ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( array( 'items' => hsprintf('%s', $column_menu), )); return $column_button; } private function initializeWorkboardDialog(PhabricatorProject $project) { $instructions = pht('This workboard has not been setup yet.'); $new_selector = id(new AphrontFormRadioButtonControl()) ->setName('initialize-type') ->setValue('backlog-only') ->addButton( 'backlog-only', pht('New Empty Board'), pht('Create a new board with just a backlog column.')) ->addButton( 'import', pht('Import Columns'), pht('Import board columns from another project.')); $dialog = id(new AphrontDialogView()) ->setUser($this->getRequest()->getUser()) ->setTitle(pht('New Workboard')) ->addSubmitButton('Continue') ->addCancelButton($this->getApplicationURI('view/'.$project->getID().'/')) ->appendParagraph($instructions) ->appendChild($new_selector); return id(new AphrontDialogResponse()) ->setDialog($dialog); } /** * Add current state parameters (like order and the visibility of hidden * columns) to a URI. * * This allows actions which toggle or adjust one piece of state to keep * the rest of the board state persistent. If no URI is provided, this method * starts with the request URI. * * @param string|null URI to add state parameters to. * @return PhutilURI URI with state parameters. */ private function getURIWithState($base = null) { if ($base === null) { $base = $this->getRequest()->getRequestURI(); } $base = new PhutilURI($base); if ($this->sortKey != PhabricatorProjectColumn::DEFAULT_ORDER) { $base->setQueryParam('order', $this->sortKey); } else { $base->setQueryParam('order', null); } $base->setQueryParam('hidden', $this->showHidden ? 'true' : null); return $base; } } diff --git a/src/applications/project/controller/PhabricatorProjectColumnHideController.php b/src/applications/project/controller/PhabricatorProjectColumnHideController.php index 782b1c8ae9..2e613ee7da 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnHideController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnHideController.php @@ -1,110 +1,112 @@ <?php final class PhabricatorProjectColumnHideController extends PhabricatorProjectBoardController { private $id; private $projectID; public function willProcessRequest(array $data) { $this->projectID = $data['projectID']; $this->id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($this->projectID)) ->executeOne(); if (!$project) { return new Aphront404Response(); } $this->setProject($project); $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT)) + PhabricatorPolicyCapability::CAN_EDIT, + )) ->executeOne(); if (!$column) { return new Aphront404Response(); } $column_phid = $column->getPHID(); $view_uri = $this->getApplicationURI('/board/'.$this->projectID.'/'); $view_uri = new PhutilURI($view_uri); foreach ($request->getPassthroughRequestData() as $key => $value) { $view_uri->setQueryParam($key, $value); } if ($column->isDefaultColumn()) { return $this->newDialog() ->setTitle(pht('Can Not Hide Default Column')) ->appendParagraph( pht('You can not hide the default/backlog column on a board.')) ->addCancelButton($view_uri, pht('Okay')); } if ($request->isFormPost()) { if ($column->isHidden()) { $new_status = PhabricatorProjectColumn::STATUS_ACTIVE; } else { $new_status = PhabricatorProjectColumn::STATUS_HIDDEN; } $type_status = PhabricatorProjectColumnTransaction::TYPE_STATUS; $xactions = array(id(new PhabricatorProjectColumnTransaction()) ->setTransactionType($type_status) - ->setNewValue($new_status)); + ->setNewValue($new_status), + ); $editor = id(new PhabricatorProjectColumnTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->applyTransactions($column, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); } if ($column->isHidden()) { $title = pht('Show Column'); } else { $title = pht('Hide Column'); } if ($column->isHidden()) { $body = pht( 'Are you sure you want to show this column?'); } else { $body = pht( 'Are you sure you want to hide this column? It will no longer '. 'appear on the workboard.'); } $dialog = $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle($title) ->appendChild($body) ->setDisableWorkflowOnCancel(true) ->addCancelButton($view_uri) ->addSubmitButton($title); foreach ($request->getPassthroughRequestData() as $key => $value) { $dialog->addHiddenInput($key, $value); } return $dialog; } } diff --git a/src/applications/project/controller/PhabricatorProjectEditIconController.php b/src/applications/project/controller/PhabricatorProjectEditIconController.php index b715d6ae6b..c3d2b06bb8 100644 --- a/src/applications/project/controller/PhabricatorProjectEditIconController.php +++ b/src/applications/project/controller/PhabricatorProjectEditIconController.php @@ -1,97 +1,97 @@ <?php final class PhabricatorProjectEditIconController extends PhabricatorProjectController { private $id; public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $edit_uri = $this->getApplicationURI('edit/'.$project->getID().'/'); require_celerity_resource('project-icon-css'); Javelin::initBehavior('phabricator-tooltips'); $project_icons = PhabricatorProjectIcon::getIconMap(); if ($request->isFormPost()) { $v_icon = $request->getStr('icon'); return id(new AphrontAjaxResponse())->setContent( array( 'value' => $v_icon, 'display' => PhabricatorProjectIcon::renderIconForChooser($v_icon), )); } $ii = 0; $buttons = array(); foreach ($project_icons as $icon => $label) { $view = id(new PHUIIconView()) ->setIconFont($icon); $aural = javelin_tag( 'span', array( 'aural' => true, ), pht('Choose "%s" Icon', $label)); if ($icon == $project->getIcon()) { $class_extra = ' selected'; } else { $class_extra = null; } $buttons[] = javelin_tag( 'button', array( 'class' => 'icon-button'.$class_extra, 'name' => 'icon', 'value' => $icon, 'type' => 'submit', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $label, - ) + ), ), array( $aural, $view, )); if ((++$ii % 4) == 0) { $buttons[] = phutil_tag('br'); } } $buttons = phutil_tag( 'div', array( 'class' => 'icon-grid', ), $buttons); return $this->newDialog() ->setTitle(pht('Choose Project Icon')) ->appendChild($buttons) ->addCancelButton($edit_uri); } } diff --git a/src/applications/project/controller/PhabricatorProjectEditPictureController.php b/src/applications/project/controller/PhabricatorProjectEditPictureController.php index 98282407be..53d5c4cbe1 100644 --- a/src/applications/project/controller/PhabricatorProjectEditPictureController.php +++ b/src/applications/project/controller/PhabricatorProjectEditPictureController.php @@ -1,272 +1,273 @@ <?php final class PhabricatorProjectEditPictureController extends PhabricatorProjectController { private $id; public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $edit_uri = $this->getApplicationURI('edit/'.$project->getID().'/'); $view_uri = $this->getApplicationURI('view/'.$project->getID().'/'); $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_file = true; $errors = array(); if ($request->isFormPost()) { $phid = $request->getStr('phid'); $is_default = false; if ($phid == PhabricatorPHIDConstants::PHID_VOID) { $phid = null; $is_default = true; } else if ($phid) { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); } else { if ($request->getFileExists('picture')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['picture'], array( 'authorPHID' => $viewer->getPHID(), 'canCDN' => true, )); } else { $e_file = pht('Required'); $errors[] = pht( 'You must choose a file when uploading a new project picture.'); } } if (!$errors && !$is_default) { if (!$file->isTransformableImage()) { $e_file = pht('Not Supported'); $errors[] = pht( 'This server only supports these image formats: %s.', implode(', ', $supported_formats)); } else { $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); } } if (!$errors) { if ($is_default) { $new_value = null; } else { $new_value = $xformed->getPHID(); } $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorProjectTransaction::TYPE_IMAGE) ->setNewValue($new_value); $editor = id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true); $editor->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse())->setURI($edit_uri); } } $title = pht('Edit Project Picture'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($project->getName(), $view_uri); $crumbs->addTextCrumb(pht('Edit'), $edit_uri); $crumbs->addTextCrumb(pht('Picture')); $form = id(new PHUIFormLayoutView()) ->setUser($viewer); $default_image = PhabricatorFile::loadBuiltin($viewer, 'project.png'); $images = array(); $current = $project->getProfileImagePHID(); $has_current = false; if ($current) { $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($current)) ->execute(); if ($files) { $file = head($files); if ($file->isTransformableImage()) { $has_current = true; $images[$current] = array( 'uri' => $file->getBestURI(), 'tip' => pht('Current Picture'), ); } } } $images[PhabricatorPHIDConstants::PHID_VOID] = array( 'uri' => $default_image->getBestURI(), 'tip' => pht('Default Picture'), ); require_celerity_resource('people-profile-css'); Javelin::initBehavior('phabricator-tooltips', array()); $buttons = array(); foreach ($images as $phid => $spec) { $button = javelin_tag( 'button', array( 'class' => 'grey profile-image-button', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $spec['tip'], 'size' => 300, ), ), phutil_tag( 'img', array( 'height' => 50, 'width' => 50, 'src' => $spec['uri'], ))); $button = array( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'phid', 'value' => $phid, )), - $button); + $button, + ); $button = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', ), $button); $buttons[] = $button; } if ($has_current) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Current Picture')) ->setValue(array_shift($buttons))); } $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Use Picture')) ->setValue($buttons)); $launch_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'launch-icon-composer', array( 'launchID' => $launch_id, 'inputID' => $input_id, )); $compose_button = javelin_tag( 'button', array( 'class' => 'grey', 'id' => $launch_id, 'sigil' => 'icon-composer', ), pht('Choose Icon and Color...')); $compose_input = javelin_tag( 'input', array( 'type' => 'hidden', 'id' => $input_id, 'name' => 'phid', )); $compose_form = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', ), array( $compose_input, $compose_button, )); $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Quick Create')) ->setValue($compose_form)); $upload_form = id(new AphrontFormView()) ->setUser($viewer) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormFileControl()) ->setName('picture') ->setLabel(pht('Upload Picture')) ->setError($e_file) ->setCaption( pht('Supported formats: %s', implode(', ', $supported_formats)))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($edit_uri) ->setValue(pht('Upload Picture'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $upload_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New Picture')) ->setForm($upload_form); return $this->buildApplicationPage( array( $crumbs, $form_box, $upload_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 5302877093..a589632e40 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -1,176 +1,178 @@ <?php final class PhabricatorProjectMoveController extends PhabricatorProjectController { private $id; public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $column_phid = $request->getStr('columnPHID'); $object_phid = $request->getStr('objectPHID'); $after_phid = $request->getStr('afterPHID'); $before_phid = $request->getStr('beforePHID'); $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, )) ->withIDs(array($this->id)) ->executeOne(); if (!$project) { return new Aphront404Response(); } $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($object_phid)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$object) { return new Aphront404Response(); } $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())) ->execute(); $columns = mpull($columns, null, 'getPHID'); $column = idx($columns, $column_phid); if (!$column) { // User is trying to drop this object into a nonexistent column, just kick // them out. return new Aphront404Response(); } $positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($viewer) ->withColumns($columns) ->withObjectPHIDs(array($object_phid)) ->execute(); $xactions = array(); if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { $order_params = array( 'afterPHID' => $after_phid, 'beforePHID' => $before_phid, ); } else { $order_params = array(); } $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_PROJECT_COLUMN) ->setNewValue( array( 'columnPHIDs' => array($column->getPHID()), 'projectPHID' => $column->getProjectPHID(), ) + $order_params) ->setOldValue( array( 'columnPHIDs' => mpull($positions, 'getColumnPHID'), 'projectPHID' => $column->getProjectPHID(), )); $task_phids = array(); if ($after_phid) { $task_phids[] = $after_phid; } if ($before_phid) { $task_phids[] = $before_phid; } if ($task_phids && ($order == PhabricatorProjectColumn::ORDER_PRIORITY)) { $tasks = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withPHIDs($task_phids) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->execute(); if (count($tasks) != count($task_phids)) { return new Aphront404Response(); } $tasks = mpull($tasks, null, 'getPHID'); $a_task = idx($tasks, $after_phid); $b_task = idx($tasks, $before_phid); if ($a_task && (($a_task->getPriority() < $object->getPriority()) || ($a_task->getPriority() == $object->getPriority() && $a_task->getSubpriority() >= $object->getSubpriority()))) { $after_pri = $a_task->getPriority(); $after_sub = $a_task->getSubpriority(); $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) ->setNewValue(array( 'newPriority' => $after_pri, 'newSubpriorityBase' => $after_sub, - 'direction' => '>')); + 'direction' => '>', + )); } else if ($b_task && (($b_task->getPriority() > $object->getPriority()) || ($b_task->getPriority() == $object->getPriority() && $b_task->getSubpriority() <= $object->getSubpriority()))) { $before_pri = $b_task->getPriority(); $before_sub = $b_task->getSubpriority(); $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) ->setNewValue(array( 'newPriority' => $before_pri, 'newSubpriorityBase' => $before_sub, - 'direction' => '<')); + 'direction' => '<', + )); } } $editor = id(new ManiphestTransactionEditor()) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $editor->applyTransactions($object, $xactions); $owner = null; if ($object->getOwnerPHID()) { $owner = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array($object->getOwnerPHID())) ->executeOne(); } $card = id(new ProjectBoardTaskCard()) ->setViewer($viewer) ->setTask($object) ->setOwner($owner) ->setCanEdit(true) ->getItem(); return id(new AphrontAjaxResponse())->setContent( array('task' => $card)); } } diff --git a/src/applications/project/events/PhabricatorProjectUIEventListener.php b/src/applications/project/events/PhabricatorProjectUIEventListener.php index 0dc7ee8693..3dba5328b8 100644 --- a/src/applications/project/events/PhabricatorProjectUIEventListener.php +++ b/src/applications/project/events/PhabricatorProjectUIEventListener.php @@ -1,101 +1,102 @@ <?php final class PhabricatorProjectUIEventListener extends PhabricatorEventListener { public function register() { $this->listen(PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhabricatorEventType::TYPE_UI_WILLRENDERPROPERTIES: $this->handlePropertyEvent($event); break; } } private function handlePropertyEvent($event) { $user = $event->getUser(); $object = $event->getValue('object'); if (!$object || !$object->getPHID()) { // No object, or the object has no PHID yet.. return; } if (!($object instanceof PhabricatorProjectInterface)) { // This object doesn't have projects. return; } $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); if ($project_phids) { $project_phids = array_reverse($project_phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs($project_phids) ->execute(); } else { $handles = array(); } // If this object can appear on boards, build the workboard annotations. // Some day, this might be a generic interface. For now, only tasks can // appear on boards. $can_appear_on_boards = ($object instanceof ManiphestTask); $annotations = array(); if ($handles && $can_appear_on_boards) { // TDOO: Generalize this UI and move it out of Maniphest. require_celerity_resource('maniphest-task-summary-css'); $positions = id(new PhabricatorProjectColumnPositionQuery()) ->setViewer($user) ->withBoardPHIDs($project_phids) ->withObjectPHIDs(array($object->getPHID())) ->needColumns(true) ->execute(); $positions = mpull($positions, null, 'getBoardPHID'); foreach ($project_phids as $project_phid) { $handle = $handles[$project_phid]; $position = idx($positions, $project_phid); if ($position) { $column = $position->getColumn(); $column_name = pht('(%s)', $column->getDisplayName()); $column_link = phutil_tag( 'a', array( 'href' => $handle->getURI().'board/', 'class' => 'maniphest-board-link', ), $column_name); $annotations[$project_phid] = array( ' ', - $column_link); + $column_link, + ); } } } if ($handles) { $list = id(new PHUIHandleTagListView()) ->setHandles($handles) ->setAnnotations($annotations); } else { $list = phutil_tag('em', array(), pht('None')); } $view = $event->getValue('view'); $view->addProperty(pht('Projects'), $list); } } diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php index 76c8f8f2b4..d2c69fda4c 100644 --- a/src/applications/project/query/PhabricatorProjectSearchEngine.php +++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php @@ -1,256 +1,256 @@ <?php final class PhabricatorProjectSearchEngine extends PhabricatorApplicationSearchEngine { public function getResultTypeDescription() { return pht('Projects'); } public function getApplicationClassName() { return 'PhabricatorProjectApplication'; } public function getCustomFieldObject() { return new PhabricatorProject(); } public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); $saved->setParameter( 'memberPHIDs', $this->readUsersFromRequest($request, 'members')); $saved->setParameter('status', $request->getStr('status')); $saved->setParameter('name', $request->getStr('name')); $saved->setParameter( 'icons', $this->readListFromRequest($request, 'icons')); $saved->setParameter( 'colors', $this->readListFromRequest($request, 'colors')); $this->readCustomFieldsFromRequest($request, $saved); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhabricatorProjectQuery()) ->needImages(true); $member_phids = $saved->getParameter('memberPHIDs', array()); if ($member_phids && is_array($member_phids)) { $query->withMemberPHIDs($member_phids); } $status = $saved->getParameter('status'); $status = idx($this->getStatusValues(), $status); if ($status) { $query->withStatus($status); } $name = $saved->getParameter('name'); if (strlen($name)) { $query->withDatasourceQuery($name); } $icons = $saved->getParameter('icons'); if ($icons) { $query->withIcons($icons); } $colors = $saved->getParameter('colors'); if ($colors) { $query->withColors($colors); } $this->applyCustomFieldsToQuery($query, $saved); return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $phids = $saved->getParameter('memberPHIDs', array()); $member_handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->withPHIDs($phids) ->execute(); $status = $saved->getParameter('status'); $name_match = $saved->getParameter('name'); $icons = array_fuse($saved->getParameter('icons', array())); $colors = array_fuse($saved->getParameter('colors', array())); $icon_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Icons')); foreach (PhabricatorProjectIcon::getIconMap() as $icon => $name) { $image = id(new PHUIIconView()) ->setIconFont($icon); $icon_control->addCheckbox( 'icons[]', $icon, array($image, ' ', $name), isset($icons[$icon])); } $color_control = id(new AphrontFormCheckboxControl()) ->setLabel(pht('Colors')); foreach (PhabricatorProjectIcon::getColorMap() as $color => $name) { $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setShade($color) ->setName($name); $color_control->addCheckbox( 'colors[]', $color, $tag, isset($colors[$color])); } $form ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($name_match)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setName('members') ->setLabel(pht('Members')) ->setValue($member_handles)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('status') ->setOptions($this->getStatusOptions()) ->setValue($status)) ->appendChild($icon_control) ->appendChild($color_control); $this->appendCustomFieldsToForm($form, $saved); } protected function getURI($path) { return '/project/'.$path; } public function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['joined'] = pht('Joined'); } $names['active'] = pht('Active'); $names['all'] = pht('All'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer_phid = $this->requireViewer()->getPHID(); switch ($query_key) { case 'all': return $query; case 'active': return $query ->setParameter('status', 'active'); case 'joined': return $query ->setParameter('memberPHIDs', array($viewer_phid)) ->setParameter('status', 'active'); } return parent::buildSavedQueryFromBuiltin($query_key); } private function getStatusOptions() { return array( 'active' => pht('Show Only Active Projects'), 'all' => pht('Show All Projects'), ); } private function getStatusValues() { return array( 'active' => PhabricatorProjectQuery::STATUS_ACTIVE, 'all' => PhabricatorProjectQuery::STATUS_ANY, ); } private function getColorValues() {} private function getIconValues() {} protected function getRequiredHandlePHIDsForResultList( array $projects, PhabricatorSavedQuery $query) { return mpull($projects, 'getPHID'); } protected function renderResultList( array $projects, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($projects, 'PhabricatorProject'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); $list->setUser($viewer); foreach ($projects as $project) { $id = $project->getID(); $workboards_uri = $this->getApplicationURI("board/{$id}/"); $members_uri = $this->getApplicationURI("members/{$id}/"); $workboards_url = phutil_tag( 'a', array( - 'href' => $workboards_uri + 'href' => $workboards_uri, ), pht('Workboards')); $members_url = phutil_tag( 'a', array( - 'href' => $members_uri + 'href' => $members_uri, ), pht('Members')); $tag_list = id(new PHUIHandleTagListView()) ->setSlim(true) ->setHandles(array($handles[$project->getPHID()])); $item = id(new PHUIObjectItemView()) ->setHeader($project->getName()) ->setHref($this->getApplicationURI("view/{$id}/")) ->setImageURI($project->getProfileImageURI()) ->addAttribute($tag_list) ->addAttribute($workboards_url) ->addAttribute($members_url); if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) { $item->addIcon('delete-grey', pht('Archived')); $item->setDisabled(true); } $list->addItem($item); } return $list; } } diff --git a/src/applications/releeph/application/PhabricatorReleephApplication.php b/src/applications/releeph/application/PhabricatorReleephApplication.php index e04337f912..e33e2796f3 100644 --- a/src/applications/releeph/application/PhabricatorReleephApplication.php +++ b/src/applications/releeph/application/PhabricatorReleephApplication.php @@ -1,81 +1,81 @@ <?php final class PhabricatorReleephApplication extends PhabricatorApplication { public function getName() { return pht('Releeph'); } public function getShortDescription() { return pht('Pull Requests'); } public function getBaseURI() { return '/releeph/'; } public function getIconName() { return 'releeph'; } public function isPrototype() { return true; } public function getRoutes() { return array( '/Y(?P<requestID>[1-9]\d*)' => 'ReleephRequestViewController', // TODO: Remove these older routes eventually. '/RQ(?P<requestID>[1-9]\d*)' => 'ReleephRequestViewController', '/releeph/request/(?P<requestID>[1-9]\d*)/' => 'ReleephRequestViewController', '/releeph/' => array( '' => 'ReleephProductListController', '(?:product|project)/' => array( '(?:query/(?P<queryKey>[^/]+)/)?' => 'ReleephProductListController', 'create/' => 'ReleephProductCreateController', '(?P<projectID>[1-9]\d*)/' => array( '(?:query/(?P<queryKey>[^/]+)/)?' => 'ReleephProductViewController', 'edit/' => 'ReleephProductEditController', 'cutbranch/' => 'ReleephBranchCreateController', 'action/(?P<action>.+)/' => 'ReleephProductActionController', 'history/' => 'ReleephProductHistoryController', ), ), 'branch/' => array( 'edit/(?P<branchID>[1-9]\d*)/' => 'ReleephBranchEditController', '(?P<action>close|re-open)/(?P<branchID>[1-9]\d*)/' => 'ReleephBranchAccessController', 'preview/' => 'ReleephBranchNamePreviewController', '(?P<branchID>[1-9]\d*)/' => array( 'history/' => 'ReleephBranchHistoryController', '(?:query/(?P<queryKey>[^/]+)/)?' => 'ReleephBranchViewController', ), 'pull/(?P<branchID>[1-9]\d*)/' => 'ReleephRequestEditController', ), 'request/' => array( 'create/' => 'ReleephRequestEditController', 'differentialcreate/' => array( 'D(?P<diffRevID>[1-9]\d*)' => 'ReleephRequestDifferentialCreateController', ), 'edit/(?P<requestID>[1-9]\d*)/' => 'ReleephRequestEditController', 'action/(?P<action>.+)/(?P<requestID>[1-9]\d*)/' => 'ReleephRequestActionController', 'typeahead/' => 'ReleephRequestTypeaheadController', 'comment/(?P<requestID>[1-9]\d*)/' => 'ReleephRequestCommentController', ), - ) + ), ); } } diff --git a/src/applications/releeph/commitfinder/ReleephCommitFinder.php b/src/applications/releeph/commitfinder/ReleephCommitFinder.php index 00821cdd65..036d0eaf2a 100644 --- a/src/applications/releeph/commitfinder/ReleephCommitFinder.php +++ b/src/applications/releeph/commitfinder/ReleephCommitFinder.php @@ -1,105 +1,105 @@ <?php final class ReleephCommitFinder { private $releephProject; private $user; private $objectPHID; public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } public function setReleephProject(ReleephProject $rp) { $this->releephProject = $rp; return $this; } public function getRequestedObjectPHID() { return $this->objectPHID; } public function fromPartial($partial_string) { $this->objectPHID = null; // Look for diffs $matches = array(); if (preg_match('/^D([1-9]\d*)$/', $partial_string, $matches)) { $diff_id = $matches[1]; // TOOD: (T603) This is all slated for annihilation. $diff_rev = id(new DifferentialRevision())->load($diff_id); if (!$diff_rev) { throw new ReleephCommitFinderException( "{$partial_string} does not refer to an existing diff."); } $commit_phids = $diff_rev->loadCommitPHIDs(); if (!$commit_phids) { throw new ReleephCommitFinderException( "{$partial_string} has no commits associated with it yet."); } $this->objectPHID = $diff_rev->getPHID(); $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( 'phid IN (%Ls) ORDER BY epoch ASC', $commit_phids); return head($commits); } // Look for a raw commit number, or r<callsign><commit-number>. $repository = $this->releephProject->getRepository(); $dr_data = null; $matches = array(); if (preg_match('/^r(?P<callsign>[A-Z]+)(?P<commit>\w+)$/', $partial_string, $matches)) { $callsign = $matches['callsign']; if ($callsign != $repository->getCallsign()) { throw new ReleephCommitFinderException(sprintf( '%s is in a different repository to this Releeph project (%s).', $partial_string, $repository->getCallsign())); } else { $dr_data = $matches; } } else { $dr_data = array( 'callsign' => $repository->getCallsign(), - 'commit' => $partial_string + 'commit' => $partial_string, ); } try { $dr_data['user'] = $this->getUser(); $dr = DiffusionRequest::newFromDictionary($dr_data); } catch (Exception $ex) { $message = "No commit matches {$partial_string}: ".$ex->getMessage(); throw new ReleephCommitFinderException($message); } $phabricator_repository_commit = $dr->loadCommit(); if (!$phabricator_repository_commit) { throw new ReleephCommitFinderException( "The commit {$partial_string} doesn't exist in this repository."); } // When requesting a single commit, if it has an associated review we // imply the review was requested instead. This is always correct for now // and consistent with the older behavior, although it might not be the // right rule in the future. $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $phabricator_repository_commit->getPHID(), PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV); if ($phids) { $this->objectPHID = head($phids); } return $phabricator_repository_commit; } } diff --git a/src/applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php b/src/applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php index 643f61b2b6..a4ae91f10e 100644 --- a/src/applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php +++ b/src/applications/releeph/conduit/ReleephQueryRequestsConduitAPIMethod.php @@ -1,81 +1,81 @@ <?php final class ReleephQueryRequestsConduitAPIMethod extends ReleephConduitAPIMethod { public function getAPIMethodName() { return 'releeph.queryrequests'; } public function getMethodDescription() { return 'Return information about all Releeph requests linked to the given ids.'; } public function defineParamTypes() { return array( 'revisionPHIDs' => 'optional list<phid>', - 'requestedCommitPHIDs' => 'optional list<phid>' + 'requestedCommitPHIDs' => 'optional list<phid>', ); } public function defineReturnType() { return 'dict<string, wild>'; } public function defineErrorTypes() { return array(); } protected function execute(ConduitAPIRequest $conduit_request) { $revision_phids = $conduit_request->getValue('revisionPHIDs'); $requested_commit_phids = $conduit_request->getValue('requestedCommitPHIDs'); $result = array(); if (!$revision_phids && !$requested_commit_phids) { return $result; } $query = new ReleephRequestQuery(); $query->setViewer($conduit_request->getUser()); if ($revision_phids) { $query->withRequestedObjectPHIDs($revision_phids); } else if ($requested_commit_phids) { $query->withRequestedCommitPHIDs($requested_commit_phids); } $releephRequests = $query->execute(); foreach ($releephRequests as $releephRequest) { $branch = $releephRequest->getBranch(); $request_commit_phid = $releephRequest->getRequestCommitPHID(); $object = $releephRequest->getRequestedObject(); if ($object instanceof DifferentialRevision) { $object_phid = $object->getPHID(); } else { $object_phid = null; } $status = $releephRequest->getStatus(); $statusName = ReleephRequestStatus::getStatusDescriptionFor($status); $url = PhabricatorEnv::getProductionURI('/RQ'.$releephRequest->getID()); $result[] = array( 'branchBasename' => $branch->getBasename(), 'branchSymbolic' => $branch->getSymbolicName(), 'requestID' => $releephRequest->getID(), 'revisionPHID' => $object_phid, 'status' => $status, 'statusName' => $statusName, 'url' => $url, ); } return $result; } } diff --git a/src/applications/releeph/controller/branch/ReleephBranchEditController.php b/src/applications/releeph/controller/branch/ReleephBranchEditController.php index 1fc79351b2..2e1e4ccda8 100644 --- a/src/applications/releeph/controller/branch/ReleephBranchEditController.php +++ b/src/applications/releeph/controller/branch/ReleephBranchEditController.php @@ -1,115 +1,115 @@ <?php final class ReleephBranchEditController extends ReleephBranchController { private $branchID; public function willProcessRequest(array $data) { $this->branchID = $data['branchID']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $branch = id(new ReleephBranchQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($this->branchID)) ->executeOne(); if (!$branch) { return new Aphront404Response(); } $this->setBranch($branch); $symbolic_name = $request->getStr( 'symbolicName', $branch->getSymbolicName()); if ($request->isFormPost()) { $existing_with_same_symbolic_name = id(new ReleephBranch()) ->loadOneWhere( 'id != %d AND releephProjectID = %d AND symbolicName = %s', $branch->getID(), $branch->getReleephProjectID(), $symbolic_name); $branch->openTransaction(); $branch ->setSymbolicName($symbolic_name); if ($existing_with_same_symbolic_name) { $existing_with_same_symbolic_name ->setSymbolicName(null) ->save(); } $branch->save(); $branch->saveTransaction(); return id(new AphrontRedirectResponse()) ->setURI($this->getBranchViewURI($branch)); } $phids = array(); $phids[] = $creator_phid = $branch->getCreatedByUserPHID(); $phids[] = $cut_commit_phid = $branch->getCutPointCommitPHID(); $handles = id(new PhabricatorHandleQuery()) ->setViewer($request->getUser()) ->withPHIDs($phids) ->execute(); $form = id(new AphrontFormView()) ->setUser($request->getUser()) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Branch Name')) ->setValue($branch->getName())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Cut Point')) ->setValue($handles[$cut_commit_phid]->renderLink())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Created By')) ->setValue($handles[$creator_phid]->renderLink())) ->appendChild( - id(new AphrontFormTextControl) + id(new AphrontFormTextControl()) ->setLabel(pht('Symbolic Name')) ->setName('symbolicName') ->setValue($symbolic_name) ->setCaption(pht('Mutable alternate name, for easy reference, '. '(e.g. "LATEST")'))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getBranchViewURI($branch)) ->setValue(pht('Save Branch'))); $title = pht( 'Edit Branch %s', $branch->getDisplayNameWithDetail()); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Edit')); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->appendChild($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } } diff --git a/src/applications/releeph/controller/request/ReleephRequestActionController.php b/src/applications/releeph/controller/request/ReleephRequestActionController.php index d8c83e7b2e..64125db5ff 100644 --- a/src/applications/releeph/controller/request/ReleephRequestActionController.php +++ b/src/applications/releeph/controller/request/ReleephRequestActionController.php @@ -1,129 +1,130 @@ <?php final class ReleephRequestActionController extends ReleephRequestController { private $action; private $requestID; public function willProcessRequest(array $data) { $this->action = $data['action']; $this->requestID = $data['requestID']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $request->validateCSRF(); $pull = id(new ReleephRequestQuery()) ->setViewer($viewer) ->withIDs(array($this->requestID)) ->executeOne(); if (!$pull) { return new Aphront404Response(); } $branch = $pull->getBranch(); $product = $branch->getProduct(); $action = $this->action; $origin_uri = '/'.$pull->getMonogram(); $editor = id(new ReleephRequestTransactionalEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $xactions = array(); switch ($action) { case 'want': case 'pass': static $action_map = array( 'want' => ReleephRequest::INTENT_WANT, - 'pass' => ReleephRequest::INTENT_PASS); + 'pass' => ReleephRequest::INTENT_PASS, + ); $intent = $action_map[$action]; $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_USER_INTENT) ->setMetadataValue( 'isAuthoritative', $product->isAuthoritative($viewer)) ->setNewValue($intent); break; case 'mark-manually-picked': case 'mark-manually-reverted': if ( $pull->getRequestUserPHID() === $viewer->getPHID() || $product->isAuthoritative($viewer)) { // We're all good! } else { throw new Exception( "Bug! Only pushers or the requestor can manually change a ". "request's in-branch status!"); } if ($action === 'mark-manually-picked') { $in_branch = 1; $intent = ReleephRequest::INTENT_WANT; } else { $in_branch = 0; $intent = ReleephRequest::INTENT_PASS; } $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_USER_INTENT) ->setMetadataValue('isManual', true) ->setMetadataValue('isAuthoritative', true) ->setNewValue($intent); $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_MANUAL_IN_BRANCH) ->setNewValue($in_branch); break; default: throw new Exception("unknown or unimplemented action {$action}"); } $editor->applyTransactions($pull, $xactions); if ($request->getBool('render')) { $field_list = PhabricatorCustomField::getObjectFields( $pull, PhabricatorCustomField::ROLE_VIEW); $field_list ->setViewer($viewer) ->readFieldsFromStorage($pull); // TODO: This should be more modern and general. $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); foreach ($field_list->getFields() as $field) { if ($field->shouldMarkup()) { $field->setMarkupEngine($engine); } } $engine->process(); $pull_box = id(new ReleephRequestView()) ->setUser($viewer) ->setCustomFields($field_list) ->setPullRequest($pull) ->setIsListView(true); return id(new AphrontAjaxResponse())->setContent( array( 'markup' => hsprintf('%s', $pull_box), )); } return id(new AphrontRedirectResponse())->setURI($origin_uri); } } diff --git a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php index 658c98eb53..8d27ef028d 100644 --- a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php +++ b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php @@ -1,106 +1,106 @@ <?php // TODO: After T2222, this is likely unreachable? final class ReleephRequestDifferentialCreateController extends ReleephController { private $revisionID; private $revision; public function willProcessRequest(array $data) { $this->revisionID = $data['diffRevID']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $diff_rev = id(new DifferentialRevisionQuery()) ->setViewer($user) ->withIDs(array($this->revisionID)) ->executeOne(); if (!$diff_rev) { return new Aphront404Response(); } $this->revision = $diff_rev; $arc_project = id(new PhabricatorRepositoryArcanistProject()) ->loadOneWhere('phid = %s', $this->revision->getArcanistProjectPHID()); $projects = id(new ReleephProject())->loadAllWhere( 'arcanistProjectID = %d AND isActive = 1', $arc_project->getID()); if (!$projects) { throw new Exception(sprintf( "D%d belongs to the '%s' Arcanist project, ". "which is not part of any Releeph project!", $this->revision->getID(), $arc_project->getName())); } $branches = id(new ReleephBranch())->loadAllWhere( 'releephProjectID IN (%Ld) AND isActive = 1', mpull($projects, 'getID')); if (!$branches) { throw new Exception(sprintf( 'D%d could be in the Releeph project(s) %s, '. 'but this project / none of these projects have open branches.', $this->revision->getID(), implode(', ', mpull($projects, 'getName')))); } if (count($branches) === 1) { return id(new AphrontRedirectResponse()) ->setURI($this->buildReleephRequestURI(head($branches))); } $projects = msort( mpull($projects, null, 'getID'), 'getName'); $branch_groups = mgroup($branches, 'getReleephProjectID'); require_celerity_resource('releeph-request-differential-create-dialog'); $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Choose Releeph Branch')) ->setClass('releeph-request-differential-create-dialog') ->addCancelButton('/D'.$request->getStr('D')); $dialog->appendChild( pht('This differential revision changes code that is associated '. 'with multiple Releeph branches. '. 'Please select the branch where you would like this code to be picked.')); foreach ($branch_groups as $project_id => $branches) { $project = idx($projects, $project_id); $dialog->appendChild( phutil_tag( 'h1', array(), $project->getName())); $branches = msort($branches, 'getBasename'); foreach ($branches as $branch) { $uri = $this->buildReleephRequestURI($branch); $dialog->appendChild( phutil_tag( 'a', array( 'href' => $uri, ), $branch->getDisplayNameWithDetail())); } } - return id(new AphrontDialogResponse) + return id(new AphrontDialogResponse()) ->setDialog($dialog); } private function buildReleephRequestURI(ReleephBranch $branch) { $uri = $branch->getURI('request/'); return id(new PhutilURI($uri)) ->setQueryParam('D', $this->revision->getID()); } } diff --git a/src/applications/releeph/controller/request/ReleephRequestEditController.php b/src/applications/releeph/controller/request/ReleephRequestEditController.php index d18894d70c..acbf5246ba 100644 --- a/src/applications/releeph/controller/request/ReleephRequestEditController.php +++ b/src/applications/releeph/controller/request/ReleephRequestEditController.php @@ -1,312 +1,312 @@ <?php final class ReleephRequestEditController extends ReleephBranchController { private $requestID; private $branchID; public function willProcessRequest(array $data) { $this->requestID = idx($data, 'requestID'); $this->branchID = idx($data, 'branchID'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); if ($this->requestID) { $pull = id(new ReleephRequestQuery()) ->setViewer($viewer) ->withIDs(array($this->requestID)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$pull) { return new Aphront404Response(); } $branch = $pull->getBranch(); $is_edit = true; } else { $branch = id(new ReleephBranchQuery()) ->setViewer($viewer) ->withIDs(array($this->branchID)) ->executeOne(); if (!$branch) { return new Aphront404Response(); } $pull = id(new ReleephRequest()) ->setRequestUserPHID($viewer->getPHID()) ->setBranchID($branch->getID()) ->setInBranch(0) ->attachBranch($branch); $is_edit = false; } $this->setBranch($branch); $product = $branch->getProduct(); $request_identifier = $request->getStr('requestIdentifierRaw'); $e_request_identifier = true; // Load all the ReleephFieldSpecifications $selector = $branch->getProduct()->getReleephFieldSelector(); $fields = $selector->getFieldSpecifications(); foreach ($fields as $field) { $field ->setReleephProject($product) ->setReleephBranch($branch) ->setReleephRequest($pull); } $field_list = PhabricatorCustomField::getObjectFields( $pull, PhabricatorCustomField::ROLE_EDIT); foreach ($field_list->getFields() as $field) { $field ->setReleephProject($product) ->setReleephBranch($branch) ->setReleephRequest($pull); } $field_list->readFieldsFromStorage($pull); if ($this->branchID) { $cancel_uri = $this->getApplicationURI('branch/'.$this->branchID.'/'); } else { $cancel_uri = '/'.$pull->getMonogram(); } // Make edits $errors = array(); if ($request->isFormPost()) { $xactions = array(); // The commit-identifier being requested... if (!$is_edit) { if ($request_identifier === ReleephRequestTypeaheadControl::PLACEHOLDER) { $errors[] = 'No commit ID was provided.'; $e_request_identifier = 'Required'; } else { $pr_commit = null; $finder = id(new ReleephCommitFinder()) ->setUser($viewer) ->setReleephProject($product); try { $pr_commit = $finder->fromPartial($request_identifier); } catch (Exception $e) { $e_request_identifier = 'Invalid'; $errors[] = "Request {$request_identifier} is probably not a valid commit"; $errors[] = $e->getMessage(); } if (!$errors) { $object_phid = $finder->getRequestedObjectPHID(); if (!$object_phid) { $object_phid = $pr_commit->getPHID(); } $pull->setRequestedObjectPHID($object_phid); } } if (!$errors) { $existing = id(new ReleephRequest()) ->loadOneWhere('requestCommitPHID = %s AND branchID = %d', $pr_commit->getPHID(), $branch->getID()); if ($existing) { return id(new AphrontRedirectResponse()) ->setURI('/releeph/request/edit/'.$existing->getID(). '?existing=1'); } $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_REQUEST) ->setNewValue($pr_commit->getPHID()); $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_USER_INTENT) // To help hide these implicit intents... ->setMetadataValue('isRQCreate', true) ->setMetadataValue('userPHID', $viewer->getPHID()) ->setMetadataValue( 'isAuthoritative', $product->isAuthoritative($viewer)) ->setNewValue(ReleephRequest::INTENT_WANT); } } // TODO: This should happen implicitly while building transactions // instead. foreach ($field_list->getFields() as $field) { $field->readValueFromRequest($request); } if (!$errors) { foreach ($fields as $field) { if ($field->isEditable()) { try { $data = $request->getRequestData(); $value = idx($data, $field->getRequiredStorageKey()); $field->validate($value); $xactions[] = id(new ReleephRequestTransaction()) ->setTransactionType(ReleephRequestTransaction::TYPE_EDIT_FIELD) ->setMetadataValue('fieldClass', get_class($field)) ->setNewValue($value); } catch (ReleephFieldParseException $ex) { $errors[] = $ex->getMessage(); } } } } if (!$errors) { $editor = id(new ReleephRequestTransactionalEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $editor->applyTransactions($pull, $xactions); return id(new AphrontRedirectResponse())->setURI($cancel_uri); } } $handle_phids = array( $pull->getRequestUserPHID(), $pull->getRequestCommitPHID(), ); $handle_phids = array_filter($handle_phids); if ($handle_phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($handle_phids) ->execute(); } else { $handles = array(); } $age_string = ''; if ($is_edit) { $age_string = phutil_format_relative_time( time() - $pull->getDateCreated()).' ago'; } // Warn the user if we've been redirected here because we tried to // re-request something. $notice_view = null; if ($request->getInt('existing')) { $notice_messages = array( 'You are editing an existing pick request!', hsprintf( 'Requested %s by %s', $age_string, - $handles[$pull->getRequestUserPHID()]->renderLink()) + $handles[$pull->getRequestUserPHID()]->renderLink()), ); $notice_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setErrors($notice_messages); } $form = id(new AphrontFormView()) ->setUser($viewer); if ($is_edit) { $form ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Original Commit') ->setValue( $handles[$pull->getRequestCommitPHID()]->renderLink())) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Requestor') ->setValue(hsprintf( '%s %s', $handles[$pull->getRequestUserPHID()]->renderLink(), $age_string))); } else { $origin = null; $diff_rev_id = $request->getStr('D'); if ($diff_rev_id) { $diff_rev = id(new DifferentialRevisionQuery()) ->setViewer($viewer) ->withIDs(array($diff_rev_id)) ->executeOne(); $origin = '/D'.$diff_rev->getID(); $title = sprintf( 'D%d: %s', $diff_rev_id, $diff_rev->getTitle()); $form ->addHiddenInput('requestIdentifierRaw', 'D'.$diff_rev_id) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Diff') ->setValue($title)); } else { $origin = $branch->getURI(); $repo = $product->getRepository(); $branch_cut_point = id(new PhabricatorRepositoryCommit()) ->loadOneWhere( 'phid = %s', $branch->getCutPointCommitPHID()); $form->appendChild( id(new ReleephRequestTypeaheadControl()) ->setName('requestIdentifierRaw') ->setLabel('Commit ID') ->setRepo($repo) ->setValue($request_identifier) ->setError($e_request_identifier) ->setStartTime($branch_cut_point->getEpoch()) ->setCaption( 'Start typing to autocomplete on commit title, '. 'or give a Phabricator commit identifier like rFOO1234')); } } $field_list->appendFieldsToForm($form); $crumbs = $this->buildApplicationCrumbs(); if ($is_edit) { $title = pht('Edit Pull Request'); $submit_name = pht('Save'); $crumbs->addTextCrumb($pull->getMonogram(), '/'.$pull->getMonogram()); $crumbs->addTextCrumb(pht('Edit')); } else { $title = pht('Create Pull Request'); $submit_name = pht('Create Pull Request'); $crumbs->addTextCrumb(pht('New Pull Request')); } $form->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri, 'Cancel') ->setValue($submit_name)); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->appendChild($form); return $this->buildApplicationPage( array( $crumbs, $notice_view, $box, ), array( 'title' => $title, )); } } diff --git a/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php b/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php index 9430c1145e..671511deb0 100644 --- a/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php +++ b/src/applications/releeph/field/specification/ReleephDiffSizeFieldSpecification.php @@ -1,113 +1,115 @@ <?php final class ReleephDiffSizeFieldSpecification extends ReleephFieldSpecification { const LINES_WEIGHT = 1; const PATHS_WEIGHT = 30; const MAX_POINTS = 1000; public function getFieldKey() { return 'commit:size'; } public function getName() { return 'Size'; } public function renderPropertyViewValue(array $handles) { $requested_object = $this->getObject()->getRequestedObject(); if (!($requested_object instanceof DifferentialRevision)) { return null; } $diff_rev = $requested_object; $diffs = $diff_rev->loadRelatives( new DifferentialDiff(), 'revisionID', 'getID', 'creationMethod <> "commit"'); $all_changesets = array(); $most_recent_changesets = null; foreach ($diffs as $diff) { $changesets = $diff->loadRelatives(new DifferentialChangeset(), 'diffID'); $all_changesets += $changesets; $most_recent_changesets = $changesets; } // The score is based on all changesets for all versions of this diff $all_changes = $this->countLinesAndPaths($all_changesets); $points = self::LINES_WEIGHT * $all_changes['code']['lines'] + self::PATHS_WEIGHT * count($all_changes['code']['paths']); // The blurb is just based on the most recent version of the diff $mr_changes = $this->countLinesAndPaths($most_recent_changesets); $test_tag = ''; if ($mr_changes['tests']['paths']) { Javelin::initBehavior('phabricator-tooltips'); require_celerity_resource('aphront-tooltip-css'); $test_blurb = pht('%d line(s)', $mr_changes['tests']['lines']).' and '. pht('%d path(s)', count($mr_changes['tests']['paths'])). " contain changes to test code:\n"; foreach ($mr_changes['tests']['paths'] as $mr_test_path) { $test_blurb .= pht("%s\n", $mr_test_path); } $test_tag = javelin_tag( 'span', array( 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $test_blurb, 'align' => 'E', - 'size' => 'auto'), - 'style' => ''), + 'size' => 'auto', + ), + 'style' => '', + ), ' + tests'); } $blurb = hsprintf('%s%s.', pht('%d line(s)', $mr_changes['code']['lines']).' and '. pht('%d path(s)', count($mr_changes['code']['paths'])).' over '. pht('%d diff(s)', count($diffs)), $test_tag); return id(new AphrontProgressBarView()) ->setValue($points) ->setMax(self::MAX_POINTS) ->setCaption($blurb) ->render(); } private function countLinesAndPaths(array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $lines = 0; $paths_touched = array(); $test_lines = 0; $test_paths_touched = array(); foreach ($changesets as $ch) { if ($this->getReleephProject()->isTestFile($ch->getFilename())) { $test_lines += $ch->getAddLines() + $ch->getDelLines(); $test_paths_touched[] = $ch->getFilename(); } else { $lines += $ch->getAddLines() + $ch->getDelLines(); $paths_touched[] = $ch->getFilename(); } } return array( 'code' => array( 'lines' => $lines, 'paths' => array_unique($paths_touched), ), 'tests' => array( 'lines' => $test_lines, 'paths' => array_unique($test_paths_touched), - ) + ), ); } } diff --git a/src/applications/releeph/storage/ReleephProject.php b/src/applications/releeph/storage/ReleephProject.php index b4397ebf75..cfe0026a85 100644 --- a/src/applications/releeph/storage/ReleephProject.php +++ b/src/applications/releeph/storage/ReleephProject.php @@ -1,146 +1,146 @@ <?php final class ReleephProject extends ReleephDAO implements PhabricatorPolicyInterface { const DEFAULT_BRANCH_NAMESPACE = 'releeph-releases'; const SYSTEM_AGENT_USERNAME_PREFIX = 'releeph-agent-'; protected $name; // Specifying the place to pick from is a requirement for svn, though not // for git. It's always useful though for reasoning about what revs have // been picked and which haven't. protected $trunkBranch; protected $repositoryPHID; protected $isActive; protected $createdByUserPHID; protected $arcanistProjectID; protected $details = array(); private $repository = self::ATTACHABLE; private $arcanistProject = self::ATTACHABLE; public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text128', 'trunkBranch' => 'text255', 'isActive' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'projectName' => array( 'columns' => array('name'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ReleephProductPHIDType::TYPECONST); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function getURI($path = null) { $components = array( '/releeph/product', $this->getID(), - $path + $path, ); return implode('/', $components); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getArcanistProject() { return $this->assertAttached($this->arcanistProject); } public function attachArcanistProject( PhabricatorRepositoryArcanistProject $arcanist_project = null) { $this->arcanistProject = $arcanist_project; return $this; } public function getPushers() { return $this->getDetail('pushers', array()); } public function isPusher(PhabricatorUser $user) { // TODO Deprecate this once `isPusher` is out of the Facebook codebase. return $this->isAuthoritative($user); } public function isAuthoritative(PhabricatorUser $user) { return $this->isAuthoritativePHID($user->getPHID()); } public function isAuthoritativePHID($phid) { $pushers = $this->getPushers(); if (!$pushers) { return true; } else { return in_array($phid, $pushers); } } public function attachRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function getReleephFieldSelector() { return new ReleephDefaultFieldSelector(); } public function isTestFile($filename) { $test_paths = $this->getDetail('testPaths', array()); foreach ($test_paths as $test_path) { if (preg_match($test_path, $filename)) { return true; } } return false; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/releeph/view/branch/ReleephBranchPreviewView.php b/src/applications/releeph/view/branch/ReleephBranchPreviewView.php index 8f5f344f69..e90861ae4c 100644 --- a/src/applications/releeph/view/branch/ReleephBranchPreviewView.php +++ b/src/applications/releeph/view/branch/ReleephBranchPreviewView.php @@ -1,60 +1,60 @@ <?php final class ReleephBranchPreviewView extends AphrontFormControl { private $statics = array(); private $dynamics = array(); public function addControl($param_name, AphrontFormControl $control) { $celerity_id = celerity_generate_unique_node_id(); $control->setID($celerity_id); $this->dynamics[$param_name] = $celerity_id; return $this; } public function addStatic($param_name, $value) { $this->statics[$param_name] = $value; return $this; } public function getCustomControlClass() { require_celerity_resource('releeph-preview-branch'); return 'releeph-preview-branch'; } public function renderInput() { static $required_params = array( 'arcProjectID', 'projectName', 'isSymbolic', 'template', ); $all_params = array_merge($this->statics, $this->dynamics); foreach ($required_params as $param_name) { if (idx($all_params, $param_name) === null) { throw new Exception( "'{$param_name}' is not set as either a static or dynamic!"); } } $output_id = celerity_generate_unique_node_id(); Javelin::initBehavior('releeph-preview-branch', array( 'uri' => '/releeph/branch/preview/', 'outputID' => $output_id, 'params' => array( 'static' => $this->statics, 'dynamic' => $this->dynamics, - ) + ), )); return phutil_tag( 'div', array( 'id' => $output_id, ), ''); } } diff --git a/src/applications/releeph/view/request/ReleephRequestTypeaheadControl.php b/src/applications/releeph/view/request/ReleephRequestTypeaheadControl.php index a285791367..0cb9a7ed6d 100644 --- a/src/applications/releeph/view/request/ReleephRequestTypeaheadControl.php +++ b/src/applications/releeph/view/request/ReleephRequestTypeaheadControl.php @@ -1,60 +1,60 @@ <?php final class ReleephRequestTypeaheadControl extends AphrontFormControl { const PLACEHOLDER = 'Type a commit id or first line of commit message...'; private $repo; private $startTime; public function setRepo(PhabricatorRepository $repo) { $this->repo = $repo; return $this; } public function setStartTime($epoch) { $this->startTime = $epoch; return $this; } public function getCustomControlClass() { return 'releeph-request-typeahead'; } public function renderInput() { $id = celerity_generate_unique_node_id(); $div = phutil_tag( 'div', array( 'style' => 'position: relative;', 'id' => $id, ), phutil_tag( 'input', array( 'autocomplete' => 'off', 'type' => 'text', 'name' => $this->getName(), ), '')); require_celerity_resource('releeph-request-typeahead-css'); Javelin::initBehavior('releeph-request-typeahead', array( 'id' => $id, 'src' => '/releeph/request/typeahead/', 'placeholder' => self::PLACEHOLDER, 'value' => $this->getValue(), 'aux' => array( 'repo' => $this->repo->getID(), 'callsign' => $this->repo->getCallsign(), 'since' => $this->startTime, 'limit' => 16, - ) + ), )); return $div; } } diff --git a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php index 88f2607ffe..21caf30ada 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryPullEngine.php @@ -1,470 +1,470 @@ <?php /** * Manages execution of `git pull` and `hg pull` commands for * @{class:PhabricatorRepository} objects. Used by * @{class:PhabricatorRepositoryPullLocalDaemon}. * * This class also covers initial working copy setup through `git clone`, * `git init`, `hg clone`, `hg init`, or `svnadmin create`. * * @task pull Pulling Working Copies * @task git Pulling Git Working Copies * @task hg Pulling Mercurial Working Copies * @task svn Pulling Subversion Working Copies * @task internal Internals */ final class PhabricatorRepositoryPullEngine extends PhabricatorRepositoryEngine { /* -( Pulling Working Copies )--------------------------------------------- */ public function pullRepository() { $repository = $this->getRepository(); $is_hg = false; $is_git = false; $is_svn = false; $vcs = $repository->getVersionControlSystem(); $callsign = $repository->getCallsign(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: // We never pull a local copy of non-hosted Subversion repositories. if (!$repository->isHosted()) { $this->skipPull( pht( "Repository '%s' is a non-hosted Subversion repository, which ". "does not require a local working copy to be pulled.", $callsign)); return; } $is_svn = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $is_git = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $is_hg = true; break; default: $this->abortPull(pht('Unknown VCS "%s"!', $vcs)); } $callsign = $repository->getCallsign(); $local_path = $repository->getLocalPath(); if ($local_path === null) { $this->abortPull( pht( "No local path is configured for repository '%s'.", $callsign)); } try { $dirname = dirname($local_path); if (!Filesystem::pathExists($dirname)) { Filesystem::createDirectory($dirname, 0755, $recursive = true); } if (!Filesystem::pathExists($local_path)) { $this->logPull( pht( "Creating a new working copy for repository '%s'.", $callsign)); if ($is_git) { $this->executeGitCreate(); } else if ($is_hg) { $this->executeMercurialCreate(); } else { $this->executeSubversionCreate(); } } else { if (!$repository->isHosted()) { $this->logPull( pht( "Updating the working copy for repository '%s'.", $callsign)); if ($is_git) { $this->executeGitUpdate(); } else if ($is_hg) { $this->executeMercurialUpdate(); } } } if ($repository->isHosted()) { if ($is_git) { $this->installGitHook(); } else if ($is_svn) { $this->installSubversionHook(); } else if ($is_hg) { $this->installMercurialHook(); } foreach ($repository->getHookDirectories() as $directory) { $this->installHookDirectory($directory); } } } catch (Exception $ex) { $this->abortPull( pht('Pull of "%s" failed: %s', $callsign, $ex->getMessage()), $ex); } $this->donePull(); return $this; } private function skipPull($message) { $this->log('%s', $message); $this->donePull(); } private function abortPull($message, Exception $ex = null) { $code_error = PhabricatorRepositoryStatusMessage::CODE_ERROR; $this->updateRepositoryInitStatus($code_error, $message); if ($ex) { throw $ex; } else { throw new Exception($message); } } private function logPull($message) { $code_working = PhabricatorRepositoryStatusMessage::CODE_WORKING; $this->updateRepositoryInitStatus($code_working, $message); $this->log('%s', $message); } private function donePull() { $code_okay = PhabricatorRepositoryStatusMessage::CODE_OKAY; $this->updateRepositoryInitStatus($code_okay); } private function updateRepositoryInitStatus($code, $message = null) { $this->getRepository()->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_INIT, $code, array( - 'message' => $message + 'message' => $message, )); } private function installHook($path) { $this->log('%s', pht('Installing commit hook to "%s"...', $path)); $repository = $this->getRepository(); $callsign = $repository->getCallsign(); $root = dirname(phutil_get_library_root('phabricator')); $bin = $root.'/bin/commit-hook'; $full_php_path = Filesystem::resolveBinary('php'); $cmd = csprintf( 'exec %s -f %s -- %s "$@"', $full_php_path, $bin, $callsign); $hook = "#!/bin/sh\nexport TERM=dumb\n{$cmd}\n"; Filesystem::writeFile($path, $hook); Filesystem::changePermissions($path, 0755); } private function installHookDirectory($path) { $readme = pht( "To add custom hook scripts to this repository, add them to this ". "directory.\n\nPhabricator will run any executables in this directory ". "after running its own checks, as though they were normal hook ". "scripts."); Filesystem::createDirectory($path, 0755); Filesystem::writeFile($path.'/README', $readme); } /* -( Pulling Git Working Copies )----------------------------------------- */ /** * @task git */ private function executeGitCreate() { $repository = $this->getRepository(); $path = rtrim($repository->getLocalPath(), '/'); if ($repository->isHosted()) { $repository->execxRemoteCommand( 'init --bare -- %s', $path); } else { $repository->execxRemoteCommand( 'clone --bare -- %P %s', $repository->getRemoteURIEnvelope(), $path); } } /** * @task git */ private function executeGitUpdate() { $repository = $this->getRepository(); list($err, $stdout) = $repository->execLocalCommand( 'rev-parse --show-toplevel'); $message = null; $path = $repository->getLocalPath(); if ($err) { // Try to raise a more tailored error message in the more common case // of the user creating an empty directory. (We could try to remove it, // but might not be able to, and it's much simpler to raise a good // message than try to navigate those waters.) if (is_dir($path)) { $files = Filesystem::listDirectory($path, $include_hidden = true); if (!$files) { $message = "Expected to find a git repository at '{$path}', but there ". "is an empty directory there. Remove the directory: the daemon ". "will run 'git clone' for you."; } else { $message = "Expected to find a git repository at '{$path}', but there is ". "a non-repository directory (with other stuff in it) there. Move ". "or remove this directory (or reconfigure the repository to use a ". "different directory), and then either clone a repository ". "yourself or let the daemon do it."; } } else if (is_file($path)) { $message = "Expected to find a git repository at '{$path}', but there is a ". "file there instead. Remove it and let the daemon clone a ". "repository for you."; } else { $message = "Expected to find a git repository at '{$path}', but did not."; } } else { $repo_path = rtrim($stdout, "\n"); if (empty($repo_path)) { // This can mean one of two things: we're in a bare repository, or // we're inside a git repository inside another git repository. Since // the first is dramatically more likely now that we perform bare // clones and I don't have a great way to test for the latter, assume // we're OK. } else if (!Filesystem::pathsAreEquivalent($repo_path, $path)) { $err = true; $message = "Expected to find repo at '{$path}', but the actual ". "git repository root for this directory is '{$repo_path}'. ". "Something is misconfigured. The repository's 'Local Path' should ". "be set to some place where the daemon can check out a working ". "copy, and should not be inside another git repository."; } } if ($err && $repository->canDestroyWorkingCopy()) { phlog("Repository working copy at '{$path}' failed sanity check; ". "destroying and re-cloning. {$message}"); Filesystem::remove($path); $this->executeGitCreate(); } else if ($err) { throw new Exception($message); } $retry = false; do { // This is a local command, but needs credentials. if ($repository->isWorkingCopyBare()) { // For bare working copies, we need this magic incantation. $future = $repository->getRemoteCommandFuture( 'fetch origin %s --prune', '+refs/heads/*:refs/heads/*'); } else { $future = $repository->getRemoteCommandFuture( 'fetch --all --prune'); } $future->setCWD($path); list($err, $stdout, $stderr) = $future->resolve(); if ($err && !$retry && $repository->canDestroyWorkingCopy()) { $retry = true; // Fix remote origin url if it doesn't match our configuration $origin_url = $repository->execLocalCommand( 'config --get remote.origin.url'); $remote_uri = $repository->getRemoteURIEnvelope(); if ($origin_url != $remote_uri->openEnvelope()) { $repository->execLocalCommand( 'remote set-url origin %P', $remote_uri); } } else if ($err) { throw new Exception( "git fetch failed with error #{$err}:\n". "stdout:{$stdout}\n\n". "stderr:{$stderr}\n"); } else { $retry = false; } } while ($retry); } /** * @task git */ private function installGitHook() { $repository = $this->getRepository(); $root = $repository->getLocalPath(); if ($repository->isWorkingCopyBare()) { $path = '/hooks/pre-receive'; } else { $path = '/.git/hooks/pre-receive'; } $this->installHook($root.$path); } /* -( Pulling Mercurial Working Copies )----------------------------------- */ /** * @task hg */ private function executeMercurialCreate() { $repository = $this->getRepository(); $path = rtrim($repository->getLocalPath(), '/'); if ($repository->isHosted()) { $repository->execxRemoteCommand( 'init -- %s', $path); } else { $repository->execxRemoteCommand( 'clone --noupdate -- %P %s', $repository->getRemoteURIEnvelope(), $path); } } /** * @task hg */ private function executeMercurialUpdate() { $repository = $this->getRepository(); $path = $repository->getLocalPath(); // This is a local command, but needs credentials. $future = $repository->getRemoteCommandFuture('pull -u'); $future->setCWD($path); try { $future->resolvex(); } catch (CommandException $ex) { $err = $ex->getError(); $stdout = $ex->getStdOut(); // NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the behavior // of "hg pull" to return 1 in case of a successful pull with no changes. // This behavior has been reverted, but users who updated between Feb 1, // 2012 and Mar 1, 2012 will have the erroring version. Do a dumb test // against stdout to check for this possibility. // See: https://github.com/phacility/phabricator/issues/101/ // NOTE: Mercurial has translated versions, which translate this error // string. In a translated version, the string will be something else, // like "aucun changement trouve". There didn't seem to be an easy way // to handle this (there are hard ways but this is not a common problem // and only creates log spam, not application failures). Assume English. // TODO: Remove this once we're far enough in the future that deployment // of 2.1 is exceedingly rare? if ($err == 1 && preg_match('/no changes found/', $stdout)) { return; } else { throw $ex; } } } /** * @task hg */ private function installMercurialHook() { $repository = $this->getRepository(); $path = $repository->getLocalPath().'/.hg/hgrc'; $root = dirname(phutil_get_library_root('phabricator')); $bin = $root.'/bin/commit-hook'; $data = array(); $data[] = '[hooks]'; // This hook handles normal pushes. $data[] = csprintf( 'pretxnchangegroup.phabricator = %s %s %s', $bin, $repository->getCallsign(), 'pretxnchangegroup'); // This one handles creating bookmarks. $data[] = csprintf( 'prepushkey.phabricator = %s %s %s', $bin, $repository->getCallsign(), 'prepushkey'); $data[] = null; $data = implode("\n", $data); $this->log('%s', pht('Installing commit hook config to "%s"...', $path)); Filesystem::writeFile($path, $data); } /* -( Pulling Subversion Working Copies )---------------------------------- */ /** * @task svn */ private function executeSubversionCreate() { $repository = $this->getRepository(); $path = rtrim($repository->getLocalPath(), '/'); execx('svnadmin create -- %s', $path); } /** * @task svn */ private function installSubversionHook() { $repository = $this->getRepository(); $root = $repository->getLocalPath(); $path = '/hooks/pre-commit'; $this->installHook($root.$path); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php index 721003f933..c4dfd10b75 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php @@ -1,201 +1,202 @@ <?php /** * Records a push to a hosted repository. This allows us to store metadata * about who pushed commits, when, and from where. We can also record the * history of branches and tags, which is not normally persisted outside of * the reflog. * * This log is written by commit hooks installed into hosted repositories. * See @{class:DiffusionCommitHookEngine}. */ final class PhabricatorRepositoryPushLog extends PhabricatorRepositoryDAO implements PhabricatorPolicyInterface { const REFTYPE_BRANCH = 'branch'; const REFTYPE_TAG = 'tag'; const REFTYPE_BOOKMARK = 'bookmark'; const REFTYPE_COMMIT = 'commit'; const CHANGEFLAG_ADD = 1; const CHANGEFLAG_DELETE = 2; const CHANGEFLAG_APPEND = 4; const CHANGEFLAG_REWRITE = 8; const CHANGEFLAG_DANGEROUS = 16; const REJECT_ACCEPT = 0; const REJECT_DANGEROUS = 1; const REJECT_HERALD = 2; const REJECT_EXTERNAL = 3; const REJECT_BROKEN = 4; protected $repositoryPHID; protected $epoch; protected $pusherPHID; protected $pushEventPHID; protected $refType; protected $refNameHash; protected $refNameRaw; protected $refNameEncoding; protected $refOld; protected $refNew; protected $mergeBase; protected $changeFlags; private $dangerousChangeDescription = self::ATTACHABLE; private $pushEvent = self::ATTACHABLE; private $repository = self::ATTACHABLE; public static function initializeNewLog(PhabricatorUser $viewer) { return id(new PhabricatorRepositoryPushLog()) ->setPusherPHID($viewer->getPHID()); } public static function getHeraldChangeFlagConditionOptions() { return array( PhabricatorRepositoryPushLog::CHANGEFLAG_ADD => pht('change creates ref'), PhabricatorRepositoryPushLog::CHANGEFLAG_DELETE => pht('change deletes ref'), PhabricatorRepositoryPushLog::CHANGEFLAG_REWRITE => pht('change rewrites ref'), PhabricatorRepositoryPushLog::CHANGEFLAG_DANGEROUS => - pht('dangerous change')); + pht('dangerous change'), + ); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_BINARY => array( 'refNameRaw' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'refType' => 'text12', 'refNameHash' => 'bytes12?', 'refNameRaw' => 'bytes?', 'refNameEncoding' => 'text16?', 'refOld' => 'text40?', 'refNew' => 'text40', 'mergeBase' => 'text40?', 'changeFlags' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_repository' => array( 'columns' => array('repositoryPHID'), ), 'key_ref' => array( 'columns' => array('repositoryPHID', 'refNew'), ), 'key_name' => array( 'columns' => array('repositoryPHID', 'refNameHash'), ), 'key_event' => array( 'columns' => array('pushEventPHID'), ), 'key_pusher' => array( 'columns' => array('pusherPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryPushLogPHIDType::TYPECONST); } public function attachPushEvent(PhabricatorRepositoryPushEvent $push_event) { $this->pushEvent = $push_event; return $this; } public function getPushEvent() { return $this->assertAttached($this->pushEvent); } public function getRefName() { return $this->getUTF8StringFromStorage( $this->getRefNameRaw(), $this->getRefNameEncoding()); } public function setRefName($ref_raw) { $this->setRefNameRaw($ref_raw); $this->setRefNameHash(PhabricatorHash::digestForIndex($ref_raw)); $this->setRefNameEncoding($this->detectEncodingForStorage($ref_raw)); return $this; } public function getRefOldShort() { if ($this->getRepository()->isSVN()) { return $this->getRefOld(); } return substr($this->getRefOld(), 0, 12); } public function getRefNewShort() { if ($this->getRepository()->isSVN()) { return $this->getRefNew(); } return substr($this->getRefNew(), 0, 12); } public function hasChangeFlags($mask) { return ($this->changeFlags & $mask); } public function attachDangerousChangeDescription($description) { $this->dangerousChangeDescription = $description; return $this; } public function getDangerousChangeDescription() { return $this->assertAttached($this->dangerousChangeDescription); } public function attachRepository(PhabricatorRepository $repository) { // NOTE: Some gymnastics around this because of object construction order // in the hook engine. Particularly, web build the logs before we build // their push event. $this->repository = $repository; return $this; } public function getRepository() { if ($this->repository == self::ATTACHABLE) { return $this->getPushEvent()->getRepository(); } return $this->assertAttached($this->repository); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // NOTE: We're passing through the repository rather than the push event // mostly because we need to do policy checks in Herald before we create // the event. The two approaches are equivalent in practice. return $this->getRepository()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( "A repository's push logs are visible to users who can see the ". "repository."); } } diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorOwnersPackagePathValidator.php b/src/applications/repository/worker/commitchangeparser/PhabricatorOwnersPackagePathValidator.php index 7269c74ab8..c87e4145f5 100644 --- a/src/applications/repository/worker/commitchangeparser/PhabricatorOwnersPackagePathValidator.php +++ b/src/applications/repository/worker/commitchangeparser/PhabricatorOwnersPackagePathValidator.php @@ -1,99 +1,99 @@ <?php final class PhabricatorOwnersPackagePathValidator { /* * If a file/directory was moved the paths in owners package become stale. * This method updates the stale paths in the owners packages to their new * paths. */ public static function updateOwnersPackagePaths( PhabricatorRepositoryCommit $commit) { $changes = self::loadDiffusionChangesForCommit($commit); if (!$changes) { return; } // TODO: (T603) This should be policy-aware. $repository = id(new PhabricatorRepository())->load($commit->getRepositoryID()); $move_map = array(); foreach ($changes as $change) { if ($change->getChangeType() == DifferentialChangeType::TYPE_MOVE_HERE) { $from_path = '/'.$change->getTargetPath(); $to_path = '/'.$change->getPath(); if ($change->getFileType() == DifferentialChangeType::FILE_DIRECTORY) { $to_path = $to_path.'/'; $from_path = $from_path.'/'; } $move_map[$from_path] = $to_path; } } if ($move_map) { self::updateAffectedPackages($repository, $move_map); } } private static function updateAffectedPackages($repository, array $move_map) { $paths = array_keys($move_map); if ($paths) { $packages = PhabricatorOwnersPackage::loadAffectedPackages($repository, $paths); foreach ($packages as $package) { self::updatePackagePaths($package, $move_map); } } } private static function updatePackagePaths($package, array $move_map) { $paths = array_keys($move_map); $pkg_paths = $package->loadPaths(); $new_paths = array(); foreach ($pkg_paths as $pkg_path) { $path_changed = false; foreach ($paths as $old_path) { if (strncmp($pkg_path->getPath(), $old_path, strlen($old_path)) === 0) { $new_paths[] = array ( 'packageID' => $package->getID(), 'repositoryPHID' => $pkg_path->getRepositoryPHID(), 'path' => str_replace($pkg_path->getPath(), $old_path, - $move_map[$old_path]) + $move_map[$old_path]), ); $path_changed = true; } } if (!$path_changed) { $new_paths[] = array ( 'packageID' => $package->getID(), 'repositoryPHID' => $pkg_path->getRepositoryPHID(), 'path' => $pkg_path->getPath(), ); } } if ($new_paths) { $package->attachOldPrimaryOwnerPHID($package->getPrimaryOwnerPHID()); $package->attachUnsavedPaths($new_paths); $package->save(); // save the changes and notify the owners. } } private static function loadDiffusionChangesForCommit($commit) { $repository = id(new PhabricatorRepository())->load($commit->getRepositoryID()); $data = array( 'user' => PhabricatorUser::getOmnipotentUser(), 'initFromConduit' => false, 'repository' => $repository, - 'commit' => $commit->getCommitIdentifier() + 'commit' => $commit->getCommitIdentifier(), ); $drequest = DiffusionRequest::newFromDictionary($data); $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest($drequest); return $change_query->loadChanges(); } } diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php index a07e8b17d1..ec1d1de8a8 100644 --- a/src/applications/search/controller/PhabricatorSearchAttachController.php +++ b/src/applications/search/controller/PhabricatorSearchAttachController.php @@ -1,317 +1,319 @@ <?php final class PhabricatorSearchAttachController extends PhabricatorSearchBaseController { private $phid; private $type; private $action; const ACTION_ATTACH = 'attach'; const ACTION_MERGE = 'merge'; const ACTION_DEPENDENCIES = 'dependencies'; const ACTION_BLOCKS = 'blocks'; const ACTION_EDGE = 'edge'; public function willProcessRequest(array $data) { $this->phid = $data['phid']; $this->type = $data['type']; $this->action = idx($data, 'action', self::ACTION_ATTACH); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($this->phid)) ->executeOne(); $object_type = $handle->getType(); $attach_type = $this->type; $object = id(new PhabricatorObjectQuery()) ->setViewer($user) ->withPHIDs(array($this->phid)) ->executeOne(); if (!$object) { return new Aphront404Response(); } $edge_type = null; switch ($this->action) { case self::ACTION_EDGE: case self::ACTION_DEPENDENCIES: case self::ACTION_BLOCKS: case self::ACTION_ATTACH: $edge_type = $this->getEdgeType($object_type, $attach_type); break; } if ($request->isFormPost()) { $phids = explode(';', $request->getStr('phids')); $phids = array_filter($phids); $phids = array_values($phids); if ($edge_type) { if (!$object instanceof PhabricatorApplicationTransactionInterface) { throw new Exception( pht( 'Expected object ("%s") to implement interface "%s".', get_class($object), 'PhabricatorApplicationTransactionInterface')); } $old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->phid, $edge_type); $add_phids = $phids; $rem_phids = array_diff($old_phids, $add_phids); $txn_editor = $object->getApplicationTransactionEditor() ->setActor($user) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true); $txn_template = $object->getApplicationTransactionTemplate() ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $edge_type) ->setNewValue(array( '+' => array_fuse($add_phids), - '-' => array_fuse($rem_phids))); + '-' => array_fuse($rem_phids), + )); $txn_editor->applyTransactions( $object->getApplicationTransactionObject(), array($txn_template)); return id(new AphrontReloadResponse())->setURI($handle->getURI()); } else { return $this->performMerge($object, $handle, $phids); } } else { if ($edge_type) { $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->phid, $edge_type); } else { // This is a merge. $phids = array(); } } $strings = $this->getStrings(); $handles = $this->loadViewerHandles($phids); $obj_dialog = new PhabricatorObjectSelectorDialog(); $obj_dialog ->setUser($user) ->setHandles($handles) ->setFilters($this->getFilters($strings)) ->setSelectedFilter($strings['selected']) ->setExcluded($this->phid) ->setCancelURI($handle->getURI()) ->setSearchURI('/search/select/'.$attach_type.'/') ->setTitle($strings['title']) ->setHeader($strings['header']) ->setButtonText($strings['button']) ->setInstructions($strings['instructions']); $dialog = $obj_dialog->buildDialog(); return id(new AphrontDialogResponse())->setDialog($dialog); } private function performMerge( ManiphestTask $task, PhabricatorObjectHandle $handle, array $phids) { $user = $this->getRequest()->getUser(); $response = id(new AphrontReloadResponse())->setURI($handle->getURI()); $phids = array_fill_keys($phids, true); unset($phids[$task->getPHID()]); // Prevent merging a task into itself. if (!$phids) { return $response; } $targets = id(new ManiphestTaskQuery()) ->setViewer($user) ->withPHIDs(array_keys($phids)) ->execute(); if (empty($targets)) { return $response; } $editor = id(new ManiphestTransactionEditor()) ->setActor($user) ->setContentSourceFromRequest($this->getRequest()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $cc_vector = array(); $cc_vector[] = $task->getCCPHIDs(); foreach ($targets as $target) { $cc_vector[] = $target->getCCPHIDs(); $cc_vector[] = array( $target->getAuthorPHID(), - $target->getOwnerPHID()); + $target->getOwnerPHID(), + ); $merged_into_txn = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_MERGED_INTO) ->setNewValue($task->getPHID()); $editor->applyTransactions( $target, array($merged_into_txn)); } $all_ccs = array_mergev($cc_vector); $all_ccs = array_filter($all_ccs); $all_ccs = array_unique($all_ccs); $add_ccs = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_CCS) ->setNewValue($all_ccs); $merged_from_txn = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_MERGED_FROM) ->setNewValue(mpull($targets, 'getPHID')); $editor->applyTransactions( $task, array($add_ccs, $merged_from_txn)); return $response; } private function getStrings() { switch ($this->type) { case DifferentialRevisionPHIDType::TYPECONST: $noun = 'Revisions'; $selected = 'created'; break; case ManiphestTaskPHIDType::TYPECONST: $noun = 'Tasks'; $selected = 'assigned'; break; case PhabricatorRepositoryCommitPHIDType::TYPECONST: $noun = 'Commits'; $selected = 'created'; break; case PholioMockPHIDType::TYPECONST: $noun = 'Mocks'; $selected = 'created'; break; } switch ($this->action) { case self::ACTION_EDGE: case self::ACTION_ATTACH: $dialog_title = "Manage Attached {$noun}"; $header_text = "Currently Attached {$noun}"; $button_text = "Save {$noun}"; $instructions = null; break; case self::ACTION_MERGE: $dialog_title = 'Merge Duplicate Tasks'; $header_text = 'Tasks To Merge'; $button_text = "Merge {$noun}"; $instructions = 'These tasks will be merged into the current task and then closed. '. 'The current task will grow stronger.'; break; case self::ACTION_DEPENDENCIES: $dialog_title = 'Edit Dependencies'; $header_text = 'Current Dependencies'; $button_text = 'Save Dependencies'; $instructions = null; break; case self::ACTION_BLOCKS: $dialog_title = pht('Edit Blocking Tasks'); $header_text = pht('Current Blocking Tasks'); $button_text = pht('Save Blocking Tasks'); $instructions = null; break; } return array( 'target_plural_noun' => $noun, 'selected' => $selected, 'title' => $dialog_title, 'header' => $header_text, 'button' => $button_text, 'instructions' => $instructions, ); } private function getFilters(array $strings) { if ($this->type == PholioMockPHIDType::TYPECONST) { $filters = array( 'created' => 'Created By Me', 'all' => 'All '.$strings['target_plural_noun'], ); } else { $filters = array( 'assigned' => 'Assigned to Me', 'created' => 'Created By Me', 'open' => 'All Open '.$strings['target_plural_noun'], 'all' => 'All '.$strings['target_plural_noun'], ); } return $filters; } private function getEdgeType($src_type, $dst_type) { $t_cmit = PhabricatorRepositoryCommitPHIDType::TYPECONST; $t_task = ManiphestTaskPHIDType::TYPECONST; $t_drev = DifferentialRevisionPHIDType::TYPECONST; $t_mock = PholioMockPHIDType::TYPECONST; $map = array( $t_cmit => array( $t_task => DiffusionCommitHasTaskEdgeType::EDGECONST, ), $t_task => array( $t_cmit => ManiphestTaskHasCommitEdgeType::EDGECONST, $t_task => PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK, $t_drev => ManiphestTaskHasRevisionEdgeType::EDGECONST, $t_mock => PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK, ), $t_drev => array( $t_drev => PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV, $t_task => DifferentialRevisionHasTaskEdgeType::EDGECONST, ), $t_mock => array( $t_task => PhabricatorEdgeConfig::TYPE_MOCK_HAS_TASK, ), ); if (empty($map[$src_type][$dst_type])) { return null; } return $map[$src_type][$dst_type]; } private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) { $cycle = $ex->getCycle(); $handles = $this->loadViewerHandles($cycle); $names = array(); foreach ($cycle as $cycle_phid) { $names[] = $handles[$cycle_phid]->getFullName(); } $names = implode(" \xE2\x86\x92 ", $names); throw new Exception( "You can not create that dependency, because it would create a ". "circular dependency: {$names}."); } } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php b/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php index 38a9cf7b07..6b7fa0a9a8 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php @@ -1,382 +1,382 @@ <?php final class PhabricatorSettingsPanelEmailAddresses extends PhabricatorSettingsPanel { public function getPanelKey() { return 'email'; } public function getPanelName() { return pht('Email Addresses'); } public function getPanelGroup() { return pht('Email'); } public function processRequest(AphrontRequest $request) { $user = $request->getUser(); $editable = PhabricatorEnv::getEnvConfig('account.editable'); $uri = $request->getRequestURI(); $uri->setQueryParams(array()); if ($editable) { $new = $request->getStr('new'); if ($new) { return $this->returnNewAddressResponse($request, $uri, $new); } $delete = $request->getInt('delete'); if ($delete) { return $this->returnDeleteAddressResponse($request, $uri, $delete); } } $verify = $request->getInt('verify'); if ($verify) { return $this->returnVerifyAddressResponse($request, $uri, $verify); } $primary = $request->getInt('primary'); if ($primary) { return $this->returnPrimaryAddressResponse($request, $uri, $primary); } $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s ORDER BY address', $user->getPHID()); $rowc = array(); $rows = array(); foreach ($emails as $email) { $button_verify = javelin_tag( 'a', array( 'class' => 'button small grey', 'href' => $uri->alter('verify', $email->getID()), 'sigil' => 'workflow', ), pht('Verify')); $button_make_primary = javelin_tag( 'a', array( 'class' => 'button small grey', 'href' => $uri->alter('primary', $email->getID()), 'sigil' => 'workflow', ), pht('Make Primary')); $button_remove = javelin_tag( 'a', array( 'class' => 'button small grey', 'href' => $uri->alter('delete', $email->getID()), - 'sigil' => 'workflow' + 'sigil' => 'workflow', ), pht('Remove')); $button_primary = phutil_tag( 'a', array( 'class' => 'button small disabled', ), pht('Primary')); if (!$email->getIsVerified()) { $action = $button_verify; } else if ($email->getIsPrimary()) { $action = $button_primary; } else { $action = $button_make_primary; } if ($email->getIsPrimary()) { $remove = $button_primary; $rowc[] = 'highlighted'; } else { $remove = $button_remove; $rowc[] = null; } $rows[] = array( $email->getAddress(), $action, $remove, ); } $table = new AphrontTableView($rows); $table->setHeaders( array( pht('Email'), pht('Status'), pht('Remove'), )); $table->setColumnClasses( array( 'wide', 'action', 'action', )); $table->setRowClasses($rowc); $table->setColumnVisibility( array( true, true, $editable, )); $view = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); $header->setHeader(pht('Email Addresses')); if ($editable) { $icon = id(new PHUIIconView()) ->setIconFont('fa-plus'); $button = new PHUIButtonView(); $button->setText(pht('Add New Address')); $button->setTag('a'); $button->setHref($uri->alter('new', 'true')); $button->setIcon($icon); $button->addSigil('workflow'); $header->addActionLink($button); } $view->setHeader($header); $view->appendChild($table); return $view; } private function returnNewAddressResponse( AphrontRequest $request, PhutilURI $uri, $new) { $user = $request->getUser(); $e_email = true; $email = null; $errors = array(); if ($request->isDialogFormPost()) { $email = trim($request->getStr('email')); if ($new == 'verify') { // The user clicked "Done" from the "an email has been sent" dialog. return id(new AphrontReloadResponse())->setURI($uri); } PhabricatorSystemActionEngine::willTakeAction( array($user->getPHID()), new PhabricatorSettingsAddEmailAction(), 1); if (!strlen($email)) { $e_email = pht('Required'); $errors[] = pht('Email is required.'); } else if (!PhabricatorUserEmail::isValidAddress($email)) { $e_email = pht('Invalid'); $errors[] = PhabricatorUserEmail::describeValidAddresses(); } else if (!PhabricatorUserEmail::isAllowedAddress($email)) { $e_email = pht('Disallowed'); $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); } if (!$errors) { $object = id(new PhabricatorUserEmail()) ->setAddress($email) ->setIsVerified(0); try { id(new PhabricatorUserEditor()) ->setActor($user) ->addEmail($user, $object); $object->sendVerificationEmail($user); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addHiddenInput('new', 'verify') ->setTitle(pht('Verification Email Sent')) ->appendChild(phutil_tag('p', array(), pht( 'A verification email has been sent. Click the link in the '. 'email to verify your address.'))) ->setSubmitURI($uri) ->addSubmitButton(pht('Done')); return id(new AphrontDialogResponse())->setDialog($dialog); } catch (AphrontDuplicateKeyQueryException $ex) { $e_email = pht('Duplicate'); $errors[] = pht('Another user already has this email.'); } } } if ($errors) { $errors = id(new AphrontErrorView()) ->setErrors($errors); } $form = id(new PHUIFormLayoutView()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($email) ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->setError($e_email)); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addHiddenInput('new', 'true') ->setTitle(pht('New Address')) ->appendChild($errors) ->appendChild($form) ->addSubmitButton(pht('Save')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } private function returnDeleteAddressResponse( AphrontRequest $request, PhutilURI $uri, $email_id) { $user = $request->getUser(); // NOTE: You can only delete your own email addresses, and you can not // delete your primary address. $email = id(new PhabricatorUserEmail())->loadOneWhere( 'id = %d AND userPHID = %s AND isPrimary = 0', $email_id, $user->getPHID()); if (!$email) { return new Aphront404Response(); } if ($request->isFormPost()) { id(new PhabricatorUserEditor()) ->setActor($user) ->removeEmail($user, $email); return id(new AphrontRedirectResponse())->setURI($uri); } $address = $email->getAddress(); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addHiddenInput('delete', $email_id) ->setTitle(pht("Really delete address '%s'?", $address)) ->appendParagraph( pht( 'Are you sure you want to delete this address? You will no '. 'longer be able to use it to login.')) ->appendParagraph( pht( 'Note: Removing an email address from your account will invalidate '. 'any outstanding password reset links.')) ->addSubmitButton(pht('Delete')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } private function returnVerifyAddressResponse( AphrontRequest $request, PhutilURI $uri, $email_id) { $user = $request->getUser(); // NOTE: You can only send more email for your unverified addresses. $email = id(new PhabricatorUserEmail())->loadOneWhere( 'id = %d AND userPHID = %s AND isVerified = 0', $email_id, $user->getPHID()); if (!$email) { return new Aphront404Response(); } if ($request->isFormPost()) { $email->sendVerificationEmail($user); return id(new AphrontRedirectResponse())->setURI($uri); } $address = $email->getAddress(); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addHiddenInput('verify', $email_id) ->setTitle(pht('Send Another Verification Email?')) ->appendChild(phutil_tag('p', array(), pht( 'Send another copy of the verification email to %s?', $address))) ->addSubmitButton(pht('Send Email')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } private function returnPrimaryAddressResponse( AphrontRequest $request, PhutilURI $uri, $email_id) { $user = $request->getUser(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $user, $request, $this->getPanelURI()); // NOTE: You can only make your own verified addresses primary. $email = id(new PhabricatorUserEmail())->loadOneWhere( 'id = %d AND userPHID = %s AND isVerified = 1 AND isPrimary = 0', $email_id, $user->getPHID()); if (!$email) { return new Aphront404Response(); } if ($request->isFormPost()) { id(new PhabricatorUserEditor()) ->setActor($user) ->changePrimaryEmail($user, $email); return id(new AphrontRedirectResponse())->setURI($uri); } $address = $email->getAddress(); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addHiddenInput('primary', $email_id) ->setTitle(pht('Change primary email address?')) ->appendParagraph( pht( 'If you change your primary address, Phabricator will send all '. 'email to %s.', $address)) ->appendParagraph( pht( 'Note: Changing your primary email address will invalidate any '. 'outstanding password reset links.')) ->addSubmitButton(pht('Change Primary Address')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php b/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php index 68bb52c24d..9126ca5e83 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php @@ -1,255 +1,256 @@ <?php final class PhabricatorSettingsPanelEmailPreferences extends PhabricatorSettingsPanel { public function getPanelKey() { return 'emailpreferences'; } public function getPanelName() { return pht('Email Preferences'); } public function getPanelGroup() { return pht('Email'); } public function processRequest(AphrontRequest $request) { $user = $request->getUser(); $preferences = $user->loadPreferences(); $pref_no_mail = PhabricatorUserPreferences::PREFERENCE_NO_MAIL; $pref_no_self_mail = PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL; $value_email = PhabricatorUserPreferences::MAILTAG_PREFERENCE_EMAIL; $errors = array(); if ($request->isFormPost()) { $preferences->setPreference( $pref_no_mail, $request->getStr($pref_no_mail)); $preferences->setPreference( $pref_no_self_mail, $request->getStr($pref_no_self_mail)); $new_tags = $request->getArr('mailtags'); $mailtags = $preferences->getPreference('mailtags', array()); $all_tags = $this->getAllTags($user); foreach ($all_tags as $key => $label) { $mailtags[$key] = (int)idx($new_tags, $key, $value_email); } $preferences->setPreference('mailtags', $mailtags); $preferences->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } $form = new AphrontFormView(); $form ->setUser($user) ->appendRemarkupInstructions( pht( 'These settings let you control how Phabricator notifies you about '. 'events. You can configure Phabricator to send you an email, '. 'just send a web notification, or not notify you at all.')) ->appendRemarkupInstructions( pht( 'If you disable **Email Notifications**, Phabricator will never '. 'send email to notify you about events. This preference overrides '. 'all your other settings.'. "\n\n". "//You may still receive some administrative email, like password ". "reset email.//")) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Email Notifications')) ->setName($pref_no_mail) ->setOptions( array( '0' => pht('Send me email notifications'), '1' => pht('Never send email notifications'), )) ->setValue($preferences->getPreference($pref_no_mail, 0))) ->appendRemarkupInstructions( pht( 'If you disable **Self Actions**, Phabricator will not notify '. 'you about actions you take.')) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Self Actions')) ->setName($pref_no_self_mail) ->setOptions( array( '0' => pht('Send me an email when I take an action'), '1' => pht('Do not send me an email when I take an action'), )) ->setValue($preferences->getPreference($pref_no_self_mail, 0))); $mailtags = $preferences->getPreference('mailtags', array()); $form->appendChild( id(new PHUIFormDividerControl())); $form->appendRemarkupInstructions( pht( 'You can adjust **Application Settings** here to customize when '. 'you are emailed and notified.'. "\n\n". "| Setting | Effect\n". "| ------- | -------\n". "| Email | You will receive an email and a notification, but the ". "notification will be marked \"read\".\n". "| Notify | You will receive an unread notification only.\n". "| Ignore | You will receive nothing.\n". "\n\n". 'If an update makes several changes (like adding CCs to a task, '. 'closing it, and adding a comment) you will receive the strongest '. 'notification any of the changes is configured to deliver.'. "\n\n". 'These preferences **only** apply to objects you are connected to '. '(for example, Revisions where you are a reviewer or tasks you are '. 'CC\'d on). To receive email alerts when other objects are created, '. 'configure [[ /herald/ | Herald Rules ]].')); $editors = $this->getAllEditorsWithTags($user); // Find all the tags shared by more than one application, and put them // in a "common" group. $all_tags = array(); foreach ($editors as $editor) { foreach ($editor->getMailTagsMap() as $tag => $name) { if (empty($all_tags[$tag])) { $all_tags[$tag] = array( 'count' => 0, 'name' => $name, ); } $all_tags[$tag]['count']; } } $common_tags = array(); foreach ($all_tags as $tag => $info) { if ($info['count'] > 1) { $common_tags[$tag] = $info['name']; } } // Build up the groups of application-specific options. $tag_groups = array(); foreach ($editors as $editor) { $tag_groups[] = array( $editor->getEditorObjectsDescription(), - array_diff_key($editor->getMailTagsMap(), $common_tags)); + array_diff_key($editor->getMailTagsMap(), $common_tags), + ); } // Sort them, then put "Common" at the top. $tag_groups = isort($tag_groups, 0); if ($common_tags) { array_unshift($tag_groups, array(pht('Common'), $common_tags)); } // Finally, build the controls. foreach ($tag_groups as $spec) { list($label, $map) = $spec; $control = $this->buildMailTagControl($label, $map, $mailtags); $form->appendChild($control); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Preferences'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Email Preferences')) ->setFormSaved($request->getStr('saved')) ->setFormErrors($errors) ->setForm($form); return id(new AphrontNullView()) ->appendChild( array( $form_box, )); } private function getAllEditorsWithTags(PhabricatorUser $user) { $editors = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorApplicationTransactionEditor') ->loadObjects(); foreach ($editors as $key => $editor) { // Remove editors which do not support mail tags. if (!$editor->getMailTagsMap()) { unset($editors[$key]); } // Remove editors for applications which are not installed. $app = $editor->getEditorApplicationClass(); if ($app !== null) { if (!PhabricatorApplication::isClassInstalledForViewer($app, $user)) { unset($editors[$key]); } } } return $editors; } private function getAllTags(PhabricatorUser $user) { $tags = array(); foreach ($this->getAllEditorsWithTags($user) as $editor) { $tags += $editor->getMailTagsMap(); } return $tags; } private function buildMailTagControl( $control_label, array $tags, array $prefs) { $value_email = PhabricatorUserPreferences::MAILTAG_PREFERENCE_EMAIL; $value_notify = PhabricatorUserPreferences::MAILTAG_PREFERENCE_NOTIFY; $value_ignore = PhabricatorUserPreferences::MAILTAG_PREFERENCE_IGNORE; $content = array(); foreach ($tags as $key => $label) { $select = AphrontFormSelectControl::renderSelectTag( (int)idx($prefs, $key, $value_email), array( $value_email => pht("\xE2\x9A\xAB Email"), $value_notify => pht("\xE2\x97\x90 Notify"), $value_ignore => pht("\xE2\x9A\xAA Ignore"), ), array( 'name' => 'mailtags['.$key.']', )); $content[] = phutil_tag( 'div', array( 'class' => 'psb', ), array( $select, ' ', $label, )); } $control = new AphrontFormStaticControl(); $control->setLabel($control_label); $control->setValue($content); return $control; } } diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php index 2443519eeb..6cfeae0ac0 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php @@ -1,108 +1,109 @@ <?php final class PhabricatorSlowvoteVoteController extends PhabricatorSlowvoteController { private $id; public function willProcessRequest(array $data) { $this->id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $poll = id(new PhabricatorSlowvoteQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->needOptions(true) ->needViewerChoices(true) ->executeOne(); if (!$poll) { return new Aphront404Response(); } if ($poll->getIsClosed()) { return new Aphront400Response(); } $options = $poll->getOptions(); $user_choices = $poll->getViewerChoices($user); $old_votes = mpull($user_choices, null, 'getOptionID'); if ($request->isAjax()) { $vote = $request->getInt('vote'); $votes = array_keys($old_votes); $votes = array_fuse($votes, $votes); if ($poll->getMethod() == PhabricatorSlowvotePoll::METHOD_PLURALITY) { if (idx($votes, $vote, false)) { $votes = array(); } else { $votes = array($vote); } } else { if (idx($votes, $vote, false)) { unset($votes[$vote]); } else { $votes[$vote] = $vote; } } $this->updateVotes($user, $poll, $old_votes, $votes); $updated_choices = id(new PhabricatorSlowvoteChoice())->loadAllWhere( 'pollID = %d AND authorPHID = %s', $poll->getID(), $user->getPHID()); $embed = id(new SlowvoteEmbedView()) ->setPoll($poll) ->setOptions($options) ->setViewerChoices($updated_choices); return id(new AphrontAjaxResponse()) ->setContent(array( 'pollID' => $poll->getID(), - 'contentHTML' => $embed->render())); + 'contentHTML' => $embed->render(), + )); } if (!$request->isFormPost()) { return id(new Aphront404Response()); } $votes = $request->getArr('vote'); $votes = array_fuse($votes, $votes); $this->updateVotes($user, $poll, $old_votes, $votes); return id(new AphrontRedirectResponse())->setURI('/V'.$poll->getID()); } private function updateVotes($user, $poll, $old_votes, $votes) { if (!empty($votes) && count($votes) > 1 && $poll->getMethod() == PhabricatorSlowvotePoll::METHOD_PLURALITY) { return id(new Aphront400Response()); } foreach ($old_votes as $old_vote) { if (!idx($votes, $old_vote->getOptionID(), false)) { $old_vote->delete(); } } foreach ($votes as $vote) { if (idx($old_votes, $vote, false)) { continue; } id(new PhabricatorSlowvoteChoice()) ->setAuthorPHID($user->getPHID()) ->setPollID($poll->getID()) ->setOptionID($vote) ->save(); } } } diff --git a/src/applications/slowvote/view/SlowvoteEmbedView.php b/src/applications/slowvote/view/SlowvoteEmbedView.php index 4222079f99..79e4c0bb1d 100644 --- a/src/applications/slowvote/view/SlowvoteEmbedView.php +++ b/src/applications/slowvote/view/SlowvoteEmbedView.php @@ -1,359 +1,362 @@ <?php final class SlowvoteEmbedView extends AphrontView { private $poll; private $handles; private $headless; public function setHeadless($headless) { $this->headless = $headless; return $this; } public function setPoll(PhabricatorSlowvotePoll $poll) { $this->poll = $poll; return $this; } public function getPoll() { return $this->poll; } public function render() { if (!$this->poll) { throw new Exception('Call setPoll() before render()!'); } $poll = $this->poll; $phids = array(); foreach ($poll->getChoices() as $choice) { $phids[] = $choice->getAuthorPHID(); } $phids[] = $poll->getAuthorPHID(); $this->handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getUser()) ->withPHIDs($phids) ->execute(); $options = $poll->getOptions(); if ($poll->getShuffle()) { shuffle($options); } require_celerity_resource('phabricator-slowvote-css'); require_celerity_resource('javelin-behavior-slowvote-embed'); $config = array( - 'pollID' => $poll->getID()); + 'pollID' => $poll->getID(), + ); Javelin::initBehavior('slowvote-embed', $config); $user_choices = $poll->getViewerChoices($this->getUser()); $user_choices = mpull($user_choices, 'getOptionID', 'getOptionID'); $out = array(); foreach ($options as $option) { $is_selected = isset($user_choices[$option->getID()]); $out[] = $this->renderLabel($option, $is_selected); } $link_to_slowvote = phutil_tag( 'a', array( - 'href' => '/V'.$poll->getID() + 'href' => '/V'.$poll->getID(), ), $poll->getQuestion()); if ($this->headless) { $header = null; } else { $header = phutil_tag( 'div', array( 'class' => 'slowvote-header', ), phutil_tag( 'div', array( 'class' => 'slowvote-header-content', ), array( 'V'.$poll->getID(), ' ', - $link_to_slowvote))); + $link_to_slowvote, + ))); $description = null; if ($poll->getDescription()) { $description = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent( $poll->getDescription()), 'default', $this->getUser()); $description = phutil_tag( 'div', array( 'class' => 'slowvote-description', ), $description); } $header = array( $header, - $description); + $description, + ); } $vis = $poll->getResponseVisibility(); if ($this->areResultsVisible()) { if ($vis == PhabricatorSlowvotePoll::RESPONSES_OWNER) { $quip = pht('Only you can see the results.'); } else { $quip = pht('Voting improves cardiovascular endurance.'); } } else if ($vis == PhabricatorSlowvotePoll::RESPONSES_VOTERS) { $quip = pht('You must vote to see the results.'); } else if ($vis == PhabricatorSlowvotePoll::RESPONSES_OWNER) { $quip = pht('Only the author can see the results.'); } $hint = phutil_tag( 'span', array( 'class' => 'slowvote-hint', ), $quip); if ($poll->getIsClosed()) { $submit = null; } else { $submit = phutil_tag( 'div', array( 'class' => 'slowvote-footer', ), phutil_tag( 'div', array( 'class' => 'slowvote-footer-content', ), array( $hint, phutil_tag( 'button', array( ), pht('Engage in Deliberations')), ))); } $body = phabricator_form( $this->getUser(), array( 'action' => '/vote/'.$poll->getID().'/', 'method' => 'POST', 'class' => 'slowvote-body', ), array( phutil_tag( 'div', array( 'class' => 'slowvote-body-content', ), $out), $submit, )); return javelin_tag( 'div', array( 'class' => 'slowvote-embed', 'sigil' => 'slowvote-embed', 'meta' => array( - 'pollID' => $poll->getID() - ) + 'pollID' => $poll->getID(), + ), ), array($header, $body)); } private function renderLabel(PhabricatorSlowvoteOption $option, $selected) { $classes = array(); $classes[] = 'slowvote-option-label'; $status = $this->renderStatus($option); $voters = $this->renderVoters($option); return phutil_tag( 'div', array( 'class' => 'slowvote-option-label-group', ), array( phutil_tag( 'label', array( 'class' => implode(' ', $classes), ), array( phutil_tag( 'div', array( 'class' => 'slowvote-control-offset', ), $option->getName()), $this->renderBar($option), phutil_tag( 'div', array( 'class' => 'slowvote-above-the-bar', ), array( $this->renderControl($option, $selected), )), )), $status, $voters, )); } private function renderBar(PhabricatorSlowvoteOption $option) { if (!$this->areResultsVisible()) { return null; } $poll = $this->getPoll(); $choices = mgroup($poll->getChoices(), 'getOptionID'); $choices = count(idx($choices, $option->getID(), array())); $count = count(mgroup($poll->getChoices(), 'getAuthorPHID')); return phutil_tag( 'div', array( 'class' => 'slowvote-bar', 'style' => sprintf( 'width: %.1f%%;', $count ? 100 * ($choices / $count) : 0), ), array( phutil_tag( 'div', array( 'class' => 'slowvote-control-offset', ), $option->getName()), )); } private function renderControl(PhabricatorSlowvoteOption $option, $selected) { $types = array( PhabricatorSlowvotePoll::METHOD_PLURALITY => 'radio', PhabricatorSlowvotePoll::METHOD_APPROVAL => 'checkbox', ); $closed = $this->getPoll()->getIsClosed(); return phutil_tag( 'input', array( 'type' => idx($types, $this->getPoll()->getMethod()), 'name' => 'vote[]', 'value' => $option->getID(), 'checked' => ($selected ? 'checked' : null), 'disabled' => ($closed ? 'disabled' : null), )); } private function renderVoters(PhabricatorSlowvoteOption $option) { if (!$this->areResultsVisible()) { return null; } $poll = $this->getPoll(); $choices = mgroup($poll->getChoices(), 'getOptionID'); $choices = idx($choices, $option->getID(), array()); if (!$choices) { return null; } $handles = $this->handles; $authors = mpull($choices, 'getAuthorPHID', 'getAuthorPHID'); $viewer_phid = $this->getUser()->getPHID(); // Put the viewer first if they've voted for this option. $authors = array_select_keys($authors, array($viewer_phid)) + $authors; $voters = array(); foreach ($authors as $author_phid) { $handle = $handles[$author_phid]; $voters[] = javelin_tag( 'div', array( 'class' => 'slowvote-voter', 'style' => 'background-image: url('.$handle->getImageURI().')', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $handle->getName(), ), )); } return phutil_tag( 'div', array( 'class' => 'slowvote-voters', ), $voters); } private function renderStatus(PhabricatorSlowvoteOption $option) { if (!$this->areResultsVisible()) { return null; } $poll = $this->getPoll(); $choices = mgroup($poll->getChoices(), 'getOptionID'); $choices = count(idx($choices, $option->getID(), array())); $count = count(mgroup($poll->getChoices(), 'getAuthorPHID')); $percent = sprintf('%d%%', $count ? 100 * $choices / $count : 0); switch ($poll->getMethod()) { case PhabricatorSlowvotePoll::METHOD_PLURALITY: $status = pht('%s (%d / %d)', $percent, $choices, $count); break; case PhabricatorSlowvotePoll::METHOD_APPROVAL: $status = pht('%s Approval (%d / %d)', $percent, $choices, $count); break; } return phutil_tag( 'div', array( 'class' => 'slowvote-status', ), $status); } private function areResultsVisible() { $poll = $this->getPoll(); $vis = $poll->getResponseVisibility(); if ($vis == PhabricatorSlowvotePoll::RESPONSES_VISIBLE) { return true; } else if ($vis == PhabricatorSlowvotePoll::RESPONSES_OWNER) { return ($poll->getAuthorPHID() == $this->getUser()->getPHID()); } else { $choices = mgroup($poll->getChoices(), 'getAuthorPHID'); return (bool)idx($choices, $this->getUser()->getPHID()); } } } diff --git a/src/applications/subscriptions/view/SubscriptionListStringBuilder.php b/src/applications/subscriptions/view/SubscriptionListStringBuilder.php index a10ffa5d6d..7e96984d4a 100644 --- a/src/applications/subscriptions/view/SubscriptionListStringBuilder.php +++ b/src/applications/subscriptions/view/SubscriptionListStringBuilder.php @@ -1,84 +1,84 @@ <?php final class SubscriptionListStringBuilder { private $handles; private $objectPHID; public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function getHandles() { return $this->handles; } public function setObjectPHID($object_phid) { $this->objectPHID = $object_phid; return $this; } public function getObjectPHID() { return $this->objectPHID; } public function buildTransactionString($change_type) { $handles = $this->getHandles(); if (!$handles) { return; } $list_uri = '/subscriptions/transaction/'. $change_type.'/'. $this->getObjectPHID().'/'; return $this->buildString($list_uri); } public function buildPropertyString() { $handles = $this->getHandles(); if (!$handles) { return phutil_tag('em', array(), pht('None')); } $list_uri = '/subscriptions/list/'.$this->getObjectPHID().'/'; return $this->buildString($list_uri); } private function buildString($list_uri) { $handles = $this->getHandles(); // Always show this many subscribers. $show_count = 3; $subscribers_count = count($handles); // It looks a bit silly to render "a, b, c, and 1 other", since we could // have just put that other subscriber there in place of the "1 other" // link. Instead, render "a, b, c, d" in this case, and then when we get one // more render "a, b, c, and 2 others". if ($subscribers_count <= ($show_count + 1)) { return phutil_implode_html(', ', mpull($handles, 'renderLink')); } $show = array_slice($handles, 0, $show_count); $show = array_values($show); $not_shown_count = $subscribers_count - $show_count; $not_shown_txt = pht('%d other(s)', $not_shown_count); $not_shown_link = javelin_tag( 'a', array( 'href' => $list_uri, - 'sigil' => 'workflow' + 'sigil' => 'workflow', ), $not_shown_txt); return pht( '%s, %s, %s and %s', $show[0]->renderLink(), $show[1]->renderLink(), $show[2]->renderLink(), $not_shown_link); } } diff --git a/src/applications/tokens/controller/PhabricatorTokenGiveController.php b/src/applications/tokens/controller/PhabricatorTokenGiveController.php index 0bf1b0871c..ab413dc6f6 100644 --- a/src/applications/tokens/controller/PhabricatorTokenGiveController.php +++ b/src/applications/tokens/controller/PhabricatorTokenGiveController.php @@ -1,130 +1,130 @@ <?php final class PhabricatorTokenGiveController extends PhabricatorTokenController { private $phid; public function willProcessRequest(array $data) { $this->phid = $data['phid']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($this->phid)) ->executeOne(); if (!$handle->isComplete()) { return new Aphront404Response(); } $current = id(new PhabricatorTokenGivenQuery()) ->setViewer($user) ->withAuthorPHIDs(array($user->getPHID())) ->withObjectPHIDs(array($handle->getPHID())) ->execute(); if ($current) { $is_give = false; $title = pht('Rescind Token'); } else { $is_give = true; $title = pht('Give Token'); } $done_uri = $handle->getURI(); if ($request->isDialogFormPost()) { $content_source = PhabricatorContentSource::newFromRequest($request); $editor = id(new PhabricatorTokenGivenEditor()) ->setActor($user) ->setContentSource($content_source); if ($is_give) { $token_phid = $request->getStr('tokenPHID'); $editor->addToken($handle->getPHID(), $token_phid); } else { $editor->deleteToken($handle->getPHID()); } return id(new AphrontReloadResponse())->setURI($done_uri); } if ($is_give) { $dialog = $this->buildGiveTokenDialog(); } else { $dialog = $this->buildRescindTokenDialog(head($current)); } $dialog->setUser($user); $dialog->addCancelButton($done_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } private function buildGiveTokenDialog() { $user = $this->getRequest()->getUser(); $tokens = id(new PhabricatorTokenQuery()) ->setViewer($user) ->execute(); $buttons = array(); $ii = 0; foreach ($tokens as $token) { $aural = javelin_tag( 'span', array( 'aural' => true, ), pht('Award "%s" Token', $token->getName())); $buttons[] = javelin_tag( 'button', array( 'class' => 'token-button', 'name' => 'tokenPHID', 'value' => $token->getPHID(), 'type' => 'submit', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $token->getName(), - ) + ), ), array( $aural, $token->renderIcon(), )); if ((++$ii % 4) == 0) { $buttons[] = phutil_tag('br'); } } $buttons = phutil_tag( 'div', array( 'class' => 'token-grid', ), $buttons); $dialog = new AphrontDialogView(); $dialog->setTitle(pht('Give Token')); $dialog->appendChild($buttons); return $dialog; } private function buildRescindTokenDialog(PhabricatorTokenGiven $token_given) { $dialog = new AphrontDialogView(); $dialog->setTitle(pht('Rescind Token')); $dialog->appendChild( pht('Really rescind this lovely token?')); $dialog->addSubmitButton(pht('Rescind Token')); return $dialog; } } diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php index 889bd8ddf4..17edba68b4 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php @@ -1,146 +1,147 @@ <?php final class PhabricatorApplicationTransactionValueController extends PhabricatorApplicationTransactionController { private $value; private $phid; public function willProcessRequest(array $data) { $this->phid = $data['phid']; $this->value = $data['value']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $xaction = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($this->phid)) ->executeOne(); if (!$xaction) { return new Aphront404Response(); } // For now, this pathway only supports policy transactions // to show the details of custom policies. If / when this pathway // supports more transaction types, rendering coding should be moved // into PhabricatorTransactions e.g. feed rendering code. // TODO: This should be some kind of "hey do you support this?" thing on // the transactions themselves. switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY: break; default: return new Aphront404Response(); break; } if ($this->value == 'old') { $value = $xaction->getOldValue(); } else { $value = $xaction->getNewValue(); } $policy = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->withPHIDs(array($value)) ->executeOne(); if (!$policy) { return new Aphront404Response(); } if ($policy->getType() != PhabricatorPolicyType::TYPE_CUSTOM) { return new Aphront404Response(); } $rule_objects = array(); foreach ($policy->getCustomRuleClasses() as $class) { $rule_objects[$class] = newv($class, array()); } $policy->attachRuleObjects($rule_objects); $handle_phids = $this->extractPHIDs($policy, $rule_objects); $handles = $this->loadHandles($handle_phids); $this->requireResource('policy-transaction-detail-css'); $cancel_uri = $this->guessCancelURI($viewer, $xaction); $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle($policy->getFullName()) ->setWidth(AphrontDialogView::WIDTH_FORM) ->appendChild( $this->renderPolicyDetails($policy, $rule_objects)) ->addCancelButton($cancel_uri, pht('Close')); return id(new AphrontDialogResponse())->setDialog($dialog); } private function extractPHIDs( PhabricatorPolicy $policy, array $rule_objects) { $phids = array(); foreach ($policy->getRules() as $rule) { $rule_object = $rule_objects[$rule['rule']]; $phids[] = $rule_object->getRequiredHandlePHIDsForSummary($rule['value']); } return array_filter(array_mergev($phids)); } private function renderPolicyDetails( PhabricatorPolicy $policy, array $rule_objects) { $details = array(); $details[] = phutil_tag( 'p', array( - 'class' => 'policy-transaction-detail-intro' + 'class' => 'policy-transaction-detail-intro', ), pht('These rules are processed in order:')); foreach ($policy->getRules() as $index => $rule) { $rule_object = $rule_objects[$rule['rule']]; if ($rule['action'] == 'allow') { $icon = 'fa-check-circle green'; } else { $icon = 'fa-minus-circle red'; } $icon = id(new PHUIIconView()) ->setIconFont($icon) ->setText( ucfirst($rule['action']).' '.$rule_object->getRuleDescription()); $handle_phids = $rule_object->getRequiredHandlePHIDsForSummary($rule['value']); if ($handle_phids) { $value = $this->renderHandlesForPHIDs($handle_phids, ','); } else { $value = $rule['value']; } $details[] = phutil_tag('div', array( - 'class' => 'policy-transaction-detail-row' + 'class' => 'policy-transaction-detail-row', ), array( $icon, - $value)); + $value, + )); } $details[] = phutil_tag( 'p', array( - 'class' => 'policy-transaction-detail-end' + 'class' => 'policy-transaction-detail-end', ), pht( 'If no rules match, %s all other users.', phutil_tag('b', array(), $policy->getDefaultAction()))); return $details; } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index 1077ab3a4f..77cc6a6371 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -1,235 +1,235 @@ <?php /** * @concrete-extensible */ class PhabricatorApplicationTransactionCommentView extends AphrontView { private $submitButtonName; private $action; private $previewPanelID; private $previewTimelineID; private $previewToggleID; private $formID; private $statusID; private $commentID; private $draft; private $requestURI; private $showPreview = true; private $objectPHID; private $headerText; public function setObjectPHID($object_phid) { $this->objectPHID = $object_phid; return $this; } public function getObjectPHID() { return $this->objectPHID; } public function setShowPreview($show_preview) { $this->showPreview = $show_preview; return $this; } public function getShowPreview() { return $this->showPreview; } public function setRequestURI(PhutilURI $request_uri) { $this->requestURI = $request_uri; return $this; } public function getRequestURI() { return $this->requestURI; } public function setDraft(PhabricatorDraft $draft) { $this->draft = $draft; return $this; } public function getDraft() { return $this->draft; } public function setSubmitButtonName($submit_button_name) { $this->submitButtonName = $submit_button_name; return $this; } public function getSubmitButtonName() { return $this->submitButtonName; } public function setAction($action) { $this->action = $action; return $this; } public function getAction() { return $this->action; } public function setHeaderText($text) { $this->headerText = $text; return $this; } public function render() { $user = $this->getUser(); if (!$user->isLoggedIn()) { $uri = id(new PhutilURI('/login/')) ->setQueryParam('next', (string) $this->getRequestURI()); return id(new PHUIObjectBoxView()) ->setFlush(true) ->setHeaderText(pht('Add Comment')) ->appendChild( javelin_tag( 'a', array( 'class' => 'login-to-comment button', - 'href' => $uri + 'href' => $uri, ), pht('Login to Comment'))); } $data = array(); $comment = $this->renderCommentPanel(); if ($this->getShowPreview()) { $preview = $this->renderPreviewPanel(); } else { $preview = null; } Javelin::initBehavior( 'phabricator-transaction-comment-form', array( 'formID' => $this->getFormID(), 'timelineID' => $this->getPreviewTimelineID(), 'panelID' => $this->getPreviewPanelID(), 'statusID' => $this->getStatusID(), 'commentID' => $this->getCommentID(), 'loadingString' => pht('Loading Preview...'), 'savingString' => pht('Saving Draft...'), 'draftString' => pht('Saved Draft'), 'showPreview' => $this->getShowPreview(), 'actionURI' => $this->getAction(), )); $comment_box = id(new PHUIObjectBoxView()) ->setFlush(true) ->setHeaderText($this->headerText) ->appendChild($comment); return array($comment_box, $preview); } private function renderCommentPanel() { $status = phutil_tag( 'div', array( 'id' => $this->getStatusID(), ), ''); $draft_comment = ''; $draft_key = null; if ($this->getDraft()) { $draft_comment = $this->getDraft()->getDraft(); $draft_key = $this->getDraft()->getDraftKey(); } if (!$this->getObjectPHID()) { throw new Exception('Call setObjectPHID() before render()!'); } return id(new AphrontFormView()) ->setUser($this->getUser()) ->addSigil('transaction-append') ->setWorkflow(true) ->setMetadata( array( 'objectPHID' => $this->getObjectPHID(), )) ->setAction($this->getAction()) ->setID($this->getFormID()) ->addHiddenInput('__draft__', $draft_key) ->appendChild( id(new PhabricatorRemarkupControl()) ->setID($this->getCommentID()) ->setName('comment') ->setLabel(pht('Comment')) ->setUser($this->getUser()) ->setValue($draft_comment)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($this->getSubmitButtonName())) ->appendChild( id(new AphrontFormMarkupControl()) ->setValue($status)); } private function renderPreviewPanel() { $preview = id(new PHUITimelineView()) ->setID($this->getPreviewTimelineID()); return phutil_tag( 'div', array( 'id' => $this->getPreviewPanelID(), 'style' => 'display: none', ), $preview); } private function getPreviewPanelID() { if (!$this->previewPanelID) { $this->previewPanelID = celerity_generate_unique_node_id(); } return $this->previewPanelID; } private function getPreviewTimelineID() { if (!$this->previewTimelineID) { $this->previewTimelineID = celerity_generate_unique_node_id(); } return $this->previewTimelineID; } public function setFormID($id) { $this->formID = $id; return $this; } private function getFormID() { if (!$this->formID) { $this->formID = celerity_generate_unique_node_id(); } return $this->formID; } private function getStatusID() { if (!$this->statusID) { $this->statusID = celerity_generate_unique_node_id(); } return $this->statusID; } private function getCommentID() { if (!$this->commentID) { $this->commentID = celerity_generate_unique_node_id(); } return $this->commentID; } } diff --git a/src/applications/uiexample/examples/JavelinReactorExample.php b/src/applications/uiexample/examples/JavelinReactorExample.php index 95a88b8c0c..e452fe0c75 100644 --- a/src/applications/uiexample/examples/JavelinReactorExample.php +++ b/src/applications/uiexample/examples/JavelinReactorExample.php @@ -1,90 +1,90 @@ <?php final class JavelinReactorExample extends PhabricatorUIExample { public function getName() { return 'Javelin Reactor'; } public function getDescription() { return 'Lots of code'; } public function renderExample() { $rows = array(); $examples = array( array( 'Reactive button only generates a stream of events', 'ReactorButtonExample', 'phabricator-uiexample-reactor-button', array(), ), array( 'Reactive checkbox generates a boolean dynamic value', 'ReactorCheckboxExample', 'phabricator-uiexample-reactor-checkbox', - array('checked' => true) + array('checked' => true), ), array( 'Reactive focus detector generates a boolean dynamic value', 'ReactorFocusExample', 'phabricator-uiexample-reactor-focus', array(), ), array( 'Reactive input box, with normal and calmed output', 'ReactorInputExample', 'phabricator-uiexample-reactor-input', array('init' => 'Initial value'), ), array( 'Reactive mouseover detector generates a boolean dynamic value', 'ReactorMouseoverExample', 'phabricator-uiexample-reactor-mouseover', array(), ), array( 'Reactive radio buttons generate a string dynamic value', 'ReactorRadioExample', 'phabricator-uiexample-reactor-radio', array(), ), array( 'Reactive select box generates a string dynamic value', 'ReactorSelectExample', 'phabricator-uiexample-reactor-select', array(), ), array( 'sendclass makes the class of an element a string dynamic value', 'ReactorSendClassExample', 'phabricator-uiexample-reactor-sendclass', - array() + array(), ), array( 'sendproperties makes some properties of an object into dynamic values', 'ReactorSendPropertiesExample', 'phabricator-uiexample-reactor-sendproperties', array(), ), ); foreach ($examples as $example) { list($desc, $name, $resource, $params) = $example; $template = new AphrontJavelinView(); $template ->setName($name) ->setParameters($params) ->setCelerityResource($resource); $rows[] = array($desc, $template->render()); } $table = new AphrontTableView($rows); $panel = new AphrontPanelView(); $panel->appendChild($table); return $panel; } } diff --git a/src/applications/uiexample/examples/PHUIActionHeaderExample.php b/src/applications/uiexample/examples/PHUIActionHeaderExample.php index b72bff1cd1..5962871f63 100644 --- a/src/applications/uiexample/examples/PHUIActionHeaderExample.php +++ b/src/applications/uiexample/examples/PHUIActionHeaderExample.php @@ -1,270 +1,270 @@ <?php final class PHUIActionHeaderExample extends PhabricatorUIExample { public function getName() { return 'Action Headers'; } public function getDescription() { return 'Various header layouts with and without icons'; } public function renderExample() { /* Colors */ $title1 = id(new PHUIHeaderView()) ->setHeader(pht('Header Plain')); $header1 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Colorless'); $header2 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Light Grey') ->setHeaderColor(PHUIActionHeaderView::HEADER_GREY); $header3 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Light Blue') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE); $header4 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Light Green') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTGREEN); $header5 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Light Red') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTRED); $header6 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Light Violet') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTVIOLET); $layout1 = id(new AphrontMultiColumnView()) ->addColumn($header1) ->addColumn($header2) ->addColumn($header3) ->addColumn($header4) ->addColumn($header5) ->addColumn($header6) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_SMALL); $wrap1 = id(new PHUIBoxView()) ->appendChild($layout1) ->addMargin(PHUI::MARGIN_LARGE); /* Policy Icons */ $title2 = id(new PHUIHeaderView()) ->setHeader(pht('With Icons')); $header1 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Quack') ->setHeaderIcon( id(new PHUIIconView()) ->setIconFont('fa-coffee')); $header2 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Moo') ->setHeaderColor(PHUIActionHeaderView::HEADER_GREY) ->setHeaderIcon( id(new PHUIIconView()) ->setIconFont('fa-magic')); $header3 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Woof') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE) ->setHeaderIcon( id(new PHUIIconView()) ->setIconFont('fa-fighter-jet')); $header4 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Buzz') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTGREEN) ->setHeaderIcon( id(new PHUIIconView()) ->setIconFont('fa-child')); $header5 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Fizz') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTRED) ->setHeaderIcon( id(new PHUIIconView()) ->setIconFont('fa-car')); $header6 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Blarp') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTVIOLET) ->setHeaderIcon( id(new PHUIIconView()) ->setIconFont('fa-truck')); $layout2 = id(new AphrontMultiColumnView()) ->addColumn($header1) ->addColumn($header2) ->addColumn($header3) ->addColumn($header4) ->addColumn($header5) ->addColumn($header6) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_SMALL); $wrap2 = id(new PHUIBoxView()) ->appendChild($layout2) ->addMargin(PHUI::MARGIN_LARGE); /* Action Icons */ $title3 = id(new PHUIHeaderView()) ->setHeader(pht('With Action Icons')); $action1 = new PHUIIconView(); $action1->setIconFont('fa-cog'); $action1->setHref('#'); $action2 = new PHUIIconView(); $action2->setIconFont('fa-heart'); $action2->setHref('#'); $action3 = new PHUIIconView(); $action3->setIconFont('fa-tag'); $action3->setHref('#'); $action4 = new PHUIIconView(); $action4->setIconFont('fa-plus'); $action4->setHref('#'); $action5 = new PHUIIconView(); $action5->setIconFont('fa-search'); $action5->setHref('#'); $action6 = new PHUIIconView(); $action6->setIconFont('fa-arrows'); $action6->setHref('#'); $header1 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Company') ->setHeaderHref('http://example.com/') ->addAction($action1); $header2 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Public') ->setHeaderHref('http://example.com/') ->setHeaderColor(PHUIActionHeaderView::HEADER_GREY) ->addAction($action1); $header3 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Restricted') ->setHeaderHref('http://example.com/') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE) ->addAction($action2); $header4 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Company') ->setHeaderHref('http://example.com/') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTGREEN) ->addAction($action3); $header5 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Public') ->setHeaderHref('http://example.com/') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTRED) ->addAction($action4) ->addAction($action5); $header6 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Restricted') ->setHeaderHref('http://example.com/') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTVIOLET) ->addAction($action6); $layout3 = id(new AphrontMultiColumnView()) ->addColumn($header1) ->addColumn($header2) ->addColumn($header3) ->addColumn($header4) ->addColumn($header5) ->addColumn($header6) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_SMALL); $wrap3 = id(new PHUIBoxView()) ->appendChild($layout3) ->addMargin(PHUI::MARGIN_LARGE); /* Action Icons */ $title4 = id(new PHUIHeaderView()) ->setHeader(pht('With Tags')); $tag1 = id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE) ->setBackgroundColor(PHUITagView::COLOR_RED) ->setName('Open'); $tag2 = id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE) ->setBackgroundColor(PHUITagView::COLOR_BLUE) ->setName('Closed'); $action1 = new PHUIIconView(); $action1->setIconFont('fa-flag'); $action1->setHref('#'); $header1 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Company') ->setTag($tag2); $header2 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Public') ->setHeaderColor(PHUIActionHeaderView::HEADER_GREY) ->addAction($action1) ->setTag($tag1); $header3 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Restricted') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE) ->setTag($tag2); $header4 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Company') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTGREEN) ->setTag($tag1); $header5 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Public') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTRED) ->setTag($tag2); $header6 = id(new PHUIActionHeaderView()) ->setHeaderTitle('Restricted') ->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTVIOLET) ->setTag($tag1); $layout4 = id(new AphrontMultiColumnView()) ->addColumn($header1) ->addColumn($header2) ->addColumn($header3) ->addColumn($header4) ->addColumn($header5) ->addColumn($header6) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_SMALL); $wrap4 = id(new PHUIBoxView()) ->appendChild($layout4) ->addMargin(PHUI::MARGIN_LARGE); return phutil_tag( 'div', array(), array( $title1, $wrap1, $title2, $wrap2, $title3, $wrap3, $title4, - $wrap4 + $wrap4, )); } } diff --git a/src/applications/uiexample/examples/PHUIBoxExample.php b/src/applications/uiexample/examples/PHUIBoxExample.php index 8e03452de1..a8999f5cf9 100644 --- a/src/applications/uiexample/examples/PHUIBoxExample.php +++ b/src/applications/uiexample/examples/PHUIBoxExample.php @@ -1,118 +1,121 @@ <?php final class PHUIBoxExample extends PhabricatorUIExample { public function getName() { return 'Box'; } public function getDescription() { return 'It\'s a fancy or non-fancy box. Put stuff in it.'; } public function renderExample() { $content1 = 'Asmund and Signy'; $content2 = 'The Cottager and his Cat'; $content3 = 'Geirlug The King\'s Daughter'; $layout1 = array( id(new PHUIBoxView()) ->appendChild($content1), id(new PHUIBoxView()) ->appendChild($content2), id(new PHUIBoxView()) - ->appendChild($content3)); + ->appendChild($content3), + ); $layout2 = array( id(new PHUIBoxView()) ->appendChild($content1) ->addMargin(PHUI::MARGIN_SMALL_LEFT), id(new PHUIBoxView()) ->appendChild($content2) ->addMargin(PHUI::MARGIN_MEDIUM_LEFT) ->addMargin(PHUI::MARGIN_MEDIUM_TOP), id(new PHUIBoxView()) ->appendChild($content3) ->addMargin(PHUI::MARGIN_LARGE_LEFT) - ->addMargin(PHUI::MARGIN_LARGE_TOP)); + ->addMargin(PHUI::MARGIN_LARGE_TOP), + ); $layout3 = array( id(new PHUIBoxView()) ->appendChild($content1) ->setBorder(true) ->addPadding(PHUI::PADDING_SMALL) ->addMargin(PHUI::MARGIN_LARGE_BOTTOM), id(new PHUIBoxView()) ->appendChild($content2) ->setBorder(true) ->addPadding(PHUI::PADDING_MEDIUM) ->addMargin(PHUI::MARGIN_LARGE_BOTTOM), id(new PHUIBoxView()) ->appendChild($content3) ->setBorder(true) ->addPadding(PHUI::PADDING_LARGE) - ->addMargin(PHUI::MARGIN_LARGE_BOTTOM)); + ->addMargin(PHUI::MARGIN_LARGE_BOTTOM), + ); $image = id(new PHUIIconView()) ->setIconFont('fa-heart'); $button = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::SIMPLE) ->setIcon($image) ->setText('Such Wow') ->addClass(PHUI::MARGIN_SMALL_RIGHT); $header = id(new PHUIHeaderView()) ->setHeader('Fancy Box') ->addActionLink($button); $obj4 = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild(id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_MEDIUM) ->appendChild('Such Fancy, Nice Box, Many Corners.')); $head1 = id(new PHUIHeaderView()) ->setHeader(pht('Plain Box')); $head2 = id(new PHUIHeaderView()) ->setHeader(pht('Plain Box with space')); $head3 = id(new PHUIHeaderView()) ->setHeader(pht('Border Box with space')); $head4 = id(new PHUIHeaderView()) ->setHeader(pht('PHUIObjectBoxView')); $wrap1 = id(new PHUIBoxView()) ->appendChild($layout1) ->addMargin(PHUI::MARGIN_LARGE); $wrap2 = id(new PHUIBoxView()) ->appendChild($layout2) ->addMargin(PHUI::MARGIN_LARGE); $wrap3 = id(new PHUIBoxView()) ->appendChild($layout3) ->addMargin(PHUI::MARGIN_LARGE); return phutil_tag( 'div', array(), array( $head1, $wrap1, $head2, $wrap2, $head3, $wrap3, $head4, $obj4, )); } } diff --git a/src/applications/uiexample/examples/PHUIButtonBarExample.php b/src/applications/uiexample/examples/PHUIButtonBarExample.php index c8fad2c968..613eb7f160 100644 --- a/src/applications/uiexample/examples/PHUIButtonBarExample.php +++ b/src/applications/uiexample/examples/PHUIButtonBarExample.php @@ -1,46 +1,47 @@ <?php final class PHUIButtonBarExample extends PhabricatorUIExample { public function getName() { return pht('Button Bar'); } public function getDescription() { return pht('A minimal UI for Buttons'); } public function renderExample() { $request = $this->getRequest(); $user = $request->getUser(); // Icon Buttons $icons = array( 'Go Back' => 'fa-chevron-left bluegrey', 'Choose Date' => 'fa-calendar bluegrey', 'Edit View' => 'fa-pencil bluegrey', - 'Go Forward' => 'fa-chevron-right bluegrey'); + 'Go Forward' => 'fa-chevron-right bluegrey', + ); $button_bar = new PHUIButtonBarView(); foreach ($icons as $text => $icon) { $image = id(new PHUIIconView()) ->setIconFont($icon); $button = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setTitle($text) ->setIcon($image); $button_bar->addButton($button); } $layout = id(new PHUIBoxView()) ->appendChild($button_bar) ->addPadding(PHUI::PADDING_LARGE); $wrap1 = id(new PHUIObjectBoxView()) ->setHeaderText('Button Bar Example') ->appendChild($layout); return array($wrap1); } } diff --git a/src/applications/uiexample/examples/PHUIButtonExample.php b/src/applications/uiexample/examples/PHUIButtonExample.php index ee7a752183..ddb3d7aed8 100644 --- a/src/applications/uiexample/examples/PHUIButtonExample.php +++ b/src/applications/uiexample/examples/PHUIButtonExample.php @@ -1,200 +1,204 @@ <?php final class PHUIButtonExample extends PhabricatorUIExample { public function getName() { return 'Buttons'; } public function getDescription() { return hsprintf('Use <tt><button></tt> to render buttons.'); } public function renderExample() { $request = $this->getRequest(); $user = $request->getUser(); $colors = array('', 'green', 'grey', 'black', 'disabled'); $sizes = array('', 'small'); $tags = array('a', 'button'); // phutil_tag $column = array(); foreach ($tags as $tag) { foreach ($colors as $color) { foreach ($sizes as $key => $size) { $class = implode(' ', array($color, $size)); if ($tag == 'a') { $class .= ' button'; } $column[$key][] = phutil_tag( $tag, array( 'class' => $class, ), phutil_utf8_ucwords($size.' '.$color.' '.$tag)); $column[$key][] = hsprintf('<br /><br />'); } } } $column3 = array(); foreach ($colors as $color) { $caret = phutil_tag('span', array('class' => 'caret'), ''); $column3[] = phutil_tag( 'a', array( - 'class' => $color.' button dropdown' + 'class' => $color.' button dropdown', ), array( phutil_utf8_ucwords($color.' Dropdown'), $caret, )); $column3[] = hsprintf('<br /><br />'); } $layout1 = id(new AphrontMultiColumnView()) ->addColumn($column[0]) ->addColumn($column[1]) ->addColumn($column3) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM); // PHUIButtonView $colors = array(null, - PHUIButtonView::GREEN, - PHUIButtonView::GREY, - PHUIButtonView::BLACK, - PHUIButtonView::DISABLED); + PHUIButtonView::GREEN, + PHUIButtonView::GREY, + PHUIButtonView::BLACK, + PHUIButtonView::DISABLED, + ); $sizes = array(null, PHUIButtonView::SMALL); $column = array(); foreach ($colors as $color) { foreach ($sizes as $key => $size) { $column[$key][] = id(new PHUIButtonView()) ->setColor($color) ->setSize($size) ->setTag('a') ->setText('Clicky'); $column[$key][] = hsprintf('<br /><br />'); } } foreach ($colors as $color) { $column[2][] = id(new PHUIButtonView()) ->setColor($color) ->setTag('button') ->setText('Button') ->setDropdown(true); $column[2][] = hsprintf('<br /><br />'); } $layout2 = id(new AphrontMultiColumnView()) ->addColumn($column[0]) ->addColumn($column[1]) ->addColumn($column[2]) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM); // Icon Buttons $column = array(); $icons = array( 'Comment' => 'fa-comment', 'Give Token' => 'fa-trophy', 'Reverse Time' => 'fa-clock-o', - 'Implode Earth' => 'fa-exclamation-triangle red'); + 'Implode Earth' => 'fa-exclamation-triangle red', + ); foreach ($icons as $text => $icon) { $image = id(new PHUIIconView()) ->setIconFont($icon); $column[] = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setIcon($image) ->setText($text) ->addClass(PHUI::MARGIN_SMALL_RIGHT); } $column2 = array(); $icons = array( 'Subscribe' => 'fa-check-circle bluegrey', - 'Edit' => 'fa-pencil bluegrey'); + 'Edit' => 'fa-pencil bluegrey', + ); foreach ($icons as $text => $icon) { $image = id(new PHUIIconView()) ->setIconFont($icon); $column2[] = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::SIMPLE) ->setIcon($image) ->setText($text) ->addClass(PHUI::MARGIN_SMALL_RIGHT); } $layout3 = id(new AphrontMultiColumnView()) ->addColumn($column) ->addColumn($column2) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM); // Baby Got Back Buttons $column = array(); $icons = array('Asana', 'Github', 'Facebook', 'Google', 'LDAP'); foreach ($icons as $icon) { $image = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) ->setSpriteIcon($icon); $column[] = id(new PHUIButtonView()) ->setTag('a') ->setSize(PHUIButtonView::BIG) ->setColor(PHUIButtonView::GREY) ->setIcon($image) ->setText('Login or Register') ->setSubtext($icon) ->addClass(PHUI::MARGIN_MEDIUM_RIGHT); } $layout4 = id(new AphrontMultiColumnView()) ->addColumn($column) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM); // Set it and forget it $head1 = id(new PHUIHeaderView()) ->setHeader('phutil_tag'); $head2 = id(new PHUIHeaderView()) ->setHeader('PHUIButtonView'); $head3 = id(new PHUIHeaderView()) ->setHeader('Icon Buttons'); $head4 = id(new PHUIHeaderView()) ->setHeader('Big Icon Buttons'); $wrap1 = id(new PHUIBoxView()) ->appendChild($layout1) ->addMargin(PHUI::MARGIN_LARGE); $wrap2 = id(new PHUIBoxView()) ->appendChild($layout2) ->addMargin(PHUI::MARGIN_LARGE); $wrap3 = id(new PHUIBoxView()) ->appendChild($layout3) ->addMargin(PHUI::MARGIN_LARGE); $wrap4 = id(new PHUIBoxView()) ->appendChild($layout4) ->addMargin(PHUI::MARGIN_LARGE); return array($head1, $wrap1, $head2, $wrap2, $head3, $wrap3, - $head4, $wrap4); + $head4, $wrap4, + ); } } diff --git a/src/applications/uiexample/examples/PHUIColorPalletteExample.php b/src/applications/uiexample/examples/PHUIColorPalletteExample.php index a339f76e3b..c13e230959 100644 --- a/src/applications/uiexample/examples/PHUIColorPalletteExample.php +++ b/src/applications/uiexample/examples/PHUIColorPalletteExample.php @@ -1,119 +1,123 @@ <?php final class PHUIColorPalletteExample extends PhabricatorUIExample { public function getName() { return 'Colors'; } public function getDescription() { return 'A Standard Palette of Colors for use.'; } public function renderExample() { $colors = array( 'c0392b' => 'Base Red {$red}', 'f4dddb' => '83% Red {$lightred}', 'e67e22' => 'Base Orange {$orange}', 'f7e2d4' => '83% Orange {$lightorange}', 'f1c40f' => 'Base Yellow {$yellow}', 'fdf5d4' => '83% Yellow {$lightyellow}', '139543' => 'Base Green {$green}', 'd7eddf' => '83% Green {$lightgreen}', '2980b9' => 'Base Blue {$blue}', 'daeaf3' => '83% Blue {$lightblue}', '3498db' => 'Sky Base {$sky}', 'ddeef9' => '83% Sky {$lightsky}', 'c6539d' => 'Base Indigo {$indigo}', 'f5e2ef' => '83% Indigo {$lightindigo}', '8e44ad' => 'Base Violet {$violet}', - 'ecdff1' => '83% Violet {$lightviolet}' + 'ecdff1' => '83% Violet {$lightviolet}', ); $greys = array( 'C7CCD9' => 'Light Grey Border {$lightgreyborder}', 'A1A6B0' => 'Grey Border {$greyborder}', '676A70' => 'Dark Grey Border {$darkgreyborder}', '92969D' => 'Light Grey Text {$lightgreytext}', '74777D' => 'Grey Text {$greytext}', '4B4D51' => 'Dark Grey Text {$darkgreytext}', 'F7F7F7' => 'Light Grey Background {$lightgreybackground}', 'EBECEE' => 'Grey Background {$greybackground}', 'DFE0E2' => 'Dark Grey Background {$darkgreybackground}', ); $blues = array( 'DDE8EF' => 'Thin Blue Border {$thinblueborder}', 'BFCFDA' => 'Light Blue Border {$lightblueborder}', '95A6C5' => 'Blue Border {$blueborder}', '626E82' => 'Dark Blue Border {$darkblueborder}', 'F8F9FC' => 'Light Blue Background {$lightbluebackground}', 'DAE7FF' => 'Blue Background {$bluebackground}', '8C98B8' => 'Light Blue Text {$lightbluetext}', '6B748C' => 'Blue Text {$bluetext}', '464C5C' => 'Dark Blue Text {$darkbluetext}', ); $d_column = array(); foreach ($greys as $color => $name) { $d_column[] = phutil_tag( 'div', array( 'style' => 'background-color: #'.$color.';', - 'class' => 'pl'), + 'class' => 'pl', + ), $name.' #'.$color); } $b_column = array(); foreach ($blues as $color => $name) { $b_column[] = phutil_tag( 'div', array( 'style' => 'background-color: #'.$color.';', - 'class' => 'pl'), + 'class' => 'pl', + ), $name.' #'.$color); } $c_column = array(); $url = array(); foreach ($colors as $color => $name) { $url[] = $color; $c_column[] = phutil_tag( 'div', array( 'style' => 'background-color: #'.$color.';', - 'class' => 'pl'), + 'class' => 'pl', + ), $name.' #'.$color); } $color_url = phutil_tag( 'a', array( 'href' => 'http://color.hailpixel.com/#'.implode(',', $url), - 'class' => 'button grey mlb'), + 'class' => 'button grey mlb', + ), 'Color Palette'); $wrap1 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Greys')) ->appendChild($d_column); $wrap2 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Blues')) ->appendChild($b_column); $wrap3 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Colors')) ->appendChild($c_column); return phutil_tag( 'div', array(), array( $wrap1, $wrap2, - $wrap3 + $wrap3, )); - } + } } diff --git a/src/applications/uiexample/examples/PHUIDocumentExample.php b/src/applications/uiexample/examples/PHUIDocumentExample.php index 89b05a800f..83ae5e9dbf 100644 --- a/src/applications/uiexample/examples/PHUIDocumentExample.php +++ b/src/applications/uiexample/examples/PHUIDocumentExample.php @@ -1,198 +1,198 @@ <?php final class PHUIDocumentExample extends PhabricatorUIExample { public function getName() { return pht('Document View'); } public function getDescription() { return pht('Useful for areas of large content navigation'); } public function renderExample() { $request = $this->getRequest(); $user = $request->getUser(); $action = id(new PHUIListItemView()) ->setName('Actions') ->setType(PHUIListItemView::TYPE_LABEL); $action1 = id(new PHUIListItemView()) ->setName('Edit Document') ->setHref('#') ->setIcon('fa-edit') ->setType(PHUIListItemView::TYPE_LINK); $action2 = id(new PHUIListItemView()) ->setName('Move Document') ->setHref('#') ->setIcon('fa-arrows') ->setType(PHUIListItemView::TYPE_LINK); $action3 = id(new PHUIListItemView()) ->setName('Delete Document') ->setHref('#') ->setIcon('fa-times') ->setType(PHUIListItemView::TYPE_LINK); $action4 = id(new PHUIListItemView()) ->setName('View History') ->setHref('#') ->setIcon('fa-list') ->setType(PHUIListItemView::TYPE_LINK); $action5 = id(new PHUIListItemView()) ->setName('Subscribe') ->setHref('#') ->setIcon('fa-plus-circle') ->setType(PHUIListItemView::TYPE_LINK); - $divider = id(new PHUIListItemView) + $divider = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_DIVIDER); $header = id(new PHUIHeaderView()) ->setHeader('Installation'); $label1 = id(new PHUIListItemView()) ->setName('Getting Started') ->setType(PHUIListItemView::TYPE_LABEL); $label2 = id(new PHUIListItemView()) ->setName('Documentation') ->setType(PHUIListItemView::TYPE_LABEL); $item1 = id(new PHUIListItemView()) ->setName('Installation') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $item2 = id(new PHUIListItemView()) ->setName('Webserver Config') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $item3 = id(new PHUIListItemView()) ->setName('Adding Users') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $item4 = id(new PHUIListItemView()) ->setName('Debugging') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $sidenav = id(new PHUIListView()) ->setType(PHUIListView::SIDENAV_LIST) ->addMenuItem($action) ->addMenuItem($action1) ->addMenuItem($action2) ->addMenuItem($action3) ->addMenuItem($action4) ->addMenuItem($action5) ->addMenuItem($divider) ->addMenuItem($label1) ->addMenuItem($item1) ->addMenuItem($item2) ->addMenuItem($item3) ->addMenuItem($item4) ->addMenuItem($label2) ->addMenuItem($item2) ->addMenuItem($item3) ->addMenuItem($item4) ->addMenuItem($item1); $home = id(new PHUIListItemView()) ->setIcon('home') ->setHref('#') ->setType(PHUIListItemView::TYPE_ICON); $item1 = id(new PHUIListItemView()) ->setName('Installation') ->setHref('#') ->setSelected(true) ->setType(PHUIListItemView::TYPE_LINK); $item2 = id(new PHUIListItemView()) ->setName('Webserver Config') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $item3 = id(new PHUIListItemView()) ->setName('Adding Users') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $item4 = id(new PHUIListItemView()) ->setName('Debugging') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $topnav = id(new PHUIListView()) ->setType(PHUIListView::NAVBAR_LIST) ->addMenuItem($home) ->addMenuItem($item1) ->addMenuItem($item2) ->addMenuItem($item3) ->addMenuItem($item4); $document = hsprintf( '<p class="pl">Lorem ipsum dolor sit amet, consectetur adipisicing, '. 'sed do eiusmod tempor incididunt ut labore et dolore magna '. 'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '. 'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '. 'aute irure dolor in reprehenderit in voluptate velit esse cillum '. 'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '. 'cupidatat non proident, sunt in culpa qui officia deserunt '. 'mollit anim id est laborum.</p>'. '<p class="plr pll plb">Lorem ipsum dolor sit amet, consectetur, '. 'sed do eiusmod tempor incididunt ut labore et dolore magna '. 'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '. 'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '. 'aute irure dolor in reprehenderit in voluptate velit esse cillum '. 'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '. 'cupidatat non proident, sunt in culpa qui officia deserunt '. 'mollit anim id est laborum.</p>'. '<p class="plr pll plb">Lorem ipsum dolor sit amet, consectetur, '. 'sed do eiusmod tempor incididunt ut labore et dolore magna '. 'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '. 'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '. 'aute irure dolor in reprehenderit in voluptate velit esse cillum '. 'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '. 'cupidatat non proident, sunt in culpa qui officia deserunt '. 'mollit anim id est laborum.</p>'. '<p class="plr pll plb">Lorem ipsum dolor sit amet, consectetur, '. 'sed do eiusmod tempor incididunt ut labore et dolore magna '. 'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '. 'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '. 'aute irure dolor in reprehenderit in voluptate velit esse cillum '. 'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '. 'cupidatat non proident, sunt in culpa qui officia deserunt '. 'mollit anim id est laborum.</p>'. '<p class="plr pll plb">Lorem ipsum dolor sit amet, consectetur, '. 'sed do eiusmod tempor incididunt ut labore et dolore magna '. 'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '. 'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '. 'aute irure dolor in reprehenderit in voluptate velit esse cillum '. 'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '. 'cupidatat non proident, sunt in culpa qui officia deserunt '. 'mollit anim id est laborum.</p>'. '<p class="plr pll plb">Lorem ipsum dolor sit amet, consectetur, '. 'sed do eiusmod tempor incididunt ut labore et dolore magna '. 'aliqua. Ut enim ad minim veniam, quis nostrud exercitation '. 'ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis '. 'aute irure dolor in reprehenderit in voluptate velit esse cillum '. 'dolore eu fugiat nulla pariatur. Excepteur sint occaecat '. 'cupidatat non proident, sunt in culpa qui officia deserunt '. 'mollit anim id est laborum.</p>'); $content = new PHUIDocumentView(); $content->setBook('Book or Project Name', 'Article'); $content->setHeader($header); $content->setTopNav($topnav); $content->setSidenav($sidenav); $content->appendChild($document); return $content; } } diff --git a/src/applications/uiexample/examples/PHUIFeedStoryExample.php b/src/applications/uiexample/examples/PHUIFeedStoryExample.php index 5176f4e30a..5dcfe53564 100644 --- a/src/applications/uiexample/examples/PHUIFeedStoryExample.php +++ b/src/applications/uiexample/examples/PHUIFeedStoryExample.php @@ -1,210 +1,217 @@ <?php final class PHUIFeedStoryExample extends PhabricatorUIExample { public function getName() { return 'Feed Story'; } public function getDescription() { return 'An outlandish exaggeration of intricate tales from '. 'around the realm'; } public function renderExample() { $request = $this->getRequest(); $user = $request->getUser(); /* Basic Story */ $text = hsprintf( '<strong><a>harding (Tom Harding)</a></strong> closed <a>'. 'D12: New spacer classes for blog views</a>.'); $story1 = id(new PHUIFeedStoryView()) ->setTitle($text) ->setImage(celerity_get_resource_uri('/rsrc/image/people/harding.png')) ->setImageHref('http://en.wikipedia.org/wiki/Warren_G._Harding') ->setEpoch(1) ->setAppIcon('differential-dark') ->setUser($user); /* Text Story, useful in Blogs, Ponders, Status */ $tokens = array( 'like-1', 'like-2', 'heart-1', - 'heart-2'); + 'heart-2', + ); $tokenview = array(); foreach ($tokens as $token) { $tokenview[] = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_TOKENS) ->setSpriteIcon($token); } $text = hsprintf('<strong><a>lincoln (Honest Abe)</a></strong> wrote a '. 'new blog post.'); $story2 = id(new PHUIFeedStoryView()) ->setTitle($text) ->setImage(celerity_get_resource_uri('/rsrc/image/people/lincoln.png')) ->setImageHref('http://en.wikipedia.org/wiki/Abraham_Lincoln') ->setEpoch(strtotime('November 19, 1863')) ->setAppIcon('phame-dark') ->setUser($user) ->setTokenBar($tokenview) ->setPontification('Four score and seven years ago our fathers brought '. 'forth on this continent, a new nation, conceived in Liberty, and '. 'dedicated to the proposition that all men are created equal. '. 'Now we are engaged in a great civil war, testing whether that '. 'nation, or any nation so conceived and so dedicated, can long '. 'endure. We are met on a great battle-field of that war. We have '. 'come to dedicate a portion of that field, as a final resting '. 'place for those who here gave their lives that that nation might '. 'live. It is altogether fitting and proper that we should do this.', 'Gettysburg Address'); /* Action Story, let's give people tokens! */ $text = hsprintf('<strong><a>harding (Tom Harding)</a></strong> awarded '. '<a>M10: Workboards</a> a token.'); $action1 = id(new PHUIIconView()) ->setIconFont('fa-trophy bluegrey') ->setHref('#'); $token = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_TOKENS) ->setSpriteIcon('like-1'); $story3 = id(new PHUIFeedStoryView()) ->setTitle($text) ->setImage(celerity_get_resource_uri('/rsrc/image/people/harding.png')) ->setImageHref('http://en.wikipedia.org/wiki/Warren_G._Harding') ->appendChild($token) ->setEpoch(1) ->addAction($action1) ->setAppIcon('token-dark') ->setUser($user); /* Image Story, used in Pholio, Macro */ $text = hsprintf('<strong><a>wgharding (Warren Harding)</a></strong> '. 'asked a new question.'); $action1 = id(new PHUIIconView()) ->setIconFont('fa-chevron-up bluegrey') ->setHref('#'); $action2 = id(new PHUIIconView()) ->setIconFont('fa-chevron-down bluegrey') ->setHref('#'); $story4 = id(new PHUIFeedStoryView()) ->setTitle($text) ->setImage(celerity_get_resource_uri('/rsrc/image/people/harding.png')) ->setImageHref('http://en.wikipedia.org/wiki/Warren_G._Harding') ->setEpoch(1) ->setAppIcon('ponder-dark') ->setPontification('Why does inline-block add space under my spans and '. 'anchors?') ->addAction($action1) ->addAction($action2) ->setUser($user); /* Text Story, useful in Blogs, Ponders, Status */ $text = hsprintf('<strong><a>lincoln (Honest Abe)</a></strong> updated '. 'his status.'); $story5 = id(new PHUIFeedStoryView()) ->setTitle($text) ->setImage(celerity_get_resource_uri('/rsrc/image/people/lincoln.png')) ->setImageHref('http://en.wikipedia.org/wiki/Abraham_Lincoln') ->setEpoch(strtotime('November 19, 1863')) ->setAppIcon('phame-dark') ->setUser($user) ->setPontification('If we ever create a lightweight status app '. 'this story would be how that would be displayed.'); /* Basic "One Line" Story */ $text = hsprintf( '<strong><a>harding (Tom Harding)</a></strong> updated <a>'. 'D12: New spacer classes for blog views</a>.'); $story6 = id(new PHUIFeedStoryView()) ->setTitle($text) ->setImage(celerity_get_resource_uri('/rsrc/image/people/harding.png')) ->setImageHref('http://en.wikipedia.org/wiki/Warren_G._Harding') ->setEpoch(1) ->setAppIcon('differential-dark') ->setUser($user); $head1 = id(new PHUIHeaderView()) ->setHeader(pht('Basic Story')); $head2 = id(new PHUIHeaderView()) ->setHeader(pht('Title / Text Story')); $head3 = id(new PHUIHeaderView()) ->setHeader(pht('Token Story')); $head4 = id(new PHUIHeaderView()) ->setHeader(pht('Action Story')); $head5 = id(new PHUIHeaderView()) ->setHeader(pht('Status Story')); $head6 = id(new PHUIHeaderView()) ->setHeader(pht('One Line Story')); $wrap1 = array( id(new PHUIBoxView()) ->appendChild($story1) ->addMargin(PHUI::MARGIN_MEDIUM) - ->addPadding(PHUI::PADDING_SMALL)); + ->addPadding(PHUI::PADDING_SMALL), + ); $wrap2 = array( id(new PHUIBoxView()) ->appendChild($story2) ->addMargin(PHUI::MARGIN_MEDIUM) - ->addPadding(PHUI::PADDING_SMALL)); + ->addPadding(PHUI::PADDING_SMALL), + ); $wrap3 = array( id(new PHUIBoxView()) ->appendChild($story3) ->addMargin(PHUI::MARGIN_MEDIUM) - ->addPadding(PHUI::PADDING_SMALL)); + ->addPadding(PHUI::PADDING_SMALL), + ); $wrap4 = array( id(new PHUIBoxView()) ->appendChild($story4) ->addMargin(PHUI::MARGIN_MEDIUM) - ->addPadding(PHUI::PADDING_SMALL)); + ->addPadding(PHUI::PADDING_SMALL), + ); $wrap5 = array( id(new PHUIBoxView()) ->appendChild($story5) ->addMargin(PHUI::MARGIN_MEDIUM) - ->addPadding(PHUI::PADDING_SMALL)); + ->addPadding(PHUI::PADDING_SMALL), + ); $wrap6 = array( id(new PHUIBoxView()) ->appendChild($story6) ->addMargin(PHUI::MARGIN_MEDIUM) - ->addPadding(PHUI::PADDING_SMALL)); + ->addPadding(PHUI::PADDING_SMALL), + ); return phutil_tag( 'div', array(), array( $head1, $wrap1, $head2, $wrap2, $head3, $wrap3, $head4, $wrap4, $head5, $wrap5, $head6, - $wrap6 + $wrap6, )); } } diff --git a/src/applications/uiexample/examples/PHUIIconExample.php b/src/applications/uiexample/examples/PHUIIconExample.php index 3209698f66..e160d7dcb7 100644 --- a/src/applications/uiexample/examples/PHUIIconExample.php +++ b/src/applications/uiexample/examples/PHUIIconExample.php @@ -1,209 +1,211 @@ <?php final class PHUIIconExample extends PhabricatorUIExample { public function getName() { return 'Icons and Images'; } public function getDescription() { return 'Easily render icons or images with links and sprites.'; } private function listTransforms() { return array( 'ph-rotate-90', 'ph-rotate-180', 'ph-rotate-270', 'ph-flip-horizontal', 'ph-flip-vertical', 'ph-spin', ); } public function renderExample() { $colors = PHUIIconView::getFontIconColors(); $colors = array_merge(array(null), $colors); $fas = PHUIIconView::getFontIcons(); $trans = $this->listTransforms(); $cicons = array(); foreach ($colors as $color) { $cicons[] = id(new PHUIIconView()) ->addClass('phui-example-icon-transform') ->setIconFont('fa-tag '.$color) ->setText(pht('fa-tag %s', $color)); } $ficons = array(); sort($fas); foreach ($fas as $fa) { $ficons[] = id(new PHUIIconView()) ->addClass('phui-example-icon-name') ->setIconFont($fa) ->setText($fa); } $person1 = new PHUIIconView(); $person1->setHeadSize(PHUIIconView::HEAD_MEDIUM); $person1->setHref('http://en.wikipedia.org/wiki/George_Washington'); $person1->setImage( celerity_get_resource_uri('/rsrc/image/people/washington.png')); $person2 = new PHUIIconView(); $person2->setHeadSize(PHUIIconView::HEAD_MEDIUM); $person2->setHref('http://en.wikipedia.org/wiki/Warren_G._Harding'); $person2->setImage( celerity_get_resource_uri('/rsrc/image/people/harding.png')); $person3 = new PHUIIconView(); $person3->setHeadSize(PHUIIconView::HEAD_MEDIUM); $person3->setHref('http://en.wikipedia.org/wiki/William_Howard_Taft'); $person3->setImage( celerity_get_resource_uri('/rsrc/image/people/taft.png')); $person4 = new PHUIIconView(); $person4->setHeadSize(PHUIIconView::HEAD_SMALL); $person4->setHref('http://en.wikipedia.org/wiki/George_Washington'); $person4->setImage( celerity_get_resource_uri('/rsrc/image/people/washington.png')); $person5 = new PHUIIconView(); $person5->setHeadSize(PHUIIconView::HEAD_SMALL); $person5->setHref('http://en.wikipedia.org/wiki/Warren_G._Harding'); $person5->setImage( celerity_get_resource_uri('/rsrc/image/people/harding.png')); $person6 = new PHUIIconView(); $person6->setHeadSize(PHUIIconView::HEAD_SMALL); $person6->setHref('http://en.wikipedia.org/wiki/William_Howard_Taft'); $person6->setImage( celerity_get_resource_uri('/rsrc/image/people/taft.png')); $card1 = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_PAYMENTS) ->setSpriteIcon('visa') ->addClass(PHUI::MARGIN_SMALL_RIGHT); $card2 = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_PAYMENTS) ->setSpriteIcon('mastercard') ->addClass(PHUI::MARGIN_SMALL_RIGHT); $card3 = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_PAYMENTS) ->setSpriteIcon('paypal') ->addClass(PHUI::MARGIN_SMALL_RIGHT); $card4 = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_PAYMENTS) ->setSpriteIcon('americanexpress') ->addClass(PHUI::MARGIN_SMALL_RIGHT); $card5 = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_PAYMENTS) ->setSpriteIcon('googlecheckout'); $tokens = array( 'like-1', 'like-2', 'heart-1', - 'heart-2'); + 'heart-2', + ); $tokenview = array(); foreach ($tokens as $token) { $tokenview[] = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_TOKENS) ->setSpriteIcon($token); } $logins = array( 'Asana', 'Dropbox', 'Google', - 'Github'); + 'Github', + ); $loginview = array(); foreach ($logins as $login) { $loginview[] = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) ->setSpriteIcon($login) ->addClass(PHUI::MARGIN_SMALL_RIGHT); } $layout_cicons = id(new PHUIBoxView()) ->appendChild($cicons) ->addMargin(PHUI::MARGIN_LARGE); $layout_fa = id(new PHUIBoxView()) ->appendChild($ficons) ->addMargin(PHUI::MARGIN_LARGE); $layout2 = id(new PHUIBoxView()) ->appendChild(array($person1, $person2, $person3)) ->addMargin(PHUI::MARGIN_MEDIUM); $layout2a = id(new PHUIBoxView()) ->appendChild(array($person4, $person5, $person6)) ->addMargin(PHUI::MARGIN_MEDIUM); $layout3 = id(new PHUIBoxView()) ->appendChild($tokenview) ->addMargin(PHUI::MARGIN_MEDIUM); $layout4 = id(new PHUIBoxView()) ->appendChild(array($card1, $card2, $card3, $card4, $card5)) ->addMargin(PHUI::MARGIN_MEDIUM); $layout5 = id(new PHUIBoxView()) ->appendChild($loginview) ->addMargin(PHUI::MARGIN_MEDIUM); $fa_link = phutil_tag( 'a', array( - 'href' => 'http://fontawesome.io' + 'href' => 'http://fontawesome.io', ), 'http://fontawesome.io'); $fa_text = pht('Font Awesome by Dave Gandy - %s', $fa_link); $fontawesome = id(new PHUIObjectBoxView()) ->setHeaderText($fa_text) ->appendChild($layout_fa); $transforms = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Colors and Transforms')) ->appendChild($layout_cicons); $wrap2 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('People!')) ->appendChild(array($layout2, $layout2a)); $wrap3 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Tokens')) ->appendChild($layout3); $wrap4 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Payments')) ->appendChild($layout4); $wrap5 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Authentication')) ->appendChild($layout5); return phutil_tag( 'div', array( 'class' => 'phui-icon-example', ), array( $fontawesome, $transforms, $wrap2, $wrap3, $wrap4, - $wrap5 + $wrap5, )); - } + } } diff --git a/src/applications/uiexample/examples/PHUIImageMaskExample.php b/src/applications/uiexample/examples/PHUIImageMaskExample.php index d126dd6789..c43da63028 100644 --- a/src/applications/uiexample/examples/PHUIImageMaskExample.php +++ b/src/applications/uiexample/examples/PHUIImageMaskExample.php @@ -1,88 +1,88 @@ <?php final class PHUIImageMaskExample extends PhabricatorUIExample { public function getName() { return 'Image Masks'; } public function getDescription() { return 'Display images with crops.'; } public function renderExample() { $image = celerity_get_resource_uri('/rsrc/image/examples/hero.png'); $display_height = 100; $display_width = 200; $mask1 = id(new PHUIImageMaskView()) ->addClass('ml') ->setImage($image) ->setDisplayHeight($display_height) ->setDisplayWidth($display_width) ->centerViewOnPoint(265, 185, 30, 140); $mask2 = id(new PHUIImageMaskView()) ->addClass('ml') ->setImage($image) ->setDisplayHeight($display_height) ->setDisplayWidth($display_width) ->centerViewOnPoint(18, 18, 40, 80); $mask3 = id(new PHUIImageMaskView()) ->addClass('ml') ->setImage($image) ->setDisplayHeight($display_height) ->setDisplayWidth($display_width) ->centerViewOnPoint(265, 185, 30, 140) ->withMask(true); $mask4 = id(new PHUIImageMaskView()) ->addClass('ml') ->setImage($image) ->setDisplayHeight($display_height) ->setDisplayWidth($display_width) ->centerViewOnPoint(18, 18, 40, 80) ->withMask(true); $mask5 = id(new PHUIImageMaskView()) ->addClass('ml') ->setImage($image) ->setDisplayHeight($display_height) ->setDisplayWidth($display_width) ->centerViewOnPoint(254, 272, 60, 240) ->withMask(true); $box1 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Center is in the middle')) ->appendChild($mask1); $box2 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Center is on an edge')) ->appendChild($mask2); $box3 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Center Masked')) ->appendChild($mask3); $box4 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edge Masked')) ->appendChild($mask4); $box5 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Wide Masked')) ->appendChild($mask5); return phutil_tag( 'div', array(), array( $box1, $box2, $box3, $box4, - $box5 + $box5, )); } } diff --git a/src/applications/uiexample/examples/PHUIInfoPanelExample.php b/src/applications/uiexample/examples/PHUIInfoPanelExample.php index efdc8d0dc9..7f75656b37 100644 --- a/src/applications/uiexample/examples/PHUIInfoPanelExample.php +++ b/src/applications/uiexample/examples/PHUIInfoPanelExample.php @@ -1,139 +1,139 @@ <?php final class PHUIInfoPanelExample extends PhabricatorUIExample { public function getName() { return 'Info Panel'; } public function getDescription() { return 'A medium sized box with bits of gooey information.'; } public function renderExample() { $header1 = id(new PHUIHeaderView()) ->setHeader(pht('Conpherence')); $header2 = id(new PHUIHeaderView()) ->setHeader(pht('Diffusion')); $header3 = id(new PHUIHeaderView()) ->setHeader(pht('Backend Ops Projects')); $header4 = id(new PHUIHeaderView()) ->setHeader(pht('Revamp Liberty')) ->setSubHeader(pht('For great justice')) ->setImage( celerity_get_resource_uri('/rsrc/image/people/washington.png')); $header5 = id(new PHUIHeaderView()) ->setHeader(pht('Phacility Redesign')) ->setSubHeader(pht('Move them pixels')) ->setImage( celerity_get_resource_uri('/rsrc/image/people/harding.png')); $header6 = id(new PHUIHeaderView()) ->setHeader(pht('Python Phlux')) ->setSubHeader(pht('No. Sleep. Till Brooklyn.')) ->setImage( celerity_get_resource_uri('/rsrc/image/people/taft.png')); $column1 = id(new PHUIInfoPanelView()) ->setHeader($header1) ->setColumns(3) ->addInfoBlock(3, 'Needs Triage') ->addInfoBlock(5, 'Unbreak Now') ->addInfoBlock(0, 'High') ->addInfoBlock(0, 'Normal') ->addInfoBlock(12, 'Low') ->addInfoBlock(123, 'Wishlist'); $column2 = id(new PHUIInfoPanelView()) ->setHeader($header2) ->setColumns(3) ->addInfoBlock(3, 'Needs Triage') ->addInfoBlock(5, 'Unbreak Now') ->addInfoBlock(0, 'High') ->addInfoBlock(0, 'Normal') ->addInfoBlock(12, 'Low') ->addInfoBlock(123, 'Wishlist'); $column3 = id(new PHUIInfoPanelView()) ->setHeader($header3) ->setColumns(3) ->addInfoBlock(3, 'Needs Triage') ->addInfoBlock(5, 'Unbreak Now') ->addInfoBlock(0, 'High') ->addInfoBlock(0, 'Normal') ->addInfoBlock(12, 'Low') ->addInfoBlock(123, 'Wishlist'); $column4 = id(new PHUIInfoPanelView()) ->setHeader($header4) ->setColumns(3) ->setProgress(90) ->addInfoBlock(3, 'Needs Triage') ->addInfoBlock(5, 'Unbreak Now') ->addInfoBlock(0, 'High') ->addInfoBlock(0, 'Normal') ->addInfoBlock(0, 'Wishlist'); $column5 = id(new PHUIInfoPanelView()) ->setHeader($header5) ->setColumns(2) ->setProgress(25) ->addInfoBlock(3, 'Needs Triage') ->addInfoBlock(5, 'Unbreak Now') ->addInfoBlock(0, 'High') ->addInfoBlock(0, 'Normal'); $column6 = id(new PHUIInfoPanelView()) ->setHeader($header6) ->setColumns(2) ->setProgress(50) ->addInfoBlock(3, 'Needs Triage') ->addInfoBlock(5, 'Unbreak Now') ->addInfoBlock(0, 'High') ->addInfoBlock(0, 'Normal'); $layout1 = id(new AphrontMultiColumnView()) ->addColumn($column1) ->addColumn($column2) ->addColumn($column3) ->setFluidLayout(true); $layout2 = id(new AphrontMultiColumnView()) ->addColumn($column4) ->addColumn($column5) ->addColumn($column6) ->setFluidLayout(true); $head1 = id(new PHUIHeaderView()) ->setHeader(pht('Flagged')); $head2 = id(new PHUIHeaderView()) ->setHeader(pht('Sprints')); $wrap1 = id(new PHUIBoxView()) ->appendChild($layout1) ->addMargin(PHUI::MARGIN_LARGE_BOTTOM); $wrap2 = id(new PHUIBoxView()) ->appendChild($layout2) ->addMargin(PHUI::MARGIN_LARGE_BOTTOM); return phutil_tag( 'div', array(), array( $head1, $wrap1, $head2, - $wrap2 + $wrap2, )); - } + } } diff --git a/src/applications/uiexample/examples/PHUIListExample.php b/src/applications/uiexample/examples/PHUIListExample.php index 958bb53705..bda1e1a2ba 100644 --- a/src/applications/uiexample/examples/PHUIListExample.php +++ b/src/applications/uiexample/examples/PHUIListExample.php @@ -1,297 +1,302 @@ <?php final class PHUIListExample extends PhabricatorUIExample { public function getName() { return 'Lists'; } public function getDescription() { return 'Create a fanciful list of objects and prismatic donuts.'; } public function renderExample() { /* Action Menu */ $action1 = id(new PHUIListItemView()) ->setName('Edit Document') ->setHref('#') ->setIcon('fa-pencil') ->setType(PHUIListItemView::TYPE_LINK); $action2 = id(new PHUIListItemView()) ->setName('Move Document') ->setHref('#') ->setIcon('fa-arrows') ->setType(PHUIListItemView::TYPE_LINK); $action3 = id(new PHUIListItemView()) ->setName('Delete Document') ->setHref('#') ->setIcon('fa-times') ->setType(PHUIListItemView::TYPE_LINK); $action4 = id(new PHUIListItemView()) ->setName('View History') ->setHref('#') ->setIcon('fa-list') ->setType(PHUIListItemView::TYPE_LINK); $action5 = id(new PHUIListItemView()) ->setName('Subscribe') ->setHref('#') ->setIcon('fa-plus-circle') ->setType(PHUIListItemView::TYPE_LINK); $actionmenu = id(new PHUIListView()) ->setType(PHUIListView::SIDENAV_LIST) ->addMenuItem($action1) ->addMenuItem($action2) ->addMenuItem($action3) ->addMenuItem($action4) ->addMenuItem($action5); /* Side Navigation */ $label1 = id(new PHUIListItemView()) ->setName('Getting Started') ->setType(PHUIListItemView::TYPE_LABEL); $label2 = id(new PHUIListItemView()) ->setName('Documentation') ->setType(PHUIListItemView::TYPE_LABEL); $item1 = id(new PHUIListItemView()) ->setName('Installation') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $item2 = id(new PHUIListItemView()) ->setName('Webserver Config') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $item3 = id(new PHUIListItemView()) ->setName('Adding Users') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $item4 = id(new PHUIListItemView()) ->setName('Debugging') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); - $divider = id(new PHUIListItemView) + $divider = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_DIVIDER); $sidenav = id(new PHUIListView()) ->setType(PHUIListView::SIDENAV_LIST) ->addMenuItem($label1) ->addMenuItem($item3) ->addMenuItem($item2) ->addMenuItem($item1) ->addMenuItem($item4) ->addMenuItem($divider) ->addMenuItem($label2) ->addMenuItem($item3) ->addMenuItem($item2) ->addMenuItem($item1) ->addMenuItem($item4); /* Unstyled */ $item1 = id(new PHUIListItemView()) ->setName('Rain'); $item2 = id(new PHUIListItemView()) ->setName('Spain'); $item3 = id(new PHUIListItemView()) ->setName('Mainly'); $item4 = id(new PHUIListItemView()) ->setName('Plains'); $unstyled = id(new PHUIListView()) ->addMenuItem($item1) ->addMenuItem($item2) ->addMenuItem($item3) ->addMenuItem($item4); /* Top Navigation */ $home = id(new PHUIListItemView()) ->setIcon('fa-home') ->setHref('#') ->setType(PHUIListItemView::TYPE_ICON); $item1 = id(new PHUIListItemView()) ->setName('Installation') ->setHref('#') ->setSelected(true) ->setType(PHUIListItemView::TYPE_LINK); $item2 = id(new PHUIListItemView()) ->setName('Webserver Config') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $item3 = id(new PHUIListItemView()) ->setName('Adding Users') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $item4 = id(new PHUIListItemView()) ->setName('Debugging') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $item1 = id(new PHUIListItemView()) ->setName('Installation') ->setHref('#') ->setSelected(true) ->setType(PHUIListItemView::TYPE_LINK); $item2 = id(new PHUIListItemView()) ->setName('Webserver Config') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $details1 = id(new PHUIListItemView()) ->setName('Details') ->setHref('#') ->setSelected(true) ->setType(PHUIListItemView::TYPE_LINK); $details2 = id(new PHUIListItemView()) ->setName('Lint (OK)') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $details3 = id(new PHUIListItemView()) ->setName('Unit (5/5)') ->setHref('#') ->setType(PHUIListItemView::TYPE_LINK); $details4 = id(new PHUIListItemView()) ->setName('Lint (Warn)') ->setHref('#') ->setStatusColor(PHUIListItemView::STATUS_WARN) ->setType(PHUIListItemView::TYPE_LINK); $details5 = id(new PHUIListItemView()) ->setName('Unit (3/5)') ->setHref('#') ->setStatusColor(PHUIListItemView::STATUS_FAIL) ->setType(PHUIListItemView::TYPE_LINK); $topnav = id(new PHUIListView()) ->setType(PHUIListView::NAVBAR_LIST) ->addMenuItem($home) ->addMenuItem($item1) ->addMenuItem($item2) ->addMenuItem($item3) ->addMenuItem($item4); $statustabs = id(new PHUIListView()) ->setType(PHUIListView::NAVBAR_LIST) ->addMenuItem($details1) ->addMenuItem($details2) ->addMenuItem($details3) ->addMenuItem($details4) ->addMenuItem($details5); $layout1 = array( id(new PHUIBoxView()) ->appendChild($unstyled) ->addMargin(PHUI::MARGIN_MEDIUM) ->addPadding(PHUI::PADDING_SMALL) - ->setBorder(true)); + ->setBorder(true), + ); $layout2 = array( id(new PHUIBoxView()) ->appendChild($sidenav) ->addMargin(PHUI::MARGIN_MEDIUM) - ->setBorder(true)); + ->setBorder(true), + ); $layout3 = array( id(new PHUIBoxView()) ->appendChild($topnav) ->addMargin(PHUI::MARGIN_MEDIUM) - ->setBorder(true)); + ->setBorder(true), + ); $layout4 = array( id(new PHUIBoxView()) ->appendChild($actionmenu) ->addMargin(PHUI::MARGIN_MEDIUM) - ->setBorder(true)); + ->setBorder(true), + ); $layout5 = array( id(new PHUIBoxView()) ->appendChild($statustabs) ->addMargin(PHUI::MARGIN_MEDIUM) - ->setBorder(true)); + ->setBorder(true), + ); $head1 = id(new PHUIHeaderView()) ->setHeader(pht('Unstyled')); $head2 = id(new PHUIHeaderView()) ->setHeader(pht('Side Navigation')); $head3 = id(new PHUIHeaderView()) ->setHeader(pht('Top Navigation')); $head4 = id(new PHUIHeaderView()) ->setHeader(pht('Action Menu')); $head5 = id(new PHUIHeaderView()) ->setHeader(pht('Status Tabs')); $wrap1 = id(new PHUIBoxView()) ->appendChild($layout1) ->addMargin(PHUI::MARGIN_LARGE); $wrap2 = id(new PHUIBoxView()) ->appendChild($layout2) ->addMargin(PHUI::MARGIN_LARGE); $wrap3 = id(new PHUIBoxView()) ->appendChild($layout3) ->addMargin(PHUI::MARGIN_LARGE); $wrap4 = id(new PHUIBoxView()) ->appendChild($layout4) ->addMargin(PHUI::MARGIN_LARGE); $wrap5 = id(new PHUIBoxView()) ->appendChild($layout5) ->addMargin(PHUI::MARGIN_LARGE); return phutil_tag( 'div', array( 'class' => 'phui-list-example', ), array( $head1, $wrap1, $head2, $wrap2, $head3, $wrap3, $head5, $wrap5, $head4, - $wrap4 + $wrap4, )); - } + } } diff --git a/src/applications/uiexample/examples/PHUITextExample.php b/src/applications/uiexample/examples/PHUITextExample.php index 7d650a9237..d61c7b9ceb 100644 --- a/src/applications/uiexample/examples/PHUITextExample.php +++ b/src/applications/uiexample/examples/PHUITextExample.php @@ -1,105 +1,107 @@ <?php final class PHUITextExample extends PhabricatorUIExample { public function getName() { return 'Text'; } public function getDescription() { return 'Simple styles for displaying text.'; } public function renderExample() { $color1 = 'This is RED. '; $color2 = 'This is ORANGE. '; $color3 = 'This is YELLOW. '; $color4 = 'This is GREEN. '; $color5 = 'This is BLUE. '; $color6 = 'This is INDIGO. '; $color7 = 'This is VIOLET. '; $color8 = 'This is WHITE. '; $color9 = 'This is BLACK. '; $text1 = 'This is BOLD. '; $text2 = 'This is Uppercase. '; $text3 = 'This is Stricken.'; $content = array( id(new PHUITextView()) ->setText($color1) ->addClass(PHUI::TEXT_RED), id(new PHUITextView()) ->setText($color2) ->addClass(PHUI::TEXT_ORANGE), id(new PHUITextView()) ->setText($color3) ->addClass(PHUI::TEXT_YELLOW), id(new PHUITextView()) ->setText($color4) ->addClass(PHUI::TEXT_GREEN), id(new PHUITextView()) ->setText($color5) ->addClass(PHUI::TEXT_BLUE), id(new PHUITextView()) ->setText($color6) ->addClass(PHUI::TEXT_INDIGO), id(new PHUITextView()) ->setText($color7) ->addClass(PHUI::TEXT_VIOLET), id(new PHUITextView()) ->setText($color8) ->addClass(PHUI::TEXT_WHITE), id(new PHUITextView()) ->setText($color9) - ->addClass(PHUI::TEXT_BLACK)); + ->addClass(PHUI::TEXT_BLACK), + ); $content2 = array( id(new PHUITextView()) ->setText($text1) ->addClass(PHUI::TEXT_BOLD), id(new PHUITextView()) ->setText($text2) ->addClass(PHUI::TEXT_UPPERCASE), id(new PHUITextView()) ->setText($text3) - ->addClass(PHUI::TEXT_STRIKE)); + ->addClass(PHUI::TEXT_STRIKE), + ); $layout1 = id(new PHUIBoxView()) ->appendChild($content) ->setBorder(true) ->addPadding(PHUI::PADDING_MEDIUM); $head1 = id(new PHUIHeaderView()) ->setHeader(pht('Basic Colors')); $wrap1 = id(new PHUIBoxView()) ->appendChild($layout1) ->addMargin(PHUI::MARGIN_LARGE); $layout2 = id(new PHUIBoxView()) ->appendChild($content2) ->setBorder(true) ->addPadding(PHUI::PADDING_MEDIUM); $head2 = id(new PHUIHeaderView()) ->setHeader(pht('Basic Transforms')); $wrap2 = id(new PHUIBoxView()) ->appendChild($layout2) ->addMargin(PHUI::MARGIN_LARGE); return phutil_tag( 'div', array(), array( $head1, $wrap1, $head2, - $wrap2 + $wrap2, )); - } + } } diff --git a/src/applications/uiexample/examples/PhabricatorMultiColumnExample.php b/src/applications/uiexample/examples/PhabricatorMultiColumnExample.php index 83df527c1c..d437c269bb 100644 --- a/src/applications/uiexample/examples/PhabricatorMultiColumnExample.php +++ b/src/applications/uiexample/examples/PhabricatorMultiColumnExample.php @@ -1,225 +1,225 @@ <?php final class PhabricatorMultiColumnExample extends PhabricatorUIExample { public function getName() { return 'Multiple Column Layouts'; } public function getDescription() { return 'A container good for 1-7 equally spaced columns. '. 'Fixed and Fluid layouts.'; } public function renderExample() { $request = $this->getRequest(); $user = $request->getUser(); $column1 = phutil_tag( 'div', array( 'class' => 'pm', - 'style' => 'border: 1px solid green;' + 'style' => 'border: 1px solid green;', ), 'Bruce Campbell'); $column2 = phutil_tag( 'div', array( 'class' => 'pm', - 'style' => 'border: 1px solid blue;' + 'style' => 'border: 1px solid blue;', ), 'Army of Darkness'); $head1 = id(new PHUIHeaderView()) ->setHeader(pht('2 Column Fixed')); $layout1 = id(new AphrontMultiColumnView()) ->addColumn($column1) ->addColumn($column2) ->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM); $head2 = id(new PHUIHeaderView()) ->setHeader(pht('2 Column Fluid')); $layout2 = id(new AphrontMultiColumnView()) ->addColumn($column1) ->addColumn($column2) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_MEDIUM); $head3 = id(new PHUIHeaderView()) ->setHeader(pht('4 Column Fixed')); $layout3 = id(new AphrontMultiColumnView()) ->addColumn($column1) ->addColumn($column2) ->addColumn($column1) ->addColumn($column2) ->setGutter(AphrontMultiColumnView::GUTTER_SMALL); $head4 = id(new PHUIHeaderView()) ->setHeader(pht('4 Column Fluid')); $layout4 = id(new AphrontMultiColumnView()) ->addColumn($column1) ->addColumn($column2) ->addColumn($column1) ->addColumn($column2) ->setFluidLayout(true) ->setGutter(AphrontMultiColumnView::GUTTER_SMALL); $sunday = hsprintf('<strong>Sunday</strong><br /><br />Watch Football'. '<br />Code<br />Eat<br />Sleep'); $monday = hsprintf('<strong>Monday</strong><br /><br />Code'. '<br />Eat<br />Sleep'); $tuesday = hsprintf('<strong>Tuesday</strong><br />'. '<br />Code<br />Eat<br />Sleep'); $wednesday = hsprintf('<strong>Wednesday</strong><br /><br />Code'. '<br />Eat<br />Sleep'); $thursday = hsprintf('<strong>Thursday</strong><br />'. '<br />Code<br />Eat<br />Sleep'); $friday = hsprintf('<strong>Friday</strong><br /><br />Code'. '<br />Eat<br />Sleep'); $saturday = hsprintf('<strong>Saturday</strong><br /><br />StarCraft II'. '<br />All<br />Damn<br />Day'); $head5 = id(new PHUIHeaderView()) ->setHeader(pht('7 Column Fluid')); $layout5 = id(new AphrontMultiColumnView()) ->addColumn($sunday) ->addColumn($monday) ->addColumn($tuesday) ->addColumn($wednesday) ->addColumn($thursday) ->addColumn($friday) ->addColumn($saturday) ->setFluidLayout(true) ->setBorder(true); $shipping = id(new PHUIFormLayoutView()) ->setUser($user) ->setFullWidth(true) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Name') ->setDisableAutocomplete(true) ->setSigil('name-input')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Address') ->setDisableAutocomplete(true) ->setSigil('address-input')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('City/State') ->setDisableAutocomplete(true) ->setSigil('city-input')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Country') ->setDisableAutocomplete(true) ->setSigil('country-input')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Postal Code') ->setDisableAutocomplete(true) ->setSigil('postal-input')); $cc = id(new PHUIFormLayoutView()) ->setUser($user) ->setFullWidth(true) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Card Number') ->setDisableAutocomplete(true) ->setSigil('number-input') ->setError('')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('CVC') ->setDisableAutocomplete(true) ->setSigil('cvc-input') ->setError('')) ->appendChild( id(new PhortuneMonthYearExpiryControl()) ->setLabel('Expiration') ->setUser($user) ->setError('')); $shipping_title = pht('Shipping Address'); $billing_title = pht('Billing Address'); $cc_title = pht('Payment Information'); $head6 = id(new PHUIHeaderView()) ->setHeader(pht('Let\'s Go Shopping')); $layout6 = id(new AphrontMultiColumnView()) ->addColumn(hsprintf('<h1>%s</h1>%s', $shipping_title, $shipping)) ->addColumn(hsprintf('<h1>%s</h1>%s', $billing_title, $shipping)) ->addColumn(hsprintf('<h1>%s</h1>%s', $cc_title, $cc)) ->setFluidLayout(true) ->setBorder(true); $wrap1 = phutil_tag( 'div', array( - 'class' => 'ml' + 'class' => 'ml', ), $layout1); $wrap2 = phutil_tag( 'div', array( - 'class' => 'ml' + 'class' => 'ml', ), $layout2); $wrap3 = phutil_tag( 'div', array( - 'class' => 'ml' + 'class' => 'ml', ), $layout3); $wrap4 = phutil_tag( 'div', array( - 'class' => 'ml' + 'class' => 'ml', ), $layout4); $wrap5 = phutil_tag( 'div', array( - 'class' => 'ml' + 'class' => 'ml', ), $layout5); $wrap6 = phutil_tag( 'div', array( - 'class' => 'ml' + 'class' => 'ml', ), $layout6); return phutil_tag( 'div', array(), array( $head1, $wrap1, $head2, $wrap2, $head3, $wrap3, $head4, $wrap4, $head5, $wrap5, $head6, - $wrap6 + $wrap6, )); } } diff --git a/src/applications/uiexample/examples/PhabricatorTwoColumnExample.php b/src/applications/uiexample/examples/PhabricatorTwoColumnExample.php index a407ac3ebc..6ce57bc2b3 100644 --- a/src/applications/uiexample/examples/PhabricatorTwoColumnExample.php +++ b/src/applications/uiexample/examples/PhabricatorTwoColumnExample.php @@ -1,36 +1,36 @@ <?php final class PhabricatorTwoColumnExample extends PhabricatorUIExample { public function getName() { return 'Two Column Layout'; } public function getDescription() { return 'Two Column mobile friendly layout'; } public function renderExample() { $main = phutil_tag( 'div', array( - 'style' => 'border: 1px solid blue; padding: 20px;' + 'style' => 'border: 1px solid blue; padding: 20px;', ), 'Mary, mary quite contrary.'); $side = phutil_tag( 'div', array( - 'style' => 'border: 1px solid red; padding: 20px;' + 'style' => 'border: 1px solid red; padding: 20px;', ), 'How does your garden grow?'); - $content = id(new AphrontTwoColumnView) - ->setMainColumn($main) - ->setSideColumn($side); + $content = id(new AphrontTwoColumnView()) + ->setMainColumn($main) + ->setSideColumn($side); return $content; } } diff --git a/src/infrastructure/celerity/CelerityStaticResourceResponse.php b/src/infrastructure/celerity/CelerityStaticResourceResponse.php index 8d15286ad5..e710e5121d 100644 --- a/src/infrastructure/celerity/CelerityStaticResourceResponse.php +++ b/src/infrastructure/celerity/CelerityStaticResourceResponse.php @@ -1,307 +1,308 @@ <?php /** * Tracks and resolves dependencies the page declares with * @{function:require_celerity_resource}, and then builds appropriate HTML or * Ajax responses. */ final class CelerityStaticResourceResponse { private $symbols = array(); private $needsResolve = true; private $resolved; private $packaged; private $metadata = array(); private $metadataBlock = 0; private $behaviors = array(); private $hasRendered = array(); public function __construct() { if (isset($_REQUEST['__metablock__'])) { $this->metadataBlock = (int)$_REQUEST['__metablock__']; } } public function addMetadata($metadata) { $id = count($this->metadata); $this->metadata[$id] = $metadata; return $this->metadataBlock.'_'.$id; } public function getMetadataBlock() { return $this->metadataBlock; } /** * Register a behavior for initialization. NOTE: if $config is empty, * a behavior will execute only once even if it is initialized multiple times. * If $config is nonempty, the behavior will be invoked once for each config. */ public function initBehavior( $behavior, array $config = array(), $source_name = 'phabricator') { $this->requireResource('javelin-behavior-'.$behavior, $source_name); if (empty($this->behaviors[$behavior])) { $this->behaviors[$behavior] = array(); } if ($config) { $this->behaviors[$behavior][] = $config; } return $this; } public function requireResource($symbol, $source_name) { if (isset($this->symbols[$source_name][$symbol])) { return $this; } // Verify that the resource exists. $map = CelerityResourceMap::getNamedInstance($source_name); $name = $map->getResourceNameForSymbol($symbol); if ($name === null) { throw new Exception( pht( 'No resource with symbol "%s" exists in source "%s"!', $symbol, $source_name)); } $this->symbols[$source_name][$symbol] = true; $this->needsResolve = true; return $this; } private function resolveResources() { if ($this->needsResolve) { $this->packaged = array(); foreach ($this->symbols as $source_name => $symbols_map) { $symbols = array_keys($symbols_map); $map = CelerityResourceMap::getNamedInstance($source_name); $packaged = $map->getPackagedNamesForSymbols($symbols); $this->packaged[$source_name] = $packaged; } $this->needsResolve = false; } return $this; } public function renderSingleResource($symbol, $source_name) { $map = CelerityResourceMap::getNamedInstance($source_name); $packaged = $map->getPackagedNamesForSymbols(array($symbol)); return $this->renderPackagedResources($map, $packaged); } public function renderResourcesOfType($type) { $this->resolveResources(); $result = array(); foreach ($this->packaged as $source_name => $resource_names) { $map = CelerityResourceMap::getNamedInstance($source_name); $resources_of_type = array(); foreach ($resource_names as $resource_name) { $resource_type = $map->getResourceTypeForName($resource_name); if ($resource_type == $type) { $resources_of_type[] = $resource_name; } } $result[] = $this->renderPackagedResources($map, $resources_of_type); } return phutil_implode_html('', $result); } private function renderPackagedResources( CelerityResourceMap $map, array $resources) { $output = array(); foreach ($resources as $name) { if (isset($this->hasRendered[$name])) { continue; } $this->hasRendered[$name] = true; $output[] = $this->renderResource($map, $name); } return $output; } private function renderResource( CelerityResourceMap $map, $name) { $uri = $this->getURI($map, $name); $type = $map->getResourceTypeForName($name); switch ($type) { case 'css': return phutil_tag( 'link', array( 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => $uri, )); case 'js': return phutil_tag( 'script', array( 'type' => 'text/javascript', 'src' => $uri, ), ''); } throw new Exception( pht( 'Unable to render resource "%s", which has unknown type "%s".', $name, $type)); } public function renderHTMLFooter() { $data = array(); if ($this->metadata) { $json_metadata = AphrontResponse::encodeJSONForHTTPResponse( $this->metadata); $this->metadata = array(); } else { $json_metadata = '{}'; } // Even if there is no metadata on the page, Javelin uses the mergeData() // call to start dispatching the event queue. $data[] = 'JX.Stratcom.mergeData('.$this->metadataBlock.', '. $json_metadata.');'; $onload = array(); if ($this->behaviors) { $behaviors = $this->behaviors; $this->behaviors = array(); $higher_priority_names = array( 'refresh-csrf', 'aphront-basic-tokenizer', 'dark-console', 'history-install', ); $higher_priority_behaviors = array_select_keys( $behaviors, $higher_priority_names); foreach ($higher_priority_names as $name) { unset($behaviors[$name]); } $behavior_groups = array( $higher_priority_behaviors, - $behaviors); + $behaviors, + ); foreach ($behavior_groups as $group) { if (!$group) { continue; } $group_json = AphrontResponse::encodeJSONForHTTPResponse( $group); $onload[] = 'JX.initBehaviors('.$group_json.')'; } } if ($onload) { foreach ($onload as $func) { $data[] = 'JX.onload(function(){'.$func.'});'; } } if ($data) { $data = implode("\n", $data); return self::renderInlineScript($data); } else { return ''; } } public static function renderInlineScript($data) { if (stripos($data, '</script>') !== false) { throw new Exception( 'Literal </script> is not allowed inside inline script.'); } if (strpos($data, '<!') !== false) { throw new Exception('Literal <! is not allowed inside inline script.'); } // We don't use <![CDATA[ ]]> 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 @@ <?php final class PhabricatorBotFlowdockProtocolAdapter extends PhabricatorBotBaseStreamingProtocolAdapter { public function getServiceType() { return 'Flowdock'; } protected function buildStreamingUrl($channel) { $organization = $this->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 @@ <?php final class PhabricatorCampfireProtocolAdapter extends PhabricatorBotBaseStreamingProtocolAdapter { public function getServiceType() { return 'Campfire'; } protected function buildStreamingUrl($channel) { $ssl = $this->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 @@ <?php final class PhabricatorIRCProtocolAdapter extends PhabricatorBaseProtocolAdapter { private $socket; private $writeBuffer; private $readBuffer; private $nickIncrement = 0; public function getServiceType() { return 'IRC'; } public function getServiceName() { return $this->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<sender>(\S+?))(?:!\S*)? )?'. // This may not be present. '(?P<command>[A-Z0-9]+) '. '(?P<data>.*)'. '$/'; $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 @@ <?php /** * Responds to "Whats new?" with some recent feed content */ final class PhabricatorBotWhatsNewHandler extends PhabricatorBotHandler { private $floodblock = 0; public function receiveMessage(PhabricatorBotMessage $message) { switch ($message->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 @@ <?php final class PhabricatorInlineSummaryView extends AphrontView { private $groups = array(); public function addCommentGroup($name, array $items) { if (!isset($this->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 @@ <?php abstract class PhabricatorBaseEnglishTranslation extends PhabricatorTranslation { final public function getLanguage() { return 'en'; } public function getTranslations() { return array( 'No daemon(s) with id(s) "%s" exist!' => 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 @@ <?php /** * This is useful for testing, but otherwise your SMS ends up in a blackhole. */ final class PhabricatorSMSImplementationTestBlackholeAdapter extends PhabricatorSMSImplementationAdapter { public function getProviderShortName() { return 'testtesttest'; } public function send() { // I guess this is what a blackhole looks like } public function getSMSDataFromResult($result) { return array( Filesystem::readRandomCharacters(40), - PhabricatorSMS::STATUS_SENT); + PhabricatorSMS::STATUS_SENT, + ); } public function pollSMSSentStatus(PhabricatorSMS $sms) { if ($sms->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 @@ <?php final class PhabricatorSMSDemultiplexWorker extends PhabricatorSMSWorker { public function doWork() { $viewer = PhabricatorUser::getOmnipotentUser(); $task_data = $this->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 @@ <?php final class LiskChunkTestCase extends PhabricatorTestCase { public function testSQLChunking() { $fragments = array( 'a', 'a', 'b', 'b', 'ccc', 'dd', 'e', ); $this->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 @@ <?php final class PhabricatorStorageManagementDestroyWorkflow extends PhabricatorStorageManagementWorkflow { public function didConstruct() { $this ->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 @@ <?php final class AphrontDialogView extends AphrontView { private $title; private $shortTitle; private $submitButton; private $cancelURI; private $cancelText = 'Cancel'; private $submitURI; private $hidden = array(); private $class; private $renderAsForm = true; private $formID; private $headerColor = PHUIActionHeaderView::HEADER_LIGHTBLUE; private $footers = array(); private $isStandalone; private $method = 'POST'; private $disableWorkflowOnSubmit; private $disableWorkflowOnCancel; private $width = 'default'; private $errors = array(); private $flush; private $validationException; const WIDTH_DEFAULT = 'default'; const WIDTH_FORM = 'form'; const WIDTH_FULL = 'full'; public function setMethod($method) { $this->method = $method; return $this; } public function setIsStandalone($is_standalone) { $this->isStandalone = $is_standalone; return $this; } public function setErrors(array $errors) { $this->errors = $errors; return $this; } public function getIsStandalone() { return $this->isStandalone; } public function setSubmitURI($uri) { $this->submitURI = $uri; return $this; } public function setTitle($title) { $this->title = $title; return $this; } public function getTitle() { return $this->title; } public function setShortTitle($short_title) { $this->shortTitle = $short_title; return $this; } public function getShortTitle() { return $this->shortTitle; } public function addSubmitButton($text = null) { if (!$text) { $text = pht('Okay'); } $this->submitButton = $text; return $this; } public function addCancelButton($uri, $text = null) { if (!$text) { $text = pht('Cancel'); } $this->cancelURI = $uri; $this->cancelText = $text; return $this; } public function addFooter($footer) { $this->footers[] = $footer; return $this; } public function addHiddenInput($key, $value) { if (is_array($value)) { foreach ($value as $hidden_key => $hidden_value) { $this->hidden[] = array($key.'['.$hidden_key.']', $hidden_value); } } else { $this->hidden[] = array($key, $value); } return $this; } public function setClass($class) { $this->class = $class; return $this; } public function setFlush($flush) { $this->flush = $flush; return $this; } public function setRenderDialogAsDiv() { // TODO: This API is awkward. $this->renderAsForm = false; return $this; } public function setFormID($id) { $this->formID = $id; return $this; } public function setWidth($width) { $this->width = $width; return $this; } public function setHeaderColor($color) { $this->headerColor = $color; return $this; } public function appendParagraph($paragraph) { return $this->appendChild( phutil_tag( 'p', array( 'class' => 'aphront-dialog-view-paragraph', ), $paragraph)); } public function 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 @@ <?php final class AphrontTokenizerTemplateView extends AphrontView { private $value; private $name; private $id; public function setID($id) { $this->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 @@ <?php final class AphrontFormCheckboxControl extends AphrontFormControl { private $boxes = array(); public function addCheckbox($name, $value, $label, $checked = false) { $this->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 @@ <?php final class AphrontFormCropControl extends AphrontFormControl { private $width = 50; private $height = 50; public function setHeight($height) { $this->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 @@ <?php final class AphrontFormTextControl extends AphrontFormControl { private $disableAutocomplete; private $sigil; private $placeholder; public function setDisableAutocomplete($disable) { $this->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 @@ <?php final class AphrontFormTextWithSubmitControl extends AphrontFormControl { private $submitLabel; public function setSubmitLabel($submit_label) { $this->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 @@ <?php final class AphrontListFilterView extends AphrontView { private $showAction; private $hideAction; private $showHideDescription; private $showHideHref; public function setCollapsed($show, $hide, $description, $href) { $this->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 @@ <?php final class AphrontMultiColumnView extends AphrontView { const GUTTER_SMALL = 'msr'; const GUTTER_MEDIUM = 'mmr'; const GUTTER_LARGE = 'mlr'; private $id; private $columns = array(); private $fluidLayout = false; private $fluidishLayout = false; private $gutter; private $border; public function setID($id) { $this->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 @@ <?php /** * Provides a navigation sidebar. For example: * * $nav = new AphrontSideNavFilterView(); * $nav * ->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 @@ <?php final class AphrontTwoColumnView extends AphrontView { private $mainColumn; private $sideColumn; private $centered = false; private $padding = true; public function setMainColumn($main) { $this->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 @@ <?php final class PhabricatorActionListView extends AphrontView { private $actions = array(); private $object; private $objectURI; private $id = null; public function setObject(PhabricatorLiskDAO $object) { $this->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 @@ <?php final class PhabricatorCrumbsView extends AphrontView { private $crumbs = array(); private $actions = array(); private $actionListID = null; protected function canAppendChild() { return false; } /** * Convenience method for adding a simple crumb with just text, or text and * a link. * * @param string Text of the crumb. * @param string? Optional href for the crumb. * @return this */ public function addTextCrumb($text, $href = null) { return $this->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 @@ <?php final class PhabricatorSourceCodeView extends AphrontView { private $lines; private $limit; private $uri; private $highlights = array(); private $canClickHighlight = true; public function setLimit($limit) { $this->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 @@ <?php /** * This is a bare HTML page view which has access to Phabricator page * infrastructure like Celerity, but no content or builtin static resources. * You basically get a valid HMTL5 document and an empty body tag. * * @concrete-extensible */ class PhabricatorBarePageView extends AphrontPageView { private $request; private $controller; private $frameable; private $deviceReady; private $bodyContent; public function setController(AphrontController $controller) { $this->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 @@ <?php final class PhabricatorMainMenuView extends AphrontView { private $controller; private $applicationMenu; public function setApplicationMenu(PHUIListView $application_menu) { $this->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 @@ <?php final class PHUIActionHeaderView extends AphrontView { const HEADER_GREY = 'grey'; const HEADER_DARK_GREY = 'dark-grey'; const HEADER_LIGHTGREEN = 'lightgreen'; const HEADER_LIGHTRED = 'lightred'; const HEADER_LIGHTVIOLET = 'lightviolet'; const HEADER_LIGHTBLUE ='lightblue'; const HEADER_WHITE = 'white'; private $headerTitle; private $headerHref; private $headerIcon; private $headerSigils = array(); private $actions = array(); private $headerColor; private $tag = null; private $dropdown; public function setDropdown($dropdown) { $this->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 @@ <?php final class PHUIDocumentView extends AphrontTagView { /* For mobile displays, where do you want the sidebar */ const NAV_BOTTOM = 'nav_bottom'; const NAV_TOP = 'nav_top'; const FONT_SOURCE_SANS = 'source-sans'; private $offset; private $header; private $sidenav; private $topnav; private $crumbs; private $bookname; private $bookdescription; private $mobileview; private $fontKit; public function setOffset($offset) { $this->offset = $offset; return $this; } public function setHeader(PHUIHeaderView $header) { $header->setHeaderColor(PHUIActionHeaderView::HEADER_LIGHTBLUE); $this->header = $header; return $this; } public function setSideNav(PHUIListView $list, $display = self::NAV_BOTTOM) { $list->setType(PHUIListView::SIDENAV_LIST); $this->sidenav = $list; $this->mobileview = $display; return $this; } public function setTopNav(PHUIListView $list) { $list->setType(PHUIListView::NAVBAR_LIST); $this->topnav = $list; return $this; } public function setCrumbs(PHUIListView $list) { $this->crumbs = $list; return $this; } public function setBook($name, $description) { $this->bookname = $name; $this->bookdescription = $description; return $this; } public function setFontKit($kit) { $this->fontKit = $kit; return $this; } public function 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 @@ <?php final class PHUIFeedStoryView extends AphrontView { private $title; private $image; private $imageHref; private $appIcon; private $phid; private $epoch; private $viewed; private $href; private $pontification = null; private $tokenBar = array(); private $projects = array(); private $actions = array(); private $chronologicalKey; private $tags; public function setTags($tags) { $this->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 @@ <?php final class PHUIObjectItemListView extends AphrontTagView { private $header; private $items; private $pager; private $stackable; private $noDataString; private $flush; private $plain; private $allowEmptyList; private $states; public function setAllowEmptyList($allow_empty_list) { $this->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 @@ <?php final class PHUIPropertyListView extends AphrontView { private $parts = array(); private $hasKeyboardShortcuts; private $object; private $invokedWillRenderEvent; private $actionList; private $classes = array(); private $stacked; const ICON_SUMMARY = 'fa-align-left bluegrey'; const ICON_TESTPLAN = 'fa-file-text-o bluegrey'; protected function canAppendChild() { return false; } public function setObject($object) { $this->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 @@ <?php final class PHUITimelineEventView extends AphrontView { const DELIMITER = " \xC2\xB7 "; private $userHandle; private $title; private $icon; private $color; private $classes = array(); private $contentSource; private $dateCreated; private $anchor; private $isEditable; private $isEdited; private $isRemovable; private $transactionPHID; private $isPreview; private $eventGroup = array(); private $hideByDefault; private $token; private $tokenRemoved; private $quoteTargetID; private $quoteRef; public function setQuoteRef($quote_ref) { $this->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 @@ <?php final class PHUIWorkboardView extends AphrontTagView { private $panels = array(); private $fluidLayout = false; private $fluidishLayout = false; private $actions = array(); public function addPanel(PHUIWorkpanelView $panel) { $this->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 @@ <?php final class PHUIWorkpanelView extends AphrontTagView { private $cards = array(); private $header; private $footerAction; private $headerColor = PHUIActionHeaderView::HEADER_GREY; private $headerActions = array(); private $headerTag; private $headerIcon; public function setHeaderIcon(PHUIIconView $header_icon) { $this->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 @@ <?php final class PHUICalendarListView extends AphrontTagView { private $events = array(); private $blankState; public function addEvent(AphrontCalendarEventView $event) { $this->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 @@ <?php final class PHUICalendarMonthView extends AphrontView { private $day; private $month; private $year; private $holidays = array(); private $events = array(); private $browseURI; private $image; public function setBrowseURI($browse_uri) { $this->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 @@ <?php final class AphrontGlyphBarView extends AphrontBarView { const BLACK_STAR = "\xE2\x98\x85"; const WHITE_STAR = "\xE2\x98\x86"; private $value; private $max = 100; private $numGlyphs = 5; private $fgGlyph; private $bgGlyph; public function getDefaultColor() { return AphrontBarView::COLOR_AUTO_GOODNESS; } public function setValue($value) { $this->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 @@ <?php final class AphrontProgressBarView extends AphrontBarView { const WIDTH = 100; private $value; private $max = 100; private $alt = ''; public function getDefaultColor() { return AphrontBarView::COLOR_AUTO_BADNESS; } public function setValue($value) { $this->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 @@ <?php /** * The default one-for-all hovercard. We may derive from this one to create * more specialized ones */ final class PhabricatorHovercardView extends AphrontView { /** * @var PhabricatorObjectHandle */ private $handle; private $title = array(); private $detail; private $tags = array(); private $fields = array(); private $actions = array(); private $color = 'lightblue'; public function setObjectHandle(PhabricatorObjectHandle $handle) { $this->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 @@ <?php /** * Handle request startup, before loading the environment or libraries. This * class bootstraps the request state up to the point where we can enter * Phabricator code. * * NOTE: This class MUST NOT have any dependencies. It runs before libraries * load. * * Rate Limiting * ============= * * Phabricator limits the rate at which clients can request pages, and issues * HTTP 429 "Too Many Requests" responses if clients request too many pages too * quickly. Although this is not a complete defense against high-volume attacks, * it can protect an install against aggressive crawlers, security scanners, * and some types of malicious activity. * * To perform rate limiting, each page increments a score counter for the * requesting user's IP. The page can give the IP more points for an expensive * request, or fewer for an authetnicated request. * * Score counters are kept in buckets, and writes move to a new bucket every * minute. After a few minutes (defined by @{method:getRateLimitBucketCount}), * the oldest bucket is discarded. This provides a simple mechanism for keeping * track of scores without needing to store, access, or read very much data. * * Users are allowed to accumulate up to 1000 points per minute, averaged across * all of the tracked buckets. * * @task info Accessing Request Information * @task hook Startup Hooks * @task apocalypse In Case Of Apocalypse * @task validation Validation * @task ratelimit Rate Limiting */ final class PhabricatorStartup { private static $startTime; private static $debugTimeLimit; private static $globals = array(); private static $capturingOutput; private static $rawInput; private static $oldMemoryLimit; // TODO: For now, disable rate limiting entirely by default. We need to // iterate on it a bit for Conduit, some of the specific score levels, and // to deal with NAT'd offices. private static $maximumRate = 0; /* -( Accessing Request Information )-------------------------------------- */ /** * @task info */ public static function getStartTime() { return self::$startTime; } /** * @task info */ public static function getMicrosecondsSinceStart() { return (int)(1000000 * (microtime(true) - self::getStartTime())); } /** * @task info */ public static function setGlobal($key, $value) { self::validateGlobal($key); self::$globals[$key] = $value; } /** * @task info */ public static function getGlobal($key, $default = null) { self::validateGlobal($key); if (!array_key_exists($key, self::$globals)) { return $default; } return self::$globals[$key]; } /** * @task info */ public static function getRawInput() { return self::$rawInput; } /* -( Startup Hooks )------------------------------------------------------ */ /** * @task hook */ public static function didStartup() { self::$startTime = microtime(true); self::$globals = array(); static $registered; if (!$registered) { // NOTE: This protects us against multiple calls to didStartup() in the // same request, but also against repeated requests to the same // interpreter state, which we may implement in the future. register_shutdown_function(array(__CLASS__, 'didShutdown')); $registered = true; } self::setupPHP(); self::verifyPHP(); if (isset($_SERVER['REMOTE_ADDR'])) { self::rateLimitRequest($_SERVER['REMOTE_ADDR']); } self::normalizeInput(); self::verifyRewriteRules(); self::detectPostMaxSizeTriggered(); self::beginOutputCapture(); self::$rawInput = (string)file_get_contents('php://input'); } /** * @task hook */ public static function didShutdown() { $event = error_get_last(); if (!$event) { return; } switch ($event['type']) { case E_ERROR: case E_PARSE: case E_COMPILE_ERROR: break; default: return; } $msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n"; if ($event) { // Even though we should be emitting this as text-plain, escape things // just to be sure since we can't really be sure what the program state // is when we get here. $msg .= htmlspecialchars( $event['message']."\n\n".$event['file'].':'.$event['line'], ENT_QUOTES, 'UTF-8'); } // flip dem tables $msg .= "\n\n\n"; $msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf". "\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20". "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb"; self::didFatal($msg); } public static function loadCoreLibraries() { $phabricator_root = dirname(dirname(__FILE__)); $libraries_root = dirname($phabricator_root); $root = null; if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) { $root = $_SERVER['PHUTIL_LIBRARY_ROOT']; } ini_set( 'include_path', $libraries_root.PATH_SEPARATOR.ini_get('include_path')); @include_once $root.'libphutil/src/__phutil_library_init__.php'; if (!@constant('__LIBPHUTIL__')) { self::didFatal( "Unable to load libphutil. Put libphutil/ next to phabricator/, or ". "update your PHP 'include_path' to include the parent directory of ". "libphutil/."); } phutil_load_library('arcanist/src'); // Load Phabricator itself using the absolute path, so we never end up doing // anything surprising (loading index.php and libraries from different // directories). phutil_load_library($phabricator_root.'/src'); } /* -( Output Capture )----------------------------------------------------- */ public static function beginOutputCapture() { if (self::$capturingOutput) { self::didFatal('Already capturing output!'); } self::$capturingOutput = true; ob_start(); } public static function endOutputCapture() { if (!self::$capturingOutput) { return null; } self::$capturingOutput = false; return ob_get_clean(); } /* -( Debug Time Limit )--------------------------------------------------- */ /** * Set a time limit (in seconds) for the current script. After time expires, * the script fatals. * * This works like `max_execution_time`, but prints out a useful stack trace * when the time limit expires. This is primarily intended to make it easier * to debug pages which hang by allowing extraction of a stack trace: set a * short debug limit, then use the trace to figure out what's happening. * * The limit is implemented with a tick function, so enabling it implies * some accounting overhead. * * @param int Time limit in seconds. * @return void */ public static function setDebugTimeLimit($limit) { self::$debugTimeLimit = $limit; static $initialized; if (!$initialized) { declare(ticks=1); register_tick_function(array('PhabricatorStartup', 'onDebugTick')); } } /** * 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 @@ <?php $phabricator_root = dirname(dirname(__FILE__)); require_once $phabricator_root.'/support/PhabricatorStartup.php'; // If the preamble script exists, load it. $preamble_path = $phabricator_root.'/support/preamble.php'; if (file_exists($preamble_path)) { require_once $preamble_path; } PhabricatorStartup::didStartup(); $show_unexpected_traces = false; try { PhabricatorStartup::loadCoreLibraries(); PhabricatorEnv::initializeWebEnvironment(); $debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit'); if ($debug_time_limit) { PhabricatorStartup::setDebugTimeLimit($debug_time_limit); } $show_unexpected_traces = PhabricatorEnv::getEnvConfig( 'phabricator.developer-mode'); // This is the earliest we can get away with this, we need env config first. PhabricatorAccessLog::init(); $access_log = PhabricatorAccessLog::getLog(); PhabricatorStartup::setGlobal('log.access', $access_log); $access_log->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); }