diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php index 69fdf7c02c..51118cc834 100644 --- a/src/applications/feed/story/PhabricatorFeedStory.php +++ b/src/applications/feed/story/PhabricatorFeedStory.php @@ -1,334 +1,379 @@ List of @{class:PhabricatorFeedStoryData} rows from the * database. * @return list List of @{class:PhabricatorFeedStory} * objects. * @task load */ public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) { $stories = array(); $data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows); foreach ($data as $story_data) { $class = $story_data->getStoryType(); try { $ok = class_exists($class) && is_subclass_of($class, 'PhabricatorFeedStory'); } catch (PhutilMissingSymbolException $ex) { $ok = false; } // If the story type isn't a valid class or isn't a subclass of // PhabricatorFeedStory, decline to load it. if (!$ok) { continue; } $key = $story_data->getChronologicalKey(); $stories[$key] = newv($class, array($story_data)); } $object_phids = array(); $key_phids = array(); foreach ($stories as $key => $story) { $phids = array(); foreach ($story->getRequiredObjectPHIDs() as $phid) { $phids[$phid] = true; } if ($story->getPrimaryObjectPHID()) { $phids[$story->getPrimaryObjectPHID()] = true; } $key_phids[$key] = $phids; $object_phids += $phids; } $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($object_phids)) ->execute(); foreach ($key_phids as $key => $phids) { if (!$phids) { continue; } $story_objects = array_select_keys($objects, array_keys($phids)); if (count($story_objects) != count($phids)) { // An object this story requires either does not exist or is not visible // to the user. Decline to render the story. unset($stories[$key]); unset($key_phids[$key]); continue; } $stories[$key]->setObjects($story_objects); } $handle_phids = array(); foreach ($stories as $key => $story) { foreach ($story->getRequiredHandlePHIDs() as $phid) { $key_phids[$key][$phid] = true; } if ($story->getAuthorPHID()) { $key_phids[$key][$story->getAuthorPHID()] = true; } $handle_phids += $key_phids[$key]; } $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($handle_phids)) ->execute(); foreach ($key_phids as $key => $phids) { if (!$phids) { continue; } $story_handles = array_select_keys($handles, array_keys($phids)); $stories[$key]->setHandles($story_handles); } return $stories; } public function setHovercard($hover) { $this->hovercard = $hover; return $this; } + public function setRenderingTarget($target) { + $this->validateRenderingTarget($target); + $this->renderingTarget = $target; + return $this; + } + + public function getRenderingTarget() { + return $this->renderingTarget; + } + + private function validateRenderingTarget($target) { + switch ($target) { + case PhabricatorApplicationTransaction::TARGET_HTML: + case PhabricatorApplicationTransaction::TARGET_TEXT: + break; + default: + throw new Exception('Unknown rendering target: '.$target); + break; + } + } + public function setObjects(array $objects) { $this->objects = $objects; return $this; } public function getObject($phid) { $object = idx($this->objects, $phid); if (!$object) { throw new Exception( "Story is asking for an object it did not request ('{$phid}')!"); } return $object; } public function getPrimaryObject() { $phid = $this->getPrimaryObjectPHID(); if (!$phid) { throw new Exception("Story has no primary object!"); } return $this->getObject($phid); } public function getPrimaryObjectPHID() { return null; } final public function __construct(PhabricatorFeedStoryData $data) { $this->data = $data; } abstract public function renderView(); public function getRequiredHandlePHIDs() { return array(); } public function getRequiredObjectPHIDs() { return array(); } public function setHasViewed($has_viewed) { $this->hasViewed = $has_viewed; return $this; } public function getHasViewed() { return $this->hasViewed; } final public function setFramed($framed) { $this->framed = $framed; return $this; } final public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } final protected function getObjects() { return $this->objects; } final protected function getHandles() { return $this->handles; } final protected function getHandle($phid) { if (isset($this->handles[$phid])) { if ($this->handles[$phid] instanceof PhabricatorObjectHandle) { return $this->handles[$phid]; } } $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setName("Unloaded Object '{$phid}'"); return $handle; } final public function getStoryData() { return $this->data; } final public function getEpoch() { return $this->getStoryData()->getEpoch(); } final public function getChronologicalKey() { return $this->getStoryData()->getChronologicalKey(); } final public function getValue($key, $default = null) { return $this->getStoryData()->getValue($key, $default); } final public function getAuthorPHID() { return $this->getStoryData()->getAuthorPHID(); } final protected function renderHandleList(array $phids) { - $list = array(); + $items = array(); foreach ($phids as $phid) { - $list[] = $this->linkTo($phid); + $items[] = $this->linkTo($phid); + } + $list = null; + switch ($this->getRenderingTarget()) { + case PhabricatorApplicationTransaction::TARGET_TEXT: + $list = implode(', ', $items); + break; + case PhabricatorApplicationTransaction::TARGET_HTML: + $list = phutil_implode_html(', ', $items); + break; } - return phutil_implode_html(', ', $list); + return $list; } final protected function linkTo($phid) { $handle = $this->getHandle($phid); + switch ($this->getRenderingTarget()) { + case PhabricatorApplicationTransaction::TARGET_TEXT: + return $handle->getLinkName(); + } + // NOTE: We render our own link here to customize the styling and add // the '_top' target for framed feeds. $class = null; if ($handle->getType() == PhabricatorPeoplePHIDTypeUser::TYPECONST) { $class = 'phui-link-person'; } return javelin_tag( 'a', array( 'href' => $handle->getURI(), 'target' => $this->framed ? '_top' : null, 'sigil' => $this->hovercard ? 'hovercard' : null, 'meta' => $this->hovercard ? array('hoverPHID' => $phid) : null, 'class' => $class, ), $handle->getLinkName()); } final protected function renderString($str) { - return phutil_tag('strong', array(), $str); + switch ($this->getRenderingTarget()) { + case PhabricatorApplicationTransaction::TARGET_TEXT: + return $str; + case PhabricatorApplicationTransaction::TARGET_HTML: + return phutil_tag('strong', array(), $str); + } } final protected function renderSummary($text, $len = 128) { if ($len) { $text = phutil_utf8_shorten($text, $len); } - $text = phutil_escape_html_newlines($text); + switch ($this->getRenderingTarget()) { + case PhabricatorApplicationTransaction::TARGET_HTML: + $text = phutil_escape_html_newlines($text); + break; + } return $text; } public function getNotificationAggregations() { return array(); } protected function newStoryView() { return id(new PHUIFeedStoryView()) ->setChronologicalKey($this->getChronologicalKey()) ->setEpoch($this->getEpoch()) ->setViewed($this->getHasViewed()); } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getPHID() { return null; } /** * @task policy */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } /** * @task policy */ public function getPolicy($capability) { // If this story's primary object is a policy-aware object, use its policy // to control story visiblity. $primary_phid = $this->getPrimaryObjectPHID(); if (isset($this->objects[$primary_phid])) { $object = $this->objects[$primary_phid]; if ($object instanceof PhabricatorPolicyInterface) { return $object->getPolicy($capability); } } // TODO: Remove this once all objects are policy-aware. For now, keep // respecting the `feed.public` setting. return PhabricatorEnv::getEnvConfig('feed.public') ? PhabricatorPolicies::POLICY_PUBLIC : PhabricatorPolicies::POLICY_USER; } /** * @task policy */ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php index 181f1df0c5..2ca12c1846 100644 --- a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php +++ b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php @@ -1,266 +1,266 @@ getRequest(); $user = $request->getUser(); $task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($request->getStr('taskID'))) ->executeOne(); if (!$task) { return new Aphront404Response(); } $task_uri = '/'.$task->getMonogram(); $transactions = array(); $action = $request->getStr('action'); // If we have drag-and-dropped files, attach them first in a separate // transaction. These can come in on any transaction type, which is why we // handle them separately. $files = array(); // Look for drag-and-drop uploads first. $file_phids = $request->getArr('files'); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setViewer($user) ->withPHIDs(array($file_phids)) ->execute(); } // This means "attach a file" even though we store other types of data // as 'attached'. if ($action == ManiphestTransaction::TYPE_ATTACH) { if (!empty($_FILES['file'])) { $err = idx($_FILES['file'], 'error'); if ($err != UPLOAD_ERR_NO_FILE) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['file'], array( 'authorPHID' => $user->getPHID(), )); $files[] = $file; } } } // If we had explicit or drag-and-drop files, create a transaction // for those before we deal with whatever else might have happened. $file_transaction = null; if ($files) { $files = mpull($files, 'getPHID', 'getPHID'); $new = $task->getAttached(); foreach ($files as $phid) { if (empty($new[PhabricatorFilePHIDTypeFile::TYPECONST])) { $new[PhabricatorFilePHIDTypeFile::TYPECONST] = array(); } $new[PhabricatorFilePHIDTypeFile::TYPECONST][$phid] = array(); } $transaction = new ManiphestTransaction(); $transaction ->setTransactionType(ManiphestTransaction::TYPE_ATTACH); $transaction->setNewValue($new); $transactions[] = $transaction; } // Compute new CCs added by @mentions. Several things can cause CCs to // be added as side effects: mentions, explicit CCs, users who aren't // CC'd interacting with the task, and ownership changes. We build up a // list of all the CCs and then construct a transaction for them at the // end if necessary. $added_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions( array( $request->getStr('comments'), )); $cc_transaction = new ManiphestTransaction(); $cc_transaction ->setTransactionType(ManiphestTransaction::TYPE_CCS); $transaction = new ManiphestTransaction(); $transaction ->setTransactionType($action); switch ($action) { case ManiphestTransaction::TYPE_STATUS: $transaction->setNewValue($request->getStr('resolution')); break; case ManiphestTransaction::TYPE_OWNER: $assign_to = $request->getArr('assign_to'); $assign_to = reset($assign_to); $transaction->setNewValue($assign_to); break; case ManiphestTransaction::TYPE_PROJECTS: $projects = $request->getArr('projects'); $projects = array_merge($projects, $task->getProjectPHIDs()); $projects = array_filter($projects); $projects = array_unique($projects); $transaction->setNewValue($projects); break; case ManiphestTransaction::TYPE_CCS: // Accumulate the new explicit CCs into the array that we'll add in // the CC transaction later. $added_ccs = array_merge($added_ccs, $request->getArr('ccs')); // Throw away the primary transaction. $transaction = null; break; case ManiphestTransaction::TYPE_PRIORITY: $transaction->setNewValue($request->getInt('priority')); break; case ManiphestTransaction::TYPE_ATTACH: // Nuke this, we created it above. $transaction = null; break; case PhabricatorTransactions::TYPE_COMMENT: // Nuke this, we're going to create it below. $transaction = null; break; default: throw new Exception('unknown action'); } if ($transaction) { $transactions[] = $transaction; } $resolution = $request->getStr('resolution'); $did_scuttle = false; if ($action !== ManiphestTransaction::TYPE_STATUS) { if ($request->getStr('scuttle')) { $transactions[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_STATUS) ->setNewValue(ManiphestTaskStatus::getDefaultClosedStatus()); $did_scuttle = true; $resolution = ManiphestTaskStatus::getDefaultClosedStatus(); } } // When you interact with a task, we add you to the CC list so you get // further updates, and possibly assign the task to you if you took an // ownership action (closing it) but it's currently unowned. We also move // previous owners to CC if ownership changes. Detect all these conditions // and create side-effect transactions for them. $implicitly_claimed = false; if ($action == ManiphestTransaction::TYPE_OWNER) { if ($task->getOwnerPHID() == $transaction->getNewValue()) { // If this is actually no-op, don't generate the side effect. - break; + } else { + // Otherwise, when a task is reassigned, move the previous owner to CC. + $added_ccs[] = $task->getOwnerPHID(); } - // Otherwise, when a task is reassigned, move the previous owner to CC. - $added_ccs[] = $task->getOwnerPHID(); } if ($did_scuttle || ($action == ManiphestTransaction::TYPE_STATUS)) { if (!$task->getOwnerPHID() && ManiphestTaskStatus::isClosedStatus($resolution)) { // Closing an unassigned task. Assign the user as the owner of // this task. $assign = new ManiphestTransaction(); $assign->setTransactionType(ManiphestTransaction::TYPE_OWNER); $assign->setNewValue($user->getPHID()); $transactions[] = $assign; $implicitly_claimed = true; } } $user_owns_task = false; if ($implicitly_claimed) { $user_owns_task = true; } else { if ($action == ManiphestTransaction::TYPE_OWNER) { if ($transaction->getNewValue() == $user->getPHID()) { $user_owns_task = true; } } else if ($task->getOwnerPHID() == $user->getPHID()) { $user_owns_task = true; } } if (!$user_owns_task) { // If we aren't making the user the new task owner and they aren't the // existing task owner, add them to CC unless they're aleady CC'd. if (!in_array($user->getPHID(), $task->getCCPHIDs())) { $added_ccs[] = $user->getPHID(); } } // Evade no-effect detection in the new editor stuff until we can switch // to subscriptions. $added_ccs = array_filter(array_diff($added_ccs, $task->getCCPHIDs())); if ($added_ccs) { // We've added CCs, so include a CC transaction. $all_ccs = array_merge($task->getCCPHIDs(), $added_ccs); $cc_transaction->setNewValue($all_ccs); $transactions[] = $cc_transaction; } $comments = $request->getStr('comments'); if (strlen($comments) || !$transactions) { $transactions[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(new ManiphestTransactionComment()) ->setContent($comments)); } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'new' => false, '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) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect($request->isContinueRequest()); try { $editor->applyTransactions($task, $transactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($task_uri) ->setException($ex); } $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), $task->getPHID()); if ($draft) { $draft->delete(); } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array( 'task' => $task, 'new' => false, 'transactions' => $transactions, )); $event->setUser($user); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); return id(new AphrontRedirectResponse())->setURI($task_uri); } } diff --git a/src/applications/tokens/feed/PhabricatorTokenGivenFeedStory.php b/src/applications/tokens/feed/PhabricatorTokenGivenFeedStory.php index f8a89d968b..831f2cd507 100644 --- a/src/applications/tokens/feed/PhabricatorTokenGivenFeedStory.php +++ b/src/applications/tokens/feed/PhabricatorTokenGivenFeedStory.php @@ -1,54 +1,56 @@ getValue('objectPHID'); } public function getRequiredHandlePHIDs() { $phids = array(); $phids[] = $this->getValue('objectPHID'); $phids[] = $this->getValue('authorPHID'); return $phids; } public function getRequiredObjectPHIDs() { $phids = array(); $phids[] = $this->getValue('tokenPHID'); return $phids; } public function renderView() { $view = $this->newStoryView(); $view->setAppIcon('token-dark'); $author_phid = $this->getValue('authorPHID'); $href = $this->getHandle($this->getPrimaryObjectPHID())->getURI(); $view->setHref($href); - $token = $this->getObject($this->getValue('tokenPHID')); + $view->setTitle($this->renderTitle()); + $view->setImage($this->getHandle($author_phid)->getImageURI()); + + return $view; + } + private function renderTitle() { + $token = $this->getObject($this->getValue('tokenPHID')); $title = pht( '%s awarded %s a %s token.', $this->linkTo($this->getValue('authorPHID')), $this->linkTo($this->getValue('objectPHID')), $token->getName()); - $view->setTitle($title); - $view->setImage($this->getHandle($author_phid)->getImageURI()); - - return $view; + return $title; } public function renderText() { - // TODO: This is grotesque; the feed notification handler relies on it. - return htmlspecialchars_decode( - strip_tags( - hsprintf( - '%s', - $this->renderView()->getTitle()))); + $old_target = $this->getRenderingTarget(); + $this->setRenderingTarget(PhabricatorApplicationTransaction::TARGET_TEXT); + $title = $this->renderTitle(); + $this->setRenderingTarget($old_target); + return $title; } } diff --git a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php index c6916ce38b..34945abdc6 100644 --- a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php +++ b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php @@ -1,76 +1,77 @@ getValue('objectPHID'); } public function getRequiredObjectPHIDs() { return $this->getValue('transactionPHIDs'); } public function getRequiredHandlePHIDs() { $phids = array(); $phids[] = $this->getValue('objectPHID'); foreach ($this->getValue('transactionPHIDs') as $xaction_phid) { $xaction = $this->getObject($xaction_phid); foreach ($xaction->getRequiredHandlePHIDs() as $handle_phid) { $phids[] = $handle_phid; } } return $phids; } protected function getPrimaryTransactionPHID() { return head($this->getValue('transactionPHIDs')); } protected function getPrimaryTransaction() { return $this->getObject($this->getPrimaryTransactionPHID()); } public function renderView() { $view = $this->newStoryView(); $handle = $this->getHandle($this->getPrimaryObjectPHID()); $view->setHref($handle->getURI()); $view->setAppIconFromPHID($handle->getPHID()); $xaction_phids = $this->getValue('transactionPHIDs'); $xaction = $this->getPrimaryTransaction(); $xaction->setHandles($this->getHandles()); $view->setTitle($xaction->getTitleForFeed($this)); foreach ($xaction_phids as $xaction_phid) { $secondary_xaction = $this->getObject($xaction_phid); $secondary_xaction->setHandles($this->getHandles()); $body = $secondary_xaction->getBodyForFeed($this); if (nonempty($body)) { $view->appendChild($body); } } $view->setImage( $this->getHandle($xaction->getAuthorPHID())->getImageURI()); return $view; } public function renderText() { - // TODO: This is grotesque; the feed notification handler relies on it. - return htmlspecialchars_decode( - strip_tags( - hsprintf( - '%s', - $this->renderView()->getTitle()))); + $xaction = $this->getPrimaryTransaction(); + $old_target = $xaction->getRenderingTarget(); + $new_target = PhabricatorApplicationTransaction::TARGET_TEXT; + $xaction->setRenderingTarget($new_target); + $text = $xaction->getTitleForFeed($this); + $xaction->setRenderingTarget($old_target); + return $text; } }