/** * 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')); }