EBernhardson has uploaded a new change for review.
https://gerrit.wikimedia.org/r/178968
Change subject: Expose the convertToText method via Special:FlowExport
......................................................................
Expose the convertToText method via Special:FlowExport
As we progress towards users being able to enable flow boards
themselves rather than only when we whitelist them, communities
need the tools necessary to turn pages they did not want to be
flow boards back into wikitext.
This patch moves the bulk of the convertToText.php maintenance
script into a utility class and exposes it from a new special
page, Special:FlowExport.
Change-Id: Ib52468323040be273a68a6072dda3811e27982bf
---
M Flow.php
M autoload.php
M i18n/en.json
M i18n/qqq.json
A includes/SpecialFlowExport.php
A includes/Utils/Export.php
M maintenance/convertToText.php
7 files changed, 327 insertions(+), 149 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow
refs/changes/68/178968/1
diff --git a/Flow.php b/Flow.php
index 8c68f64..96de9f2 100644
--- a/Flow.php
+++ b/Flow.php
@@ -54,6 +54,7 @@
// Special:Flow
$wgExtensionMessagesFiles['FlowAlias'] = $dir . 'Flow.alias.php';
$wgSpecialPages['Flow'] = 'Flow\SpecialFlow';
+$wgSpecialPages['FlowExport'] = 'Flow\SpecialFlowExport';
$wgSpecialPageGroups['Flow'] = 'redirects';
// Housekeeping hooks
diff --git a/autoload.php b/autoload.php
index 1177875..dd8d5ec 100644
--- a/autoload.php
+++ b/autoload.php
@@ -263,6 +263,7 @@
'Flow\\SpamFilter\\SpamFilter' => __DIR__ .
'/includes/SpamFilter/SpamFilter.php',
'Flow\\SpamFilter\\SpamRegex' => __DIR__ .
'/includes/SpamFilter/SpamRegex.php',
'Flow\\SpecialFlow' => __DIR__ . '/includes/SpecialFlow.php',
+ 'Flow\\SpecialFlowExport' => __DIR__ .
'/includes/SpecialFlowExport.php',
'Flow\\SubmissionHandler' => __DIR__ .
'/includes/SubmissionHandler.php',
'Flow\\TalkpageManager' => __DIR__ . '/includes/TalkpageManager.php',
'Flow\\TemplateHelper' => __DIR__ . '/includes/TemplateHelper.php',
@@ -280,6 +281,7 @@
'Flow\\Tests\\Api\\ApiTestCase' => __DIR__ .
'/tests/phpunit/api/ApiTestCase.php',
'Flow\\Tests\\Api\\ApiWatchTopicTest' => __DIR__ .
'/tests/phpunit/api/ApiWatchTopicTest.php',
'Flow\\Tests\\BlockFactoryTest' => __DIR__ .
'/tests/phpunit/BlockFactoryTest.php',
+ 'Flow\\Tests\\Block\\TopicListTest' => __DIR__ .
'/tests/phpunit/Block/TopicListTest.php',
'Flow\\Tests\\BufferedBagOStuffTest' => __DIR__ .
'/tests/phpunit/Data/BagOStuff/BufferedBagOStuffTest.php',
'Flow\\Tests\\BufferedCacheTest' => __DIR__ .
'/tests/phpunit/Data/BufferedCacheTest.php',
'Flow\\Tests\\Collection\\PostCollectionTest' => __DIR__ .
'/tests/phpunit/Collection/PostCollectionTest.php',
@@ -341,6 +343,7 @@
'Flow\\Tests\\UrlGeneratorTest' => __DIR__ .
'/tests/phpunit/UrlGeneratorTest.php',
'Flow\\Tests\\WatchedTopicItemTest' => __DIR__ .
'/tests/phpunit/WatchedTopicItemsTest.php',
'Flow\\UrlGenerator' => __DIR__ . '/includes/UrlGenerator.php',
+ 'Flow\\Utils\\Export' => __DIR__ . '/includes/Utils/Export.php',
'Flow\\Utils\\NamespaceIterator' => __DIR__ .
'/includes/Utils/NamespaceIterator.php',
'Flow\\Utils\\PagesWithPropertyIterator' => __DIR__ .
'/includes/Utils/PagesWithPropertyIterator.php',
'Flow\\View' => __DIR__ . '/includes/View.php',
diff --git a/i18n/en.json b/i18n/en.json
index 4737897..6bfa1cf 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -463,5 +463,11 @@
"flow-edited-by": "Edited by $1",
"flow-lqt-redirect-reason": "Redirecting retired LiquidThreads post to
its converted Flow post",
"flow-talk-conversion-move-reason": "Conversion of wikitext talk to
Flow from $1",
- "flow-talk-conversion-archive-edit-reason": "Wikitext talk to Flow
conversion"
+ "flow-talk-conversion-archive-edit-reason": "Wikitext talk to Flow
conversion",
+ "flow-special-export-desc": "Export flow board to wikitext",
+ "flow-special-export-title-label": "Page to export",
+ "flow-special-export-error-invalid-title": "An invalid title was
provided",
+ "flow-special-export-error-non-existant-title": "The provided page does
not exist",
+ "flow-special-export-error-wrong-content-model": "The provided page is
not a flow board",
+ "flow-special-export-error-topic-not-implemented": "Export of
individual topics not yet implemented"
}
diff --git a/i18n/qqq.json b/i18n/qqq.json
index f9a9c1a..348a3f4 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -467,5 +467,11 @@
"flow-edited-by": "Message displayed below a post to indicate it has
last been edited by a user other than the original author",
"flow-lqt-redirect-reason": "Edit summary used to redirect old LQT
thread pages to Flow topics",
"flow-talk-conversion-move-reason": "Message used as an edit summary
when moving an existing talk page to an archive location in preparation for
enabling flow on that page.\nParameters:\n* $1 - Title the page was moved from",
- "flow-talk-conversion-archive-edit-reason": "Message used as an edit
summary when appending a template to a wikitext talk page after archiving it in
preparation for conversion to Flow."
+ "flow-talk-conversion-archive-edit-reason": "Message used as an edit
summary when appending a template to a wikitext talk page after archiving it in
preparation for conversion to Flow.",
+ "flow-special-export-desc": "Description at the top of
Special:FlowExport describing what this special page is for",
+ "flow-special-export-title-label": "Label on the form input of
Special:FlowExport for inputting the article to be exported",
+ "flow-special-export-error-invalid-title": "An error message from
Special:FlowExport when the user provided title to export is not a valid
mediawiki page title.",
+ "flow-special-export-error-non-existant-title": "An error message from
Special:FlowExport when the user provided title to export does not exist.",
+ "flow-special-export-error-wrong-content-model": "An error message from
Special:FlowExport when the user provided title to export is not a flow board.",
+ "flow-special-export-error-topic-not-implemented": "An error message
from Special:FlowExport when the user attempts to export a single topic rather
than a complete flow board."
}
diff --git a/includes/SpecialFlowExport.php b/includes/SpecialFlowExport.php
new file mode 100644
index 0000000..caf8d80
--- /dev/null
+++ b/includes/SpecialFlowExport.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * A special page that redirects to a workflow or PostRevision given a UUID
+ */
+
+namespace Flow;
+
+use Flow\Exception\FlowException;
+use FormSpecialPage;
+use HTMLForm;
+use Status;
+use Title;
+
+class SpecialFlowExport extends FormSpecialPage {
+
+ /**
+ * The type of content, e.g. 'post', 'workflow'
+ * @var string $type
+ */
+ protected $type;
+
+ /**
+ * Flow UUID
+ * @var string $uuid
+ */
+ protected $uuid;
+
+ public function __construct() {
+ parent::__construct( 'FlowExport' );
+ }
+
+ protected function getFormFields() {
+ return array(
+ 'export' => array(
+ 'id' => 'mw-flow-special-export-title',
+ 'name' => 'export',
+ 'type' => 'text',
+ 'label-message' =>
'flow-special-export-title-label',
+ ),
+ );
+ }
+
+ /**
+ * Description shown at the top of the page
+ * @return string
+ */
+ protected function preText() {
+ return '<p>' . $this->msg( 'flow-special-export-desc'
)->escaped() . '</p>';
+ }
+
+ protected function alterForm( HTMLForm $form ) {
+ // Style the form.
+ $form->setDisplayFormat( 'vform' );
+ $form->setWrapperLegend( false );
+
+ $form->setMethod( 'get' ); // This also submits the form every
time the page loads.
+ }
+
+ /**
+ * Set redirect and return true if $data['uuid'] or $this->par exists
and is
+ * a valid UUID; otherwise return false or a Status object
encapsulating any
+ * error, which causes the form to be shown.
+ * @param array $data
+ * @return bool|Status
+ */
+ public function onSubmit( array $data ) {
+ if ( !isset( $data['export'] ) ) {
+ return false;
+ }
+
+ $title = Title::newFromText( $data['export'] );
+ if ( !$title ) {
+ return Status::newFatal(
'flow-special-export-error-invalid-title' );
+ }
+ if ( !$title->exists() ) {
+ return Status::newFatal(
'flow-special-export-error-non-existent-title' );
+ }
+ if ( $title->getContentModel() !== CONTENT_MODEL_FLOW_BOARD ) {
+ return Status::newFatal(
'flow-special-export-error-wrong-content-model' );
+ }
+ if ( $title->getNamespace() === NS_TOPIC ) {
+ return Status::newFatal(
'flow-special-export-error-topic-not-implemented' );
+ }
+
+ $exporter = new \Flow\Utils\Export;
+ $renderer = Container::get( 'lightncandy' )->getTemplate(
'flow_board_export_wikitext' );
+
+ $this->getOutput()->addHTML( $renderer( array(
+ 'title' => $title->getPrefixedText(),
+ 'wikitext' => $exporter->export( $title ),
+ ) ) );
+
+ return true;
+ }
+}
diff --git a/includes/Utils/Export.php b/includes/Utils/Export.php
new file mode 100644
index 0000000..fb5e872
--- /dev/null
+++ b/includes/Utils/Export.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace Flow\Utils;
+
+use APIMain;
+use FauxRequest;
+use Flow\Model\UUID;
+use Flow\Parsoid\Utils;
+use MWException;
+use MWTimestamp;
+use StubObject;
+use Title;
+use User;
+
+class Export {
+ /**
+ * @param Title $title
+ * @return string wikitext
+ */
+ public function export( Title $title ) {
+ return $this->getHeaderWikitext( $title ) . "\n" .
$this->getTopicsWikitext( $title );
+ }
+
+ /**
+ * @param Title $title
+ * @return string wikitext
+ */
+ public function getHeaderWikitext( Title $title ) {
+ $headerData = $this->flowApi(
+ $title,
+ 'view-header',
+ array( 'vhcontentFormat' => 'wikitext' ),
+ 'header'
+ );
+
+ if ( !isset( $headerData['header']['revision'] ) ) {
+ return '';
+ }
+
+ $headerRevision = $headerData['header']['revision'];
+ if ( !isset( $headerRevision['content'] ) ) {
+ return '';
+ }
+
+ return $headerRevision['content']['content'];
+ }
+
+ /**
+ * @param Title $title
+ * @return string wikitext
+ */
+ public function getTopicsWikitext( $title ) {
+ $pagerParams = array( 'vtllimit' => 1 );
+ $topics = array();
+
+ do {
+ $flowData = $this->flowApi(
+ $title,
+ 'view-topiclist',
+ $pagerParams,
+ 'topiclist'
+ );
+
+ $topicListBlock = $flowData['topiclist'];
+
+ foreach( $topicListBlock['roots'] as $rootPostId ) {
+ $revisionId = reset(
$topicListBlock['posts'][$rootPostId] );
+ $revision =
$topicListBlock['revisions'][$revisionId];
+
+ $topics[] =
"=={$revision['content']['content']}==\n"
+ . $this->processPostCollection( $title,
$topicListBlock, $revision['replies'] );
+ }
+ } while( $pagerParams = $this->getNextPageParams(
$topicListBlock ) );
+
+ // The topics were received from newest to oldest, flip that
aroud so it matches
+ // wikitext where the oldest topic is at the top.
+ return implode( "\n", array_reverse( $topics ) );
+ }
+
+ protected function getNextPageParams( $topicListBlock ) {
+ // No forward pagination exists, we have reached the end
+ if ( !isset(
$topicListBlock['links']['pagination']['fwd']['url'] ) ) {
+ return null;
+ }
+
+ $query = parse_url(
$topicListBlock['links']['pagination']['fwd']['url'] , PHP_URL_QUERY );
+ if ( !$query ) {
+ throw new FlowException( __METHOD__ . ': Forward
pagination has no url' );
+ return null;
+ }
+
+ parse_str( $query, $queryParams );
+ if ( !isset( $queryParams['topiclist_offset-id'] ) ) {
+ throw new FlowException( __METHOD__ . ': expected query
parameters to include topiclist_offset-id' );
+ return null;
+ }
+
+ return array(
+ 'vtloffset-id' => $queryParams['topiclist_offset-id'],
+ 'vtloffset-dir' => 'fwd',
+ 'vtloffset-limit' => '1',
+ );
+ }
+
+ /**
+ * @param Title $title
+ * @param string $submodule
+ * @param array $request
+ * @param bool $requiredBlock
+ * @return array
+ * @throws MWException
+ * @todo its much harder to test with this embedded inside, should add
an abstraction
+ * for calling ApiMain through an instantiated class instead of newing
it up.
+ */
+ public function flowApi( Title $title, $submodule, array $request,
$requiredBlock = false ) {
+ $request = new FauxRequest( $request + array(
+ 'action' => 'flow',
+ 'submodule' => $submodule,
+ 'page' => $title->getPrefixedText(),
+ ) );
+
+ $api = new ApiMain( $request );
+ $api->execute();
+
+ $apiResponse = $api->getResult()->getData();
+
+ if ( ! isset( $apiResponse['flow'] ) ) {
+ throw new MWException( "API response has no Flow data"
);
+ }
+
+ $flowData = $apiResponse['flow'][$submodule]['result'];
+
+ if( $requiredBlock !== false && ! isset(
$flowData[$requiredBlock] ) ) {
+ throw new MWException( "No $requiredBlock block in API
response" );
+ }
+
+ return $flowData;
+ }
+
+ public function processPostCollection( Title $title, 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 = UUID::create( $postId );
+
+ $content = $revision['content']['content'];
+ $contentFormat = $revision['content']['format'];
+
+ if ( $contentFormat !== 'wikitext' ) {
+ $content = Utils::convert( $contentFormat,
'wikitext', $content, $title );
+ }
+
+ $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(
$title, $context, $revision['replies'], $indentLevel + 1 );
+ }
+
+ if ( $indentLevel == 0 ) {
+ $output .= "\n";
+ }
+ }
+
+ return $output;
+ }
+
+ public function getSignature( $user, $timestamp ) {
+ global $wgContLang, $wgParser;
+
+ // Force unstub
+ StubObject::unstub( $wgParser );
+
+ $timestamp = MWTimestamp::getLocalInstance( $timestamp );
+ $ts = $timestamp->format( 'YmdHis' );
+ $tzMsg = $timestamp->format( 'T' ); # might vary on DST
changeover!
+
+ # Allow translation of timezones through wiki. format() can
return
+ # whatever crap the system uses, localised or not, so we cannot
+ # ship premade translations.
+ $key = 'timezone-' . strtolower( trim( $tzMsg ) );
+ $msg = wfMessage( $key )->inContentLanguage();
+ if ( $msg->exists() ) {
+ $tzMsg = $msg->text();
+ }
+
+ $d = $wgContLang->timeanddate( $ts, false, false ) . "
($tzMsg)";
+
+ if ( $user ) {
+ return $wgParser->getUserSig( $user, false, false ) . '
' . $d;
+ } else {
+ return "[Unknown user] $d";
+ }
+ }
+}
+
diff --git a/maintenance/convertToText.php b/maintenance/convertToText.php
index 32f6159..b6c7ec0 100644
--- a/maintenance/convertToText.php
+++ b/maintenance/convertToText.php
@@ -27,153 +27,8 @@
$this->error( 'Invalid page title', true );
}
- $continue = true;
- $pagerParams = array( 'vtllimit' => 1 );
- $topics = array();
- $headerContent = '';
-
- $headerData = $this->flowApi( $this->pageTitle, 'view-header',
array( 'vhcontentFormat' => 'wikitext' ), 'header' );
-
- $headerRevision = $headerData['header']['revision'];
- if ( isset( $headerRevision['content'] ) ) {
- $headerContent = $headerRevision['content'];
- }
-
- while( $continue ) {
- $continue = false;
- $flowData = $this->flowApi( $this->pageTitle,
'view-topiclist', $pagerParams, 'topiclist' );
-
- $topicListBlock = $flowData['topiclist'];
-
- foreach( $topicListBlock['roots'] as $rootPostId ) {
- $revisionId = reset(
$topicListBlock['posts'][$rootPostId] );
- $revision =
$topicListBlock['revisions'][$revisionId];
-
- $topicOutput = '==' .
$revision['content']['content'] . '==' . "\n";
- $topicOutput .= $this->processPostCollection(
$topicListBlock, $revision['replies'] );
-
- $topics[] = $topicOutput;
- }
-
- $paginationLinks =
$topicListBlock['links']['pagination'];
- if ( isset( $paginationLinks['fwd'] ) ) {
- list( $junk, $query ) = explode( '?',
$paginationLinks['fwd']['url'] );
- $queryParams = wfCGIToArray( $query );
-
- $pagerParams = array(
- 'vtloffset-id' =>
$queryParams['topiclist_offset-id'],
- 'vtloffset-dir' => 'fwd',
- 'vtloffset-limit' => '1',
- );
- $continue = true;
- }
- }
-
- print $headerContent . implode( "\n", array_reverse( $topics )
);
- }
-
- /**
- * @param Title $title
- * @param string $submodule
- * @param array $request
- * @param bool $requiredBlock
- * @return array
- * @throws MWException
- */
- public function flowApi( Title $title, $submodule, array $request,
$requiredBlock = false ) {
- $request = new FauxRequest( $request + array(
- 'action' => 'flow',
- 'submodule' => $submodule,
- 'page' => $title->getPrefixedText(),
- ) );
-
- $api = new ApiMain( $request );
- $api->execute();
-
- $apiResponse = $api->getResult()->getData();
-
- if ( ! isset( $apiResponse['flow'] ) ) {
- throw new MWException( "API response has no Flow data"
);
- }
-
- $flowData = $apiResponse['flow'][$submodule]['result'];
-
- if( $requiredBlock !== false && ! isset(
$flowData[$requiredBlock] ) ) {
- throw new MWException( "No $requiredBlock block in API
response" );
- }
-
- return $flowData;
- }
-
- 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;
- }
-
- public function getSignature( $user, $timestamp ) {
- global $wgContLang, $wgParser;
-
- // Force unstub
- StubObject::unstub( $wgParser );
-
- $timestamp = MWTimestamp::getLocalInstance( $timestamp );
- $ts = $timestamp->format( 'YmdHis' );
- $tzMsg = $timestamp->format( 'T' ); # might vary on DST
changeover!
-
- # Allow translation of timezones through wiki. format() can
return
- # whatever crap the system uses, localised or not, so we cannot
- # ship premade translations.
- $key = 'timezone-' . strtolower( trim( $tzMsg ) );
- $msg = wfMessage( $key )->inContentLanguage();
- if ( $msg->exists() ) {
- $tzMsg = $msg->text();
- }
-
- $d = $wgContLang->timeanddate( $ts, false, false ) . "
($tzMsg)";
-
- if ( $user ) {
- return $wgParser->getUserSig( $user, false, false ) . '
' . $d;
- } else {
- return "[Unknown user] $d";
- }
+ $exporter = new Flow\Utils\Export;
+ print $exporter->export( $this->pageTitle );
}
}
--
To view, visit https://gerrit.wikimedia.org/r/178968
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ib52468323040be273a68a6072dda3811e27982bf
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: EBernhardson <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits