diff --git a/src/aphront/AphrontController.php b/src/aphront/AphrontController.php index 55c2f9f30e..df6cff6abc 100644 --- a/src/aphront/AphrontController.php +++ b/src/aphront/AphrontController.php @@ -1,103 +1,107 @@ delegatingController = $delegating_controller; return $this; } public function getDelegatingController() { return $this->delegatingController; } public function willBeginExecution() { return; } public function willProcessRequest(array $uri_data) { return; } public function didProcessRequest($response) { return $response; } public function handleRequest(AphrontRequest $request) { if (method_exists($this, 'processRequest')) { return $this->processRequest(); } throw new PhutilMethodNotImplementedException( pht( - 'Controllers must implement either handleRequest() (recommended) '. - 'or processRequest() (deprecated).')); + 'Controllers must implement either %s (recommended) '. + 'or %s (deprecated).', + 'handleRequest()', + 'processRequest()')); } final public function setRequest(AphrontRequest $request) { $this->request = $request; return $this; } final public function getRequest() { if (!$this->request) { - throw new Exception(pht('Call setRequest() before getRequest()!')); + throw new PhutilInvalidStateException('setRequest'); } return $this->request; } final public function getViewer() { return $this->getRequest()->getViewer(); } final public function delegateToController(AphrontController $controller) { $request = $this->getRequest(); $controller->setDelegatingController($this); $controller->setRequest($request); $application = $this->getCurrentApplication(); if ($application) { $controller->setCurrentApplication($application); } return $controller->handleRequest($request); } final public function setCurrentApplication( PhabricatorApplication $current_application) { $this->currentApplication = $current_application; return $this; } final public function getCurrentApplication() { return $this->currentApplication; } public function getDefaultResourceSource() { - throw new Exception( + throw new MethodNotImplementedException( pht( - 'A Controller must implement getDefaultResourceSource() before you '. - 'can invoke requireResource() or initBehavior().')); + 'A Controller must implement %s before you can invoke %s or %s.', + 'getDefaultResourceSource()', + 'requireResource()', + 'initBehavior()')); } public function requireResource($symbol) { $response = CelerityAPI::getStaticResourceResponse(); $response->requireResource($symbol, $this->getDefaultResourceSource()); return $this; } public function initBehavior($name, $config = array()) { Javelin::initBehavior( $name, $config, $this->getDefaultResourceSource()); } } diff --git a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php index bbdb6000f8..f1cb45483e 100644 --- a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php @@ -1,256 +1,256 @@ viewer = $viewer; return $this; } public function getViewer() { if (!$this->viewer) { - throw new Exception(pht('Call setViewer() before getViewer()!')); + throw new PhutilInvalidStateException('setViewer'); } return $this->viewer; } public function setUserHasConfirmedVerify($confirmed) { $this->userHasConfirmedVerify = $confirmed; return $this; } private function shouldVerify() { return $this->userHasConfirmedVerify; } public function processInviteCode($code) { $viewer = $this->getViewer(); $invite = id(new PhabricatorAuthInviteQuery()) ->setViewer($viewer) ->withVerificationCodes(array($code)) ->executeOne(); if (!$invite) { throw id(new PhabricatorAuthInviteInvalidException( pht('Bad Invite Code'), pht( 'The invite code in the link you clicked is invalid. Check that '. 'you followed the link correctly.'))) ->setCancelButtonURI('/') ->setCancelButtonText(pht('Curses!')); } $accepted_phid = $invite->getAcceptedByPHID(); if ($accepted_phid) { if ($accepted_phid == $viewer->getPHID()) { throw id(new PhabricatorAuthInviteInvalidException( pht('Already Accepted'), pht( 'You have already accepted this invitation.'))) ->setCancelButtonURI('/') ->setCancelButtonText(pht('Awesome')); } else { throw id(new PhabricatorAuthInviteInvalidException( pht('Already Accepted'), pht( 'The invite code in the link you clicked has already '. 'been accepted.'))) ->setCancelButtonURI('/') ->setCancelButtonText(pht('Continue')); } } $email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $invite->getEmailAddress()); if ($viewer->isLoggedIn()) { $this->handleLoggedInInvite($invite, $viewer, $email); } if ($email) { $other_user = $this->loadUserForEmail($email); if ($email->getIsVerified()) { throw id(new PhabricatorAuthInviteLoginException( pht('Already Registered'), pht( 'The email address you just clicked a link from is already '. 'verified and associated with a registered account (%s). Log '. 'in to continue.', phutil_tag('strong', array(), $other_user->getName())))) ->setCancelButtonText(pht('Log In')) ->setCancelButtonURI($this->getLoginURI()); } else if ($email->getIsPrimary()) { throw id(new PhabricatorAuthInviteLoginException( pht('Already Registered'), pht( 'The email address you just clicked a link from is already '. 'the primary email address for a registered account (%s). Log '. 'in to continue.', phutil_tag('strong', array(), $other_user->getName())))) ->setCancelButtonText(pht('Log In')) ->setCancelButtonURI($this->getLoginURI()); } else if (!$this->shouldVerify()) { throw id(new PhabricatorAuthInviteVerifyException( pht('Already Associated'), pht( 'The email address you just clicked a link from is already '. 'associated with a registered account (%s), but is not '. 'verified. Log in to that account to continue. If you can not '. 'log in, you can register a new account.', phutil_tag('strong', array(), $other_user->getName())))) ->setCancelButtonText(pht('Log In')) ->setCancelButtonURI($this->getLoginURI()) ->setSubmitButtonText(pht('Register New Account')); } else { // NOTE: The address is not verified and not a primary address, so // we will eventually steal it if the user completes registration. } } // The invite and email address are OK, but the user needs to register. return $invite; } private function handleLoggedInInvite( PhabricatorAuthInvite $invite, PhabricatorUser $viewer, PhabricatorUserEmail $email = null) { if ($email && ($email->getUserPHID() !== $viewer->getPHID())) { $other_user = $this->loadUserForEmail($email); if ($email->getIsVerified()) { throw id(new PhabricatorAuthInviteAccountException( pht('Wrong Account'), pht( 'You are logged in as %s, but the email address you just '. 'clicked a link from is already verified and associated '. 'with another account (%s). Switch accounts, then try again.', phutil_tag('strong', array(), $viewer->getUsername()), phutil_tag('strong', array(), $other_user->getName())))) ->setSubmitButtonText(pht('Log Out')) ->setSubmitButtonURI($this->getLogoutURI()) ->setCancelButtonURI('/'); } else if ($email->getIsPrimary()) { // NOTE: We never steal primary addresses from other accounts, even // if they are unverified. This would leave the other account with // no address. Users can use password recovery to access the other // account if they really control the address. throw id(new PhabricatorAuthInviteAccountException( pht('Wrong Acount'), pht( 'You are logged in as %s, but the email address you just '. 'clicked a link from is already the primary email address '. 'for another account (%s). Switch accounts, then try again.', phutil_tag('strong', array(), $viewer->getUsername()), phutil_tag('strong', array(), $other_user->getName())))) ->setSubmitButtonText(pht('Log Out')) ->setSubmitButtonURI($this->getLogoutURI()) ->setCancelButtonURI('/'); } else if (!$this->shouldVerify()) { throw id(new PhabricatorAuthInviteVerifyException( pht('Verify Email'), pht( 'You are logged in as %s, but the email address (%s) you just '. 'clicked a link from is already associated with another '. 'account (%s). You can log out to switch accounts, or verify '. 'the address and attach it to your current account. Attach '. 'email address %s to user account %s?', phutil_tag('strong', array(), $viewer->getUsername()), phutil_tag('strong', array(), $invite->getEmailAddress()), phutil_tag('strong', array(), $other_user->getName()), phutil_tag('strong', array(), $invite->getEmailAddress()), phutil_tag('strong', array(), $viewer->getUsername())))) ->setSubmitButtonText( pht( 'Verify %s', $invite->getEmailAddress())) ->setCancelButtonText(pht('Log Out')) ->setCancelButtonURI($this->getLogoutURI()); } } if (!$email) { $email = id(new PhabricatorUserEmail()) ->setAddress($invite->getEmailAddress()) ->setIsVerified(0) ->setIsPrimary(0); } if (!$email->getIsVerified()) { // We're doing this check here so that we can verify the address if // it's already attached to the viewer's account, just not verified. if (!$this->shouldVerify()) { throw id(new PhabricatorAuthInviteVerifyException( pht('Verify Email'), pht( 'Verify this email address (%s) and attach it to your '. 'account (%s)?', phutil_tag('strong', array(), $invite->getEmailAddress()), phutil_tag('strong', array(), $viewer->getUsername())))) ->setSubmitButtonText( pht( 'Verify %s', $invite->getEmailAddress())) ->setCancelButtonURI('/'); } $editor = id(new PhabricatorUserEditor()) ->setActor($viewer); // If this is a new email, add it to the user's account. if (!$email->getUserPHID()) { $editor->addEmail($viewer, $email); } // If another user added this email (but has not verified it), // take it from them. $editor->reassignEmail($viewer, $email); $editor->verifyEmail($viewer, $email); } $invite->setAcceptedByPHID($viewer->getPHID()); $invite->save(); // If we make it here, the user was already logged in with the email // address attached to their account and verified, or we attached it to // their account (if it was not already attached) and verified it. throw new PhabricatorAuthInviteRegisteredException(); } private function loadUserForEmail(PhabricatorUserEmail $email) { $user = id(new PhabricatorHandleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($email->getUserPHID())) ->executeOne(); if (!$user) { throw new Exception( pht( 'Email record ("%s") has bad associated user PHID ("%s").', $email->getAddress(), $email->getUserPHID())); } return $user; } private function getLoginURI() { return '/auth/start/'; } private function getLogoutURI() { return '/logout/'; } } diff --git a/src/applications/conpherence/view/ConpherenceTransactionView.php b/src/applications/conpherence/view/ConpherenceTransactionView.php index 57bec773fc..fa94b26ba6 100644 --- a/src/applications/conpherence/view/ConpherenceTransactionView.php +++ b/src/applications/conpherence/view/ConpherenceTransactionView.php @@ -1,280 +1,280 @@ conpherenceThread = $t; return $this; } private function getConpherenceThread() { return $this->conpherenceThread; } public function setConpherenceTransaction(ConpherenceTransaction $tx) { $this->conpherenceTransaction = $tx; return $this; } private function getConpherenceTransaction() { return $this->conpherenceTransaction; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function getHandles() { return $this->handles; } public function setMarkupEngine(PhabricatorMarkupEngine $markup_engine) { $this->markupEngine = $markup_engine; return $this; } private function getMarkupEngine() { return $this->markupEngine; } public function setFullDisplay($bool) { $this->fullDisplay = $bool; return $this; } private function getFullDisplay() { return $this->fullDisplay; } public function addClass($class) { $this->classes[] = $class; return $this; } public function render() { $viewer = $this->getUser(); if (!$viewer) { - throw new Exception(pht('Call setUser() before render()!')); + throw new PhutilInvalidStateException('setUser'); } require_celerity_resource('conpherence-transaction-css'); $transaction = $this->getConpherenceTransaction(); switch ($transaction->getTransactionType()) { case ConpherenceTransaction::TYPE_DATE_MARKER: return javelin_tag( 'div', array( 'class' => 'conpherence-transaction-view date-marker', 'sigil' => 'conpherence-transaction-view', 'meta' => array( 'id' => $transaction->getID() + 0.5, ), ), array( phutil_tag( 'span', array( 'class' => 'date', ), phabricator_format_local_time( $transaction->getDateCreated(), $viewer, 'M jS, Y')), )); break; } $info = $this->renderTransactionInfo(); $actions = $this->renderTransactionActions(); $image = $this->renderTransactionImage(); $content = $this->renderTransactionContent(); $classes = implode(' ', $this->classes); $transaction_dom_id = null; if ($this->getFullDisplay()) { $transaction_dom_id = 'anchor-'.$transaction->getID(); } $header = phutil_tag_div( 'conpherence-transaction-header grouped', array($actions, $info)); return javelin_tag( 'div', array( 'class' => 'conpherence-transaction-view '.$classes, 'id' => $transaction_dom_id, 'sigil' => 'conpherence-transaction-view', 'meta' => array( 'id' => $transaction->getID(), ), ), array( $image, phutil_tag_div('conpherence-transaction-detail grouped', array($header, $content)), )); } private function renderTransactionInfo() { $viewer = $this->getUser(); $thread = $this->getConpherenceThread(); $transaction = $this->getConpherenceTransaction(); $info = array(); if ($this->getFullDisplay() && $transaction->getContentSource()) { $content_source = id(new PhabricatorContentSourceView()) ->setContentSource($transaction->getContentSource()) ->setUser($viewer) ->render(); if ($content_source) { $info[] = $content_source; } } Javelin::initBehavior('phabricator-tooltips'); $tip = phabricator_datetime($transaction->getDateCreated(), $viewer); $label = phabricator_time($transaction->getDateCreated(), $viewer); $width = 360; if ($this->getFullDisplay()) { Javelin::initBehavior('phabricator-watch-anchor'); $anchor = id(new PhabricatorAnchorView()) ->setAnchorName($transaction->getID()) ->render(); $info[] = hsprintf( '%s%s', $anchor, javelin_tag( 'a', array( 'href' => '#'.$transaction->getID(), 'class' => 'anchor-link', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tip, 'size' => $width, ), ), $label)); } else { $href = '/'.$thread->getMonogram().'#'.$transaction->getID(); $info[] = javelin_tag( 'a', array( 'href' => $href, 'class' => 'epoch-link', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tip, 'size' => $width, ), ), $label); } $info = phutil_implode_html(" \xC2\xB7 ", $info); return phutil_tag( 'span', array( 'class' => 'conpherence-transaction-info', ), $info); } private function renderTransactionActions() { $transaction = $this->getConpherenceTransaction(); switch ($transaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $handles = $this->getHandles(); $author = $handles[$transaction->getAuthorPHID()]; $actions = array($author->renderLink()); break; default: $actions = null; break; } return $actions; } private function renderTransactionImage() { $image = null; if ($this->getFullDisplay()) { $transaction = $this->getConpherenceTransaction(); switch ($transaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $handles = $this->getHandles(); $author = $handles[$transaction->getAuthorPHID()]; $image_uri = $author->getImageURI(); $image = phutil_tag( 'span', array( 'class' => 'conpherence-transaction-image', 'style' => 'background-image: url('.$image_uri.');', )); break; } } return $image; } private function renderTransactionContent() { $transaction = $this->getConpherenceTransaction(); $content = null; $content_class = null; $content = null; $handles = $this->getHandles(); switch ($transaction->getTransactionType()) { case ConpherenceTransaction::TYPE_FILES: $content = $transaction->getTitle(); break; case ConpherenceTransaction::TYPE_TITLE: case ConpherenceTransaction::TYPE_PICTURE: case ConpherenceTransaction::TYPE_PICTURE_CROP: case ConpherenceTransaction::TYPE_PARTICIPANTS: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorTransactions::TYPE_EDGE: $content = $transaction->getTitle(); $this->addClass('conpherence-edited'); break; case PhabricatorTransactions::TYPE_COMMENT: $this->addClass('conpherence-comment'); $author = $handles[$transaction->getAuthorPHID()]; $comment = $transaction->getComment(); $content = $this->getMarkupEngine()->getOutput( $comment, PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT); $content_class = 'conpherence-message'; break; } $this->appendChild( phutil_tag( 'div', array( 'class' => $content_class, ), $content)); return phutil_tag_div( 'conpherence-transaction-content', $this->renderChildren()); } } diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php index dfba37eaa5..fcf1e3d7c9 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -1,305 +1,303 @@ 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', ), array( $header, $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); $panel_type = $panel->getImplementation(); $header = $panel_type->adjustPanelHeader( $this->getViewer(), $panel, $this, $header); 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.')); + throw new PhutilInvalidStateException('setParentPanelPHIDs'); } $max_depth = 4; if (count($this->parentPanelPHIDs) >= $max_depth) { throw new Exception( pht( 'To render more than %s levels of panels nested inside other '. 'panels, purchase a subscription to Phabricator Gold.', new PhutilNumber($max_depth))); } if (in_array($panel->getPHID(), $this->parentPanelPHIDs)) { throw new Exception( pht( 'You awake in a twisting maze of mirrors, all alike. '. 'You are likely to be eaten by a graph cycle. '. 'Should you escape alive, you resolve to be more careful about '. 'putting dashboard panels inside themselves.')); } } } diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index 1694d42db0..f00c8a1ef5 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -1,639 +1,636 @@ addObject($comment, $field); * } * * Now, call @{method:process} to perform the actual cache/rendering * step. This is a heavyweight call which does batched data access and * transforms the markup into output. * * $engine->process(); * * Finally, do something with the results: * * $results = array(); * foreach ($comments as $comment) { * $results[] = $engine->getOutput($comment, $field); * } * * If you have a single object to render, you can use the convenience method * @{method:renderOneObject}. * * @task markup Markup Pipeline * @task engine Engine Construction */ final class PhabricatorMarkupEngine extends Phobject { private $objects = array(); private $viewer; private $contextObject; private $version = 15; private $engineCaches = array(); /* -( Markup Pipeline )---------------------------------------------------- */ /** * Convenience method for pushing a single object through the markup * pipeline. * * @param PhabricatorMarkupInterface The object to render. * @param string The field to render. * @param PhabricatorUser User viewing the markup. * @param object A context object for policy checks * @return string Marked up output. * @task markup */ public static function renderOneObject( PhabricatorMarkupInterface $object, $field, PhabricatorUser $viewer, $context_object = null) { return id(new PhabricatorMarkupEngine()) ->setViewer($viewer) ->setContextObject($context_object) ->addObject($object, $field) ->process() ->getOutput($object, $field); } /** * Queue an object for markup generation when @{method:process} is * called. You can retrieve the output later with @{method:getOutput}. * * @param PhabricatorMarkupInterface The object to render. * @param string The field to render. * @return this * @task markup */ public function addObject(PhabricatorMarkupInterface $object, $field) { $key = $this->getMarkupFieldKey($object, $field); $this->objects[$key] = array( 'object' => $object, 'field' => $field, ); return $this; } /** * Process objects queued with @{method:addObject}. You can then retrieve * the output with @{method:getOutput}. * * @return this * @task markup */ public function process() { $keys = array(); foreach ($this->objects as $key => $info) { if (!isset($info['markup'])) { $keys[] = $key; } } if (!$keys) { return; } $objects = array_select_keys($this->objects, $keys); // Build all the markup engines. We need an engine for each field whether // we have a cache or not, since we still need to postprocess the cache. $engines = array(); foreach ($objects as $key => $info) { $engines[$key] = $info['object']->newMarkupEngine($info['field']); $engines[$key]->setConfig('viewer', $this->viewer); $engines[$key]->setConfig('contextObject', $this->contextObject); } // Load or build the preprocessor caches. $blocks = $this->loadPreprocessorCaches($engines, $objects); $blocks = mpull($blocks, 'getCacheData'); $this->engineCaches = $blocks; // Finalize the output. foreach ($objects as $key => $info) { $engine = $engines[$key]; $field = $info['field']; $object = $info['object']; $output = $engine->postprocessText($blocks[$key]); $output = $object->didMarkupText($field, $output, $engine); $this->objects[$key]['output'] = $output; } return $this; } /** * Get the output of markup processing for a field queued with * @{method:addObject}. Before you can call this method, you must call * @{method:process}. * * @param PhabricatorMarkupInterface The object to retrieve. * @param string The field to retrieve. * @return string Processed output. * @task markup */ public function getOutput(PhabricatorMarkupInterface $object, $field) { $key = $this->getMarkupFieldKey($object, $field); $this->requireKeyProcessed($key); return $this->objects[$key]['output']; } /** * Retrieve engine metadata for a given field. * * @param PhabricatorMarkupInterface The object to retrieve. * @param string The field to retrieve. * @param string The engine metadata field to retrieve. * @param wild Optional default value. * @task markup */ public function getEngineMetadata( PhabricatorMarkupInterface $object, $field, $metadata_key, $default = null) { $key = $this->getMarkupFieldKey($object, $field); $this->requireKeyProcessed($key); return idx($this->engineCaches[$key]['metadata'], $metadata_key, $default); } /** * @task markup */ private function requireKeyProcessed($key) { if (empty($this->objects[$key])) { throw new Exception( pht( "Call %s before using results (key = '%s').", 'addObject()', $key)); } if (!isset($this->objects[$key]['output'])) { - throw new Exception( - pht( - 'Call %s before using results.', - 'process()')); + throw new PhutilInvalidStateException('process'); } } /** * @task markup */ private function getMarkupFieldKey( PhabricatorMarkupInterface $object, $field) { static $custom; if ($custom === null) { $custom = array_merge( self::loadCustomInlineRules(), self::loadCustomBlockRules()); $custom = mpull($custom, 'getRuleVersion', null); ksort($custom); $custom = PhabricatorHash::digestForIndex(serialize($custom)); } return $object->getMarkupFieldKey($field).'@'.$this->version.'@'.$custom; } /** * @task markup */ private function loadPreprocessorCaches(array $engines, array $objects) { $blocks = array(); $use_cache = array(); foreach ($objects as $key => $info) { if ($info['object']->shouldUseMarkupCache($info['field'])) { $use_cache[$key] = true; } } if ($use_cache) { try { $blocks = id(new PhabricatorMarkupCache())->loadAllWhere( 'cacheKey IN (%Ls)', array_keys($use_cache)); $blocks = mpull($blocks, null, 'getCacheKey'); } catch (Exception $ex) { phlog($ex); } } foreach ($objects as $key => $info) { // False check in case MySQL doesn't support unicode characters // in the string (T1191), resulting in unserialize returning false. if (isset($blocks[$key]) && $blocks[$key]->getCacheData() !== false) { // If we already have a preprocessing cache, we don't need to rebuild // it. continue; } $text = $info['object']->getMarkupText($info['field']); $data = $engines[$key]->preprocessText($text); // NOTE: This is just debugging information to help sort out cache issues. // If one machine is misconfigured and poisoning caches you can use this // field to hunt it down. $metadata = array( 'host' => php_uname('n'), ); $blocks[$key] = id(new PhabricatorMarkupCache()) ->setCacheKey($key) ->setCacheData($data) ->setMetadata($metadata); if (isset($use_cache[$key])) { // This is just filling a cache and always safe, even on a read pathway. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $blocks[$key]->replace(); unset($unguarded); } } return $blocks; } /** * Set the viewing user. Used to implement object permissions. * * @param PhabricatorUser The viewing user. * @return this * @task markup */ public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } /** * Set the context object. Used to implement object permissions. * * @param The object in which context this remarkup is used. * @return this * @task markup */ public function setContextObject($object) { $this->contextObject = $object; return $this; } /* -( Engine Construction )------------------------------------------------ */ /** * @task engine */ public static function newManiphestMarkupEngine() { return self::newMarkupEngine(array( )); } /** * @task engine */ public static function newPhrictionMarkupEngine() { return self::newMarkupEngine(array( 'header.generate-toc' => true, )); } /** * @task engine */ public static function newPhameMarkupEngine() { return self::newMarkupEngine(array( 'macros' => false, 'uri.full' => true, )); } /** * @task engine */ public static function newFeedMarkupEngine() { return self::newMarkupEngine( array( 'macros' => false, 'youtube' => false, )); } /** * @task engine */ public static function newCalendarMarkupEngine() { return self::newMarkupEngine(array( )); } /** * @task engine */ public static function newDifferentialMarkupEngine(array $options = array()) { return self::newMarkupEngine(array( 'differential.diff' => idx($options, 'differential.diff'), )); } /** * @task engine */ public static function newDiffusionMarkupEngine(array $options = array()) { return self::newMarkupEngine(array( 'header.generate-toc' => true, )); } /** * @task engine */ public static function getEngine($ruleset = 'default') { static $engines = array(); if (isset($engines[$ruleset])) { return $engines[$ruleset]; } $engine = null; switch ($ruleset) { case 'default': $engine = self::newMarkupEngine(array()); break; case 'nolinebreaks': $engine = self::newMarkupEngine(array()); $engine->setConfig('preserve-linebreaks', false); break; case 'diffusion-readme': $engine = self::newMarkupEngine(array()); $engine->setConfig('preserve-linebreaks', false); $engine->setConfig('header.generate-toc', true); break; case 'diviner': $engine = self::newMarkupEngine(array()); $engine->setConfig('preserve-linebreaks', false); // $engine->setConfig('diviner.renderer', new DivinerDefaultRenderer()); $engine->setConfig('header.generate-toc', true); break; case 'extract': // Engine used for reference/edge extraction. Turn off anything which // is slow and doesn't change reference extraction. $engine = self::newMarkupEngine(array()); $engine->setConfig('pygments.enabled', false); break; default: throw new Exception(pht('Unknown engine ruleset: %s!', $ruleset)); } $engines[$ruleset] = $engine; return $engine; } /** * @task engine */ private static function getMarkupEngineDefaultConfiguration() { return array( 'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'), 'youtube' => PhabricatorEnv::getEnvConfig( 'remarkup.enable-embedded-youtube'), 'differential.diff' => null, 'header.generate-toc' => false, 'macros' => true, 'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig( 'uri.allowed-protocols'), 'uri.full' => false, 'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig( 'syntax-highlighter.engine'), 'preserve-linebreaks' => true, ); } /** * @task engine */ public static function newMarkupEngine(array $options) { $options += self::getMarkupEngineDefaultConfiguration(); $engine = new PhutilRemarkupEngine(); $engine->setConfig('preserve-linebreaks', $options['preserve-linebreaks']); $engine->setConfig('pygments.enabled', $options['pygments']); $engine->setConfig( 'uri.allowed-protocols', $options['uri.allowed-protocols']); $engine->setConfig('differential.diff', $options['differential.diff']); $engine->setConfig('header.generate-toc', $options['header.generate-toc']); $engine->setConfig( 'syntax-highlighter.engine', $options['syntax-highlighter.engine']); $engine->setConfig('uri.full', $options['uri.full']); $rules = array(); $rules[] = new PhutilRemarkupEscapeRemarkupRule(); $rules[] = new PhutilRemarkupMonospaceRule(); $rules[] = new PhutilRemarkupDocumentLinkRule(); $rules[] = new PhabricatorNavigationRemarkupRule(); if ($options['youtube']) { $rules[] = new PhabricatorYoutubeRemarkupRule(); } $applications = PhabricatorApplication::getAllInstalledApplications(); foreach ($applications as $application) { foreach ($application->getRemarkupRules() as $rule) { $rules[] = $rule; } } $rules[] = new PhutilRemarkupHyperlinkRule(); if ($options['macros']) { $rules[] = new PhabricatorImageMacroRemarkupRule(); $rules[] = new PhabricatorMemeRemarkupRule(); } $rules[] = new PhutilRemarkupBoldRule(); $rules[] = new PhutilRemarkupItalicRule(); $rules[] = new PhutilRemarkupDelRule(); $rules[] = new PhutilRemarkupUnderlineRule(); foreach (self::loadCustomInlineRules() as $rule) { $rules[] = $rule; } $blocks = array(); $blocks[] = new PhutilRemarkupQuotesBlockRule(); $blocks[] = new PhutilRemarkupReplyBlockRule(); $blocks[] = new PhutilRemarkupLiteralBlockRule(); $blocks[] = new PhutilRemarkupHeaderBlockRule(); $blocks[] = new PhutilRemarkupHorizontalRuleBlockRule(); $blocks[] = new PhutilRemarkupListBlockRule(); $blocks[] = new PhutilRemarkupCodeBlockRule(); $blocks[] = new PhutilRemarkupNoteBlockRule(); $blocks[] = new PhutilRemarkupTableBlockRule(); $blocks[] = new PhutilRemarkupSimpleTableBlockRule(); $blocks[] = new PhutilRemarkupInterpreterBlockRule(); $blocks[] = new PhutilRemarkupDefaultBlockRule(); foreach (self::loadCustomBlockRules() as $rule) { $blocks[] = $rule; } foreach ($blocks as $block) { $block->setMarkupRules($rules); } $engine->setBlockRules($blocks); return $engine; } public static function extractPHIDsFromMentions( PhabricatorUser $viewer, array $content_blocks) { $mentions = array(); $engine = self::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $viewer); foreach ($content_blocks as $content_block) { $engine->markupText($content_block); $phids = $engine->getTextMetadata( PhabricatorMentionRemarkupRule::KEY_MENTIONED, array()); $mentions += $phids; } return $mentions; } public static function extractFilePHIDsFromEmbeddedFiles( PhabricatorUser $viewer, array $content_blocks) { $files = array(); $engine = self::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $viewer); foreach ($content_blocks as $content_block) { $engine->markupText($content_block); $phids = $engine->getTextMetadata( PhabricatorEmbedFileRemarkupRule::KEY_EMBED_FILE_PHIDS, array()); foreach ($phids as $phid) { $files[$phid] = $phid; } } return array_values($files); } /** * Produce a corpus summary, in a way that shortens the underlying text * without truncating it somewhere awkward. * * TODO: We could do a better job of this. * * @param string Remarkup corpus to summarize. * @return string Summarized corpus. */ public static function summarize($corpus) { // Major goals here are: // - Don't split in the middle of a character (utf-8). // - Don't split in the middle of, e.g., **bold** text, since // we end up with hanging '**' in the summary. // - Try not to pick an image macro, header, embedded file, etc. // - Hopefully don't return too much text. We don't explicitly limit // this right now. $blocks = preg_split("/\n *\n\s*/", $corpus); $best = null; foreach ($blocks as $block) { // This is a test for normal spaces in the block, i.e. a heuristic to // distinguish standard paragraphs from things like image macros. It may // not work well for non-latin text. We prefer to summarize with a // paragraph of normal words over an image macro, if possible. $has_space = preg_match('/\w\s\w/', $block); // This is a test to find embedded images and headers. We prefer to // summarize with a normal paragraph over a header or an embedded object, // if possible. $has_embed = preg_match('/^[{=]/', $block); if ($has_space && !$has_embed) { // This seems like a good summary, so return it. return $block; } if (!$best) { // This is the first block we found; if everything is garbage just // use the first block. $best = $block; } } return $best; } private static function loadCustomInlineRules() { return id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorRemarkupCustomInlineRule') ->loadObjects(); } private static function loadCustomBlockRules() { return id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorRemarkupCustomBlockRule') ->loadObjects(); } } diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php index 7c4fefcac6..f062bb1b5a 100644 --- a/src/view/form/control/AphrontFormPolicyControl.php +++ b/src/view/form/control/AphrontFormPolicyControl.php @@ -1,327 +1,327 @@ object = $object; return $this; } public function setPolicies(array $policies) { assert_instances_of($policies, 'PhabricatorPolicy'); $this->policies = $policies; return $this; } public function setSpacePHID($space_phid) { $this->spacePHID = $space_phid; return $this; } public function getSpacePHID() { return $this->spacePHID; } public function setTemplatePHIDType($type) { $this->templatePHIDType = $type; return $this; } public function setTemplateObject($object) { $this->templateObject = $object; return $this; } public function setCapability($capability) { $this->capability = $capability; $labels = array( PhabricatorPolicyCapability::CAN_VIEW => pht('Visible To'), PhabricatorPolicyCapability::CAN_EDIT => pht('Editable By'), PhabricatorPolicyCapability::CAN_JOIN => pht('Joinable By'), ); if (isset($labels[$capability])) { $label = $labels[$capability]; } else { $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if ($capobj) { $label = $capobj->getCapabilityName(); } else { $label = pht('Capability "%s"', $capability); } } $this->setLabel($label); return $this; } protected function getCustomControlClass() { return 'aphront-form-control-policy'; } protected function getOptions() { $capability = $this->capability; $policies = $this->policies; // Exclude object policies which don't make sense here. This primarily // filters object policies associated from template capabilities (like // "Default Task View Policy" being set to "Task Author") so they aren't // made available on non-template capabilities (like "Can Bulk Edit"). foreach ($policies as $key => $policy) { if ($policy->getType() != PhabricatorPolicyType::TYPE_OBJECT) { continue; } $rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy->getPHID()); if (!$rule) { continue; } $target = nonempty($this->templateObject, $this->object); if (!$rule->canApplyToObject($target)) { unset($policies[$key]); continue; } } $options = array(); foreach ($policies as $policy) { if ($policy->getPHID() == PhabricatorPolicies::POLICY_PUBLIC) { // Never expose "Public" for capabilities which don't support it. $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) { continue; } } $policy_short_name = id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(28) ->truncateString($policy->getName()); $options[$policy->getType()][$policy->getPHID()] = array( 'name' => $policy_short_name, 'full' => $policy->getName(), 'icon' => $policy->getIcon(), ); } // If we were passed several custom policy options, throw away the ones // which aren't the value for this capability. For example, an object might // have a custom view pollicy and a custom edit policy. When we render // the selector for "Can View", we don't want to show the "Can Edit" // custom policy -- if we did, the menu would look like this: // // Custom // Custom Policy // Custom Policy // // ...where one is the "view" custom policy, and one is the "edit" custom // policy. $type_custom = PhabricatorPolicyType::TYPE_CUSTOM; if (!empty($options[$type_custom])) { $options[$type_custom] = array_select_keys( $options[$type_custom], array($this->getValue())); } // If there aren't any custom policies, add a placeholder policy so we // render a menu item. This allows the user to switch to a custom policy. if (empty($options[$type_custom])) { $placeholder = new PhabricatorPolicy(); $placeholder->setName(pht('Custom Policy...')); $options[$type_custom][$this->getCustomPolicyPlaceholder()] = array( 'name' => $placeholder->getName(), 'full' => $placeholder->getName(), 'icon' => $placeholder->getIcon(), ); } $options = array_select_keys( $options, array( PhabricatorPolicyType::TYPE_GLOBAL, PhabricatorPolicyType::TYPE_OBJECT, PhabricatorPolicyType::TYPE_USER, PhabricatorPolicyType::TYPE_CUSTOM, PhabricatorPolicyType::TYPE_PROJECT, )); return $options; } protected function renderInput() { if (!$this->object) { - throw new Exception(pht('Call setPolicyObject() before rendering!')); + throw new PhutilInvalidStateException('setPolicyObject'); } if (!$this->capability) { - throw new Exception(pht('Call setCapability() before rendering!')); + throw new PhutilInvalidStateException('setCapability'); } $policy = $this->object->getPolicy($this->capability); if (!$policy) { // TODO: Make this configurable. $policy = PhabricatorPolicies::POLICY_USER; } if (!$this->getValue()) { $this->setValue($policy); } $control_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); $caret = phutil_tag( 'span', array( 'class' => 'caret', )); $input = phutil_tag( 'input', array( 'type' => 'hidden', 'id' => $input_id, 'name' => $this->getName(), 'value' => $this->getValue(), )); $options = $this->getOptions(); $order = array(); $labels = array(); foreach ($options as $key => $values) { $order[$key] = array_keys($values); $labels[$key] = PhabricatorPolicyType::getPolicyTypeName($key); } $flat_options = array_mergev($options); $icons = array(); foreach (igroup($flat_options, 'icon') as $icon => $ignored) { $icons[$icon] = id(new PHUIIconView()) ->setIconFont($icon); } if ($this->templatePHIDType) { $context_path = 'template/'.$this->templatePHIDType.'/'; } else { $object_phid = $this->object->getPHID(); if ($object_phid) { $context_path = 'object/'.$object_phid.'/'; } else { $object_type = phid_get_type($this->object->generatePHID()); $context_path = 'type/'.$object_type.'/'; } } Javelin::initBehavior( 'policy-control', array( 'controlID' => $control_id, 'inputID' => $input_id, 'options' => $flat_options, 'groups' => array_keys($options), 'order' => $order, 'icons' => $icons, 'labels' => $labels, 'value' => $this->getValue(), 'capability' => $this->capability, 'editURI' => '/policy/edit/'.$context_path, 'customPlaceholder' => $this->getCustomPolicyPlaceholder(), )); $selected = idx($flat_options, $this->getValue(), array()); $selected_icon = idx($selected, 'icon'); $selected_name = idx($selected, 'name'); $spaces_control = $this->buildSpacesControl(); return phutil_tag( 'div', array( ), array( $spaces_control, javelin_tag( 'a', array( 'class' => 'grey button dropdown has-icon policy-control', 'href' => '#', 'mustcapture' => true, 'sigil' => 'policy-control', 'id' => $control_id, ), array( $caret, javelin_tag( 'span', array( 'sigil' => 'policy-label', 'class' => 'phui-button-text', ), array( idx($icons, $selected_icon), $selected_name, )), )), $input, )); return AphrontFormSelectControl::renderSelectTag( $this->getValue(), $this->getOptions(), array( 'name' => $this->getName(), 'disabled' => $this->getDisabled() ? 'disabled' : null, 'id' => $this->getID(), )); } private function getCustomPolicyPlaceholder() { return 'custom:placeholder'; } private function buildSpacesControl() { if ($this->capability != PhabricatorPolicyCapability::CAN_VIEW) { return null; } if (!($this->object instanceof PhabricatorSpacesInterface)) { return null; } $viewer = $this->getUser(); if (!PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) { return null; } $space_phid = $this->getSpacePHID(); if ($space_phid === null) { $space_phid = $viewer->getDefaultSpacePHID(); } $select = AphrontFormSelectControl::renderSelectTag( $space_phid, PhabricatorSpacesNamespaceQuery::getSpaceOptionsForViewer( $viewer, $space_phid), array( 'name' => 'spacePHID', )); return $select; } } diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php index 841c1006e5..fd25f66a1f 100644 --- a/src/view/form/control/AphrontFormTokenizerControl.php +++ b/src/view/form/control/AphrontFormTokenizerControl.php @@ -1,131 +1,134 @@ datasource = $datasource; return $this; } public function setDisableBehavior($disable) { $this->disableBehavior = $disable; return $this; } protected function getCustomControlClass() { return 'aphront-form-control-tokenizer'; } public function setLimit($limit) { $this->limit = $limit; return $this; } public function setPlaceholder($placeholder) { $this->placeholder = $placeholder; return $this; } public function willRender() { // Load the handles now so we'll get a bulk load later on when we actually // render them. $this->loadHandles(); } protected function renderInput() { $name = $this->getName(); $handles = $this->loadHandles(); $handles = iterator_to_array($handles); if ($this->getID()) { $id = $this->getID(); } else { $id = celerity_generate_unique_node_id(); } $datasource = $this->datasource; if (!$datasource) { throw new Exception( pht('You must set a datasource to use a TokenizerControl.')); } $datasource->setViewer($this->getUser()); $placeholder = null; if (!strlen($this->placeholder)) { $placeholder = $datasource->getPlaceholderText(); } $values = nonempty($this->getValue(), array()); $tokens = $datasource->renderTokens($values); foreach ($tokens as $token) { $token->setInputName($this->getName()); } $template = new AphrontTokenizerTemplateView(); $template->setName($name); $template->setID($id); $template->setValue($tokens); $username = null; if ($this->user) { $username = $this->user->getUsername(); } $datasource_uri = $datasource->getDatasourceURI(); $browse_uri = $datasource->getBrowseURI(); if ($browse_uri) { $template->setBrowseURI($browse_uri); } if (!$this->disableBehavior) { Javelin::initBehavior('aphront-basic-tokenizer', array( 'id' => $id, 'src' => $datasource_uri, 'value' => mpull($tokens, 'getValue', 'getKey'), 'icons' => mpull($tokens, 'getIcon', 'getKey'), 'types' => mpull($tokens, 'getTokenType', 'getKey'), 'colors' => mpull($tokens, 'getColor', 'getKey'), 'limit' => $this->limit, 'username' => $username, 'placeholder' => $placeholder, 'browseURI' => $browse_uri, )); } return $template->render(); } private function loadHandles() { if ($this->handles === null) { $viewer = $this->getUser(); if (!$viewer) { throw new Exception( pht( - 'Call setUser() before rendering tokenizers. Use appendControl() '. - 'on AphrontFormView to do this easily.')); + 'Call %s before rendering tokenizers. '. + 'Use %s on %s to do this easily.', + 'setUser()', + 'appendControl()', + 'AphrontFormView')); } $values = nonempty($this->getValue(), array()); $phids = array(); foreach ($values as $value) { if (!PhabricatorTypeaheadDatasource::isFunctionToken($value)) { $phids[] = $value; } } $this->handles = $viewer->loadHandles($phids); } return $this->handles; } } diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php index fe8025026b..5e85f89254 100644 --- a/src/view/layout/AphrontSideNavFilterView.php +++ b/src/view/layout/AphrontSideNavFilterView.php @@ -1,326 +1,326 @@ 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; private $iconNav; 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(PHUICrumbsView $crumbs) { $this->crumbs = $crumbs; return $this; } public function getCrumbs() { return $this->crumbs; } public function setIconNav($nav) { $this->iconNav = $nav; return $this; } 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 addIcon($key, $name, $icon, $image = null, $uri = null) { if (!$uri) { $href = clone $this->baseURI; $href->setPath(rtrim($href->getPath().$key, '/').'/'); $href = (string)$href; } else { $href = $uri; } $item = id(new PHUIListItemView()) ->setKey($key) ->setRenderNameAsTooltip(true) ->setType(PHUIListItemView::TYPE_ICON_NAV) ->setIcon($icon) ->setHref($href) ->setName($name) ->setProfileImage($image); return $this->addMenuItem($item); } public function addButton($key, $name, $uri = null) { return $this->addThing( $key, $name, $uri, PHUIListItemView::TYPE_BUTTON); } private function addThing($key, $name, $uri, $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()!')); + throw new PhutilInvalidStateException('setBaseURI'); } if ($this->selectedFilter === false) { - throw new Exception(pht('Call selectFilter() before render()!')); + throw new PhutilInvalidStateException('selectFilter'); } } 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'; if ($this->iconNav) { $nav_classes[] = 'phabricator-icon-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 plb', 'id' => $content_id, ), array( $crumbs, $this->renderChildren(), )), )); } } diff --git a/src/view/layout/PhabricatorActionListView.php b/src/view/layout/PhabricatorActionListView.php index 6db62c129e..82449951d7 100644 --- a/src/view/layout/PhabricatorActionListView.php +++ b/src/view/layout/PhabricatorActionListView.php @@ -1,66 +1,66 @@ object = $object; return $this; } public function setObjectURI($uri) { $this->objectURI = $uri; return $this; } public function addAction(PhabricatorActionView $view) { $this->actions[] = $view; return $this; } public function setID($id) { $this->id = $id; return $this; } public function render() { if (!$this->user) { - throw new Exception(pht('Call setUser() before render()!')); + throw new PhutilInvalidStateException('setUser'); } $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, ), $actions); } } diff --git a/src/view/phui/PHUITagView.php b/src/view/phui/PHUITagView.php index 8f49caa82a..439feb77c5 100644 --- a/src/view/phui/PHUITagView.php +++ b/src/view/phui/PHUITagView.php @@ -1,253 +1,253 @@ type = $type; switch ($type) { case self::TYPE_SHADE: break; case self::TYPE_OBJECT: $this->setBackgroundColor(self::COLOR_OBJECT); break; case self::TYPE_PERSON: $this->setBackgroundColor(self::COLOR_PERSON); break; } return $this; } public function setShade($shade) { $this->shade = $shade; return $this; } public function setDotColor($dot_color) { $this->dotColor = $dot_color; return $this; } public function setBackgroundColor($background_color) { $this->backgroundColor = $background_color; return $this; } public function setPHID($phid) { $this->phid = $phid; return $this; } public function setName($name) { $this->name = $name; return $this; } public function setHref($href) { $this->href = $href; return $this; } public function setClosed($closed) { $this->closed = $closed; return $this; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function setSlimShady($mm) { $this->slimShady = $mm; return $this; } protected function getTagName() { return strlen($this->href) ? 'a' : 'span'; } protected function getTagAttributes() { require_celerity_resource('phui-tag-view-css'); $classes = array( 'phui-tag-view', 'phui-tag-type-'.$this->type, ); if ($this->shade) { $classes[] = 'phui-tag-shade'; $classes[] = 'phui-tag-shade-'.$this->shade; if ($this->slimShady) { $classes[] = 'phui-tag-shade-slim'; } } if ($this->icon) { $classes[] = 'phui-tag-icon-view'; } if ($this->phid) { Javelin::initBehavior('phabricator-hovercards'); $attributes = array( 'href' => $this->href, 'sigil' => 'hovercard', 'meta' => array( 'hoverPHID' => $this->phid, ), 'target' => $this->external ? '_blank' : null, ); } else { $attributes = array( 'href' => $this->href, 'target' => $this->external ? '_blank' : null, ); } return $attributes + array('class' => $classes); } protected function getTagContent() { if (!$this->type) { - throw new Exception(pht('You must call setType() before render()!')); + throw new PhutilInvalidStateException('setType', 'render'); } $color = null; if (!$this->shade && $this->backgroundColor) { $color = 'phui-tag-color-'.$this->backgroundColor; } if ($this->dotColor) { $dotcolor = 'phui-tag-color-'.$this->dotColor; $dot = phutil_tag( 'span', array( 'class' => 'phui-tag-dot '.$dotcolor, ), ''); } else { $dot = null; } if ($this->icon) { $icon = id(new PHUIIconView()) ->setIconFont($this->icon); } else { $icon = null; } $content = phutil_tag( 'span', array( 'class' => 'phui-tag-core '.$color, ), array($dot, $icon, $this->name)); if ($this->closed) { $content = phutil_tag( 'span', array( 'class' => 'phui-tag-core-closed', ), array($icon, $content)); } return $content; } public static function getTagTypes() { return array( self::TYPE_PERSON, self::TYPE_OBJECT, self::TYPE_STATE, ); } public static function getColors() { return array( self::COLOR_RED, self::COLOR_ORANGE, self::COLOR_YELLOW, self::COLOR_BLUE, self::COLOR_INDIGO, self::COLOR_VIOLET, self::COLOR_GREEN, self::COLOR_BLACK, self::COLOR_GREY, self::COLOR_WHITE, self::COLOR_OBJECT, self::COLOR_PERSON, ); } public static function getShades() { return array_keys(self::getShadeMap()); } public static function getShadeMap() { return array( self::COLOR_RED => pht('Red'), self::COLOR_ORANGE => pht('Orange'), self::COLOR_YELLOW => pht('Yellow'), self::COLOR_BLUE => pht('Blue'), self::COLOR_INDIGO => pht('Indigo'), self::COLOR_VIOLET => pht('Violet'), self::COLOR_GREEN => pht('Green'), self::COLOR_GREY => pht('Grey'), self::COLOR_PINK => pht('Pink'), self::COLOR_CHECKERED => pht('Checkered'), self::COLOR_DISABLED => pht('Disabled'), ); } public static function getShadeName($shade) { return idx(self::getShadeMap(), $shade, $shade); } public function setExternal($external) { $this->external = $external; return $this; } public function getExternal() { return $this->external; } }