/**
  * @param FormatterRow $row
  * @return array
  * @throws FlowException
  */
 public function buildActions(FormatterRow $row)
 {
     $user = $this->permissions->getUser();
     $workflow = $row->workflow;
     $title = $workflow->getArticleTitle();
     // If a user is blocked from performing actions on this page return
     // an empty array of actions.
     //
     // We only check actual users and not anon's because the anonymous
     // version can be cached and served to many different ip addresses
     // which will not all be blocked.
     if (!$user->isAnon() && ($user->isBlockedFrom($title, true) || !$title->quickUserCan('edit', $user))) {
         return array();
     }
     $revision = $row->revision;
     $action = $revision->getChangeType();
     $workflowId = $workflow->getId();
     $revId = $revision->getRevisionId();
     $postId = method_exists($revision, 'getPostId') ? $revision->getPostId() : null;
     $actionTypes = $this->permissions->getActions()->getValue($action, 'actions');
     if ($actionTypes === null) {
         wfDebugLog('Flow', __METHOD__ . ": No actions defined for action: {$action}");
         return array();
     }
     // actions primarily vary by revision type...
     $links = array();
     foreach ($actionTypes as $type) {
         if (!$this->permissions->isAllowed($revision, $type)) {
             continue;
         }
         switch ($type) {
             case 'thank':
                 if (class_exists('ThanksHooks') && !$user->isAnon() && $revision instanceof PostRevision && $revision->getCreatorId() > 0 && $user->getId() !== $revision->getCreatorId()) {
                     $links['thank'] = $this->urlGenerator->thankAction($postId);
                 }
                 break;
             case 'reply':
                 if (!$postId) {
                     throw new FlowException("{$type} called without \$postId");
                 } elseif (!$revision instanceof PostRevision) {
                     throw new FlowException("{$type} called without PostRevision object");
                 }
                 /*
                  * If the post being replied to is the most recent post
                  * of its depth, the reply link should point to parent
                  */
                 $replyToId = $postId;
                 $replyToRevision = $revision;
                 if ($row->isLastReply) {
                     $replyToId = $replyToRevision->getReplyToId();
                     $replyToRevision = PostCollection::newFromId($replyToId)->getLastRevision();
                 }
                 /*
                  * If the post being replied to is at or exceeds the max
                  * threading depth, the reply link should point to parent.
                  */
                 while ($replyToRevision->getDepth() >= $this->maxThreadingDepth) {
                     $replyToId = $replyToRevision->getReplyToId();
                     $replyToRevision = PostCollection::newFromId($replyToId)->getLastRevision();
                 }
                 $links['reply'] = $this->urlGenerator->replyAction($title, $workflowId, $replyToId, $revision->isTopicTitle());
                 break;
             case 'edit-header':
                 $links['edit'] = $this->urlGenerator->editHeaderAction($title, $workflowId, $revId);
                 break;
             case 'edit-title':
                 if (!$postId) {
                     throw new FlowException("{$type} called without \$postId");
                 }
                 $links['edit'] = $this->urlGenerator->editTitleAction($title, $workflowId, $postId, $revId);
                 break;
             case 'edit-post':
                 if (!$postId) {
                     throw new FlowException("{$type} called without \$postId");
                 }
                 $links['edit'] = $this->urlGenerator->editPostAction($title, $workflowId, $postId, $revId);
                 break;
             case 'undo-edit-header':
             case 'undo-edit-post':
             case 'undo-edit-topic-summary':
                 if (!$revision->isFirstRevision()) {
                     $links['undo'] = $this->urlGenerator->undoAction($revision, $title, $workflowId);
                 }
                 break;
             case 'hide-post':
                 if (!$postId) {
                     throw new FlowException("{$type} called without \$postId");
                 }
                 $links['hide'] = $this->urlGenerator->hidePostAction($title, $workflowId, $postId);
                 break;
             case 'delete-topic':
                 $links['delete'] = $this->urlGenerator->deleteTopicAction($title, $workflowId);
                 break;
             case 'delete-post':
                 if (!$postId) {
                     throw new FlowException("{$type} called without \$postId");
                 }
                 $links['delete'] = $this->urlGenerator->deletePostAction($title, $workflowId, $postId);
                 break;
             case 'suppress-topic':
                 $links['suppress'] = $this->urlGenerator->suppressTopicAction($title, $workflowId);
                 break;
             case 'suppress-post':
                 if (!$postId) {
                     throw new FlowException("{$type} called without \$postId");
                 }
                 $links['suppress'] = $this->urlGenerator->suppressPostAction($title, $workflowId, $postId);
                 break;
             case 'lock-topic':
                 // lock topic link is only available to topics
                 if (!$revision instanceof PostRevision || !$revision->isTopicTitle()) {
                     continue;
                 }
                 $links['lock'] = $this->urlGenerator->lockTopicAction($title, $workflowId);
                 break;
             case 'restore-topic':
                 $moderateAction = $flowAction = null;
                 switch ($revision->getModerationState()) {
                     case AbstractRevision::MODERATED_LOCKED:
                         $moderateAction = 'unlock';
                         $flowAction = 'lock-topic';
                         break;
                     case AbstractRevision::MODERATED_HIDDEN:
                     case AbstractRevision::MODERATED_DELETED:
                     case AbstractRevision::MODERATED_SUPPRESSED:
                         $moderateAction = 'un' . $revision->getModerationState();
                         $flowAction = 'moderate-topic';
                         break;
                 }
                 if (isset($moderateAction) && $moderateAction) {
                     $links[$moderateAction] = $this->urlGenerator->restoreTopicAction($title, $workflowId, $moderateAction, $flowAction);
                 }
                 break;
             case 'restore-post':
                 if (!$postId) {
                     throw new FlowException("{$type} called without \$postId");
                 }
                 $moderateAction = $flowAction = null;
                 switch ($revision->getModerationState()) {
                     case AbstractRevision::MODERATED_HIDDEN:
                     case AbstractRevision::MODERATED_DELETED:
                     case AbstractRevision::MODERATED_SUPPRESSED:
                         $moderateAction = 'un' . $revision->getModerationState();
                         $flowAction = 'moderate-post';
                         break;
                 }
                 if ($moderateAction) {
                     $links[$moderateAction] = $this->urlGenerator->restorePostAction($title, $workflowId, $postId, $moderateAction, $flowAction);
                 }
                 break;
             case 'hide-topic':
                 $links['hide'] = $this->urlGenerator->hideTopicAction($title, $workflowId);
                 break;
                 // Need to use 'edit-topic-summary' to match FlowActions
             // Need to use 'edit-topic-summary' to match FlowActions
             case 'edit-topic-summary':
                 // summarize link is only available to topic workflow
                 if (!in_array($workflow->getType(), array('topic', 'topicsummary'))) {
                     continue;
                 }
                 $links['summarize'] = $this->urlGenerator->editTopicSummaryAction($title, $workflowId);
                 break;
             default:
                 wfDebugLog('Flow', __METHOD__ . ': unkown action link type: ' . $type);
                 break;
         }
     }
     return $links;
 }
 public function thankAction(UUID $postId)
 {
     $sender = RequestContext::getMain()->getUser();
     $recipient = $sender;
     // Default to current user's gender if we can't find the recipient
     $postCollection = PostCollection::newFromId($postId);
     $postRevision = $postCollection->getLastRevision();
     $recipient = $postRevision->getCreatorTuple()->createUser();
     return new Anchor(wfMessage('flow-thank-link', $sender, $recipient)->text(), SpecialPage::getTitleFor('Thanks', 'Flow/' . $postId->getAlphadecimal()), array(), null, wfMessage('flow-thank-link-title', $sender, $recipient)->text());
 }
 /**
  * Get the post collection for this summary
  * @return PostCollection
  */
 public function getPost()
 {
     return PostCollection::newFromId($this->uuid);
 }
 /**
  * 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;
 }
 public function testNewFromRevision()
 {
     $revision = $this->revisions[0];
     $collection = PostCollection::newFromRevision($revision);
     $this->assertInstanceOf('Flow\\Collection\\PostCollection', $collection);
 }
 /**
  * @param array $tools Extra links
  * @param Title $title
  * @param bool $redirect Whether the page is a redirect
  * @param Skin $skin
  * @param string $link
  * @return bool
  */
 public static function onWatchlistEditorBuildRemoveLine(&$tools, $title, $redirect, $skin, &$link = '')
 {
     if ($title->getNamespace() !== NS_TOPIC) {
         // Leave all non Flow topics alone!
         return true;
     }
     /*
      * Link to talk page is no applicable for Flow topics
      * Note that key 'talk' doesn't exist prior to
      * https://gerrit.wikimedia.org/r/#/c/156522/, so on old MW's, the link
      * to talk page will still be present.
      */
     unset($tools['talk']);
     if (!$link) {
         /*
          * https://gerrit.wikimedia.org/r/#/c/156118/ adds argument $link.
          * Prior to that patch, it was impossible to change the link, so
          * let's quit early if it doesn't exist.
          */
         return true;
     }
     try {
         // Find the title text of this specific topic
         $uuid = WorkflowLoaderFactory::uuidFromTitle($title);
         $collection = PostCollection::newFromId($uuid);
         $revision = $collection->getLastRevision();
     } catch (Exception $e) {
         wfWarn(__METHOD__ . ': Failed to locate revision for: ' . $title->getDBKey());
         return true;
     }
     // Titles are never parsed, so request as wikitext
     $content = $revision->getContent('wikitext');
     $link = Linker::link($title, htmlspecialchars($content));
     return true;
 }
 /**
  * @return PostCollection
  */
 public function getCollection()
 {
     return PostCollection::newFromRevision($this);
 }
 /**
  * @param UUID $postId
  * @return PostCollection
  */
 protected function loadPost(UUID $postId)
 {
     try {
         $collection = PostCollection::newFromId($postId);
         // validate collection by attempting to fetch latest revision - if
         // this fails (likely will for old data), catch will be invoked
         $collection->getLastRevision();
         return $collection;
     } catch (InvalidDataException $e) {
         // posts used to mistakenly store revision ID instead of post ID
         /** @var ManagerGroup $storage */
         $storage = Container::get('storage');
         $result = $storage->find('PostRevision', array('rev_id' => $postId), array('LIMIT' => 1));
         if ($result) {
             /** @var PostRevision $revision */
             $revision = reset($result);
             // now build collection from real post ID
             return $this->loadPost($revision->getPostId());
         }
     }
     return false;
 }
 /**
  * @param AbstractRevision $revision
  * @return AbstractRevision
  */
 protected function getRoot(AbstractRevision $revision)
 {
     if ($revision instanceof PostSummary) {
         $topicId = $revision->getSummaryTargetId();
     } elseif ($revision instanceof PostRevision && !$revision->isTopicTitle()) {
         try {
             $topicId = $revision->getCollection()->getWorkflowId();
         } catch (DataModelException $e) {
             // failed to locate root post (most likely in unit tests, where
             // we didn't store the tree)
             return $revision;
         }
     } else {
         // if we can't the revision it back to a root, this revision is root
         return $revision;
     }
     $collection = PostCollection::newFromId($topicId);
     return $collection->getLastRevision();
 }
 /**
  * @return PostCollection|bool
  */
 protected function getRoot()
 {
     $params = $this->entry->getParameters();
     try {
         if (!isset($params['topicId'])) {
             // failed finding the expected data in storage
             wfWarn(__METHOD__ . ': Failed to locate topicId in log_params for: ' . serialize($params) . ' (forgot to run FlowFixLog.php?)');
             return false;
         }
         $uuid = UUID::create($params['topicId']);
         $collection = PostCollection::newFromId($uuid);
         // see if this post is valid
         $collection->getLastRevision();
         return $collection;
     } catch (\Exception $e) {
         // failed finding the expected data in storage
         wfWarn(__METHOD__ . ': Failed to locate root for: ' . serialize($params) . ' (potentially storage issue)');
         return false;
     }
 }