} public static function newForSlug($slug) { $slug = PhabricatorSlug::normalize($slug); $document = id(new PhrictionDocument())->loadOneWhere( 'slug = %s', $slug); $content = null; if ($document) { $content = id(new PhrictionContent())->load($document->getContentID()); } else { $document = new PhrictionDocument(); $document->setSlug($slug); } if (!$content) { $default_title = PhabricatorSlug::getDefaultTitle($slug); $content = new PhrictionContent(); $content->setSlug($slug); $content->setTitle($default_title); $content->setContent(''); } $obj = new PhrictionDocumentEditor(); $obj->document = $document; $obj->content = $content; return $obj; } public function setTitle($title) { $this->newTitle = $title; return $this; } public function setContent($content) { $this->newContent = $content; return $this; } public function setDescription($description) { $this->description = $description; return $this; } public function getDocument() { return $this->document; } public function moveAway($new_doc_id) { return $this->execute( PhrictionChangeType::CHANGE_MOVE_AWAY, true, $new_doc_id); } public function moveHere($old_doc_id, $old_doc_phid) { $this->fromDocumentPHID = $old_doc_phid; return $this->execute( PhrictionChangeType::CHANGE_MOVE_HERE, false, $old_doc_id); } private function execute( $change_type, $del_new_content = true, $doc_ref = null) { $actor = $this->requireActor(); $document = $this->document; $content = $this->content; $new_content = $this->buildContentTemplate($document, $content); $new_content->setChangeType($change_type); if ($del_new_content) { $new_content->setContent(''); } if ($doc_ref) { $new_content->setChangeRef($doc_ref); } return $this->updateDocument($document, $content, $new_content); } public function delete() { return $this->execute(PhrictionChangeType::CHANGE_DELETE, true); } private function stub() { return $this->execute(PhrictionChangeType::CHANGE_STUB, true); } public function save() { $actor = $this->requireActor(); if ($this->newContent === '') { // If this is an edit which deletes all the content, just treat it as // a delete. NOTE: null means "don't change the content", not "delete // the page"! Thus the strict type check. return $this->delete(); } $document = $this->document; $content = $this->content; $new_content = $this->buildContentTemplate($document, $content); return $this->updateDocument($document, $content, $new_content); } private function buildContentTemplate( PhrictionDocument $document, PhrictionContent $content) { $new_content = new PhrictionContent(); $new_content->setSlug($document->getSlug()); $new_content->setAuthorPHID($this->getActor()->getPHID()); $new_content->setChangeType(PhrictionChangeType::CHANGE_EDIT); $new_content->setTitle( coalesce( $this->newTitle, $content->getTitle())); $new_content->setContent( coalesce( $this->newContent, $content->getContent())); if (strlen($this->description)) { $new_content->setDescription($this->description); } return $new_content; } private function updateDocument($document, $content, $new_content) { $is_new = false; if (!$document->getID()) { $is_new = true; } $new_content->setVersion($content->getVersion() + 1); $change_type = $new_content->getChangeType(); switch ($change_type) { case PhrictionChangeType::CHANGE_EDIT: $doc_status = PhrictionDocumentStatus::STATUS_EXISTS; $feed_action = $is_new ? PhrictionActionConstants::ACTION_CREATE : PhrictionActionConstants::ACTION_EDIT; break; case PhrictionChangeType::CHANGE_DELETE: $doc_status = PhrictionDocumentStatus::STATUS_DELETED; $feed_action = PhrictionActionConstants::ACTION_DELETE; if ($is_new) { throw new Exception( "You can not delete a document which doesn't exist yet!"); } break; case PhrictionChangeType::CHANGE_STUB: $doc_status = PhrictionDocumentStatus::STATUS_STUB; $feed_action = null; break; case PhrictionChangeType::CHANGE_MOVE_AWAY: $doc_status = PhrictionDocumentStatus::STATUS_MOVED; $feed_action = null; break; case PhrictionChangeType::CHANGE_MOVE_HERE: $doc_status = PhrictionDocumentStatus::STATUS_EXISTS; $feed_action = PhrictionActionConstants::ACTION_MOVE_HERE; break; default: throw new Exception( "Unsupported content change type '{$change_type}'!"); } $document->setStatus($doc_status); // TODO: This should be transactional. if ($is_new) { $document->save(); } $new_content->setDocumentID($document->getID()); $new_content->save(); $document->setContentID($new_content->getID()); $document->save(); $document->attachContent($new_content); id(new PhabricatorSearchIndexer()) ->indexDocumentByPHID($document->getPHID()); // Stub out empty parent documents if they don't exist $ancestral_slugs = PhabricatorSlug::getAncestry($document->getSlug()); if ($ancestral_slugs) { $ancestors = id(new PhrictionDocument())->loadAllWhere( 'slug IN (%Ls)', $ancestral_slugs); $ancestors = mpull($ancestors, null, 'getSlug'); foreach ($ancestral_slugs as $slug) { // We check for change type to prevent near-infinite recursion if (!isset($ancestors[$slug]) && $new_content->getChangeType() != PhrictionChangeType::CHANGE_STUB) { id(PhrictionDocumentEditor::newForSlug($slug)) ->setActor($this->getActor()) ->setTitle(PhabricatorSlug::getDefaultTitle($slug)) ->setContent('') ->setDescription(pht('Empty Parent Document')) ->stub(); } } } $project_phid = null; $slug = $document->getSlug(); if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($this->requireActor()) ->withPhrictionSlugs(array( PhrictionDocument::getProjectSlugIdentifier($slug))) ->executeOne(); if ($project) { $project_phid = $project->getPHID(); } } $related_phids = array( $document->getPHID(), $this->getActor()->getPHID(), ); if ($project_phid) { $related_phids[] = $project_phid; } if ($this->fromDocumentPHID) { $related_phids[] = $this->fromDocumentPHID; } if ($feed_action) { id(new PhabricatorFeedStoryPublisher()) ->setRelatedPHIDs($related_phids) ->setStoryAuthorPHID($this->getActor()->getPHID()) ->setStoryTime(time()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_PHRICTION) ->setStoryData( array( 'phid' => $document->getPHID(), 'action' => $feed_action, 'content' => phutil_utf8_shorten($new_content->getContent(), 140), 'project' => $project_phid, 'movedFromPHID' => $this->fromDocumentPHID, )) ->publish(); } // TODO: Migrate to ApplicationTransactions fast, so we get rid of this code $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $document->getPHID()); $this->sendMailToSubscribers($subscribers, $content); return $this; } private function getChangeTypeDescription($const, $title) { $map = array( PhrictionChangeType::CHANGE_EDIT => pht("Phriction Document %s was edited.", $title), PhrictionChangeType::CHANGE_DELETE => pht("Phriction Document %s was deleted.", $title), PhrictionChangeType::CHANGE_MOVE_HERE => pht("Phriction Document %s was moved here.", $title), PhrictionChangeType::CHANGE_MOVE_AWAY => pht("Phriction Document %s was moved away.", $title), PhrictionChangeType::CHANGE_STUB => pht("Phriction Document %s was created through child.", $title), ); return idx($map, $const, pht('Something magical occurred.')); } private function sendMailToSubscribers(array $subscribers, $old_content) { if (!$subscribers) { return; } $author_phid = $this->getActor()->getPHID(); $document = $this->document; $content = $document->getContent(); $slug_uri = PhrictionDocument::getSlugURI($document->getSlug()); $diff_uri = new PhutilURI('/phriction/diff/'.$document->getID().'/'); $prod_uri = PhabricatorEnv::getProductionURI(''); $vs_head = $diff_uri ->alter('l', $old_content->getVersion()) ->alter('r', $content->getVersion()); $old_title = $old_content->getTitle(); $title = $content->getTitle(); $name = $this->getChangeTypeDescription($content->getChangeType(), $title); $action = PhrictionChangeType::getChangeTypeLabel( $content->getChangeType()); $body = array($name); // Content may have changed, you never know if ($content->getChangeType() == PhrictionChangeType::CHANGE_EDIT) { if ($old_title != $title) { $body[] = pht('Title was changed from "%s" to "%s"', $old_title, $title); } $body[] = pht("Link to new version:\n%s", $prod_uri.$slug_uri.'?v='.$content->getVersion()); $body[] = pht("Link to diff:\n%s", $prod_uri.$vs_head); } else if ($content->getChangeType() == PhrictionChangeType::CHANGE_MOVE_AWAY) { $target_document = id(new PhrictionDocument()) ->load($content->getChangeRef()); $slug_uri = PhrictionDocument::getSlugURI($target_document->getSlug()); $body[] = pht("Link to destination document:\n%s", $prod_uri.$slug_uri); } $body = implode("\n\n", $body); $subject_prefix = $this->getMailSubjectPrefix(); $mail = new PhabricatorMetaMTAMail(); $mail->setSubject($name) ->setSubjectPrefix($subject_prefix) ->setVarySubjectPrefix('['.$action.']') ->addHeader('Thread-Topic', $name) ->setFrom($author_phid) ->addTos($subscribers) ->setBody($body) ->setRelatedPHID($document->getPHID()) ->setIsBulk(true); $mail->saveAndSend(); } /* --( For less copy-pasting when switching to ApplicationTransactions )--- */ protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.phriction.subject-prefix'); } }