/**
  * @param Title $title
  * @param PostRevision $post
  * @param User $user
  * @param string $content
  * @param string $format wikitext|html
  * @param string $changeType
  * @return PostSummary
  */
 public static function create(Title $title, PostRevision $post, User $user, $content, $format, $changeType)
 {
     $obj = new self();
     $obj->revId = UUID::create();
     $obj->user = UserTuple::newFromUser($user);
     $obj->prevRevision = null;
     $obj->changeType = $changeType;
     $obj->summaryTargetId = $post->getPostId();
     $obj->setContent($content, $format, $title);
     return $obj;
 }
 /**
  * @param PostRevision|PostSummary $object
  * @param array $metadata
  * @return string alphadecimal uuid
  * @throws InvalidInputException When $object is not PostRevision or PostSummary
  */
 protected function findTopicRootId($object, array $metadata)
 {
     if (isset($metadata['workflow']) && $metadata['workflow'] instanceof Workflow) {
         return $metadata['workflow']->getId();
     } elseif ($object instanceof PostRevision) {
         return $object->getRootPost()->getPostId()->getAlphadecimal();
     } elseif ($object instanceof PostSummary) {
         return $object->getCollection()->getWorkflowId()->getAlphadecimal();
     } else {
         throw new InvalidInputException('Unexpected revision type: ' . get_class($object));
     }
 }
 protected function log(PostRevision $post, Workflow $workflow)
 {
     $moderationChangeTypes = self::getModerationChangeTypes();
     if (!in_array($post->getChangeType(), $moderationChangeTypes)) {
         // Do nothing for non-moderation actions
         return;
     }
     if ($this->moderationLogger->canLog($post, $post->getChangeType())) {
         $workflowId = $workflow->getId();
         $this->moderationLogger->log($post, $post->getChangeType(), $post->getModeratedReason(), $workflowId);
     }
 }
 public function somethingProvider()
 {
     return array(array('Reply recent change goes to the topic', NS_TOPIC, function ($workflow, $user) {
         $first = PostRevision::create($workflow, $user, 'blah blah', 'wikitext');
         return $first->reply($workflow, $user, 'fofofo', 'wikitext');
     }));
 }
 public function commit()
 {
     if ($this->action !== 'new-topic') {
         throw new FailCommitException('Unknown commit action', 'fail-commit');
     }
     $metadata = array('workflow' => $this->topicWorkflow, 'board-workflow' => $this->workflow, 'topic-title' => $this->topicTitle, 'first-post' => $this->firstPost);
     /*
      * Order of storage is important! We've been changing when we stored
      * workflow a couple of times. For now, it needs to be stored first:
      * * OccupationListener.php (workflow listener) must first create the
      *   board before NotificationListener.php (topic/post listeners)
      *   creates notifications (& mails) that link to the board
      * * ReferenceExtractor.php (run from ReferenceRecorder.php, a post
      *   listener) needs to parse content with Parsoid & for that it needs
      *   the board title. AbstractRevision::getContent() will figure out
      *   the title from the workflow: $this->getCollection()->getTitle()
      * If you even feel the need to change the order, make sure you come
      * up with a fix for the above things ;)
      */
     $this->storage->put($this->workflow, array());
     // 'discussion' workflow
     $this->storage->put($this->topicWorkflow, $metadata);
     // 'topic' workflow
     $this->storage->put($this->topicListEntry, $metadata);
     $this->storage->put($this->topicTitle, $metadata);
     if ($this->firstPost !== null) {
         $this->storage->put($this->firstPost, $metadata + array('reply-to' => $this->topicTitle));
     }
     $output = array('topic-page' => $this->topicWorkflow->getArticleTitle()->getPrefixedText(), 'topic-id' => $this->topicTitle->getPostId(), 'topic-revision-id' => $this->topicTitle->getRevisionId(), 'post-id' => $this->firstPost ? $this->firstPost->getPostId() : null, 'post-revision-id' => $this->firstPost ? $this->firstPost->getRevisionId() : null);
     return $output;
 }
 /**
  * @dataProvider spamProvider
  */
 public function testSpam($message, $expect, $content, $maxLength)
 {
     $title = Title::newFromText('UTPage');
     $user = User::newFromName('127.0.0.1', false);
     $workflow = Workflow::create('topic', $title);
     $topic = PostRevision::create($workflow, $user, 'title content', 'wikitext');
     $reply = $topic->reply($workflow, $user, $content, 'wikitext');
     $spamFilter = new ContentLengthFilter($maxLength);
     $status = $spamFilter->validate($this->getMock('IContextSource'), $reply, null, $title);
     $this->assertEquals($expect, $status->isOK());
 }
 /**
  * There was a bug where all anonymous users got the same
  * user links output, this checks that they are distinct.
  */
 public function testNonRepeatingUserLinksForAnonymousUsers()
 {
     $templating = $this->mockTemplating();
     $user = User::newFromName('127.0.0.1', false);
     $title = Title::newMainPage();
     $workflow = Workflow::create('topic', $title);
     $topicTitle = PostRevision::create($workflow, $user, 'some content', 'wikitext');
     $hidden = $topicTitle->moderate($user, $topicTitle::MODERATED_HIDDEN, 'hide-topic', 'hide and go seek');
     $this->assertContains('Special:Contributions/127.0.0.1', $templating->getUserLinks($hidden), 'User links should include anonymous contributions');
     $hidden = $topicTitle->moderate(User::newFromName('10.0.0.2', false), $topicTitle::MODERATED_HIDDEN, 'hide-topic', 'hide and go seek');
     $this->assertContains('Special:Contributions/10.0.0.2', $templating->getUserLinks($hidden), 'An alternate user should have the correct anonymous contributions');
 }
 public function testSetsRevIdAndPostIdForReplys()
 {
     $state = $this->createState();
     $user = User::newFromName('127.0.0.1', false);
     $title = Title::newMainPage();
     $topicWorkflow = Workflow::create('topic', $title);
     $topicTitle = PostRevision::create($topicWorkflow, $user, 'sing song', 'wikitext');
     $reply = $topicTitle->reply($topicWorkflow, $user, 'fantastic!', 'wikitext');
     $now = time();
     $state->setRevisionTimestamp($reply, $now - 54321);
     $this->assertEquals($now - 54321, $reply->getRevisionId()->getTimestampObj()->getTimestamp(TS_UNIX), 'The first reply revision must have its revision id set appropriatly');
     $this->assertTrue($reply->getPostId()->equals($reply->getRevisionId()), 'The first revision of a reply shares its postId and revId');
 }
 public function testContentLength()
 {
     $content = 'This is a topic title';
     $nextContent = 'Changed my mind';
     $title = Title::newMainPage();
     $user = User::newFromName('127.0.0.1', false);
     $workflow = Workflow::create('topic', $title);
     $topic = PostRevision::create($workflow, $user, $content, 'wikitext');
     $this->assertEquals(0, $topic->getPreviousContentLength());
     $this->assertEquals(mb_strlen($content), $topic->getContentLength());
     $next = $topic->newNextRevision($user, $nextContent, 'wikitext', 'edit-title', $title);
     $this->assertEquals(mb_strlen($content), $next->getPreviousContentLength());
     $this->assertEquals(mb_strlen($nextContent), $next->getContentLength());
 }
 public function testValidateDoesntBlowUp()
 {
     $filter = new ConfirmEdit();
     if (!$filter->enabled()) {
         $this->markTestSkipped('ConfirmEdit is not enabled');
     }
     $user = User::newFromName('127.0.0.1', false);
     $title = Title::newMainPage();
     $workflow = Workflow::create('topic', $title);
     $oldRevision = PostRevision::create($workflow, $user, 'foo', 'wikitext');
     $newRevision = $oldRevision->newNextRevision($user, 'bar', 'wikitext', 'change-type', $title);
     $context = $this->getMock('IContextSource');
     $context->expects($this->any())->method('getUser')->will($this->returnValue($user));
     $status = $filter->validate($context, $newRevision, $oldRevision, $title);
     $this->assertInstanceOf('Status', $status);
     $this->assertTrue($status->isGood());
 }
 /**
  * @param string $action Permissions action to require to return revision
  * @return AbstractRevision|null
  * @throws InvalidDataException
  */
 public function loadTopicTitle($action = 'view')
 {
     if ($this->workflow->isNew()) {
         throw new InvalidDataException('New workflows do not have any related content', 'missing-topic-title');
     }
     if ($this->topicTitle === null) {
         $found = $this->storage->find('PostRevision', array('rev_type_id' => $this->workflow->getId()), array('sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1));
         if (!$found) {
             throw new InvalidDataException('Every workflow must have an associated topic title', 'missing-topic-title');
         }
         $this->topicTitle = reset($found);
         // this method loads only title, nothing else; otherwise, you're
         // looking for loadRootPost
         $this->topicTitle->setChildren(array());
         $this->topicTitle->setDepth(0);
         $this->topicTitle->setRootPost($this->topicTitle);
     }
     if (!$this->permissions->isAllowed($this->topicTitle, $action)) {
         $this->addError('permissions', $this->getDisallowedErrorMessage($this->topicTitle));
         return null;
     }
     return $this->topicTitle;
 }
 /**
  * @param UUID[] $missing
  * @return PostRevision
  */
 protected function createFakePosts(array $missing)
 {
     $parents = $this->treeRepository->fetchParentMap($missing);
     $posts = array();
     foreach ($missing as $uuid) {
         $alpha = $uuid->getAlphadecimal();
         if (!isset($parents[$alpha])) {
             wfDebugLog('Flow', __METHOD__ . ": Unable not locate parent for postid {$alpha}");
             continue;
         }
         $content = wfMessage('flow-stub-post-content')->text();
         $username = wfMessage('flow-system-usertext')->text();
         $user = User::newFromName($username);
         // create a stub post instead of failing completely
         $post = PostRevision::newFromId($uuid, $user, $content, 'wikitext');
         $post->setReplyToId($parents[$alpha]);
         $posts[$alpha] = $post;
     }
     return $posts;
 }
 /**
  * Update history revision
  */
 private function updateHistory(PostRevision $post, $wiki)
 {
     if ($post->getPrevRevisionId()) {
         $parent = Container::get('storage.post')->get(UUID::create($post->getPrevRevisionId()));
         if ($parent) {
             $this->updateRevision($parent, $wiki);
             $this->updateHistory($parent, $wiki);
         }
     }
 }
 /**
  * Process an array of users linked to in a comment into a list of users
  * who should actually be notified.
  *
  * Removes duplicates, anonymous users, self-mentions, and mentions of the
  * owner of the talk page
  * @param  User[] $mentions Array of User objects
  * @param  PostRevision $post The Post that is being examined.
  * @param  \Title $title The Title of the page that the comment is made on.
  * @return array Array of user IDs
  */
 protected function filterMentionedUsers($mentions, PostRevision $post, $title)
 {
     $outputMentions = array();
     global $wgFlowMaxMentionCount;
     foreach ($mentions as $mentionedUser) {
         // Don't notify anonymous users
         if ($mentionedUser->isAnon()) {
             continue;
         }
         // Don't notify the user who made the post
         if ($mentionedUser->getId() == $post->getUserId()) {
             continue;
         }
         if (count($outputMentions) > $wgFlowMaxMentionCount) {
             break;
         }
         $outputMentions[$mentionedUser->getId()] = $mentionedUser->getId();
     }
     return $outputMentions;
 }
 /**
  * Generate a reply to $this->revision (which is a topic title)
  *
  * @param array $overrides
  * @return PostRevision
  * @throws \Flow\Exception\FlowException
  */
 protected function generatePost($overrides)
 {
     $uuid = UUID::create();
     return $this->generateObject($overrides + array('rev_change_type' => 'reply', 'tree_rev_descendant_id' => $uuid->getBinary(), 'rev_type_id' => $uuid->getBinary(), 'tree_parent_id' => $this->revision->getPostId()));
 }
 /**
  * @param UUID[] $topicIds
  * @return PostRevision[]
  * @throws InvalidDataException
  */
 public function getMulti(array $topicIds)
 {
     if (!$topicIds) {
         return array();
     }
     // load posts for all located post ids
     $allPostIds = $this->fetchRelatedPostIds($topicIds);
     $queries = array();
     foreach ($allPostIds as $postId) {
         $queries[] = array('rev_type_id' => $postId);
     }
     $found = $this->storage->findMulti('PostRevision', $queries, array('sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1));
     /** @var PostRevision[] $posts */
     $posts = $children = array();
     foreach ($found as $indexResult) {
         $post = reset($indexResult);
         // limit => 1 means only 1 result per query
         if (isset($posts[$post->getPostId()->getAlphadecimal()])) {
             throw new InvalidDataException('Multiple results for id: ' . $post->getPostId()->getAlphadecimal(), 'fail-load-data');
         }
         $posts[$post->getPostId()->getAlphadecimal()] = $post;
     }
     $prettyPostIds = array();
     foreach ($allPostIds as $id) {
         $prettyPostIds[] = $id->getAlphadecimal();
     }
     $missing = array_diff($prettyPostIds, array_keys($posts));
     if ($missing) {
         // convert string uuid's into UUID objects
         /** @var UUID[] $missingUUID */
         $missingUUID = array_map(array('Flow\\Model\\UUID', 'create'), $missing);
         // we'll need to know parents to add stub post correctly in post hierarchy
         $parents = $this->treeRepo->fetchParentMap($missingUUID);
         $missingParents = array_diff($missing, array_keys($parents));
         if ($missingParents) {
             // if we can't fetch a post's original position in the tree
             // hierarchy, we can't create a stub post to display, so bail
             throw new InvalidDataException('Missing Posts & parents: ' . json_encode($missingParents), 'fail-load-data');
         }
         foreach ($missingUUID as $postId) {
             $content = wfMessage('flow-stub-post-content')->text();
             $username = wfMessage('flow-system-usertext')->text();
             $user = \User::newFromName($username);
             // create a stub post instead of failing completely
             $post = PostRevision::newFromId($postId, $user, $content, 'wikitext');
             $post->setReplyToId($parents[$postId->getAlphadecimal()]);
             $posts[$postId->getAlphadecimal()] = $post;
             wfWarn('Missing Posts: ' . FormatJson::encode($missing));
         }
     }
     // another helper to catch bugs in dev
     $extra = array_diff(array_keys($posts), $prettyPostIds);
     if ($extra) {
         throw new InvalidDataException('Found unrequested posts: ' . FormatJson::encode($extra), 'fail-load-data');
     }
     // populate array of children
     foreach ($posts as $post) {
         if ($post->getReplyToId()) {
             $children[$post->getReplyToId()->getAlphadecimal()][$post->getPostId()->getAlphadecimal()] = $post;
         }
     }
     $extraParents = array_diff(array_keys($children), $prettyPostIds);
     if ($extraParents) {
         throw new InvalidDataException('Found posts with unrequested parents: ' . FormatJson::encode($extraParents), 'fail-load-data');
     }
     foreach ($posts as $postId => $post) {
         $postChildren = array();
         $postDepth = 0;
         // link parents to their children
         if (isset($children[$postId])) {
             // sort children with oldest items first
             ksort($children[$postId]);
             $postChildren = $children[$postId];
         }
         // determine threading depth of post
         $replyToId = $post->getReplyToId();
         while ($replyToId && isset($children[$replyToId->getAlphadecimal()])) {
             $postDepth++;
             $replyToId = $posts[$replyToId->getAlphadecimal()]->getReplyToId();
         }
         $post->setChildren($postChildren);
         $post->setDepth($postDepth);
     }
     // return only the requested posts, rest are available as children.
     // Return in same order as requested
     /** @var PostRevision[] $roots */
     $roots = array();
     foreach ($topicIds as $id) {
         $roots[$id->getAlphadecimal()] = $posts[$id->getAlphadecimal()];
     }
     // Attach every post in the tree to its root. setRootPost
     // recursively applies it to all children as well.
     foreach ($roots as $post) {
         $post->setRootPost($post);
     }
     return $roots;
 }
 /**
  * Saves a PostRevision to storage.
  * Be sure to add the required tables to $tablesUsed and add @group Database
  * to the class' phpDoc.
  *
  * @param PostRevision $revision
  */
 protected function store(PostRevision $revision)
 {
     if ($revision->isTopicTitle()) {
         $root = $revision;
     } else {
         /** @var PostCollection $parentCollection */
         $parentCollection = PostCollection::newFromId($revision->getReplyToId());
         $root = $parentCollection->getRoot()->getLastRevision();
     }
     $topicWorkflow = $this->workflows[$root->getCollectionId()->getAlphadecimal()];
     $boardWorkflow = Container::get('factory.loader.workflow')->createWorkflowLoader($topicWorkflow->getOwnerTitle())->getWorkflow();
     $metadata = array('workflow' => $topicWorkflow, 'board-workflow' => $boardWorkflow);
     // check if this topic (+ workflow + board workflow + board page) have
     // already been inserted or do so now
     $found = $this->getStorage()->find('TopicListEntry', array('topic_id' => $topicWorkflow->getId()));
     if (!$found) {
         $title = $boardWorkflow->getOwnerTitle();
         $user = User::newFromName('127.0.0.1', false);
         /** @var OccupationController $occupationController */
         $occupationController = Container::get('occupation_controller');
         // make sure user has rights to create board
         $user->mRights = array_merge($user->getRights(), array('flow-create-board'));
         $occupationController->allowCreation($title, $user);
         $occupationController->ensureFlowRevision(new \Article($title), $boardWorkflow);
         $topicListEntry = TopicListEntry::create($boardWorkflow, $topicWorkflow);
         $this->getStorage()->put($boardWorkflow, $metadata);
         $this->getStorage()->put($topicWorkflow, $metadata);
         $this->getStorage()->put($topicListEntry, $metadata);
     }
     $this->getStorage()->put($revision, $metadata);
     /** @var SplQueue $deferredQueue */
     $deferredQueue = Container::get('deferred_queue');
     while (!$deferredQueue->isEmpty()) {
         try {
             DeferredUpdates::addCallableUpdate($deferredQueue->dequeue());
             // doing updates 1 by 1 so an exception doesn't break others in
             // the queue
             DeferredUpdates::doUpdates();
         } catch (\MWException $e) {
             // ignoring exceptions for now, not all are phpunit-proof yet
         }
     }
     // save for removal at end of tests
     $this->revisions[] = $revision;
 }
 /**
  * Gets the root post ID for a given PostRevision
  * @param  PostRevision $revision The revision to get the root post ID for.
  * @return UUID                   The UUID for the root post.
  * @throws \MWException
  */
 protected function getRootPostId(PostRevision $revision)
 {
     $postId = $revision->getPostId();
     if ($revision->isTopicTitle()) {
         return $postId;
     } elseif (isset($this->rootPostIdCache[$postId->getAlphadecimal()])) {
         return $this->rootPostIdCache[$postId->getAlphadecimal()];
     } else {
         throw new \MWException("Unable to find root post ID for post " . $postId->getAlphadecimal());
     }
 }
 /**
  * @param TopicImportState $state
  * @param IImportPost      $post
  * @param PostRevision     $replyTo
  * @param string           $logPrefix
  */
 public function importPost(TopicImportState $state, IImportPost $post, PostRevision $replyTo, $logPrefix = ' ')
 {
     $state->parent->logger->info($logPrefix . "Importing post");
     $postId = $state->parent->getImportedId($post);
     $topRevision = false;
     if ($postId) {
         $topRevision = $state->parent->getTopRevision('PostRevision', $postId);
     }
     if ($topRevision) {
         $state->parent->logger->info($logPrefix . "Post previously imported");
     } else {
         $replyRevisions = $this->importObjectWithHistory($post, function (IObjectRevision $rev) use($replyTo, $state) {
             return $replyTo->reply($state->topicWorkflow, $state->parent->createUser($rev->getAuthor()), $rev->getText(), 'wikitext');
         }, 'edit-post', $state->parent, $state->topicWorkflow->getArticleTitle());
         $topRevision = end($replyRevisions);
         $metadata = array('workflow' => $state->topicWorkflow, 'board-workflow' => $state->parent->boardWorkflow, 'topic-title' => $state->topicTitle, 'reply-to' => $replyTo);
         $state->parent->put($replyRevisions, $metadata);
         $state->parent->recordAssociation($topRevision->getPostId(), $post);
         $state->parent->logger->info($logPrefix . "Finished importing post with " . count($replyRevisions) . " revisions");
         $state->parent->postprocessor->afterPostImported($state, $post, $topRevision->getPostId());
     }
     $state->recordModificationTime($topRevision->getRevisionId());
     foreach ($post->getReplies() as $subReply) {
         $this->importPost($state, $subReply, $topRevision, $logPrefix . ' ');
     }
 }
 public static function onIRCLineURLProvider()
 {
     // data providers do not run in the same context as the actual test, as such we
     // can't create Title objects because they can have the wrong wikiID.  Instead we
     // pass closures into the test that create the objects within the correct context.
     $newHeader = function (User $user) {
         $title = Title::newFromText('Talk:Hook_test');
         $workflow = Container::get('factory.loader.workflow')->createWorkflowLoader($title)->getWorkflow();
         $header = Header::create($workflow, $user, 'header content', 'wikitext');
         $metadata = array('workflow' => $workflow, 'revision' => $header);
         /** @var OccupationController $occupationController */
         $occupationController = Container::get('occupation_controller');
         // make sure user has rights to create board
         $user->mRights = array_merge($user->getRights(), array('flow-create-board'));
         $occupationController->allowCreation($title, $user);
         $occupationController->ensureFlowRevision(new \Article($title), $workflow);
         Container::get('storage')->put($workflow, $metadata);
         return $metadata;
     };
     $freshTopic = function (User $user) {
         $title = Title::newFromText('Talk:Hook_test');
         $boardWorkflow = Container::get('factory.loader.workflow')->createWorkflowLoader($title)->getWorkflow();
         $topicWorkflow = Workflow::create('topic', $boardWorkflow->getArticleTitle());
         $topicList = TopicListEntry::create($boardWorkflow, $topicWorkflow);
         $topicTitle = PostRevision::create($topicWorkflow, $user, 'some content', 'wikitext');
         $metadata = array('workflow' => $topicWorkflow, 'board-workflow' => $boardWorkflow, 'topic-title' => $topicTitle, 'revision' => $topicTitle);
         /** @var OccupationController $occupationController */
         $occupationController = Container::get('occupation_controller');
         // make sure user has rights to create board
         $user->mRights = array_merge($user->getRights(), array('flow-create-board'));
         $occupationController->allowCreation($title, $user);
         $occupationController->ensureFlowRevision(new \Article($title), $boardWorkflow);
         $storage = Container::get('storage');
         $storage->put($boardWorkflow, $metadata);
         $storage->put($topicWorkflow, $metadata);
         $storage->put($topicList, $metadata);
         $storage->put($topicTitle, $metadata);
         return $metadata;
     };
     $replyToTopic = function (User $user) use($freshTopic) {
         $metadata = $freshTopic($user);
         $firstPost = $metadata['topic-title']->reply($metadata['workflow'], $user, 'ffuts dna ylper', 'wikitext');
         $metadata = array('first-post' => $firstPost, 'revision' => $firstPost) + $metadata;
         Container::get('storage.post')->put($firstPost, $metadata);
         return $metadata;
     };
     return array(array('Freshly created topic', $freshTopic, array('action' => 'history')), array('Reply to topic', $replyToTopic, array('action' => 'history')), array('Edit topic title', function ($user) use($freshTopic) {
         $metadata = $freshTopic($user);
         $title = $metadata['workflow']->getArticleTitle();
         return array('revision' => $metadata['revision']->newNextRevision($user, 'gnihtemos gnihtemos', 'wikitext', 'edit-title', $title)) + $metadata;
     }, array('action' => 'compare-post-revisions')), array('Edit post', function ($user) use($replyToTopic) {
         $metadata = $replyToTopic($user);
         $title = $metadata['workflow']->getArticleTitle();
         return array('revision' => $metadata['revision']->newNextRevision($user, 'IT\'S CAPS LOCKS DAY!', 'wikitext', 'edit-post', $title)) + $metadata;
     }, array('action' => 'compare-post-revisions')), array('Edit board header', function ($user) use($newHeader) {
         $metadata = $newHeader($user);
         $title = $metadata['workflow']->getArticleTitle();
         return array('revision' => $metadata['revision']->newNextRevision($user, 'STILL CAPS LOCKS DAY!', 'wikitext', 'edit-header', $title)) + $metadata;
     }, array('action' => 'compare-header-revisions')), array('Moderate a post', function ($user) use($replyToTopic) {
         $metadata = $replyToTopic($user);
         return array('revision' => $metadata['revision']->moderate($user, $metadata['revision']::MODERATED_DELETED, 'delete-post', 'something about cruise control')) + $metadata;
     }, array('action' => 'history')), array('Moderate a topic', function ($user) use($freshTopic) {
         $metadata = $freshTopic($user);
         return array('revision' => $metadata['revision']->moderate($user, $metadata['revision']::MODERATED_HIDDEN, 'hide-topic', 'adorable kittens')) + $metadata;
     }, array('action' => 'history')));
 }
 /**
  * While topic's themselves are plaintext and do not contain any references,
  * moderation actions change what references are visible.  When transitioning
  * from or to a generically visible state (unmoderated or locked) the entire
  * topic + summary needs to be re-evaluated.
  *
  * @param Workflow $workflow
  * @param PostRevision $current Topic revision object that was inserted
  * @return array Contains two arrays, first the references to add a second
  *  the references to remove
  * @throws FlowException
  */
 protected function calculateChangesFromTopic(Workflow $workflow, PostRevision $current)
 {
     if ($current->isFirstRevision()) {
         return array(array(), array());
     }
     $previous = $this->storage->get('PostRevision', $current->getPrevRevisionId());
     if (!$previous) {
         throw new FlowException('Expcted previous revision of ' . $current->getPrevRevisionId()->getAlphadecimal());
     }
     $isHidden = self::isHidden($current);
     $wasHidden = self::isHidden($previous);
     if ($isHidden === $wasHidden) {
         return array(array(), array());
     }
     // re-run
     $revisions = $this->collectTopicRevisions($workflow);
     $added = array();
     $removed = array();
     foreach ($revisions as $revision) {
         list($add, $remove) = $this->calculateChangesFromExisting($workflow, $revision, $current);
         $added = array_merge($added, $add);
         $removed = array_merge($removed, $remove);
     }
     return array($added, $removed);
 }
 /**
  * Recursively get the data for all children. This will add the revision's
  * content to the results array, with the post ID as key.
  *
  * @param PostRevision|PostSummary $revision
  * @return array
  */
 public function getRevisionsData($revision)
 {
     // make sure we don't parse text that isn't meant to be parsed (e.g.
     // topic titles are never meant to be parsed from wikitext to html)
     $format = $revision->isFormatted() ? 'html' : 'wikitext';
     // store type of revision so we can also search for very specific types
     // (e.g. titles only)
     // possible values will be:
     // * title
     // * post
     // * post-summary
     $type = $revision->getRevisionType();
     if (method_exists($revision, 'isTopicTitle') && $revision->isTopicTitle()) {
         $type = 'title';
     }
     $data = array();
     if ($this->permissions->isAllowed($revision, 'view')) {
         $data[] = array('id' => $revision->getCollectionId()->getAlphadecimal(), 'text' => trim(Sanitizer::stripAllTags($revision->getContent($format))), 'source_text' => $revision->getContent('wikitext'), 'moderation_state' => $revision->getModerationState(), 'timestamp' => $revision->getCollectionId()->getTimestamp(TS_ISO_8601), 'update_timestamp' => $revision->getRevisionId()->getTimestamp(TS_ISO_8601), 'type' => $type);
     }
     if ($revision instanceof PostRevision) {
         // get data from all child posts too
         foreach ($revision->getChildren() as $child) {
             $data = array_merge($data, $this->getRevisionsData($child));
         }
     }
     return $data;
 }