diff --git a/resources/sql/autopatches/20141025.phriction.1.xaction.sql b/resources/sql/autopatches/20141025.phriction.1.xaction.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20141025.phriction.1.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_phriction.phriction_transaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) COLLATE utf8_bin NOT NULL, + authorPHID VARCHAR(64) COLLATE utf8_bin NOT NULL, + objectPHID VARCHAR(64) COLLATE utf8_bin NOT NULL, + viewPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL, + editPolicy VARCHAR(64) COLLATE utf8_bin NOT NULL, + commentPHID VARCHAR(64) COLLATE utf8_bin DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE utf8_bin NOT NULL, + oldValue LONGTEXT COLLATE utf8_bin NOT NULL, + newValue LONGTEXT COLLATE utf8_bin NOT NULL, + contentSource LONGTEXT COLLATE utf8_bin NOT NULL, + metadata LONGTEXT COLLATE utf8_bin NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/resources/sql/autopatches/20141025.phriction.2.xaction.sql b/resources/sql/autopatches/20141025.phriction.2.xaction.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20141025.phriction.2.xaction.sql @@ -0,0 +1,16 @@ +CREATE TABLE {$NAMESPACE}_phriction.phriction_transaction_comment ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + transactionPHID VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, + authorPHID VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + viewPolicy VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + editPolicy VARCHAR(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + commentVersion INT UNSIGNED NOT NULL, + content LONGTEXT CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + contentSource LONGTEXT CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + isDeleted TINYINT(1) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + UNIQUE KEY `key_version` (`transactionPHID`,`commentVersion`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 diff --git a/resources/sql/autopatches/20141025.phriction.mailkey.sql b/resources/sql/autopatches/20141025.phriction.mailkey.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20141025.phriction.mailkey.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_phriction.phriction_document + ADD mailKey VARCHAR(20) NOT NULL COLLATE utf8_bin; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2771,9 +2771,14 @@ 'PhrictionMoveController' => 'applications/phriction/controller/PhrictionMoveController.php', 'PhrictionNewController' => 'applications/phriction/controller/PhrictionNewController.php', 'PhrictionRemarkupRule' => 'applications/phriction/markup/PhrictionRemarkupRule.php', + 'PhrictionReplyHandler' => 'applications/phriction/mail/PhrictionReplyHandler.php', 'PhrictionSchemaSpec' => 'applications/phriction/storage/PhrictionSchemaSpec.php', 'PhrictionSearchEngine' => 'applications/phriction/query/PhrictionSearchEngine.php', 'PhrictionSearchIndexer' => 'applications/phriction/search/PhrictionSearchIndexer.php', + 'PhrictionTransaction' => 'applications/phriction/storage/PhrictionTransaction.php', + 'PhrictionTransactionComment' => 'applications/phriction/storage/PhrictionTransactionComment.php', + 'PhrictionTransactionEditor' => 'applications/phriction/editor/PhrictionTransactionEditor.php', + 'PhrictionTransactionQuery' => 'applications/phriction/query/PhrictionTransactionQuery.php', 'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php', 'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php', 'PonderAnswerCommentController' => 'applications/ponder/controller/PonderAnswerCommentController.php', @@ -5948,9 +5953,14 @@ 'PhrictionMoveController' => 'PhrictionController', 'PhrictionNewController' => 'PhrictionController', 'PhrictionRemarkupRule' => 'PhutilRemarkupRule', + 'PhrictionReplyHandler' => 'PhabricatorMailReplyHandler', 'PhrictionSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhrictionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhrictionSearchIndexer' => 'PhabricatorSearchDocumentIndexer', + 'PhrictionTransaction' => 'PhabricatorApplicationTransaction', + 'PhrictionTransactionComment' => 'PhabricatorApplicationTransactionComment', + 'PhrictionTransactionEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhrictionTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PonderAddAnswerView' => 'AphrontView', 'PonderAnswer' => array( 'PonderDAO', diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php --- a/src/applications/phriction/controller/PhrictionEditController.php +++ b/src/applications/phriction/controller/PhrictionEditController.php @@ -73,14 +73,8 @@ return new Aphront404Response(); } } - $document = new PhrictionDocument(); - $document->setSlug($slug); - - $content = new PhrictionContent(); - $content->setSlug($slug); - - $default_title = PhabricatorSlug::getDefaultTitle($slug); - $content->setTitle($default_title); + $document = PhrictionDocument::initializeNewDocument($user, $slug); + $content = $document->getContent(); } } @@ -174,13 +168,21 @@ } if (!count($errors)) { - $editor = id(PhrictionDocumentEditor::newForSlug($document->getSlug())) - ->setActor($user) - ->setTitle($title) - ->setContent($request->getStr('content')) - ->setDescription($notes); - $editor->save(); + $xactions = array(); + $xactions[] = id(new PhrictionTransaction()) + ->setTransactionType(PhrictionTransaction::TYPE_TITLE) + ->setNewValue($title); + $xactions[] = id(new PhrictionTransaction()) + ->setTransactionType(PhrictionTransaction::TYPE_CONTENT) + ->setNewValue($request->getStr('content')); + + $editor = id(new PhrictionTransactionEditor()) + ->setActor($user) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setDescription($notes) + ->applyTransactions($document, $xactions); if ($draft) { $draft->delete(); diff --git a/src/applications/phriction/editor/PhrictionDocumentEditor.php b/src/applications/phriction/editor/PhrictionDocumentEditor.php --- a/src/applications/phriction/editor/PhrictionDocumentEditor.php +++ b/src/applications/phriction/editor/PhrictionDocumentEditor.php @@ -106,7 +106,7 @@ return $this->execute(PhrictionChangeType::CHANGE_DELETE, true); } - private function stub() { + public function stub() { return $this->execute(PhrictionChangeType::CHANGE_STUB, true); } diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php new file mode 100644 --- /dev/null +++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php @@ -0,0 +1,298 @@ +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 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; + + /* TODO + $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(); + } + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhrictionTransaction::TYPE_TITLE: + case PhrictionTransaction::TYPE_CONTENT: + return $xaction->getNewValue(); + } + } + + protected function shouldApplyInitialEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhrictionTransaction::TYPE_TITLE: + case PhrictionTransaction::TYPE_CONTENT: + 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: + $object->setStatus(PhrictionDocumentStatus::STATUS_EXISTS); + return; + } + } + + 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; + 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: + $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()) { + // Stub out empty parent documents if they don't exist + $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug()); + if ($ancestral_slugs) { + $ancestors = id(new PhrictionDocument())->loadAllWhere( + 'slug IN (%Ls)', + $ancestral_slugs); + $ancestors = mpull($ancestors, null, 'getSlug'); + foreach ($ancestral_slugs as $slug) { + // We check for change type to prevent near-infinite recursion + if (!isset($ancestors[$slug]) && + $content->getChangeType() != + PhrictionChangeType::CHANGE_STUB) { + id(PhrictionDocumentEditor::newForSlug($slug)) + ->setActor($this->getActor()) + ->setTitle(PhabricatorSlug::getDefaultTitle($slug)) + ->setContent('') + ->setDescription(pht('Empty Parent Document')) + ->stub(); + } + } + } + } + return $xactions; + } + + protected function shouldSendMail( + PhabricatorLiskDAO $object, + array $xactions) { + + $xactions = mfilter($xactions, 'shouldHide', true); + return $xactions; + } + + 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."), + ); + } + + 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()); + } + + $body->addTextSection( + 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); + // TODO - once the editor supports moves, we'll need to surface the + // "from document phid" to related phids. + return $phids; + } + + protected function supportsSearch() { + return true; + } + + protected function shouldApplyHeraldRules( + PhabricatorLiskDAO $object, + array $xactions) { + return false; + } + + private function buildNewContentTemplate( + PhrictionDocument $document) { + + $new_content = new PhrictionContent(); + $new_content->setSlug($document->getSlug()); + $new_content->setAuthorPHID($this->getActor()->getPHID()); + $new_content->setChangeType(PhrictionChangeType::CHANGE_EDIT); + + $new_content->setTitle($this->getOldContent()->getTitle()); + $new_content->setContent($this->getOldContent()->getContent()); + + if (strlen($this->getDescription())) { + $new_content->setDescription($this->getDescription()); + } + $new_content->setVersion($this->getOldContent()->getVersion() + 1); + + return $new_content; + } + +} diff --git a/src/applications/phriction/mail/PhrictionReplyHandler.php b/src/applications/phriction/mail/PhrictionReplyHandler.php new file mode 100644 --- /dev/null +++ b/src/applications/phriction/mail/PhrictionReplyHandler.php @@ -0,0 +1,41 @@ +getDefaultPrivateReplyHandlerEmailAddress( + $handle, + PhrictionDocumentPHIDType::TYPECONST); + } + + public function getPublicReplyHandlerEmailAddress() { + return $this->getDefaultPublicReplyHandlerEmailAddress( + PhrictionDocumentPHIDType::TYPECONST); + } + + public function getReplyHandlerDomain() { + return PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain'); + } + + public function getReplyHandlerInstructions() { + if ($this->supportsReplies()) { + // TODO: Implement. + return null; + } else { + return null; + } + } + + protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { + // TODO: Implement. + return null; + } + +} diff --git a/src/applications/phriction/query/PhrictionTransactionQuery.php b/src/applications/phriction/query/PhrictionTransactionQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/phriction/query/PhrictionTransactionQuery.php @@ -0,0 +1,10 @@ +setSlug($slug); + + $content = new PhrictionContent(); + $content->setSlug($slug); + + $default_title = PhabricatorSlug::getDefaultTitle($slug); + $content->setTitle($default_title); + $document->attachContent($content); + + return $document; + } + + public function save() { + if (!$this->getMailKey()) { + $this->setMailKey(Filesystem::readRandomCharacters(20)); + } + return parent::save(); + } + public static function getSlugURI($slug, $type = 'document') { static $types = array( 'document' => '/w/', diff --git a/src/applications/phriction/storage/PhrictionTransaction.php b/src/applications/phriction/storage/PhrictionTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/phriction/storage/PhrictionTransaction.php @@ -0,0 +1,186 @@ +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 getActionStrength() { + switch ($this->getTransactionType()) { + case self::TYPE_TITLE: + return 1.4; + case self::TYPE_CONTENT: + return 1.3; + } + + return parent::getActionStrength(); + } + + public function getActionName() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_TITLE: + if ($old === null) { + return pht('Created'); + } + + return pht('Retitled'); + + case self::TYPE_CONTENT: + return pht('Edited'); + + } + + 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'; + } + + 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) { + 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)); + + } + + return parent::getTitle(); + } + + public function getTitleForFeed(PhabricatorFeedStory $story) { + $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)); + + } + return parent::getTitleForFeed($story); + } + + 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; + } + return $tags; + } + +} diff --git a/src/applications/phriction/storage/PhrictionTransactionComment.php b/src/applications/phriction/storage/PhrictionTransactionComment.php new file mode 100644 --- /dev/null +++ b/src/applications/phriction/storage/PhrictionTransactionComment.php @@ -0,0 +1,10 @@ +