/**
  * Receives an html string. It find all images and run them through
  * wfIsBadImage() to determine if the image can be shown.
  *
  * @param DOMNode $node
  * @param Title $title
  * @throws \MWException
  */
 public function apply(DOMNode $node, Title $title)
 {
     if (!$node instanceof DOMElement) {
         return;
     }
     $resource = $node->getAttribute('resource');
     if ($resource === '') {
         return;
     }
     $image = Utils::createRelativeTitle($resource, $title);
     if (!$image) {
         wfDebugLog('Flow', __METHOD__ . ': Could not construct title for node: ' . $node->ownerDocument->saveXML($node));
         return;
     }
     if (!call_user_func($this->isFiltered, $image->getDBkey(), $title)) {
         return;
     }
     // Move up the DOM and remove the typeof="mw:Image" node
     $nodeToRemove = $node->parentNode;
     while ($nodeToRemove instanceof DOMElement && $nodeToRemove->getAttribute('typeof') !== 'mw:Image') {
         $nodeToRemove = $nodeToRemove->parentNode;
     }
     if (!$nodeToRemove) {
         throw new \MWException('Did not find parent mw:Image to remove');
     }
     $nodeToRemove->parentNode->removeChild($nodeToRemove);
 }
 /**
  * @param ReferenceFactory $factory
  * @param Extractor[] $extractors
  * @param string $text
  * @return Reference[]
  * @throws MWException
  * @throws \Flow\Exception\WikitextException
  */
 protected function extractReferences(ReferenceFactory $factory, array $extractors, $text)
 {
     $dom = Utils::createDOM($text);
     $output = array();
     $xpath = new DOMXPath($dom);
     foreach ($extractors as $extractor) {
         $elements = $xpath->query($extractor->getXPath());
         if (!$elements) {
             $class = get_class($extractor);
             throw new MWException("Malformed xpath from {$class}: " . $extractor->getXPath());
         }
         foreach ($elements as $element) {
             try {
                 $ref = $extractor->perform($factory, $element);
             } catch (InvalidReferenceException $e) {
                 wfDebugLog('Flow', 'Invalid reference detected, skipping element');
                 $ref = null;
             }
             // no reference was generated
             if ($ref === null) {
                 continue;
             }
             // reference points to a special page
             if ($ref->getSrcTitle()->isSpecialPage()) {
                 continue;
             }
             $output[] = $ref;
         }
     }
     return $output;
 }
 /**
  * @param string $refType
  * @param string $value
  * @return WikiReference|null
  */
 public function createWikiReference($refType, $value)
 {
     $title = Utils::createRelativeTitle($value, $this->title);
     if ($title === null) {
         return null;
     }
     return new WikiReference($this->workflowId, $this->title, $this->objectType, $this->objectId, $refType, $title);
 }
 public function setUp()
 {
     parent::setUp();
     // Check for Parsoid
     try {
         Utils::convert('html', 'wikitext', 'Foo', Title::newFromText('UTPage'));
     } catch (WikitextException $excep) {
         $this->markTestSkipped('Parsoid not enabled');
     }
 }
 /**
  * Test full roundtrip (wikitext -> html -> wikitext)
  *
  * It doesn't make sense to test only a specific path, since Parsoid's HTML
  * may change beyond our control & it doesn't really matter to us what
  * exactly the HTML looks like, as long as Parsoid is able to understand it.
  *
  * @dataProvider wikitextRoundtripProvider
  */
 public function testwikitextRoundtrip($message, $expect, Title $title)
 {
     // Check for Parsoid
     try {
         $html = Utils::convert('wikitext', 'html', $expect, $title);
         $wikitext = Utils::convert('html', 'wikitext', $html, $title);
         $this->assertEquals($expect, trim($wikitext), $message);
     } catch (WikitextException $excep) {
         $this->markTestSkipped('Parsoid not enabled');
     }
 }
 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);
     }
 }
 /**
  * Creates a DOM with extra considerations for BC with
  * previous parsoid content
  *
  * @param string $content HTML from parsoid
  * @return DOMDocument
  */
 public static function createDOM($content)
 {
     /*
      * The body tag is required otherwise <meta> tags at the top are
      * magic'd into <head> rather than kept with the content.
      */
     if (substr($content, 0, 5) !== '<body') {
         // BC: content currently comes from parsoid and is stored
         // wrapped in <body> tags, but prior to I0d9659f we were
         // storing only the contents and not the body tag itself.
         $content = "<body>{$content}</body>";
     }
     return Utils::createDOM($content);
 }
 public function execute()
 {
     $params = $this->extractRequestParams();
     $page = $this->getTitleOrPageId($params);
     try {
         $content = Utils::convert($params['from'], $params['to'], $params['content'], $page->getTitle());
     } catch (WikitextException $e) {
         $code = $e->getErrorCode();
         $this->dieUsage($this->msg($code)->inContentLanguage()->useDatabase(false)->plain(), $code, $e->getStatusCode(), array('detail' => $e->getMessage()));
         return;
         // helps static analysis know execution does not continue past self::dieUsage
     }
     $result = array('format' => $params['to'], 'content' => $content);
     $this->getResult()->addValue(null, $this->getModuleName(), $result);
 }
 /**
  * @param FormatterRow $row With properties workflow, revision, previous_revision
  * @param IContextSource $ctx
  * @return string|false HTML for contributions entry, or false on failure
  * @throws FlowException
  */
 public function format(FormatterRow $row, IContextSource $ctx)
 {
     $this->serializer->setIncludeHistoryProperties(true);
     $data = $this->serializer->formatApi($row, $ctx, 'contributions');
     if (!$data) {
         return false;
     }
     $charDiff = ChangesList::showCharacterDifference($data['size']['old'], $data['size']['new']);
     $separator = $this->changeSeparator();
     $links = array();
     $links[] = $this->getDiffAnchor($data['links'], $ctx);
     $links[] = $this->getHistAnchor($data['links'], $ctx);
     $description = $this->formatDescription($data, $ctx);
     // Put it all together
     return $this->formatTimestamp($data) . ' ' . $this->formatAnchorsAsPipeList($links, $ctx) . $separator . $charDiff . $separator . $this->getTitleLink($data, $row, $ctx) . (Utils::htmlToPlaintext($description) ? $separator . $description : '') . $this->getHideUnhide($data, $row, $ctx);
 }
 protected function addModules(OutputPage $out, $action)
 {
     if ($this->actions->hasValue($action, 'modules')) {
         $out->addModuleStyles($this->actions->getValue($action, 'modules'));
     } else {
         $out->addModules(array('ext.flow'));
     }
     if ($this->actions->hasValue($action, 'moduleStyles')) {
         $out->addModuleStyles($this->actions->getValue($action, 'moduleStyles'));
     } else {
         $out->addModuleStyles(array('mediawiki.ui', 'mediawiki.ui.anchor', 'mediawiki.ui.button', 'mediawiki.ui.input', 'mediawiki.ui.icon', 'mediawiki.ui.text', 'ext.flow.styles.base', 'ext.flow.mediawiki.ui.tooltips', 'ext.flow.mediawiki.ui.form', 'ext.flow.mediawiki.ui.modal', 'ext.flow.mediawiki.ui.text', 'oojs-ui.styles.icons', 'oojs-ui.styles.icons-layout', 'oojs-ui.styles.icons-interactions', 'ext.flow.board.styles', 'ext.flow.board.topic.styles', 'oojs-ui.styles.icons', 'oojs-ui.styles.icons-alerts', 'oojs-ui.styles.icons-content', 'oojs-ui.styles.icons-layout', 'oojs-ui.styles.icons-movement', 'oojs-ui.styles.icons-indicators', 'oojs-ui.styles.icons-editing-core', 'oojs-ui.styles.icons-moderation', 'oojs-ui.styles.textures'));
     }
     // Add Parsoid modules if necessary
     Parsoid\Utils::onFlowAddModules($out);
     // Allow other extensions to add modules
     Hooks::run('FlowAddModules', array($out));
 }
 /**
  * @dataProvider referenceExtractorProvider
  */
 public function testReferenceExtractor($description, $wikitext, $expectedClass, $expectedType, $expectedTarget, $page = 'UTPage')
 {
     $referenceExtractor = Container::get('reference.extractor');
     $workflow = $this->getMock('Flow\\Model\\Workflow');
     $workflow->expects($this->any())->method('getId')->will($this->returnValue(UUID::create()));
     $workflow->expects($this->any())->method('getArticleTitle')->will($this->returnValue(Title::newMainPage()));
     $factory = new ReferenceFactory($workflow, 'foo', UUID::create());
     $reflMethod = new ReflectionMethod($referenceExtractor, 'extractReferences');
     $reflMethod->setAccessible(true);
     $reflProperty = new \ReflectionProperty($referenceExtractor, 'extractors');
     $reflProperty->setAccessible(true);
     $extractors = $reflProperty->getValue($referenceExtractor);
     $html = Utils::convert('wt', 'html', $wikitext, Title::newFromText($page));
     $result = $reflMethod->invoke($referenceExtractor, $factory, $extractors['post'], $html);
     $this->assertCount(1, $result, $html);
     $result = reset($result);
     $this->assertInstanceOf($expectedClass, $result, $description);
     $this->assertEquals($expectedType, $result->getType(), $description);
     $this->assertEquals($expectedTarget, $result->getTargetIdentifier(), $description);
 }
 /**
  * @param RecentChangesRow $row
  * @param IContextSource $ctx
  * @param bool $linkOnly
  * @return string|false Output line, or false on failure
  * @throws FlowException
  */
 public function format(RecentChangesRow $row, IContextSource $ctx, $linkOnly = false)
 {
     $this->serializer->setIncludeHistoryProperties(true);
     $this->serializer->setIncludeContent(false);
     $data = $this->serializer->formatApi($row, $ctx, 'recentchanges');
     if (!$data) {
         return false;
     }
     if ($linkOnly) {
         return $this->getTitleLink($data, $row, $ctx);
     }
     // The ' . . ' text between elements
     $separator = $this->changeSeparator();
     $links = array();
     $links[] = $this->getDiffAnchor($data['links'], $ctx);
     $links[] = $this->getHistAnchor($data['links'], $ctx);
     $description = $this->formatDescription($data, $ctx);
     $unpatrolledFlag = '';
     if (ChangesList::isUnpatrolled($row->recentChange, $ctx->getUser())) {
         $unpatrolledFlag = ChangesList::flag('unpatrolled') . ' ';
     }
     return $this->formatAnchorsAsPipeList($links, $ctx) . $separator . $unpatrolledFlag . $this->getTitleLink($data, $row, $ctx) . $ctx->msg('semicolon-separator')->escaped() . ' ' . $this->formatTimestamp($data, 'time') . $separator . ChangesList::showCharacterDifference($data['size']['old'], $data['size']['new'], $ctx) . (Utils::htmlToPlaintext($description) ? $separator . $description : '') . $this->getEditSummary($row, $ctx, $data);
 }
 /**
  * @dataProvider provideGetReferencesFromRevisionContent
  */
 public function testGetReferencesAfterRevisionInsert($content, $expectedReferences)
 {
     $content = Utils::convert('wikitext', 'html', $content, $this->workflow->getOwnerTitle());
     $revision = $this->generatePost(array('rev_content' => $content));
     // Save to storage to test if ReferenceRecorder listener picks this up
     $this->store($this->revision);
     $this->store($revision);
     $expectedReferences = $this->expandReferences($this->workflow, $revision, $expectedReferences);
     // References will be stored as linked from Topic:<id>
     $title = Title::newFromText($this->workflow->getId()->getAlphadecimal(), NS_TOPIC);
     // Retrieve references from storage
     $foundReferences = $this->updater->getReferencesForTitle($title);
     $this->assertReferenceListsEqual($expectedReferences, $foundReferences);
 }
 /**
  * Mimic Echo parameter formatting
  *
  * @param string $param The requested i18n parameter
  * @param AbstractRevision|AbstractRevision[] $revision The revision or
  *  revisions to format or an array of revisions
  * @param UUID $workflowId The UUID of the workflow $revision belongs tow
  * @param IContextSource $ctx
  * @param FormatterRow|null $row
  * @return mixed A valid parameter for a core Message instance. These
  *  parameters will be used with Message::parse
  * @throws FlowException
  */
 public function processParam($param, $revision, UUID $workflowId, IContextSource $ctx, FormatterRow $row = null)
 {
     switch ($param) {
         case 'creator-text':
             if ($revision instanceof PostRevision) {
                 return $this->usernames->getFromTuple($revision->getCreatorTuple());
             } else {
                 return '';
             }
         case 'user-text':
             return $this->usernames->getFromTuple($revision->getUserTuple());
         case 'user-links':
             return Message::rawParam($this->templating->getUserLinks($revision));
         case 'summary':
             if (!$this->permissions->isAllowed($revision, 'view')) {
                 return '';
             }
             /*
              * Fetch in HTML; unparsed wikitext in summary is pointless.
              * Larger-scale wikis will likely also store content in html, so no
              * Parsoid roundtrip is needed then (and if it *is*, it'll already
              * be needed to render Flow discussions, so this is manageable)
              */
             $content = $this->templating->getContent($revision, 'fixed-html');
             // strip html tags and decode to plaintext
             $content = Utils::htmlToPlaintext($content, 140, $ctx->getLanguage());
             return Message::plaintextParam($content);
         case 'wikitext':
             if (!$this->permissions->isAllowed($revision, 'view')) {
                 return '';
             }
             $content = $this->templating->getContent($revision, 'wikitext');
             // This must be escaped and marked raw to prevent special chars in
             // content, like $1, from changing the i18n result
             return Message::plaintextParam($content);
             // This is potentially two networked round trips, much too expensive for
             // the rendering loop
         // This is potentially two networked round trips, much too expensive for
         // the rendering loop
         case 'prev-wikitext':
             if ($revision->isFirstRevision()) {
                 return '';
             }
             if ($row === null) {
                 $previousRevision = $revision->getCollection()->getPrevRevision($revision);
             } else {
                 $previousRevision = $row->previousRevision;
             }
             if (!$previousRevision) {
                 return '';
             }
             if (!$this->permissions->isAllowed($previousRevision, 'view')) {
                 return '';
             }
             $content = $this->templating->getContent($previousRevision, 'wikitext');
             return Message::plaintextParam($content);
         case 'workflow-url':
             return $this->urlGenerator->workflowLink(null, $workflowId)->getFullUrl();
         case 'post-url':
             if (!$revision instanceof PostRevision) {
                 throw new FlowException('Expected PostRevision but received' . get_class($revision));
             }
             return $this->urlGenerator->postLink(null, $workflowId, $revision->getPostId())->getFullUrl();
         case 'moderated-reason':
             // don-t parse wikitext in the moderation reason
             return Message::plaintextParam($revision->getModeratedReason());
         case 'topic-of-post':
             if (!$revision instanceof PostRevision) {
                 throw new FlowException('Expected PostRevision but received ' . get_class($revision));
             }
             $root = $revision->getRootPost();
             if (!$this->permissions->isAllowed($root, 'view')) {
                 return '';
             }
             $content = $this->templating->getContent($root, 'wikitext');
             return Message::plaintextParam($content);
         case 'post-of-summary':
             if (!$revision instanceof PostSummary) {
                 throw new FlowException('Expected PostSummary but received ' . get_class($revision));
             }
             /** @var PostRevision $post */
             $post = $revision->getCollection()->getPost()->getLastRevision();
             if (!$this->permissions->isAllowed($post, 'view')) {
                 return '';
             }
             if ($post->isTopicTitle()) {
                 return Message::plaintextParam($this->templating->getContent($post, 'wikitext'));
             } else {
                 return Message::rawParam($this->templating->getContent($post, 'fixed-html'));
             }
         case 'bundle-count':
             return Message::numParam(count($revision));
         default:
             wfWarn(__METHOD__ . ': Unknown formatter parameter: ' . $param);
             return '';
     }
 }
 /**
  * Called when a new Post is added, whether it be a new topic or a reply.
  * Do not call directly, use notifyPostChange for new replies.
  * @param  array $data Associative array of parameters, all required:
  * * title: Title for the page on which the new Post sits.
  * * user: User who created the new Post.
  * * post: The Post that was created.
  * * topic-title: The title for the Topic.
  * @return array Array of created EchoEvent objects.
  * @throws FlowException When $data contains unexpected types/values
  */
 protected function notifyNewPost($data)
 {
     // Handle mentions.
     $newRevision = $data['post'];
     if ($newRevision !== null && !$newRevision instanceof PostRevision) {
         throw new FlowException('Expected PostRevision but received ' . get_class($newRevision));
     }
     $topicRevision = $data['topic-title'];
     if (!$topicRevision instanceof PostRevision) {
         throw new FlowException('Expected PostRevision but received ' . get_class($topicRevision));
     }
     $title = $data['title'];
     if (!$title instanceof \Title) {
         throw new FlowException('Expected Title but received ' . get_class($title));
     }
     $user = $data['user'];
     $topicWorkflow = $data['topic-workflow'];
     if (!$topicWorkflow instanceof Workflow) {
         throw new FlowException('Expected Workflow but received ' . get_class($topicWorkflow));
     }
     $events = array();
     $mentionedUsers = $newRevision ? $this->getMentionedUsers($newRevision, $title) : array();
     if (!$topicRevision instanceof PostRevision) {
         throw new FlowException('Expected PostRevision but received: ' . get_class($topicRevision));
     }
     if (count($mentionedUsers)) {
         $events[] = EchoEvent::create(array('type' => 'flow-mention', 'title' => $title, 'extra' => array('content' => $newRevision ? Utils::htmlToPlaintext($newRevision->getContent(), 200, $this->language) : null, 'topic-title' => $this->language->truncate(trim($topicRevision->getContent('wikitext')), 200), 'post-id' => $newRevision ? $newRevision->getPostId() : null, 'mentioned-users' => $mentionedUsers, 'topic-workflow' => $topicWorkflow->getId(), 'target-page' => $topicWorkflow->getArticleTitle()->getArticleID(), 'reply-to' => isset($data['reply-to']) ? $data['reply-to'] : null), 'agent' => $user));
     }
     return $events;
 }
 /**
  * The native LogFormatter::getActionText provides no clean way of handling
  * the Flow action text in a plain text format (e.g. as used by CheckUser)
  *
  * @return string
  */
 public function getActionText()
 {
     $text = $this->getActionMessage();
     return $this->plaintext ? Utils::htmlToPlaintext($text) : $text;
 }
 /**
  * Only extract templates to copy to Flow description.
  * Requires Parsoid, to reliably extract templates.
  *
  * @param string $content
  * @return string
  */
 protected function extractTemplates($content)
 {
     $content = Utils::convert('wikitext', 'html', $content, $this->title);
     $dom = Utils::createDOM($content);
     $xpath = new \DOMXPath($dom);
     $templates = $xpath->query('//*[@typeof="mw:Transclusion"]');
     $content = '';
     foreach ($templates as $template) {
         $content .= $dom->saveHTML($template) . "\n";
     }
     return Utils::convert('html', 'wikitext', $content, $this->title);
 }
 public function processPostCollection(array $context, array $collection, $indentLevel = 0)
 {
     $indent = str_repeat(':', $indentLevel);
     $output = '';
     foreach ($collection as $postId) {
         $revisionId = reset($context['posts'][$postId]);
         $revision = $context['revisions'][$revisionId];
         // Skip moderated posts
         if ($revision['isModerated']) {
             continue;
         }
         $user = User::newFromName($revision['author']['name'], false);
         $postId = Flow\Model\UUID::create($postId);
         $content = $revision['content']['content'];
         $contentFormat = $revision['content']['format'];
         if ($contentFormat !== 'wikitext') {
             $content = Utils::convert($contentFormat, 'wikitext', $content, $this->pageTitle);
         }
         $thisPost = $indent . trim($content) . ' ' . $this->getSignature($user, $postId->getTimestamp()) . "\n";
         if ($indentLevel > 0) {
             $thisPost = preg_replace("/\n+/", "\n", $thisPost);
         }
         $output .= str_replace("\n", "\n{$indent}", trim($thisPost)) . "\n";
         if (isset($revision['replies'])) {
             $output .= $this->processPostCollection($context, $revision['replies'], $indentLevel + 1);
         }
         if ($indentLevel == 0) {
             $output .= "\n";
         }
     }
     return $output;
 }
 /**
  * Should only be used for setting the initial content.  To set subsequent content
  * use self::setNextContent
  *
  * @param string $content
  * @param string $format wikitext|html
  * @param Title|null $title When null the related workflow will be lazy-loaded to locate the title
  * @throws DataModelException
  */
 protected function setContent($content, $format, Title $title = null)
 {
     if ($this->moderationState !== self::MODERATED_NONE) {
         throw new DataModelException('TODO: Cannot change content of restricted revision', 'process-data');
     }
     if ($this->content !== null) {
         throw new DataModelException('Updating content must use setNextContent method', 'process-data');
     }
     if (!$title) {
         $title = $this->getCollection()->getTitle();
     }
     // never trust incoming html - roundtrip to wikitext first
     if ($format !== 'wikitext') {
         $content = Utils::convert($format, 'wikitext', $content, $title);
         $format = 'wikitext';
     }
     // Run pre-save transform
     $content = ContentHandler::makeContent($content, $title, CONTENT_MODEL_WIKITEXT)->preSaveTransform($title, $this->getUser(), WikiPage::factory($title)->makeParserOptions($this->getUser()))->serialize('text/x-wiki');
     // Keep consistent with normal edit page, trim only trailing whitespaces
     $content = rtrim($content);
     $this->convertedContent = array($format => $content);
     // convert content to desired storage format
     $storageFormat = $this->getStorageFormat();
     if ($this->isFormatted() && $storageFormat !== $format) {
         $this->convertedContent[$storageFormat] = Utils::convert($format, $storageFormat, $content, $title);
     }
     $this->content = $this->decompressedContent = $this->convertedContent[$storageFormat];
     $this->contentUrl = null;
     // should this only remove a subset of flags?
     $this->flags = array_filter(explode(',', \Revision::compressRevisionText($this->content)));
     $this->flags[] = $storageFormat;
     $this->contentLength = mb_strlen($this->getContent('wikitext'));
 }