/** * @param Workflow $workflow * @param string $objectType * @param UUID $objectId */ public function __construct(Workflow $workflow, $objectType, UUID $objectId) { $this->workflowId = $workflow->getId(); $this->title = $workflow->getArticleTitle(); $this->objectType = $objectType; $this->objectId = $objectId; }
/** * @param Workflow $topicList * @param Workflow $topic * @return TopicListEntry */ public static function create(Workflow $topicList, Workflow $topic) { $obj = new self(); $obj->topicListId = $topicList->getId(); $obj->topicId = $topic->getId(); $obj->topicWorkflowLastUpdated = $topic->getLastModified(); return $obj; }
/** * @param Workflow $workflow * @param User $user * @param string $content * @param string $format wikitext|html * @param string[optional] $changeType * @return Header */ public static function create(Workflow $workflow, User $user, $content, $format, $changeType = 'create-header') { $obj = new self(); $obj->revId = UUID::create(); $obj->workflowId = $workflow->getId(); $obj->user = UserTuple::newFromUser($user); $obj->prevRevision = null; // no prior revision $obj->setContent($content, $format, $workflow->getArticleTitle()); $obj->changeType = $changeType; return $obj; }
public function doUpdate(Workflow $workflow) { $title = $workflow->getArticleTitle(); $page = WikiPage::factory($title); $content = $page->getContent(); if ($content === null) { $updates = array(); } else { $updates = $content->getSecondaryDataUpdates($title); } DataUpdate::runUpdates($updates); }
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); } }
/** * @param Workflow $workflow Topic list workflow * @param array $links pagination link data * * @return array link structure */ protected function buildPaginationLinks(Workflow $workflow, array $links) { $res = array(); $title = $workflow->getArticleTitle(); foreach ($links as $key => $options) { // prefix all options with topiclist_ $realOptions = array(); foreach ($options as $k => $v) { $realOptions["topiclist_{$k}"] = $v; } $res[$key] = new Anchor($key, $title, $realOptions); } return $res; }
/** * @param Title $pageTitle * @param UUID|null $workflowId * @return WorkflowLoader * @throws InvalidInputException * @throws CrossWikiException */ public function createWorkflowLoader(Title $pageTitle, $workflowId = null) { if ($pageTitle === null) { throw new InvalidInputException('Invalid article requested', 'invalid-title'); } if ($pageTitle && $pageTitle->isExternal()) { throw new CrossWikiException('Interwiki to ' . $pageTitle->getInterwiki() . ' not implemented ', 'default'); } // @todo: ideally, workflowId is always set and this stuff is done in the places that call this if ($workflowId === null) { if ($pageTitle->getNamespace() === NS_TOPIC) { // topic page: workflow UUID is page title $workflowId = self::uuidFromTitle($pageTitle); } else { // board page: workflow UUID is inside content model $page = \WikiPage::factory($pageTitle); $content = $page->getContent(); if ($content instanceof BoardContent) { $workflowId = $content->getWorkflowId(); } } } if ($workflowId === null) { // no existing workflow found, create new one $workflow = Workflow::create($this->defaultWorkflowName, $pageTitle); } else { $workflow = $this->loadWorkflowById($pageTitle, $workflowId); } return new WorkflowLoader($workflow, $this->blockFactory->createBlocks($workflow), $this->submissionHandler); }
public function testSortByOption() { $user = User::newFromId(1); $user->setOption('flow-topiclist-sortby', ''); // reset flow state, so everything ($container['permissions']) // uses this particular $user \FlowHooks::resetFlowExtension(); Container::reset(); $container = Container::getContainer(); $container['user'] = $user; $ctx = $this->getMock('IContextSource'); $ctx->expects($this->any())->method('getUser')->will($this->returnValue($user)); $workflow = Workflow::create('discussion', Title::newFromText('Talk:Flow_QA')); $block = new TopicListBlock($workflow, Container::get('storage')); $block->init($ctx, 'view'); $res = $block->renderApi(array()); $this->assertEquals('newest', $res['sortby'], 'With no sortby defaults to newest'); $res = $block->renderApi(array('sortby' => 'foo')); $this->assertEquals('newest', $res['sortby'], 'With invalid sortby defaults to newest'); $res = $block->renderApi(array('sortby' => 'updated')); $this->assertEquals('updated', $res['sortby'], 'With sortby updated output changes to updated'); $res = $block->renderApi(array()); $this->assertEquals('newest', $res['sortby'], 'Sort still defaults to newest'); $res = $block->renderApi(array('sortby' => 'updated', 'savesortby' => '1')); $this->assertEquals('updated', $res['sortby'], 'Request saving sortby option'); $res = $block->renderApi(array()); $this->assertEquals('updated', $res['sortby'], 'Default sortby now changed to updated'); $res = $block->renderApi(array('sortby' => '')); $this->assertEquals('updated', $res['sortby'], 'Default sortby with blank sortby still uses user default'); }
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; }
/** * This is a horrible test, it basically runs the whole thing * and sees if it falls over. */ public function testImportDoesntCompletelyFail() { $workflow = Workflow::create('discussion', Title::newFromText('TalkpageImportOperationTest')); $storage = $this->getMockBuilder('Flow\\Data\\ManagerGroup')->disableOriginalConstructor()->getMock(); $stored = array(); $storage->expects($this->any())->method('put')->will($this->returnCallback(function ($obj) use(&$stored) { $stored[] = $obj; })); $storage->expects($this->any())->method('multiPut')->will($this->returnCallback(function ($objs) use(&$stored) { $stored = array_merge($stored, $objs); })); $now = time(); $source = new MockImportSource(new MockImportHeader(array(new MockImportRevision(array('createdTimestamp' => $now)))), array(new MockImportTopic(new MockImportSummary(array(new MockImportRevision(array('createdTimestamp' => $now - 250)))), array(new MockImportRevision(array('createdTimestamp' => $now - 1000))), array(new MockImportPost(array(new MockImportRevision(array('createdTimestmap' => $now - 1000))), array(new MockImportPost(array(new MockImportRevision(array('createdTimestmap' => $now - 500, 'user' => User::newFromNAme('10.0.0.2', false)))), array()))))))); $op = new TalkpageImportOperation($source, Container::get('occupation_controller')); $store = new NullImportSourceStore(); $op->import(new PageImportState($workflow, $storage, $store, new NullLogger(), $this->getMockBuilder('Flow\\Data\\BufferedCache')->disableOriginalConstructor()->getMock(), Container::get('db.factory'), new ProcessorGroup(), new SplQueue())); // Count what actually came through $storedHeader = $storedDiscussion = $storedTopics = $storedTopicListEntry = $storedSummary = $storedPosts = 0; foreach ($stored as $obj) { if ($obj instanceof Workflow) { if ($obj->getType() === 'discussion') { $this->assertSame($workflow, $obj); $storedDiscussion++; } else { $alpha = $obj->getId()->getAlphadecimal(); if (!isset($seenWorkflow[$alpha])) { $seenWorkflow[$alpha] = true; $this->assertEquals('topic', $obj->getType()); $storedTopics++; $topicWorkflow = $obj; } } } elseif ($obj instanceof PostSummary) { $storedSummary++; } elseif ($obj instanceof PostRevision) { $storedPosts++; if ($obj->isTopicTitle()) { $topicTitle = $obj; } } elseif ($obj instanceof TopicListEntry) { $storedTopicListEntry++; } elseif ($obj instanceof Header) { $storedHeader++; } else { $this->fail('Unexpected object stored:' . get_class($obj)); } } // Verify we wrote the expected objects to storage $this->assertEquals(1, $storedHeader); $this->assertEquals(1, $storedDiscussion); $this->assertEquals(1, $storedTopics); $this->assertEquals(1, $storedTopicListEntry); $this->assertEquals(1, $storedSummary); $this->assertEquals(3, $storedPosts); // This total expected number of insertions should match the sum of the left assertEquals parameters above. $this->assertCount(8, array_unique(array_map('spl_object_hash', $stored))); // Other special cases we need to check $this->assertTrue($topicTitle->getPostId()->equals($topicWorkflow->getId()), 'Root post id must match its workflow'); }
/** * @param \OutputPage $out */ public function setPageTitle(\OutputPage $out) { if ($out->getPageTitle()) { // Don't override page title if another block has already set it. // If this should *really* be done, the specific block extending // this AbstractBlock should just implement this itself ;) return; } $out->setPageTitle($this->workflow->getArticleTitle()->getFullText()); }
/** * Many loaded references typically point to the same Title, cache those instead * of generating a bunch of duplicate title classes. */ public static function makeTitle($namespace, $title) { try { return Workflow::getFromTitleCache(wfWikiId(), $namespace, $title); } catch (InvalidInputException $e) { // duplicate Title::makeTitleSafe which returns null on failure, // but only for InvalidInputException return null; } }
protected function processParam($event, $param, $message, $user) { $extra = $event->getExtra(); if ($param === 'subject') { if (isset($extra['topic-title']) && $extra['topic-title']) { $this->processParamEscaped($message, trim($extra['topic-title'])); } else { $message->params(''); } } elseif ($param === 'commentText') { if (isset($extra['content']) && $extra['content']) { // @todo assumes content is html, make explicit $message->params(Utils::htmlToPlaintext($extra['content'], 200)); } else { $message->params(''); } } elseif ($param === 'post-permalink') { $anchor = $this->getPostLinkAnchor($event, $user); if ($anchor) { $message->params($anchor->getFullUrl()); } else { $message->params(''); } } elseif ($param === 'topic-permalink') { // link to individual new-topic if (isset($extra['topic-workflow'])) { $title = Workflow::getFromTitleCache(wfWikiId(), NS_TOPIC, $extra['topic-workflow']->getAlphadecimal()); } else { $title = $event->getTitle(); } $anchor = $this->getUrlGenerator()->workflowLink($title, $extra['topic-workflow']); $anchor->query['fromnotif'] = 1; $message->params($anchor->getFullUrl()); } elseif ($param === 'new-topics-permalink') { // link to board sorted by newest topics $anchor = $this->getUrlGenerator()->boardLink($event->getTitle(), 'newest'); $anchor->query['fromnotif'] = 1; $message->params($anchor->getFullUrl()); } elseif ($param == 'flow-title') { $title = $event->getTitle(); if ($title) { $formatted = $this->formatTitle($title); } else { $formatted = $this->getMessage('echo-no-title')->text(); } $message->params($formatted); } elseif ($param == 'old-subject') { $this->processParamEscaped($message, trim($extra['old-subject'])); } elseif ($param == 'new-subject') { $this->processParamEscaped($message, trim($extra['new-subject'])); } else { parent::processParam($event, $param, $message, $user); } }
/** * @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'); }
/** * Load the header and topics from the requested discussion. Does not return * anything, the goal here is to populate $this->hashBag. * * @param Workflow $workflow */ protected function fetchDiscussion(Workflow $workflow) { $results = array(); $pagers = array(); /** @var ManagerGroup $storage */ $storage = Container::get('storage'); // 'newest' sort order $pagers[] = new Pager($storage->getStorage('TopicListEntry'), array('topic_list_id' => $workflow->getId()), array('pager-limit' => 499)); // 'updated' sort order $pagers[] = new Pager($storage->getStorage('TopicListEntry'), array('topic_list_id' => $workflow->getId()), array('pager-limit' => 499, 'sort' => 'workflow_last_update_timestamp', 'order' => 'desc')); // Based on Header::init. $storage->find('Header', array('rev_type_id' => $workflow->getId()), array('sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1)); foreach ($pagers as $pager) { foreach ($pager->getPage()->getResults() as $entry) { // use array key to de-duplicate $results[$entry->getId()->getAlphadecimal()] = $entry->getId(); } } $this->fetchTopics($results); // purge the board history $storage->find('BoardHistoryEntry', array('topic_list_id' => $workflow->getId()), array('sort' => 'rev_id', 'order' => 'DESC', 'limit' => 499)); }
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()); }
/** * @param Workflow $workflow * @return AbstractBlock[] * @throws InvalidInputException When the workflow type is unrecognized * @throws InvalidDataException When multiple blocks share the same name */ public function createBlocks(Workflow $workflow) { switch ($workflow->getType()) { case 'discussion': $blocks = array(new HeaderBlock($workflow, $this->storage), new TopicListBlock($workflow, $this->storage), new BoardHistoryBlock($workflow, $this->storage)); break; case 'topic': $blocks = array(new TopicBlock($workflow, $this->storage, $this->rootPostLoader), new TopicSummaryBlock($workflow, $this->storage)); break; default: throw new InvalidInputException('Not Implemented', 'invalid-definition'); break; } $return = array(); /** @var AbstractBlock[] $blocks */ foreach ($blocks as $block) { if (isset($return[$block->getName()])) { throw new InvalidDataException('Multiple blocks with same name is not yet supported', 'fail-load-data'); } $return[$block->getName()] = $block; } return $return; }
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()); }
/** * @dataProvider somethingProvider */ public function testSomething($message, $expect, $init) { $actions = Container::get('flow_actions'); $usernames = $this->getMockBuilder('Flow\\Repository\\UserNameBatch')->disableOriginalConstructor()->getMock(); $rcFactory = $this->getMockBuilder('Flow\\Data\\Utils\\RecentChangeFactory')->disableOriginalConstructor()->getMock(); $ircFormatter = $this->getMockBuilder('Flow\\Formatter\\IRCLineUrlFormatter')->disableOriginalConstructor()->getMock(); $rc = new RecentChangesListener($actions, $usernames, $rcFactory, $ircFormatter); $change = $this->getMock('RecentChange'); $rcFactory->expects($this->once())->method('newFromRow')->will($this->returnCallback(function ($obj) use(&$ref, $change) { $ref = $obj; return $change; })); $title = Title::newMainPage(); $user = User::newFromName('127.0.0.1', false); $workflow = Workflow::create('topic', $title); $revision = $init($workflow, $user); $rc->onAfterInsert($revision, array('rev_user_id' => 0, 'rev_user_ip' => '127.0.0.1'), array('workflow' => $workflow)); $this->assertNotNull($ref); $this->assertEquals($expect, $ref->rc_namespace, $message); }
protected function buildApiActions(Workflow $workflow) { return array('newtopic' => array('url' => $this->urlGenerator->newTopicAction($workflow->getArticleTitle(), $workflow->getId()))); }
protected function getBlandTestObjects() { return array($this->workflow, $this->revision, $this->workflow->getArticleTitle()); }
/** * @param Workflow $workflow * @param string $action * @return \Title */ public function getRcTitle(Workflow $workflow, $action) { if ($this->actions->getValue($action, 'rc_title') === 'owner') { return $workflow->getOwnerTitle(); } else { return $workflow->getArticleTitle(); } }
/** * Check if a user is allowed to perform a certain action, depending on the * status (deleted?) of the board. * * @param Workflow $workflow * @param string $action * @return bool */ public function isBoardAllowed(Workflow $workflow, $action) { $permissions = $this->actions->getValue($action, 'core-delete-permissions'); // If user is allowed to see deleted page content, there's no need to // even check if it's been deleted (additional storage lookup) $allowed = call_user_func_array(array($this->user, 'isAllowedAny'), (array) $permissions); if ($allowed) { return true; } return !$workflow->isDeleted(); }
/** * @param Workflow $workflow * @param User $user * @param string $content * @param string $format wikitext|html * @param string[optional] $changeType * @return PostRevision */ public function reply(Workflow $workflow, User $user, $content, $format, $changeType = 'reply') { $reply = new self(); // UUIDs should not be reused for different entities/entity types in the future. // (It is also inconsistent with newFromId, which uses separate ones.) // This may be changed here in the future. $reply->revId = $reply->postId = UUID::create(); $reply->user = UserTuple::newFromUser($user); $reply->origUser = $reply->user; $reply->replyToId = $this->postId; $reply->setContent($content, $format, $workflow->getArticleTitle()); $reply->changeType = $changeType; $reply->setChildren(array()); $reply->setDepth($this->getDepth() + 1); $reply->rootPost = $this->rootPost; return $reply; }
/** * When a page is taken over by Flow, add a revision. * * First, it provides a clearer history should Flow be disabled again later, * and a descriptive message when people attempt to use regular API to fetch * data for this "Page", which will no longer contain any useful content, * since Flow has taken over. * * Also: Parsoid performs an API call to fetch page information, so we need * to make sure a page actually exists ;) * * This method does not do any security checks regarding content model changes * or the like. Those happen much earlier in the request and should be checked * before even attempting to create revisions which, when written to the database, * trigger this method through the OccupationListener. * * @param \Article $article * @param Workflow $workflow * @return Status Status for revision creation; On success (including if it already * had a top-most Flow revision), it will return a good status with an associative * array value. $status->getValue()['revision'] will be a Revision * $status->getValue()['already-existed'] will be set to true if no revision needed * to be created * @throws InvalidInputException */ public function ensureFlowRevision(Article $article, Workflow $workflow) { // Comment to add to the Revision to indicate Flow taking over $comment = '/* Taken over by Flow */'; $page = $article->getPage(); $revision = $page->getRevision(); if ($revision !== null) { $content = $revision->getContent(); if ($content instanceof BoardContent && $content->getWorkflowId()) { // Revision is already a valid BoardContent return Status::newGood(array('revision' => $revision, 'already-existed' => true)); } } $status = $page->doEditContent(new BoardContent(CONTENT_MODEL_FLOW_BOARD, $workflow->isNew() ? null : $workflow->getId()), $comment, EDIT_FORCE_BOT | EDIT_SUPPRESS_RC, false, $this->getTalkpageManager()); $value = $status->getValue(); $value['already-existed'] = false; $status->setResult($status->isOK(), $value); return $status; }
/** * @param Workflow $obj * @return array * @throws FailCommitException */ public static function toStorageRow(Workflow $obj) { if ($obj->pageId === 0) { /* * We try to defer creating a new page as long as possible, which * means that a new board page won't have been created by the time * Workflow object was created: new workflows will have a 0 pageId. * This method is called when the workflow is about to be inserted. * By now, the page has been inserted & we should store the real * page_id this workflow is associated with. */ // store ID of newly created page $title = $obj->getOwnerTitle(); $obj->pageId = $title->getArticleID(Title::GAID_FOR_UPDATE); if ($obj->pageId === 0) { throw new FailCommitException('No page for workflow: ' . serialize($obj)); } } return array('workflow_id' => $obj->id->getAlphadecimal(), 'workflow_type' => $obj->type, 'workflow_wiki' => $obj->wiki, 'workflow_page_id' => $obj->pageId, 'workflow_namespace' => $obj->namespace, 'workflow_title_text' => $obj->titleText, 'workflow_lock_state' => 0, 'workflow_last_update_timestamp' => $obj->lastModified, 'workflow_name' => ''); }
protected function redirect(Workflow $workflow) { $link = $this->urlGenerator->workflowLink($workflow->getArticleTitle(), $workflow->getId()); $this->getOutput()->redirect($link->getFullURL()); }
/** * Populate a fake workflow in the unittest database * * @param array $row * @return Workflow */ protected function generateWorkflow($row = array()) { $row = $row + array('workflow_id' => UUID::create()->getBinary(), 'workflow_type' => 'topic', 'workflow_wiki' => wfWikiId(), 'workflow_page_id' => 1, 'workflow_namespace' => NS_USER_TALK, 'workflow_title_text' => 'Test', 'workflow_lock_state' => 0, 'workflow_last_update_timestamp' => wfTimestampNow()); $workflow = Workflow::fromStorageRow($row); // store workflow: // * so we can retrieve it should we want to store it (see store()) // * so we can remove it on tearDown $this->workflows[$workflow->getId()->getAlphadecimal()] = $workflow; return $workflow; }