diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php index b276aaf34a..5d1534df19 100644 --- a/src/applications/phriction/editor/PhrictionTransactionEditor.php +++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php @@ -1,807 +1,811 @@ description = $description; return $this; } private function getDescription() { return $this->description; } private function setOldContent(PhrictionContent $content) { $this->oldContent = $content; return $this; } private function getOldContent() { return $this->oldContent; } private function setNewContent(PhrictionContent $content) { $this->newContent = $content; return $this; } private function getNewContent() { return $this->newContent; } public function setSkipAncestorCheck($bool) { $this->skipAncestorCheck = $bool; return $this; } public function getSkipAncestorCheck() { return $this->skipAncestorCheck; } public function setContentVersion($version) { $this->contentVersion = $version; return $this; } public function getContentVersion() { return $this->contentVersion; } public function setProcessContentVersionError($process) { $this->processContentVersionError = $process; return $this; } public function getProcessContentVersionError() { return $this->processContentVersionError; } public function getEditorApplicationClass() { return 'PhabricatorPhrictionApplication'; } public function getEditorObjectsDescription() { return pht('Phriction Documents'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhrictionTransaction::TYPE_TITLE; $types[] = PhrictionTransaction::TYPE_CONTENT; $types[] = PhrictionTransaction::TYPE_DELETE; $types[] = PhrictionTransaction::TYPE_MOVE_TO; $types[] = PhrictionTransaction::TYPE_MOVE_AWAY; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: if ($this->getIsNewObject()) { return null; } return $this->getOldContent()->getTitle(); case PhrictionTransaction::TYPE_CONTENT: if ($this->getIsNewObject()) { return null; } return $this->getOldContent()->getContent(); case PhrictionTransaction::TYPE_DELETE: case PhrictionTransaction::TYPE_MOVE_TO: case PhrictionTransaction::TYPE_MOVE_AWAY: return null; } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: case PhrictionTransaction::TYPE_DELETE: return $xaction->getNewValue(); case PhrictionTransaction::TYPE_MOVE_TO: $document = $xaction->getNewValue(); // grab the real object now for the sub-editor to come $this->moveAwayDocument = $document; $dict = array( 'id' => $document->getID(), 'phid' => $document->getPHID(), 'content' => $document->getContent()->getContent(), 'title' => $document->getContent()->getTitle(), ); return $dict; case PhrictionTransaction::TYPE_MOVE_AWAY: $document = $xaction->getNewValue(); $dict = array( 'id' => $document->getID(), 'phid' => $document->getPHID(), 'content' => $document->getContent()->getContent(), 'title' => $document->getContent()->getTitle(), ); return $dict; } } protected function shouldApplyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: case PhrictionTransaction::TYPE_DELETE: case PhrictionTransaction::TYPE_MOVE_TO: case PhrictionTransaction::TYPE_MOVE_AWAY: return true; } } return parent::shouldApplyInitialEffects($object, $xactions); } protected function applyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { $this->setOldContent($object->getContent()); $this->setNewContent($this->buildNewContentTemplate($object)); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: case PhrictionTransaction::TYPE_MOVE_TO: $object->setStatus(PhrictionDocumentStatus::STATUS_EXISTS); return; case PhrictionTransaction::TYPE_MOVE_AWAY: $object->setStatus(PhrictionDocumentStatus::STATUS_MOVED); return; case PhrictionTransaction::TYPE_DELETE: $object->setStatus(PhrictionDocumentStatus::STATUS_DELETED); return; } } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $xactions = parent::expandTransaction($object, $xaction); switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_CONTENT: if ($this->getIsNewObject()) { break; } $content = $xaction->getNewValue(); if ($content === '') { $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionTransaction::TYPE_DELETE) ->setNewValue(true) ->setMetadataValue('contentDelete', true); } break; case PhrictionTransaction::TYPE_MOVE_TO: $document = $xaction->getNewValue(); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($document->getViewPolicy()); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($document->getEditPolicy()); break; default: break; } return $xactions; } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: $this->getNewContent()->setTitle($xaction->getNewValue()); break; case PhrictionTransaction::TYPE_CONTENT: $this->getNewContent()->setContent($xaction->getNewValue()); break; case PhrictionTransaction::TYPE_DELETE: $this->getNewContent()->setContent(''); $this->getNewContent()->setChangeType( PhrictionChangeType::CHANGE_DELETE); break; case PhrictionTransaction::TYPE_MOVE_TO: $dict = $xaction->getNewValue(); $this->getNewContent()->setContent($dict['content']); $this->getNewContent()->setTitle($dict['title']); $this->getNewContent()->setChangeType( PhrictionChangeType::CHANGE_MOVE_HERE); $this->getNewContent()->setChangeRef($dict['id']); break; case PhrictionTransaction::TYPE_MOVE_AWAY: $dict = $xaction->getNewValue(); $this->getNewContent()->setContent(''); $this->getNewContent()->setChangeType( PhrictionChangeType::CHANGE_MOVE_AWAY); $this->getNewContent()->setChangeRef($dict['id']); break; default: break; } } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { $save_content = false; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: case PhrictionTransaction::TYPE_DELETE: case PhrictionTransaction::TYPE_MOVE_AWAY: case PhrictionTransaction::TYPE_MOVE_TO: $save_content = true; break; default: break; } } if ($save_content) { $content = $this->getNewContent(); $content->setDocumentID($object->getID()); $content->save(); $object->setContentID($content->getID()); $object->save(); $object->attachContent($content); } if ($this->getIsNewObject() && !$this->getSkipAncestorCheck()) { // Stub out empty parent documents if they don't exist $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug()); if ($ancestral_slugs) { $ancestors = id(new PhrictionDocumentQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withSlugs($ancestral_slugs) ->needContent(true) ->execute(); $ancestors = mpull($ancestors, null, 'getSlug'); $stub_type = PhrictionChangeType::CHANGE_STUB; foreach ($ancestral_slugs as $slug) { $ancestor_doc = idx($ancestors, $slug); // We check for change type to prevent near-infinite recursion if (!$ancestor_doc && $content->getChangeType() != $stub_type) { $ancestor_doc = PhrictionDocument::initializeNewDocument( $this->getActor(), $slug); $stub_xactions = array(); $stub_xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionTransaction::TYPE_TITLE) ->setNewValue(PhabricatorSlug::getDefaultTitle($slug)) ->setMetadataValue('stub:create:phid', $object->getPHID()); $stub_xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionTransaction::TYPE_CONTENT) ->setNewValue('') ->setMetadataValue('stub:create:phid', $object->getPHID()); $stub_xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($object->getViewPolicy()); $stub_xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($object->getEditPolicy()); $sub_editor = id(new PhrictionTransactionEditor()) ->setActor($this->getActor()) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect($this->getContinueOnNoEffect()) ->setSkipAncestorCheck(true) ->setDescription(pht('Empty Parent Document')) ->applyTransactions($ancestor_doc, $stub_xactions); } } } } if ($this->moveAwayDocument !== null) { $move_away_xactions = array(); $move_away_xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionTransaction::TYPE_MOVE_AWAY) ->setNewValue($object); $sub_editor = id(new PhrictionTransactionEditor()) ->setActor($this->getActor()) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect($this->getContinueOnNoEffect()) ->setDescription($this->getDescription()) ->applyTransactions($this->moveAwayDocument, $move_away_xactions); } // Compute the content diff URI for the publishing phase. foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_CONTENT: $uri = id(new PhutilURI('/phriction/diff/'.$object->getID().'/')) ->alter('l', $this->getOldContent()->getVersion()) ->alter('r', $this->getNewContent()->getVersion()); $this->contentDiffURI = (string)$uri; break 2; default: break; } } return $xactions; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailSubjectPrefix() { return '[Phriction]'; } protected function getMailTo(PhabricatorLiskDAO $object) { return array( $object->getContent()->getAuthorPHID(), $this->getActingAsPHID(), ); } public function getMailTagsMap() { return array( PhrictionTransaction::MAILTAG_TITLE => pht("A document's title changes."), PhrictionTransaction::MAILTAG_CONTENT => pht("A document's content changes."), PhrictionTransaction::MAILTAG_DELETE => pht('A document is deleted.'), + PhrictionTransaction::MAILTAG_SUBSCRIBERS => + pht('A document\'s subscribers change.'), + PhrictionTransaction::MAILTAG_OTHER => + pht('Other document activity not listed above occurs.'), ); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PhrictionReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getContent()->getTitle(); return id(new PhabricatorMetaMTAMail()) ->setSubject($title) ->addHeader('Thread-Topic', $object->getPHID()); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); if ($this->getIsNewObject()) { $body->addTextSection( pht('DOCUMENT CONTENT'), $object->getContent()->getContent()); } else if ($this->contentDiffURI) { $body->addLinkSection( pht('DOCUMENT DIFF'), PhabricatorEnv::getProductionURI($this->contentDiffURI)); } $body->addLinkSection( pht('DOCUMENT DETAIL'), PhabricatorEnv::getProductionURI( PhrictionDocument::getSlugURI($object->getSlug()))); return $body; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldSendMail($object, $xactions); } protected function getFeedRelatedPHIDs( PhabricatorLiskDAO $object, array $xactions) { $phids = parent::getFeedRelatedPHIDs($object, $xactions); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_MOVE_TO: $dict = $xaction->getNewValue(); $phids[] = $dict['phid']; break; } } return $phids; } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); foreach ($xactions as $xaction) { switch ($type) { case PhrictionTransaction::TYPE_TITLE: $title = $object->getContent()->getTitle(); $missing = $this->validateIsEmptyTextField( $title, $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Document title is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } else if ($this->getProcessContentVersionError()) { $error = $this->validateContentVersion($object, $type, $xaction); if ($error) { $this->setProcessContentVersionError(false); $errors[] = $error; } } break; case PhrictionTransaction::TYPE_CONTENT: if ($xaction->getMetadataValue('stub:create:phid')) { continue; } $missing = false; if ($this->getIsNewObject()) { $content = $object->getContent()->getContent(); $missing = $this->validateIsEmptyTextField( $content, $xactions); } if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Document content is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } else if ($this->getProcessContentVersionError()) { $error = $this->validateContentVersion($object, $type, $xaction); if ($error) { $this->setProcessContentVersionError(false); $errors[] = $error; } } if ($this->getIsNewObject()) { $ancestry_errors = $this->validateAncestry( $object, $type, $xaction, self::VALIDATE_CREATE_ANCESTRY); if ($ancestry_errors) { $errors = array_merge($errors, $ancestry_errors); } } break; case PhrictionTransaction::TYPE_MOVE_TO: $source_document = $xaction->getNewValue(); switch ($source_document->getStatus()) { case PhrictionDocumentStatus::STATUS_DELETED: $e_text = pht('A deleted document can not be moved.'); break; case PhrictionDocumentStatus::STATUS_MOVED: $e_text = pht('A moved document can not be moved again.'); break; case PhrictionDocumentStatus::STATUS_STUB: $e_text = pht('A stub document can not be moved.'); break; default: $e_text = null; break; } if ($e_text) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Can not move document.'), $e_text, $xaction); $errors[] = $error; } $ancestry_errors = $this->validateAncestry( $object, $type, $xaction, self::VALIDATE_MOVE_ANCESTRY); if ($ancestry_errors) { $errors = array_merge($errors, $ancestry_errors); } $target_document = id(new PhrictionDocumentQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withSlugs(array($object->getSlug())) ->needContent(true) ->executeOne(); // Prevent overwrites and no-op moves. $exists = PhrictionDocumentStatus::STATUS_EXISTS; if ($target_document) { if ($target_document->getSlug() == $source_document->getSlug()) { $message = pht( 'You can not move a document to its existing location. '. 'Choose a different location to move the document to.'); } else if ($target_document->getStatus() == $exists) { $message = pht( 'You can not move this document there, because it would '. 'overwrite an existing document which is already at that '. 'location. Move or delete the existing document first.'); } $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), $message, $xaction); $errors[] = $error; } break; case PhrictionTransaction::TYPE_DELETE: switch ($object->getStatus()) { case PhrictionDocumentStatus::STATUS_DELETED: if ($xaction->getMetadataValue('contentDelete')) { $e_text = pht( 'This document is already deleted. You must specify '. 'content to re-create the document and make further edits.'); } else { $e_text = pht( 'An already deleted document can not be deleted.'); } break; case PhrictionDocumentStatus::STATUS_MOVED: $e_text = pht('A moved document can not be deleted.'); break; case PhrictionDocumentStatus::STATUS_STUB: $e_text = pht('A stub document can not be deleted.'); break; default: break 2; } $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Can not delete document.'), $e_text, $xaction); $errors[] = $error; break; } } return $errors; } private function validateAncestry( PhabricatorLiskDAO $object, $type, PhabricatorApplicationTransaction $xaction, $verb) { $errors = array(); // NOTE: We use the ominpotent user for these checks because policy // doesn't matter; existence does. $other_doc_viewer = PhabricatorUser::getOmnipotentUser(); $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug()); if ($ancestral_slugs) { $ancestors = id(new PhrictionDocumentQuery()) ->setViewer($other_doc_viewer) ->withSlugs($ancestral_slugs) ->execute(); $ancestors = mpull($ancestors, null, 'getSlug'); foreach ($ancestral_slugs as $slug) { $ancestor_doc = idx($ancestors, $slug); if (!$ancestor_doc) { $create_uri = '/phriction/edit/?slug='.$slug; $create_link = phutil_tag( 'a', array( 'href' => $create_uri, ), $slug); switch ($verb) { case self::VALIDATE_MOVE_ANCESTRY: $message = pht( 'Can not move document because the parent document with '. 'slug %s does not exist!', $create_link); break; case self::VALIDATE_CREATE_ANCESTRY: $message = pht( 'Can not create document because the parent document with '. 'slug %s does not exist!', $create_link); break; } $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Missing Ancestor'), $message, $xaction); $errors[] = $error; } } } return $errors; } private function validateContentVersion( PhabricatorLiskDAO $object, $type, PhabricatorApplicationTransaction $xaction) { $error = null; if ($this->getContentVersion() && ($object->getContent()->getVersion() != $this->getContentVersion())) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Edit Conflict'), pht( 'Another user made changes to this document after you began '. 'editing it. Do you want to overwrite their changes? '. '(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.)'), $xaction); } return $error; } protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { /* * New objects have a special case. If a user can't see * x/y * then definitely don't let them make some * x/y/z * We need to load the direct parent to handle this case. */ if ($this->getIsNewObject()) { $actor = $this->requireActor(); $parent_doc = null; $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug()); // No ancestral slugs is "/"; the first person gets to play with "/". if ($ancestral_slugs) { $parent = end($ancestral_slugs); $parent_doc = id(new PhrictionDocumentQuery()) ->setViewer($actor) ->withSlugs(array($parent)) ->executeOne(); // If the $actor can't see the $parent_doc then they can't create // the child $object; throw a policy exception. if (!$parent_doc) { id(new PhabricatorPolicyFilter()) ->setViewer($actor) ->raisePolicyExceptions(true) ->rejectObject( $object, $object->getEditPolicy(), PhabricatorPolicyCapability::CAN_EDIT); } // If the $actor can't edit the $parent_doc then they can't create // the child $object; throw a policy exception. if (!PhabricatorPolicyFilter::hasCapability( $actor, $parent_doc, PhabricatorPolicyCapability::CAN_EDIT)) { id(new PhabricatorPolicyFilter()) ->setViewer($actor) ->raisePolicyExceptions(true) ->rejectObject( $object, $object->getEditPolicy(), PhabricatorPolicyCapability::CAN_EDIT); } } } return parent::requireCapabilities($object, $xaction); } protected function supportsSearch() { return true; } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new PhrictionDocumentHeraldAdapter()) ->setDocument($object); } private function buildNewContentTemplate( PhrictionDocument $document) { $new_content = id(new PhrictionContent()) ->setSlug($document->getSlug()) ->setAuthorPHID($this->getActor()->getPHID()) ->setChangeType(PhrictionChangeType::CHANGE_EDIT) ->setTitle($this->getOldContent()->getTitle()) ->setContent($this->getOldContent()->getContent()); if (strlen($this->getDescription())) { $new_content->setDescription($this->getDescription()); } $new_content->setVersion($this->getOldContent()->getVersion() + 1); return $new_content; } protected function getCustomWorkerState() { return array( 'contentDiffURI' => $this->contentDiffURI, ); } protected function loadCustomWorkerState(array $state) { $this->contentDiffURI = idx($state, 'contentDiffURI'); return $this; } } diff --git a/src/applications/phriction/storage/PhrictionTransaction.php b/src/applications/phriction/storage/PhrictionTransaction.php index 069e147017..a3ae6adb9d 100644 --- a/src/applications/phriction/storage/PhrictionTransaction.php +++ b/src/applications/phriction/storage/PhrictionTransaction.php @@ -1,288 +1,295 @@ getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_MOVE_TO: case self::TYPE_MOVE_AWAY: $phids[] = $new['phid']; break; case self::TYPE_TITLE: if ($this->getMetadataValue('stub:create:phid')) { $phids[] = $this->getMetadataValue('stub:create:phid'); } break; } return $phids; } public function getRemarkupBlocks() { $blocks = parent::getRemarkupBlocks(); switch ($this->getTransactionType()) { case self::TYPE_CONTENT: $blocks[] = $this->getNewValue(); break; } return $blocks; } public function shouldHide() { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: if ($this->getOldValue() === null) { return true; } else { return false; } break; } return parent::shouldHide(); } public function shouldHideForMail(array $xactions) { switch ($this->getTransactionType()) { case self::TYPE_MOVE_TO: case self::TYPE_MOVE_AWAY: return true; case self::TYPE_TITLE: return $this->getMetadataValue('stub:create:phid', false); } return parent::shouldHideForMail($xactions); } public function shouldHideForFeed() { switch ($this->getTransactionType()) { case self::TYPE_MOVE_TO: case self::TYPE_MOVE_AWAY: return true; case self::TYPE_TITLE: return $this->getMetadataValue('stub:create:phid', false); } return parent::shouldHideForFeed(); } public function getActionStrength() { switch ($this->getTransactionType()) { case self::TYPE_TITLE: return 1.4; case self::TYPE_CONTENT: return 1.3; case self::TYPE_DELETE: return 1.5; case self::TYPE_MOVE_TO: case self::TYPE_MOVE_AWAY: return 1.0; } return parent::getActionStrength(); } public function getActionName() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { if ($this->getMetadataValue('stub:create:phid')) { return pht('Stubbed'); } else { return pht('Created'); } } return pht('Retitled'); case self::TYPE_CONTENT: return pht('Edited'); case self::TYPE_DELETE: return pht('Deleted'); case self::TYPE_MOVE_TO: return pht('Moved'); case self::TYPE_MOVE_AWAY: return pht('Moved Away'); } return parent::getActionName(); } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: case self::TYPE_CONTENT: return 'fa-pencil'; case self::TYPE_DELETE: return 'fa-times'; case self::TYPE_MOVE_TO: case self::TYPE_MOVE_AWAY: return 'fa-arrows'; } return parent::getIcon(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { if ($this->getMetadataValue('stub:create:phid')) { return pht( '%s stubbed out this document when creating %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink( $this->getMetadataValue('stub:create:phid'))); } else { return pht( '%s created this document.', $this->renderHandleLink($author_phid)); } } return pht( '%s changed the title from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); case self::TYPE_CONTENT: return pht( '%s edited the document content.', $this->renderHandleLink($author_phid)); case self::TYPE_DELETE: return pht( '%s deleted this document.', $this->renderHandleLink($author_phid)); case self::TYPE_MOVE_TO: return pht( '%s moved this document from %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($new['phid'])); case self::TYPE_MOVE_AWAY: return pht( '%s moved this document to %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($new['phid'])); } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } return pht( '%s renamed %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); case self::TYPE_CONTENT: return pht( '%s edited the content of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_DELETE: return pht( '%s deleted %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } return parent::getTitleForFeed(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_CONTENT: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: $tags[] = self::MAILTAG_TITLE; break; case self::TYPE_CONTENT: $tags[] = self::MAILTAG_CONTENT; break; case self::TYPE_DELETE: $tags[] = self::MAILTAG_DELETE; break; - + case PhabricatorTransactions::TYPE_SUBSCRIBERS: + $tags[] = self::MAILTAG_SUBSCRIBERS; + break; + default: + $tags[] = self::MAILTAG_OTHER; + break; } return $tags; } } diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index 4193c2e4c4..c5253da956 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -1,516 +1,518 @@ getTransactionType()) { case PhabricatorProjectTransaction::TYPE_NAME: return $object->getName(); case PhabricatorProjectTransaction::TYPE_SLUGS: $slugs = $object->getSlugs(); $slugs = mpull($slugs, 'getSlug', 'getSlug'); unset($slugs[$object->getPrimarySlug()]); return array_keys($slugs); case PhabricatorProjectTransaction::TYPE_STATUS: return $object->getStatus(); case PhabricatorProjectTransaction::TYPE_IMAGE: return $object->getProfileImagePHID(); case PhabricatorProjectTransaction::TYPE_ICON: return $object->getIcon(); case PhabricatorProjectTransaction::TYPE_COLOR: return $object->getColor(); case PhabricatorProjectTransaction::TYPE_LOCKED: return (int)$object->getIsMembershipLocked(); } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorProjectTransaction::TYPE_NAME: case PhabricatorProjectTransaction::TYPE_SLUGS: case PhabricatorProjectTransaction::TYPE_STATUS: case PhabricatorProjectTransaction::TYPE_IMAGE: case PhabricatorProjectTransaction::TYPE_ICON: case PhabricatorProjectTransaction::TYPE_COLOR: case PhabricatorProjectTransaction::TYPE_LOCKED: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorProjectTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); // TODO - this is really "setPrimarySlug" $object->setPhrictionSlug($xaction->getNewValue()); return; case PhabricatorProjectTransaction::TYPE_SLUGS: return; case PhabricatorProjectTransaction::TYPE_STATUS: $object->setStatus($xaction->getNewValue()); return; case PhabricatorProjectTransaction::TYPE_IMAGE: $object->setProfileImagePHID($xaction->getNewValue()); return; case PhabricatorProjectTransaction::TYPE_ICON: $object->setIcon($xaction->getNewValue()); return; case PhabricatorProjectTransaction::TYPE_COLOR: $object->setColor($xaction->getNewValue()); return; case PhabricatorProjectTransaction::TYPE_LOCKED: $object->setIsMembershipLocked($xaction->getNewValue()); return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); switch ($xaction->getTransactionType()) { case PhabricatorProjectTransaction::TYPE_NAME: // First, add the old name as a secondary slug; this is helpful // for renames and generally a good thing to do. if ($old !== null) { $this->addSlug($object, $old); } $this->addSlug($object, $new); return; case PhabricatorProjectTransaction::TYPE_SLUGS: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add) { $add_slug_template = id(new PhabricatorProjectSlug()) ->setProjectPHID($object->getPHID()); foreach ($add as $add_slug_str) { $add_slug = id(clone $add_slug_template) ->setSlug($add_slug_str) ->save(); } } if ($rem) { $rem_slugs = id(new PhabricatorProjectSlug()) ->loadAllWhere('slug IN (%Ls)', $rem); foreach ($rem_slugs as $rem_slug) { $rem_slug->delete(); } } return; case PhabricatorProjectTransaction::TYPE_STATUS: case PhabricatorProjectTransaction::TYPE_IMAGE: case PhabricatorProjectTransaction::TYPE_ICON: case PhabricatorProjectTransaction::TYPE_COLOR: case PhabricatorProjectTransaction::TYPE_LOCKED: return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function applyBuiltinExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_EDGE: $edge_type = $xaction->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: case PhabricatorObjectHasWatcherEdgeType::EDGECONST: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); // When adding members or watchers, we add subscriptions. $add = array_keys(array_diff_key($new, $old)); // When removing members, we remove their subscription too. // When unwatching, we leave subscriptions, since it's fine to be // subscribed to a project but not be a member of it. $edge_const = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; if ($edge_type == $edge_const) { $rem = array_keys(array_diff_key($old, $new)); } else { $rem = array(); } // NOTE: The subscribe is "explicit" because there's no implicit // unsubscribe, so Join -> Leave -> Join doesn't resubscribe you // if we use an implicit subscribe, even though you never willfully // unsubscribed. Not sure if adding implicit unsubscribe (which // would not write the unsubscribe row) is justified to deal with // this, which is a fairly weird edge case and pretty arguable both // ways. // Subscriptions caused by watches should also clearly be explicit, // and that case is unambiguous. id(new PhabricatorSubscriptionsEditor()) ->setActor($this->requireActor()) ->setObject($object) ->subscribeExplicit($add) ->unsubscribe($rem) ->save(); if ($rem) { // When removing members, also remove any watches on the project. $edge_editor = new PhabricatorEdgeEditor(); foreach ($rem as $rem_phid) { $edge_editor->removeEdge( $object->getPHID(), PhabricatorObjectHasWatcherEdgeType::EDGECONST, $rem_phid); } $edge_editor->save(); } break; } break; } return parent::applyBuiltinExternalTransaction($object, $xaction); } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); switch ($type) { case PhabricatorProjectTransaction::TYPE_NAME: $missing = $this->validateIsEmptyTextField( $object->getName(), $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Project name is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } if (!$xactions) { break; } $name = last($xactions)->getNewValue(); $name_used_already = id(new PhabricatorProjectQuery()) ->setViewer($this->getActor()) ->withNames(array($name)) ->executeOne(); if ($name_used_already && ($name_used_already->getPHID() != $object->getPHID())) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Duplicate'), pht('Project name is already used.'), nonempty(last($xactions), null)); $errors[] = $error; } $slug_builder = clone $object; $slug_builder->setPhrictionSlug($name); $slug = $slug_builder->getPrimarySlug(); $slug_used_already = id(new PhabricatorProjectSlug()) ->loadOneWhere('slug = %s', $slug); if ($slug_used_already && $slug_used_already->getProjectPHID() != $object->getPHID()) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Duplicate'), pht('Project name can not be used due to hashtag collision.'), nonempty(last($xactions), null)); $errors[] = $error; } break; case PhabricatorProjectTransaction::TYPE_SLUGS: if (!$xactions) { break; } $slug_xaction = last($xactions); $new = $slug_xaction->getNewValue(); if ($new) { $slugs_used_already = id(new PhabricatorProjectSlug()) ->loadAllWhere('slug IN (%Ls)', $new); } else { // The project doesn't have any extra slugs. $slugs_used_already = array(); } $slugs_used_already = mgroup($slugs_used_already, 'getProjectPHID'); foreach ($slugs_used_already as $project_phid => $used_slugs) { $used_slug_strs = mpull($used_slugs, 'getSlug'); if ($project_phid == $object->getPHID()) { if (in_array($object->getPrimarySlug(), $used_slug_strs)) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht( 'Project hashtag %s is already the primary hashtag.', $object->getPrimarySlug()), $slug_xaction); $errors[] = $error; } continue; } $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht( '%d project hashtag(s) are already used: %s.', count($used_slug_strs), implode(', ', $used_slug_strs)), $slug_xaction); $errors[] = $error; } break; } return $errors; } protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorProjectTransaction::TYPE_NAME: case PhabricatorProjectTransaction::TYPE_STATUS: case PhabricatorProjectTransaction::TYPE_IMAGE: case PhabricatorProjectTransaction::TYPE_ICON: case PhabricatorProjectTransaction::TYPE_COLOR: PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, PhabricatorPolicyCapability::CAN_EDIT); return; case PhabricatorProjectTransaction::TYPE_LOCKED: PhabricatorPolicyFilter::requireCapability( $this->requireActor(), newv($this->getEditorApplicationClass(), array()), ProjectCanLockProjectsCapability::CAPABILITY); return; case PhabricatorTransactions::TYPE_EDGE: switch ($xaction->getMetadataValue('edge:type')) { case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $add = array_keys(array_diff_key($new, $old)); $rem = array_keys(array_diff_key($old, $new)); $actor_phid = $this->requireActor()->getPHID(); $is_join = (($add === array($actor_phid)) && !$rem); $is_leave = (($rem === array($actor_phid)) && !$add); if ($is_join) { // You need CAN_JOIN to join a project. PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, PhabricatorPolicyCapability::CAN_JOIN); } else if ($is_leave) { // You usually don't need any capabilities to leave a project. if ($object->getIsMembershipLocked()) { // you must be able to edit though to leave locked projects PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, PhabricatorPolicyCapability::CAN_EDIT); } } else { // You need CAN_EDIT to change members other than yourself. PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, PhabricatorPolicyCapability::CAN_EDIT); } return; } break; } return parent::requireCapabilities($object, $xaction); } protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { $member_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object->getPHID(), PhabricatorProjectProjectHasMemberEdgeType::EDGECONST); $object->attachMemberPHIDs($member_phids); return $object; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailSubjectPrefix() { return pht('[Project]'); } protected function getMailTo(PhabricatorLiskDAO $object) { return $object->getMemberPHIDs(); } protected function getMailCC(PhabricatorLiskDAO $object) { $all = parent::getMailCC($object); return array_diff($all, $object->getMemberPHIDs()); } public function getMailTagsMap() { return array( PhabricatorProjectTransaction::MAILTAG_METADATA => pht('Project name, hashtags, icon, image, or color changes.'), PhabricatorProjectTransaction::MAILTAG_MEMBERS => pht('Project membership changes.'), PhabricatorProjectTransaction::MAILTAG_WATCHERS => pht('Project watcher list changes.'), + PhabricatorProjectTransaction::MAILTAG_SUBSCRIBERS => + pht('Project subscribers change.'), PhabricatorProjectTransaction::MAILTAG_OTHER => pht('Other project activity not listed above occurs.'), ); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new ProjectReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) ->setSubject("{$name}") ->addHeader('Thread-Topic', "Project {$id}"); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $uri = '/project/profile/'.$object->getID().'/'; $body->addLinkSection( pht('PROJECT DETAIL'), PhabricatorEnv::getProductionURI($uri)); return $body; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function supportsSearch() { return true; } protected function extractFilePHIDsFromCustomTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorProjectTransaction::TYPE_IMAGE: $new = $xaction->getNewValue(); if ($new) { return array($new); } break; } return parent::extractFilePHIDsFromCustomTransaction($object, $xaction); } private function addSlug( PhabricatorLiskDAO $object, $name) { $object = (clone $object); $object->setPhrictionSlug($name); $slug = $object->getPrimarySlug(); $slug_object = id(new PhabricatorProjectSlug())->loadOneWhere( 'slug = %s', $slug); if ($slug_object) { return; } $new_slug = id(new PhabricatorProjectSlug()) ->setSlug($slug) ->setProjectPHID($object->getPHID()) ->save(); } } diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php index c8698dbe6b..01a458dee2 100644 --- a/src/applications/project/storage/PhabricatorProjectTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectTransaction.php @@ -1,398 +1,402 @@ getOldValue(); $new = $this->getNewValue(); $req_phids = array(); switch ($this->getTransactionType()) { case self::TYPE_MEMBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); $req_phids = array_merge($add, $rem); break; case self::TYPE_IMAGE: $req_phids[] = $old; $req_phids[] = $new; break; } return array_merge($req_phids, parent::getRequiredHandlePHIDs()); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_STATUS: if ($old == 0) { return 'red'; } else { return 'green'; } } return parent::getColor(); } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_STATUS: if ($old == 0) { return 'fa-ban'; } else { return 'fa-check'; } case self::TYPE_LOCKED: if ($new) { return 'fa-lock'; } else { return 'fa-unlock'; } case self::TYPE_ICON: return $new; case self::TYPE_IMAGE: return 'fa-photo'; case self::TYPE_MEMBERS: return 'fa-user'; case self::TYPE_SLUGS: return 'fa-tag'; } return parent::getIcon(); } public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); $author_handle = $this->renderHandleLink($this->getAuthorPHID()); switch ($this->getTransactionType()) { case self::TYPE_NAME: if ($old === null) { return pht( '%s created this project.', $author_handle); } else { return pht( '%s renamed this project from "%s" to "%s".', $author_handle, $old, $new); } break; case self::TYPE_STATUS: if ($old == 0) { return pht( '%s archived this project.', $author_handle); } else { return pht( '%s activated this project.', $author_handle); } break; case self::TYPE_IMAGE: // TODO: Some day, it would be nice to show the images. if (!$old) { return pht( "%s set this project's image to %s.", $author_handle, $this->renderHandleLink($new)); } else if (!$new) { return pht( "%s removed this project's image.", $author_handle); } else { return pht( "%s updated this project's image from %s to %s.", $author_handle, $this->renderHandleLink($old), $this->renderHandleLink($new)); } break; case self::TYPE_ICON: return pht( "%s set this project's icon to %s.", $author_handle, PhabricatorProjectIcon::getLabel($new)); break; case self::TYPE_COLOR: return pht( "%s set this project's color to %s.", $author_handle, PHUITagView::getShadeName($new)); break; case self::TYPE_LOCKED: if ($new) { return pht( "%s locked this project's membership.", $author_handle); } else { return pht( "%s unlocked this project's membership.", $author_handle); } break; case self::TYPE_SLUGS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s changed project hashtag(s), added %d: %s; removed %d: %s.', $author_handle, count($add), $this->renderSlugList($add), count($rem), $this->renderSlugList($rem)); } else if ($add) { return pht( '%s added %d project hashtag(s): %s.', $author_handle, count($add), $this->renderSlugList($add)); } else if ($rem) { return pht( '%s removed %d project hashtag(s): %s.', $author_handle, count($rem), $this->renderSlugList($rem)); } break; case self::TYPE_MEMBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s changed project member(s), added %d: %s; removed %d: %s.', $author_handle, count($add), $this->renderHandleList($add), count($rem), $this->renderHandleList($rem)); } else if ($add) { if (count($add) == 1 && (head($add) == $this->getAuthorPHID())) { return pht( '%s joined this project.', $author_handle); } else { return pht( '%s added %d project member(s): %s.', $author_handle, count($add), $this->renderHandleList($add)); } } else if ($rem) { if (count($rem) == 1 && (head($rem) == $this->getAuthorPHID())) { return pht( '%s left this project.', $author_handle); } else { return pht( '%s removed %d project member(s): %s.', $author_handle, count($rem), $this->renderHandleList($rem)); } } break; } return parent::getTitle(); } public function getTitleForFeed() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $author_handle = $this->renderHandleLink($author_phid); $object_handle = $this->renderHandleLink($object_phid); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_NAME: if ($old === null) { return pht( '%s created %s.', $author_handle, $object_handle); } else { return pht( '%s renamed %s from "%s" to "%s".', $author_handle, $object_handle, $old, $new); } case self::TYPE_STATUS: if ($old == 0) { return pht( '%s archived %s.', $author_handle, $object_handle); } else { return pht( '%s activated %s.', $author_handle, $object_handle); } case self::TYPE_IMAGE: // TODO: Some day, it would be nice to show the images. if (!$old) { return pht( '%s set the image for %s to %s.', $author_handle, $object_handle, $this->renderHandleLink($new)); } else if (!$new) { return pht( '%s removed the image for %s.', $author_handle, $object_handle); } else { return pht( '%s updated the image for %s from %s to %s.', $author_handle, $object_handle, $this->renderHandleLink($old), $this->renderHandleLink($new)); } case self::TYPE_ICON: return pht( '%s set the icon for %s to %s.', $author_handle, $object_handle, PhabricatorProjectIcon::getLabel($new)); case self::TYPE_COLOR: return pht( '%s set the color for %s to %s.', $author_handle, $object_handle, PHUITagView::getShadeName($new)); case self::TYPE_LOCKED: if ($new) { return pht( '%s locked %s membership.', $author_handle, $object_handle); } else { return pht( '%s unlocked %s membership.', $author_handle, $object_handle); } case self::TYPE_SLUGS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s changed %s hashtag(s), added %d: %s; removed %d: %s.', $author_handle, $object_handle, count($add), $this->renderSlugList($add), count($rem), $this->renderSlugList($rem)); } else if ($add) { return pht( '%s added %d %s hashtag(s): %s.', $author_handle, count($add), $object_handle, $this->renderSlugList($add)); } else if ($rem) { return pht( '%s removed %d %s hashtag(s): %s.', $author_handle, count($rem), $object_handle, $this->renderSlugList($rem)); } } return parent::getTitleForFeed(); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case self::TYPE_NAME: case self::TYPE_SLUGS: case self::TYPE_IMAGE: case self::TYPE_ICON: case self::TYPE_COLOR: $tags[] = self::MAILTAG_METADATA; break; + case PhabricatorTransactions::TYPE_SUBSCRIBERS: + $tags[] = self::MAILTAG_SUBSCRIBERS; + break; case PhabricatorTransactions::TYPE_EDGE: $type = $this->getMetadata('edge:type'); $type = head($type); $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; $type_watcher = PhabricatorObjectHasWatcherEdgeType::EDGECONST; if ($type == $type_member) { $tags[] = self::MAILTAG_MEMBERS; } else if ($type == $type_watcher) { $tags[] = self::MAILTAG_WATCHERS; } else { $tags[] = self::MAILTAG_OTHER; } break; case self::TYPE_STATUS: case self::TYPE_LOCKED: default: $tags[] = self::MAILTAG_OTHER; break; } return $tags; } private function renderSlugList($slugs) { return implode(', ', $slugs); } }