jenkins-bot has submitted this change and it was merged.

Change subject: Beta feature Flow on user talk page
......................................................................


Beta feature Flow on user talk page

When $wgFlowEnableOptInBetaFeature is true,
a new beta feature allows using to
enable Flow on their talk page.

There is also a maintenance script to
auto opt-in users who are already using
Flow on their user talk page.

Bug: T98270
Change-Id: Ia9950c4eb1c0e5f912761a65c76c5a2b3b99c8ee
---
M Flow.php
M Hooks.php
M autoload.php
M defines.php
M i18n/en.json
M i18n/qqq.json
A images/betafeature-flow-ltr.svg
A images/betafeature-flow-rtl.svg
A includes/Import/ArchiveNameHelper.php
M includes/Import/Converter.php
M includes/Import/Exception.php
M includes/Import/LiquidThreadsApi/ConversionStrategy.php
A includes/Import/OptInController.php
A includes/Import/OptInUpdate.php
A includes/Import/TemplateHelper.php
M includes/Import/Wikitext/ConversionStrategy.php
M includes/Notifications/Controller.php
M includes/Notifications/Notifications.php
A maintenance/FlowUpdateBetaFeaturePreference.php
A tests/browser/features/opt_in.feature
A tests/browser/features/step_definitions/opt_in_steps.rb
M tests/browser/features/support/data_manager.rb
M tests/browser/features/support/pages/abstract_flow_page.rb
A tests/browser/features/support/pages/flow_component.rb
D tests/browser/features/support/pages/new_wiki_page.rb
A tests/browser/features/support/pages/special_notifications_page.rb
A tests/browser/features/support/pages/special_preferences_page.rb
A tests/browser/features/support/pages/user_talk_page.rb
M tests/browser/features/support/pages/wiki_page.rb
A tests/phpunit/Import/ArchiveNameHelperTest.php
M tests/phpunit/Import/ConverterTest.php
A tests/phpunit/Import/TemplateHelperTest.php
32 files changed, 1,337 insertions(+), 122 deletions(-)

Approvals:
  Catrope: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/Flow.php b/Flow.php
index ccc9e4b..2906511 100644
--- a/Flow.php
+++ b/Flow.php
@@ -166,6 +166,10 @@
 $wgHooks['EchoGetDefaultNotifiedUsers'][] = 
'Flow\NotificationController::getDefaultNotifiedUsers';
 $wgHooks['EchoGetBundleRules'][] = 
'Flow\NotificationController::onEchoGetBundleRules';
 
+// Beta feature Flow on user talk page
+$wgHooks['GetBetaFeaturePreferences'][] = 
'FlowHooks::onGetBetaFeaturePreferences';
+$wgHooks['UserSaveOptions'][] = 'FlowHooks::onUserSaveOptions';
+
 // Extension initialization
 $wgExtensionFunctions[] = 'FlowHooks::initFlowExtension';
 
@@ -403,3 +407,6 @@
 
 // Temporary field to allow source wiki to be null for references until it's 
backfilled.
 $wgFlowMigrateReferenceWiki = false;
+
+// Enable/Disable Opt-in beta feature
+$wgFlowEnableOptInBetaFeature = false;
diff --git a/Hooks.php b/Hooks.php
index 6bf3a96..95f53f5 100644
--- a/Hooks.php
+++ b/Hooks.php
@@ -6,6 +6,7 @@
 use Flow\Exception\FlowException;
 use Flow\Exception\PermissionException;
 use Flow\Formatter\CheckUserQuery;
+use Flow\Import\OptInUpdate;
 use Flow\Model\UUID;
 use Flow\OccupationController;
 use Flow\SpamFilter\AbuseFilter;
@@ -230,6 +231,9 @@
 
                require_once __DIR__.'/maintenance/FlowFixLinks.php';
                $updater->addPostDatabaseUpdateMaintenance( 'FlowFixLinks' );
+
+               require_once 
__DIR__.'/maintenance/FlowUpdateBetaFeaturePreference.php';
+               $updater->addPostDatabaseUpdateMaintenance( 
'FlowUpdateBetaFeaturePreference' );
 
                return true;
        }
@@ -1577,9 +1581,92 @@
         *
         * @param array $namespaces Associative array mapping namespace index
         *  to name
+        * @return bool
         */
        public static function onSearchableNamespaces( &$namespaces ) {
                unset( $namespaces[NS_TOPIC] );
                return true;
        }
+
+       /**
+        * @return bool
+        */
+       private static function isBetaFeatureAvailable() {
+               global $wgBetaFeaturesWhitelist, $wgFlowEnableOptInBetaFeature;
+               return $wgFlowEnableOptInBetaFeature &&
+                       ( !is_array( $wgBetaFeaturesWhitelist ) || in_array( 
BETA_FEATURE_FLOW_USER_TALK_PAGE, $wgBetaFeaturesWhitelist ) );
+       }
+
+       /**
+        * @param User $user
+        * @param array $prefs
+        * @return bool
+        */
+       public static function onGetBetaFeaturePreferences( $user, &$prefs ) {
+               global $wgExtensionAssetsPath;
+
+               if ( !self::isBetaFeatureAvailable() ) {
+                       return true;
+               }
+
+               $defaultProjectUrl = 
'https://www.mediawiki.org/wiki/Extension:Flow';
+               $defaultProjectTalkUrl = 
'https://www.mediawiki.org/wiki/Extension_talk:Flow';
+
+               $prefs[BETA_FEATURE_FLOW_USER_TALK_PAGE] = array(
+                       // The first two are message keys
+                       'label-message' => 
'flow-talk-page-beta-feature-message',
+                       'desc-message' => 
'flow-talk-page-beta-feature-description',
+                       'screenshot' => array(
+                               'ltr' => 
"$wgExtensionAssetsPath/Flow/images/betafeature-flow-ltr.svg",
+                               'rtl' => 
"$wgExtensionAssetsPath/Flow/images/betafeature-flow-rtl.svg",
+                       ),
+                       'info-link' => self::getTitleUrlOrDefault( 
'Project:Flow', $defaultProjectUrl ),
+                       'discussion-link' => self::getTitleUrlOrDefault( 
'Project_talk:Flow', $defaultProjectTalkUrl ),
+               );
+
+               return true;
+       }
+
+       /**
+        * @param string $titleText
+        * @param string $default
+        * @return string
+        */
+       private static function getTitleUrlOrDefault( $titleText, $default ) {
+               $title = Title::newFromText( $titleText );
+               return $title->exists() ? $title->getLocalURL() : $default;
+       }
+
+       /**
+        * @param User $user
+        * @param array $options
+        * @return bool
+        */
+       public static function onUserSaveOptions( $user, &$options ) {
+               if ( !self::isBetaFeatureAvailable() ) {
+                       return true;
+               }
+
+               if ( !array_key_exists( BETA_FEATURE_FLOW_USER_TALK_PAGE, 
$options ) ) {
+                       return true;
+               }
+
+               $userClone = User::newFromId( $user->getId() );
+               $before = BetaFeatures::isFeatureEnabled( $userClone, 
BETA_FEATURE_FLOW_USER_TALK_PAGE );
+               $after = $options[BETA_FEATURE_FLOW_USER_TALK_PAGE];
+               $action = null;
+
+               if ( !$before && $after ) {
+                       $action = OptInUpdate::$ENABLE;
+               } elseif ( $before && !$after ) {
+                       $action = OptInUpdate::$DISABLE;
+               }
+
+               if ( $action ) {
+                       DeferredUpdates::addUpdate( new OptInUpdate( $action, 
$user->getTalkPage(), $user ) );
+               }
+
+               return true;
+       }
+
 }
diff --git a/autoload.php b/autoload.php
index 3bbd18d..5a826c6 100644
--- a/autoload.php
+++ b/autoload.php
@@ -162,6 +162,7 @@
        'Flow\\Formatter\\TopicListFormatter' => __DIR__ . 
'/includes/Formatter/TopicListFormatter.php',
        'Flow\\Formatter\\TopicListQuery' => __DIR__ . 
'/includes/Formatter/TopicListQuery.php',
        'Flow\\Formatter\\TopicRow' => __DIR__ . 
'/includes/Formatter/TopicRow.php',
+       'Flow\\Import\\ArchiveNameHelper' => __DIR__ . 
'/includes/Import/ArchiveNameHelper.php',
        'Flow\\Import\\Converter' => __DIR__ . '/includes/Import/Converter.php',
        'Flow\\Import\\EnableFlow\\EnableFlowWikitextConversionStrategy' => 
__DIR__ . 
'/includes/Import/EnableFlow/EnableFlowWikitextConversionStrategy.php',
        'Flow\\Import\\FileImportSourceStore' => __DIR__ . 
'/includes/Import/ImportSourceStore.php',
@@ -203,6 +204,8 @@
        'Flow\\Import\\LiquidThreadsApi\\ScriptedImportRevision' => __DIR__ . 
'/includes/Import/LiquidThreadsApi/Objects.php',
        'Flow\\Import\\LiquidThreadsApi\\TopicIterator' => __DIR__ . 
'/includes/Import/LiquidThreadsApi/Iterators.php',
        'Flow\\Import\\NullImportSourceStore' => __DIR__ . 
'/includes/Import/ImportSourceStore.php',
+       'Flow\\Import\\OptInController' => __DIR__ . 
'/includes/Import/OptInController.php',
+       'Flow\\Import\\OptInUpdate' => __DIR__ . 
'/includes/Import/OptInUpdate.php',
        'Flow\\Import\\PageImportState' => __DIR__ . 
'/includes/Import/Importer.php',
        'Flow\\Import\\Plain\\ImportHeader' => __DIR__ . 
'/includes/Import/Plain/ImportHeader.php',
        'Flow\\Import\\Plain\\ObjectRevision' => __DIR__ . 
'/includes/Import/Plain/ObjectRevision.php',
@@ -213,6 +216,7 @@
        'Flow\\Import\\Postprocessor\\ProcessorGroup' => __DIR__ . 
'/includes/Import/Postprocessor/ProcessorGroup.php',
        'Flow\\Import\\Postprocessor\\SpecialLogTopic' => __DIR__ . 
'/includes/Import/Postprocessor/SpecialLogTopic.php',
        'Flow\\Import\\TalkpageImportOperation' => __DIR__ . 
'/includes/Import/Importer.php',
+       'Flow\\Import\\TemplateHelper' => __DIR__ . 
'/includes/Import/TemplateHelper.php',
        'Flow\\Import\\TopicImportState' => __DIR__ . 
'/includes/Import/Importer.php',
        'Flow\\Import\\Wikitext\\ConversionStrategy' => __DIR__ . 
'/includes/Import/Wikitext/ConversionStrategy.php',
        'Flow\\Import\\Wikitext\\ImportSource' => __DIR__ . 
'/includes/Import/Wikitext/ImportSource.php',
@@ -325,11 +329,13 @@
        'Flow\\Tests\\Formatter\\RevisionFormatterTest' => __DIR__ . 
'/tests/phpunit/Formatter/RevisionFormatterTest.php',
        'Flow\\Tests\\Handlebars\\FlowPostMetaActionsTest' => __DIR__ . 
'/tests/phpunit/Handlebars/FlowPostMetaActionsTest.php',
        'Flow\\Tests\\HookTest' => __DIR__ . '/tests/phpunit/HookTest.php',
+       'Flow\\Tests\\Import\\ArchiveNameHelperTest' => __DIR__ . 
'/tests/phpunit/Import/ArchiveNameHelperTest.php',
        'Flow\\Tests\\Import\\ConverterTest' => __DIR__ . 
'/tests/phpunit/Import/ConverterTest.php',
        'Flow\\Tests\\Import\\HistoricalUIDGeneratorTest' => __DIR__ . 
'/tests/phpunit/Import/HistoricalUIDGeneratorTest.php',
        'Flow\\Tests\\Import\\LiquidThreadsApi\\ConversionStrategyTest' => 
__DIR__ . '/tests/phpunit/Import/LiquidThreadsApi/ConversionStrategyTest.php',
        'Flow\\Tests\\Import\\PageImportStateTest' => __DIR__ . 
'/tests/phpunit/Import/PageImportStateTest.php',
        'Flow\\Tests\\Import\\TalkpageImportOperationTest' => __DIR__ . 
'/tests/phpunit/Import/TalkpageImportOperationTest.php',
+       'Flow\\Tests\\Import\\TemplateHelperTest' => __DIR__ . 
'/tests/phpunit/Import/TemplateHelperTest.php',
        'Flow\\Tests\\Import\\Wikitext\\ConversionStrategyTest' => __DIR__ . 
'/tests/phpunit/Import/Wikitext/ConversionStrategyTest.php',
        'Flow\\Tests\\Import\\Wikitext\\ImportSourceTest' => __DIR__ . 
'/tests/phpunit/Import/Wikitext/ImportSourceTest.php',
        'Flow\\Tests\\LinksTableTest' => __DIR__ . 
'/tests/phpunit/LinksTableTest.php',
diff --git a/defines.php b/defines.php
index 40381ec..72b0196 100644
--- a/defines.php
+++ b/defines.php
@@ -3,4 +3,5 @@
 // Constants
 define( 'RC_FLOW', 142 ); // Random number chosen.  Can be replaced with 
rc_source; see bug 72157.
 define( 'NS_TOPIC', 2600 );
-define( 'FLOW_TALK_PAGE_MANAGER_USER', 'Flow talk page manager' );
\ No newline at end of file
+define( 'FLOW_TALK_PAGE_MANAGER_USER', 'Flow talk page manager' );
+define( 'BETA_FEATURE_FLOW_USER_TALK_PAGE', 'beta-feature-flow-user-talk-page' 
);
diff --git a/i18n/en.json b/i18n/en.json
index 5af2b4b..14ed8b9 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -398,6 +398,7 @@
        "flow-special-enableflow-page-is-liquidthreads": "There is a 
LiquidThreads page at [[:$1]].",
        "flow-special-enableflow-confirmation": "You have successfully created 
a Flow board at [[$1]].",
        "flow-conversion-archive-page-name-format": "%s/Archive 
%d\n%s/Archive%d\n%s/archive %d\n%s/archive%d",
+       "flow-conversion-archive-flow-page-name-format": "%s/Flow Archive 
%d\n%s/FlowArchive%d",
        "flow-spam-confirmedit-form": "Please confirm you are a human by 
solving the below captcha: $1",
        "flow-embedding-unsupported": "Discussions cannot be embedded yet.",
        "mw-ui-unsubmitted-confirm": "You have unsubmitted changes on this 
page. Are you sure you want to navigate away and lose your work?",
@@ -557,5 +558,14 @@
        "flow-mark-revision-patrolled-link-title": "Mark this page as 
patrolled",
        "flow-mark-diff-patrolled-link-text": "Mark as patrolled",
        "flow-mark-diff-patrolled-link-title": "Mark as patrolled",
-       "flow-description-last-modified-at": "This description was last 
modified on $1, at $2."
+       "flow-description-last-modified-at": "This description was last 
modified on $1, at $2.",
+       "flow-talk-page-beta-feature-message": "Flow on user talk",
+       "flow-talk-page-beta-feature-description": "Enables a new structured 
discussion system on your user talk page. Flow simplifies talk page discussions 
with clear places to write and reply, and allows conversation-level 
notifications. Existing wikitext discussions are moved to an archive. This 
feature is not auto-enabled; users will have to enable it separately. Disabling 
this feature will move the Flow board to a subpage and un-archive the previous 
talkpage.",
+       "flow-notification-link-text-enabled-on-talkpage": "View user talk 
page",
+       "flow-notification-enabled-on-talkpage-title-message": "New discussion 
system enabled for your user talk page<br/><small>Available at [[$1]]</small>",
+       "flow-notification-enabled-on-talkpage-email-subject": "New discussion 
system on $1",
+       "flow-notification-enabled-on-talkpage-email-batch-body": "Flow, the 
new wiki discussion system, has been enabled on your user talk page on 
{{SITENAME}}. You can get more information, provide feedback or disable the new 
system any time from the Beta features section in your preferences.",
+       "flow-beta-feature-add-archive-template-edit-summary": "Adding archive 
template",
+       "flow-beta-feature-remove-archive-template-edit-summary": "Removing 
archive template"
+
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 6bcd486..e3def7f 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -405,6 +405,7 @@
        "flow-special-enableflow-page-is-liquidthreads": "Error given on 
Special:EnableFlow if the page to enable Flow on is a LiquidThreads page.  
Parameters:\n$1 - Page name where user requested to put Flow board",
        "flow-special-enableflow-confirmation": "Confirmation message on 
Special:EnableFlow saying that you have successfully created a board  
Parameters:\n$1 - Page name of new Flow board",
        "flow-conversion-archive-page-name-format": "Archive format used when 
enabling Flow on existing pages. This is a format string. %s and %d should be 
present. %s represents the title of the page where Flow is being enabled. %d 
represents a number that will be incremented if an archive page with the same 
name already exist. Multiple formats can be specified separated by the new line 
character (\\n).",
+       "flow-conversion-archive-flow-page-name-format": "Archive format used 
when archiving a Flow page. This is a format string. %s and %d should be 
present. %s represents the title of the Flow page being archived. %d represents 
a number that will be incremented if an archive page with the same name already 
exist. Multiple formats can be specified separated by the new line character 
(\\n).",
        "flow-spam-confirmedit-form": "Error message when ConfirmEdit flagged 
the submitted content (because an anonymous user submitted external links, 
possibly spam). A captcha will be displayed after this error message. 
Parameters:\n* $1 - the HTML for the captcha form.",
        "flow-embedding-unsupported": "Error message displayed if a user tries 
to transclude a Flow page.",
        "mw-ui-unsubmitted-confirm": "You have unsubmitted changes on this 
page. Are you sure you want to navigate away and lose your work?",
@@ -564,5 +565,13 @@
        "flow-mark-revision-patrolled-link-title": "Title of the link to mark a 
revision as patrolled on a revision page.",
        "flow-mark-diff-patrolled-link-text": "Text of the link to mark a 
revision as patrolled on a diff page.\n{{Identical|Mark as patrolled}}",
        "flow-mark-diff-patrolled-link-title": "Title of the link to mark a 
revision as patrolled on a diff page.\n{{Identical|Mark as patrolled}}",
-       "flow-description-last-modified-at": "Timestamp line stating when 
description was modified. See also {{msg-mw|lastmodifiedat}}.  Parameters:\n* 
$1 - Date\n* $2 - Time"
+       "flow-description-last-modified-at": "Timestamp line stating when 
description was modified. See also {{msg-mw|lastmodifiedat}}.  Parameters:\n* 
$1 - Date\n* $2 - Time",
+       "flow-talk-page-beta-feature-message": "Title of the beta feature to 
enable Flow on the user's talk page.",
+       "flow-talk-page-beta-feature-description": "Description of the beta 
feature to enable Flow on the user's talk page.",
+       "flow-notification-link-text-enabled-on-talkpage": "Primary link text 
in email for notification when Flow was enabled on your talk page.",
+       "flow-notification-enabled-on-talkpage-title-message": "Text of the web 
notification when Flow was enabled on your talk page.\n Parameters:\n* $1 - 
Title where Flow was enabled.",
+       "flow-notification-enabled-on-talkpage-email-subject": "Email subject 
for notification when Flow was enabled on your talk page.\n Parameters:\n* $1 - 
Title where Flow was enabled.",
+       "flow-notification-enabled-on-talkpage-email-batch-body": "Email body 
for notification when Flow was enabled on your talk page.\n Parameters:\n* $1 - 
Title where Flow was enabled.",
+       "flow-beta-feature-add-archive-template-edit-summary": "Edit summary 
message for the revision adding the archive template to an archived talk page.",
+       "flow-beta-feature-remove-archive-template-edit-summary": "Edit summary 
message for the revision removing the archive template to an archived talk 
page."
 }
diff --git a/images/betafeature-flow-ltr.svg b/images/betafeature-flow-ltr.svg
new file mode 100644
index 0000000..9fb14a6
--- /dev/null
+++ b/images/betafeature-flow-ltr.svg
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg"; width="264" height="162" viewBox="0 0 
264 162" id="svg2">
+    <g id="g4">
+        <path d="M13.366 161.357L.5 151.75V.5h263v151.75l-9 
6.704V42.5h-39v112.6l-4.393-3.73-7.607 5.33V42.5h-155v110.85l-9.75 
8.03-13.917-10.02z" id="path6" fill="#fff"/>
+        <path d="M263 1v150.998l-8 
5.96V42h-40v112.02l-3.274-2.78-.59-.5-.632.442-6.504 4.556V42H48v111.1l-9.257 
7.66-13.295-9.57-.645-.463-.598.52-10.864 9.47L1 151.5V1h262m1-1H0v152l13.39 10 
11.475-10 13.89 10L49 153.6V43h154v114.66l8.078-5.66 4.922 
4.18V43h38v116.95l10-7.45V0z" id="path8" fill="#e5e5e5"/>
+    </g>
+    <path d="M203 157.66V43H49v110.6l2.145-1.6L63.7 162l13.81-10 14.228 10 
12.972-10 12.973 10 13.81-10 12.137 10 13.39-10 14.23 10 12.972-10 12.974 10 
5.804-4.34z" id="path10" fill="#e5e5e5"/>
+    <path d="M11 36c0-7.732 6.268-14 14-14s14 6.268 14 14-6.268 14-14 
14-14-6.268-14-14z" id="path12" fill="#e5e5e5"/>
+    <path d="M254 159.95V43h-38v113.18l7.55 5.82 13.812-10 13.89 10 
2.748-2.05z" id="path14" fill="#e5e5e5"/>
+    <path d="M38 132.354V72H13v60.354h25z" id="path16" fill="#e5e5e5"/>
+    <path d="M232.51 5h26v6h-26V5z" id="path18" fill="#e5e5e5"/>
+    <path d="M208.51 5h22v6h-22z" id="path20" fill="#e5e5e5"/>
+    <path d="M142 6v4H50V6h92m1-1H49v6h94V5z" id="path22" fill="#e5e5e5"/>
+    <path d="M184.51 5h22v6h-22z" id="path24" fill="#e5e5e5"/>
+    <path d="M161.51 5h13v6h-13z" id="path26" fill="#e5e5e5"/>
+    <path d="M176.51 5h6v6h-6z" id="path28" fill="#e5e5e5"/>
+    <path d="M153.51 5h6v6h-6z" id="path30" fill="#e5e5e5"/>
+    <path d="M9 5h32v6H9z" id="path32" fill="#e5e5e5"/>
+    <path d="M2 14.5h260" id="path34" fill="#e5e5e5" stroke="#e5e5e5"/>
+    <path d="M52 7h2v2h-2z" id="path36" fill="#e5e5e5"/>
+    <path d="M38 59v-5H13v5h25z" id="path38" fill="#e5e5e5"/>
+    <path d="M163.047 85.46v39.226l8.717 8.717h-61.02V85.46h52.303zM88.953 
63.668h52.302v17.434h-34.868v30.51h-26.15l8.716-8.718V63.668z" id="path4059" 
fill="#347bff"/>
+</svg>
diff --git a/images/betafeature-flow-rtl.svg b/images/betafeature-flow-rtl.svg
new file mode 100644
index 0000000..a0de77b
--- /dev/null
+++ b/images/betafeature-flow-rtl.svg
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg"; width="264" height="162" viewBox="0 0 
264 162" id="svg2">
+    <g id="g4">
+        <path d="M250.634 161.357l12.866-9.608V.5H.5v151.75l9 
6.704V42.5h39v112.6l4.393-3.73 7.607 5.33V42.5h155v110.85l9.75 8.03 
13.917-10.02z" id="path6" fill="#fff"/>
+        <path d="M1 1v150.998l8 5.96V42h40v112.02l3.274-2.78.59-.5.632.442L60 
155.738V42h156v111.1l9.257 7.66 13.295-9.57.645-.463.598.52 10.864 9.47L263 
151.5V1H1M0 0h264v152l-13.39 10-11.475-10-13.89 10L215 
153.6V43H61v114.66L52.922 152 48 156.18V43H10v116.95L0 152.5V0z" id="path8" 
fill="#e5e5e5"/>
+    </g>
+    <path d="M61 157.66V43h154v110.6l-2.145-1.6-12.555 10-13.81-10-14.228 
10-12.972-10-12.973 10-13.81-10-12.137 10-13.39-10-14.23 10-12.972-10-12.974 
10L61 157.66z" id="path10" fill="#e5e5e5"/>
+    <path d="M253 36c0-7.732-6.268-14-14-14s-14 6.268-14 14 6.268 14 14 14 
14-6.268 14-14z" id="path12" fill="#e5e5e5"/>
+    <path d="M10 159.95V43h38v113.18L40.45 162l-13.812-10-13.89 10L10 159.95z" 
id="path14" fill="#e5e5e5"/>
+    <path d="M226 132.354V72h25v60.354h-25z" id="path16" fill="#e5e5e5"/>
+    <path d="M31.49 5h-26v6h26V5z" id="path18" fill="#e5e5e5"/>
+    <path d="M55.49 5h-22v6h22z" id="path20" fill="#e5e5e5"/>
+    <path d="M122 6v4h92V6h-92m-1-1h94v6h-94V5z" id="path22" fill="#e5e5e5"/>
+    <path d="M79.49 5h-22v6h22z" id="path24" fill="#e5e5e5"/>
+    <path d="M102.49 5h-13v6h13z" id="path26" fill="#e5e5e5"/>
+    <path d="M87.49 5h-6v6h6z" id="path28" fill="#e5e5e5"/>
+    <path d="M110.49 5h-6v6h6z" id="path30" fill="#e5e5e5"/>
+    <path d="M255 5h-32v6h32z" id="path32" fill="#e5e5e5"/>
+    <path d="M262 14.5H2" id="path34" fill="#e5e5e5" stroke="#e5e5e5"/>
+    <path d="M212 7h-2v2h2z" id="path36" fill="#e5e5e5"/>
+    <path d="M226 59v-5h25v5h-25z" id="path38" fill="#e5e5e5"/>
+    <path d="M100.953 85.46v39.226l-8.717 
8.717h61.02V85.46h-52.303zm74.094-21.792h-52.302v17.434h34.868v30.51h26.15l-8.716-8.718V63.668z"
 id="path4059" fill="#347bff"/>
+</svg>
diff --git a/includes/Import/ArchiveNameHelper.php 
b/includes/Import/ArchiveNameHelper.php
new file mode 100644
index 0000000..c0a3088
--- /dev/null
+++ b/includes/Import/ArchiveNameHelper.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace Flow\Import;
+
+
+use Flow\Repository\TitleRepository;
+use Title;
+
+class ArchiveNameHelper {
+
+       /**
+        * Helper method decides on an archive title based on a set of printf 
formats.
+        * Each format should first have a %s for the base page name and a %d 
for the
+        * archive page number. Example:
+        *
+        *   %s/Archive %d
+        *
+        * It will iterate through the formats looking for an existing format.  
If no
+        * formats are currently in use the first format will be returned with 
n=1.
+        * If a format is currently in used we will look for the first unused 
page
+        * >= to n=1 and <= to n=20.
+        *
+        * @param Title $source
+        * @param string[] $formats
+        * @param TitleRepository|null $titleRepo
+        * @return Title
+        * @throws ImportException
+        */
+       public function decideArchiveTitle( Title $source, array $formats, 
TitleRepository $titleRepo = null ) {
+               $info = self::findLatestArchiveInfo( $source, $formats, 
$titleRepo );
+               $format = $info ? $info['format'] : $formats[0];
+               $counter = $info ? $info['counter'] + 1 : 1;
+               $text = $source->getPrefixedText();
+               return Title::newFromText( sprintf( $format, $text, $counter ) 
);
+       }
+
+       /**
+        * @param Title $source
+        * @param array $formats
+        * @param TitleRepository $titleRepo
+        * @return bool|mixed
+        */
+       public function findLatestArchiveTitle( Title $source, array $formats, 
TitleRepository $titleRepo = null ) {
+               $info = self::findLatestArchiveInfo( $source, $formats, 
$titleRepo );
+               return $info ? $info['title'] : false;
+       }
+
+       /**
+        * @param Title $source
+        * @param array $formats
+        * @param TitleRepository $titleRepo
+        * @return bool|mixed
+        */
+       protected function findLatestArchiveInfo( Title $source, array 
$formats, TitleRepository $titleRepo = null ) {
+               if ( $titleRepo === null ) {
+                       $titleRepo = new TitleRepository();
+               }
+
+               $format = false;
+               $n = 1;
+               $text = $source->getPrefixedText();
+               foreach ( $formats as $potential ) {
+                       $title = Title::newFromText( sprintf( $potential, 
$text, $n ) );
+                       if ( $title && $titleRepo->exists( $title ) ) {
+                               $format = $potential;
+                               break;
+                       }
+               }
+               if ( $format === false ) {
+                       // no archive page matches any format
+                       return false;
+               }
+
+               $archivePages = array();
+               for ( $n = 1; $n <= 20; ++$n ) {
+                       $title = Title::newFromText( sprintf( $format, $text, 
$n ) );
+                       if ( $title && $titleRepo->exists( $title ) ) {
+                               $archivePages[] = array(
+                                       'title' => $title,
+                                       'format' => $format,
+                                       'counter' => $n
+                               );
+                       } else {
+                               break;
+                       }
+               }
+
+               if ( $archivePages ) {
+                       return end( $archivePages );
+               }
+
+               return false;
+       }
+
+}
\ No newline at end of file
diff --git a/includes/Import/Converter.php b/includes/Import/Converter.php
index a925a0f..44c8e84 100644
--- a/includes/Import/Converter.php
+++ b/includes/Import/Converter.php
@@ -306,52 +306,4 @@
                        throw new ImportException( "Failed creating archive 
cleanup revision at {$archiveTitle}" );
                }
        }
-
-       /**
-        * Helper method decides on an archive title based on a set of printf 
formats.
-        * Each format should first have a %s for the base page name and a %d 
for the
-        * archive page number. Example:
-        *
-        *   %s/Archive %d
-        *
-        * It will iterate through the formats looking for an existing format.  
If no
-        * formats are currently in use the first format will be returned with 
n=1.
-        * If a format is currently in used we will look for the first unused 
page
-        * >= to n=1 and <= to n=20.
-        *
-        * @param Title $source
-        * @param string[] $formats
-        * @param TitleRepository|null $titleRepo
-        * @return Title
-        * @throws ImportException
-        */
-       static public function decideArchiveTitle( Title $source, array 
$formats, TitleRepository $titleRepo = null ) {
-               if ( $titleRepo === null ) {
-                       $titleRepo = new TitleRepository();
-               }
-
-               $format = false;
-               $n = 1;
-               $text = $source->getPrefixedText();
-               foreach ( $formats as $potential ) {
-                       $title = Title::newFromText( sprintf( $potential, 
$text, $n ) );
-                       if ( $title && $titleRepo->exists( $title ) ) {
-                               $format = $potential;
-                               break;
-                       }
-               }
-               if ( $format === false ) {
-                       // assumes this creates a valid title
-                       return Title::newFromText( sprintf( $formats[0], $text, 
$n ) );
-               }
-
-               for ( $n = 2; $n <= 20; ++$n ) {
-                       $title = Title::newFromText( sprintf( $format, $text, 
$n ) );
-                       if ( $title && !$titleRepo->exists( $title ) ) {
-                               return $title;
-                       }
-               }
-
-               throw new ImportException( "All titles 1 through 20 (inclusive) 
exist for format: $format" );
-       }
 }
diff --git a/includes/Import/Exception.php b/includes/Import/Exception.php
index e3b5c4c..00da767 100644
--- a/includes/Import/Exception.php
+++ b/includes/Import/Exception.php
@@ -1,11 +1,12 @@
 <?php
 
 namespace Flow\Import;
+use Flow\Exception\FlowException;
 
 /**
  * Base class for errors in the Flow\Import module
  */
-class ImportException extends \Flow\Exception\FlowException {
+class ImportException extends FlowException {
 }
 
 /**
diff --git a/includes/Import/LiquidThreadsApi/ConversionStrategy.php 
b/includes/Import/LiquidThreadsApi/ConversionStrategy.php
index fb06563..6767b43 100644
--- a/includes/Import/LiquidThreadsApi/ConversionStrategy.php
+++ b/includes/Import/LiquidThreadsApi/ConversionStrategy.php
@@ -3,6 +3,7 @@
 namespace Flow\Import\LiquidThreadsApi;
 
 use DatabaseBase;
+use Flow\Import\ArchiveNameHelper;
 use Flow\Import\Converter;
 use Flow\Import\IConversionStrategy;
 use Flow\Import\ImportSourceStore;
@@ -111,7 +112,8 @@
         * @return Title
         */
        public function decideArchiveTitle( Title $source ) {
-               return Converter::decideArchiveTitle( $source, array(
+               $archiveNameHelper = new ArchiveNameHelper();
+               return $archiveNameHelper->decideArchiveTitle( $source, array(
                        '%s/LQT Archive %d',
                ) );
        }
diff --git a/includes/Import/OptInController.php 
b/includes/Import/OptInController.php
new file mode 100644
index 0000000..e33de92
--- /dev/null
+++ b/includes/Import/OptInController.php
@@ -0,0 +1,450 @@
+<?php
+
+namespace Flow\Import;
+
+use DateTime;
+use DateTimeZone;
+use DerivativeContext;
+use Flow\Collection\HeaderCollection;
+use Flow\NotificationController;
+use Flow\OccupationController;
+use Flow\Parsoid\Utils;
+use Flow\RevisionActionPermissions;
+use Flow\WorkflowLoaderFactory;
+use IContextSource;
+use MovePage;
+use RequestContext;
+use Revision;
+use Title;
+use Flow\Container;
+use User;
+use WikiPage;
+use WikitextContent;
+
+
+/**
+ * Entry point for enabling Flow on a page.
+ */
+class OptInController {
+
+       /**
+        * @var OccupationController
+        */
+       private $occupationController;
+
+       /**
+        * @var NotificationController
+        */
+       private $notificationController;
+
+       /**
+        * @var ArchiveNameHelper
+        */
+       private $archiveNameHelper;
+
+       /**
+        * @var IContextSource
+        */
+       private $context;
+
+       /**
+        * @var User
+        */
+       private $user;
+
+       public function __construct() {
+               $this->occupationController = Container::get( 
'occupation_controller' );
+               $this->notificationController = Container::get( 
'controller.notification' );
+               $this->archiveNameHelper = new ArchiveNameHelper();
+               $this->user = $this->occupationController->getTalkpageManager();
+               $this->context = new DerivativeContext( 
RequestContext::getMain() );
+               $this->context->setUser( $this->user );
+
+               // We need to replace the 'permissions' object in the container
+               // so it is initialized with the user we are trying to
+               // impersonate (Talk page manager user).
+               $user = $this->user;
+               Container::getContainer()->extend( 'permissions', function ( 
$p, $c ) use ( $user ) {
+                       return new RevisionActionPermissions( 
$c['flow_actions'], $user );
+               } );
+       }
+
+       /**
+        * @param Title $title
+        * @param User $user
+        */
+       public function enable( Title $title, User $user ) {
+               if ( $this->isFlowBoard( $title ) ) {
+                       // already a Flow board
+                       return;
+               }
+
+               // archive existing wikitext talk page
+               $linkToArchivedTalkpage = null;
+               if ( $title->exists( Title::GAID_FOR_UPDATE ) ) {
+                       $wikitextTalkpageArchiveTitle = 
$this->archiveExistingTalkpage( $title );
+                       $this->addArchiveTemplate( 
$wikitextTalkpageArchiveTitle, $title );
+                       $linkToArchivedTalkpage = 
$this->buildLinkToArchivedTalkpage( $wikitextTalkpageArchiveTitle );
+               }
+
+               // create or restore flow board
+               $archivedFlowPage = $this->findLatestFlowArchive( $title );
+               if ( $archivedFlowPage ) {
+                       $this->restoreExistingFlowBoard( $archivedFlowPage, 
$title, $linkToArchivedTalkpage );
+               } else {
+                       $this->createFlowBoard( $title, $linkToArchivedTalkpage 
);
+                       
$this->notificationController->notifyFlowEnabledOnTalkpage( $user );
+               }
+       }
+
+       /**
+        * @param Title $title
+        */
+       public function disable( Title $title ) {
+               if ( !$this->isFlowBoard( $title ) ) {
+                       return;
+               }
+
+               // archive the flow board
+               $flowArchiveTitle = $this->findNextFlowArchive( $title );
+               $this->movePage( $title, $flowArchiveTitle );
+               $this->removeArchivedTalkpageTemplateFromFlowBoardDescription( 
$flowArchiveTitle );
+
+               // restore the original wikitext talk page
+               $archivedTalkpage = $this->findLatestArchive( $title );
+               if ( $archivedTalkpage ) {
+                       $this->movePage( $archivedTalkpage, $title );
+                       $this->removeArchiveTemplateFromWikitextTalkpage( 
$title );
+               }
+       }
+
+       /**
+        * @param Title $title
+        * @return bool
+        */
+       private function isFlowBoard( Title $title ) {
+               return $title->getContentModel( Title::GAID_FOR_UPDATE ) === 
CONTENT_MODEL_FLOW_BOARD;
+       }
+
+       /**
+        * @param Title $from
+        * @param Title $to
+        */
+       private function movePage( Title $from, Title $to ) {
+               $mp = new MovePage( $from, $to );
+               $mp->move( $this->user, null, false );
+       }
+
+       /**
+        * @param $msgKey
+        * @param array $args
+        * @throws ImportException
+        */
+       private function fatal( $msgKey, $args = array() ) {
+               throw new ImportException( wfMessage( $msgKey, $args 
)->inContentLanguage()->text() );
+       }
+
+       /**
+        * @param string $str
+        * @return array
+        */
+       private function fromNewlineSeparated( $str ) {
+               return explode( "\n", $str );
+       }
+
+       /**
+        * @param Title $title
+        * @return Title|false
+        */
+       private function findLatestArchive( Title $title ) {
+               $archiveFormats = $this->fromNewlineSeparated(
+                       wfMessage( 'flow-conversion-archive-page-name-format' 
)->inContentLanguage()->plain() );
+               return $this->archiveNameHelper->findLatestArchiveTitle( 
$title, $archiveFormats );
+       }
+
+       /**
+        * @param Title $title
+        * @return Title
+        * @throws ImportException
+        */
+       private function findNextArchive( Title $title ) {
+               $archiveFormats = $this->fromNewlineSeparated(
+                       wfMessage( 'flow-conversion-archive-page-name-format' 
)->inContentLanguage()->plain() );
+               return $this->archiveNameHelper->decideArchiveTitle( $title, 
$archiveFormats );
+       }
+
+       /**
+        * @param Title $title
+        * @return Title|false
+        */
+       private function findLatestFlowArchive( Title $title ) {
+               $archiveFormats = $this->fromNewlineSeparated(
+                       wfMessage( 
'flow-conversion-archive-flow-page-name-format' )->inContentLanguage()->plain() 
);
+               return $this->archiveNameHelper->findLatestArchiveTitle( 
$title, $archiveFormats );
+       }
+
+       /**
+        * @param Title $title
+        * @return Title
+        * @throws ImportException
+        */
+       private function findNextFlowArchive( Title $title ) {
+               $archiveFormats = $this->fromNewlineSeparated(
+                       wfMessage( 
'flow-conversion-archive-flow-page-name-format' )->inContentLanguage()->plain() 
);
+               return $this->archiveNameHelper->decideArchiveTitle( $title, 
$archiveFormats );
+       }
+
+       /**
+        * @param Title $title
+        * @param string $contentText
+        * @param string $summary
+        * @throws ImportException
+        * @throws \MWException
+        */
+       private function createRevision( Title $title, $contentText, $summary ) 
{
+               $page = WikiPage::factory( $title );
+               $newContent = new WikitextContent( $contentText );
+               $status = $page->doEditContent(
+                       $newContent,
+                       $summary,
+                       EDIT_FORCE_BOT | EDIT_SUPPRESS_RC,
+                       false,
+                       $this->user
+               );
+
+               if ( !$status->isGood() ) {
+                       throw new ImportException( "Failed creating revision at 
{$title}" );
+               }
+       }
+
+       /**
+        * @param Title $title
+        * @param $boardDescription
+        * @throws ImportException
+        * @throws \Flow\Exception\CrossWikiException
+        * @throws \Flow\Exception\InvalidInputException
+        */
+       private function createFlowBoard( Title $title, $boardDescription ) {
+               /** @var WorkflowLoaderFactory $loaderFactory */
+               $loaderFactory = Container::get( 'factory.loader.workflow' );
+               $page = $title->getPrefixedText();
+
+               $allowCreationStatus = 
$this->occupationController->allowCreation( $title, $this->user, false );
+               if ( !$allowCreationStatus->isGood() ) {
+                       $this->fatal( 
'flow-special-enableflow-board-creation-not-allowed', $page );
+               }
+
+               $loader = $loaderFactory->createWorkflowLoader( $title );
+               $blocks = $loader->getBlocks();
+
+               if ( !$boardDescription ) {
+                       $boardDescription = ' ';
+               }
+
+               $action = 'edit-header';
+               $params = array(
+                       'header' => array(
+                               'content' => $boardDescription,
+                               'format' => 'wikitext',
+                       ),
+               );
+
+               $blocksToCommit = $loader->handleSubmit(
+                       $this->context,
+                       $action,
+                       $params
+               );
+
+               foreach ( $blocks as $block ) {
+                       if ( $block->hasErrors() ) {
+                               $errors = $block->getErrors();
+
+                               foreach ( $errors as $errorKey ) {
+                                       $this->fatal( $block->getErrorMessage( 
$errorKey ) );
+                               }
+                       }
+               }
+
+               $loader->commit( $blocksToCommit );
+       }
+
+       /**
+        * @param Title $title
+        * @return Title
+        */
+       private function archiveExistingTalkpage( Title $title ) {
+               $archiveTitle = $this->findNextArchive( $title );
+               $this->movePage( $title, $archiveTitle );
+               return $archiveTitle;
+       }
+
+       /**
+        * @param Title $archivedFlowPage
+        * @param Title $title
+        * @param string|null $addToHeader
+        */
+       private function restoreExistingFlowBoard( Title $archivedFlowPage, 
Title $title, $addToHeader = null ) {
+               $this->movePage( $archivedFlowPage, $title );
+               if ( $addToHeader ) {
+                       $this->editBoardDescription( $title, function( $oldDesc 
) use ( $addToHeader ) {
+                               return $oldDesc . "\n\n" . $addToHeader;
+                       }, 'wikitext' );
+               }
+       }
+
+       /**
+        * @param Title $title
+        * @return string
+        * @throws \MWException
+        */
+       private function getContent( Title $title ) {
+               $page = WikiPage::factory( $title );
+               $page->loadPageData( 'fromdbmaster' );
+               $revision = $page->getRevision();
+               if ( $revision ) {
+                       $content = $revision->getContent( Revision::FOR_PUBLIC 
);
+                       if ( $content instanceof WikitextContent ) {
+                               return $content->getNativeData();
+                       }
+               }
+
+               return '';
+       }
+
+       /**
+        * @param Title $archiveTitle
+        * @return string
+        */
+       private function buildLinkToArchivedTalkpage( Title $archiveTitle ) {
+               $now = new DateTime( "now", new DateTimeZone( "GMT" ) );
+               $arguments = array(
+                       'archive' => $archiveTitle->getPrefixedText(),
+                       'date' => $now->format( 'Y-m-d' ),
+               );
+               $template = wfMessage( 'flow-importer-wt-converted-template' 
)->inContentLanguage()->plain();
+               return $this->formatTemplate( $template, $arguments );
+       }
+
+       /**
+        * @param string $name
+        * @param array $args
+        * @return string
+        */
+       private function formatTemplate( $name, $args ) {
+               $arguments = implode( '|',
+                       array_map(
+                               function( $key, $value ) {
+                                       return "$key=$value";
+                               },
+                               array_keys( $args ),
+                               array_values( $args ) )
+               );
+               return "{{{$name}|$arguments}}";
+       }
+
+       /**
+        * @param Title $flowArchiveTitle
+        */
+       private function 
removeArchivedTalkpageTemplateFromFlowBoardDescription( Title $flowArchiveTitle 
) {
+               $this->editBoardDescription( $flowArchiveTitle, function( 
$oldDesc ) {
+                       $templateName = wfMessage( 
'flow-importer-wt-converted-template' )->inContentLanguage()->plain();
+                       return TemplateHelper::removeFromHtml( $oldDesc, 
$templateName );
+               }, 'html' );
+       }
+
+       /**
+        * @param Title $title
+        * @param callable $newDescriptionCallback
+        * @param string $format
+        * @throws ImportException
+        * @throws \Flow\Exception\InvalidDataException
+        */
+       private function editBoardDescription( Title $title, callable 
$newDescriptionCallback, $format = 'html' ) {
+               /** @var WorkflowLoaderFactory $loader */
+               $factory = Container::get( 'factory.loader.workflow' );
+
+               /** @var WorkflowLoader $loader */
+               $loader = $factory->createWorkflowLoader( $title );
+
+               $collection = HeaderCollection::newFromId( 
$loader->getWorkflow()->getId() );
+               $revision = $collection->getLastRevision();
+               $content = $revision->getContent();
+
+               if ( $format === 'wikitext' ) {
+                       $content = Utils::convert( 'html', 'wikitext', 
$content, $title );
+               }
+               $newDescription = call_user_func( $newDescriptionCallback, 
$content );
+
+               $action = 'edit-header';
+               $params = array(
+                       'header' => array(
+                               'content' => $newDescription,
+                               'format' => $format,
+                               'prev_revision' => 
$revision->getRevisionId()->getAlphadecimal()
+                       ),
+               );
+
+               $blocks = $loader->getBlocks();
+
+               $blocksToCommit = $loader->handleSubmit(
+                       $this->context,
+                       $action,
+                       $params
+               );
+
+               foreach ( $blocks as $block ) {
+                       if ( $block->hasErrors() ) {
+                               $errors = $block->getErrors();
+
+                               foreach ( $errors as $errorKey ) {
+                                       $this->fatal( $block->getErrorMessage( 
$errorKey ) );
+                               }
+                       }
+               }
+
+               $loader->commit( $blocksToCommit );
+       }
+
+       /**
+        * @param Title $archive
+        * @param Title $current
+        * @throws ImportException
+        */
+       private function addArchiveTemplate( Title $archive, Title $current ) {
+               $templateName = wfMessage( 
'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();
+               $now = new DateTime( "now", new DateTimeZone( "GMT" ) );
+               $template = $this->formatTemplate( $templateName, array(
+                       'from' => $current->getPrefixedText(),
+                       'date' => $now->format( 'Y-m-d' ),
+               ) );
+
+               $content = $this->getContent( $archive );
+
+               $this->createRevision(
+                       $archive,
+                       $template . "\n\n" . $content,
+                       wfMessage( 
'flow-beta-feature-add-archive-template-edit-summary' 
)->inContentLanguage()->plain());
+       }
+
+       /**
+        * @param Title $title
+        * @throws ImportException
+        */
+       private function removeArchiveTemplateFromWikitextTalkpage( Title 
$title ) {
+               $content = $this->getContent( $title );
+               if ( !$content ) {
+                       return;
+               }
+
+               $content = Utils::convert( 'wikitext', 'html', $content, $title 
);
+               $templateName = wfMessage( 
'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();
+
+               $newContent = TemplateHelper::removeFromHtml( $content, 
$templateName );
+
+               $this->createRevision(
+                       $title,
+                       Utils::convert( 'html', 'wikitext', $newContent, $title 
),
+                       wfMessage( 
'flow-beta-feature-remove-archive-template-edit-summary' 
)->inContentLanguage()->plain());
+       }
+
+}
\ No newline at end of file
diff --git a/includes/Import/OptInUpdate.php b/includes/Import/OptInUpdate.php
new file mode 100644
index 0000000..c5e6f27
--- /dev/null
+++ b/includes/Import/OptInUpdate.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Flow\Import;
+
+use DeferrableUpdate;
+use MWExceptionHandler;
+use Title;
+use User;
+
+class OptInUpdate implements DeferrableUpdate {
+
+       public static $ENABLE = 'enable';
+       public static $DISABLE = 'disable';
+
+       /**
+        * @var string
+        */
+       protected $action;
+
+       /**
+        * @var Title
+        */
+       protected $talkpage;
+
+       /**
+        * @var User
+        */
+       protected $user;
+
+       /**
+        * @param string $action
+        * @param Title $talkpage
+        * @param User $user
+        */
+       public function __construct( $action, Title $talkpage, User $user ) {
+               $this->action = $action;
+               $this->talkpage = $talkpage;
+               $this->user = $user;
+       }
+
+       /**
+        * Enable or disable Flow on a talk page
+        */
+       function doUpdate() {
+               $c = new OptInController();
+               try {
+                       if ( $this->action === self::$ENABLE ) {
+                               $c->enable( $this->talkpage, $this->user );
+                       } elseif ( $this->action === self::$DISABLE ) {
+                               $c->disable( $this->talkpage );
+                       } else {
+                               wfLogWarning( 'OptInUpdate: unrecognized 
action: ' . $this->action );
+                       }
+               } catch ( \Exception $e ) {
+                       MWExceptionHandler::logException( $e );
+               }
+       }
+}
+
diff --git a/includes/Import/TemplateHelper.php 
b/includes/Import/TemplateHelper.php
new file mode 100644
index 0000000..67ccac3
--- /dev/null
+++ b/includes/Import/TemplateHelper.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace Flow\Import;
+
+use DOMDocument;
+use DOMElement;
+use DOMXPath;
+use Flow\Parsoid\Utils;
+
+class TemplateHelper {
+
+       /**
+        * @param string $htmlContent
+        * @param string $templateName
+        * @return string
+        * @throws \Flow\Exception\WikitextException
+        */
+       public static function removeFromHtml( $htmlContent, $templateName ) {
+               $dom = Utils::createDOM( $htmlContent );
+               $xpath = new DOMXPath( $dom );
+               $templateNodes = $xpath->query( 
'//*[@typeof="mw:Transclusion"]' );
+
+               foreach ( $templateNodes as $templateNode ) {
+                       /** @var DOMElement $templateNode */
+                       if ( $templateNode->hasAttribute( 'data-mw' ) ) {
+                               $name = self::getTemplateName( 
$templateNode->getAttribute( 'data-mw' ) );
+                               if ( $name === $templateName ) {
+                                       $templateNode->parentNode->removeChild( 
$templateNode );
+                                       if ( $templateNode->hasAttribute( 
'about' ) ) {
+                                               $about = 
$templateNode->getAttribute( 'about' );
+                                               self::removeAboutNodes( $dom, 
$about );
+                                       }
+                               }
+                       }
+               }
+
+               $body = $xpath->query( '/html/body' )->item(0);
+               return $dom->saveHTML( $body );
+       }
+
+       /**
+        * @param string $dataMW
+        * @return string|null
+        */
+       private static function getTemplateName( $dataMW ) {
+               try {
+                       $mwAttr = json_decode( $dataMW );
+                       return $mwAttr->parts[0]->template->target->wt;
+               } catch ( \Exception $e ) {
+                       return null;
+               }
+       }
+
+       /**
+        * @param DOMDocument $dom
+        * @param string $about
+        */
+       private static function removeAboutNodes( DOMDocument $dom, $about ) {
+               $xpath = new DOMXPath( $dom );
+               $aboutNodes = $xpath->query( '//*[@about="' . $about . '"]' );
+               foreach ( $aboutNodes as $aboutNode ) {
+                       $aboutNode->parentNode->removeChild( $aboutNode );
+               }
+       }
+
+}
\ No newline at end of file
diff --git a/includes/Import/Wikitext/ConversionStrategy.php 
b/includes/Import/Wikitext/ConversionStrategy.php
index fe0d1f9..6fd8c30 100644
--- a/includes/Import/Wikitext/ConversionStrategy.php
+++ b/includes/Import/Wikitext/ConversionStrategy.php
@@ -4,6 +4,7 @@
 
 use DateTime;
 use DateTimeZone;
+use Flow\Import\ArchiveNameHelper;
 use Flow\Import\Converter;
 use Flow\Import\IConversionStrategy;
 use Flow\Import\ImportSourceStore;
@@ -130,7 +131,8 @@
         * {@inheritDoc}
         */
        public function decideArchiveTitle( Title $source ) {
-               return Converter::decideArchiveTitle( $source, 
$this->archiveTitleSuggestions );
+               $archiveNameHelper = new ArchiveNameHelper();
+               return $archiveNameHelper->decideArchiveTitle( $source, 
$this->archiveTitleSuggestions );
        }
 
        /**
diff --git a/includes/Notifications/Controller.php 
b/includes/Notifications/Controller.php
index 136ed67..a610318 100644
--- a/includes/Notifications/Controller.php
+++ b/includes/Notifications/Controller.php
@@ -208,6 +208,25 @@
                return $events;
        }
 
+       public function notifyFlowEnabledOnTalkpage( User $user ) {
+               if ( !class_exists( 'EchoEvent' ) ) {
+                       // Nothing to do here.
+                       return array();
+               }
+
+               $events = array();
+               $events[] = EchoEvent::create( array(
+                       'type' => 'flow-enabled-on-talkpage',
+                       'agent' => $user,
+                       'title' => $user->getTalkPage(),
+                       'extra' => array(
+                               'notifyAgent' => true,
+                       ),
+               ) );
+
+               return $events;
+       }
+
        /**
         * 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.
diff --git a/includes/Notifications/Notifications.php 
b/includes/Notifications/Notifications.php
index c2a5dea..15a556a 100644
--- a/includes/Notifications/Notifications.php
+++ b/includes/Notifications/Notifications.php
@@ -103,6 +103,22 @@
                'email-body-batch-message' => 
'flow-notification-mention-email-batch-body',
                'email-body-batch-params' => array( 'agent', 'subject', 
'title', 'user' ),
        ) + $notificationTemplate,
+       'flow-enabled-on-talkpage' => array(
+               'section' => null,
+               'user-locators' => array(
+                       'EchoUserLocator::locateTalkPageOwner'
+               ),
+               'primary-link' => array(
+                       'message' => 
'flow-notification-link-text-enabled-on-talkpage',
+                       'destination' => 'title'
+               ),
+               'title-message' => 
'flow-notification-enabled-on-talkpage-title-message',
+               'title-params' => array( 'title' ),
+               'email-subject-message' => 
'flow-notification-enabled-on-talkpage-email-subject',
+               'email-subject-params' => array( 'title' ),
+               'email-body-batch-message' => 
'flow-notification-enabled-on-talkpage-email-batch-body',
+               'email-body-batch-params' => array( 'title' ),
+       ) + $notificationTemplate,
 );
 
 return $notifications;
diff --git a/maintenance/FlowUpdateBetaFeaturePreference.php 
b/maintenance/FlowUpdateBetaFeaturePreference.php
new file mode 100644
index 0000000..8e69043
--- /dev/null
+++ b/maintenance/FlowUpdateBetaFeaturePreference.php
@@ -0,0 +1,93 @@
+<?php
+
+require_once ( getenv( 'MW_INSTALL_PATH' ) !== false
+       ? getenv( 'MW_INSTALL_PATH' ) . '/maintenance/Maintenance.php'
+       : dirname( __FILE__ ) . '/../../../maintenance/Maintenance.php' );
+
+/**
+ * Sets Flow beta feature preference to true
+ * for users who are already using flow on
+ * their user talk page.
+ *
+ * @ingroup Maintenance
+ */
+class FlowUpdateBetaFeaturePreference extends LoggedUpdateMaintenance {
+
+       public function __construct() {
+               $this->setBatchSize( 300 );
+       }
+
+       /**
+        * When the Flow beta feature is enable, it finds users
+        * who already have Flow enabled on their user talk page
+        * and opt them in the beta feature so their preferences
+        * and user talk page state are in sync.
+        *
+        * @return bool
+        * @throws MWException
+        */
+       protected function doDBUpdates() {
+               global $wgFlowEnableOptInBetaFeature;
+               if ( !$wgFlowEnableOptInBetaFeature ) {
+                       return true;
+               }
+
+               $db = $this->getDB( DB_MASTER );
+
+               $innerQuery = $db->selectSQLText(
+                       'user_properties',
+                       'up_user',
+                       array(
+                               'up_property' => 
BETA_FEATURE_FLOW_USER_TALK_PAGE,
+                               'up_value' => 1
+                       )
+               );
+
+               $result = $db->select(
+                       array( 'page', 'user' ),
+                       'user_id',
+                       array(
+                               'page_content_model' => 
CONTENT_MODEL_FLOW_BOARD,
+                               "user_id NOT IN($innerQuery)"
+                       ),
+                       __METHOD__,
+                       array(),
+                       array(
+                               'user' => array( 'JOIN', array(
+                                       'page_namespace' => NS_USER_TALK,
+                                       "page_title = REPLACE(user_name, ' ', 
'_')"
+                               ) ),
+                       )
+               );
+
+               $i = 0;
+               $users = UserArray::newFromResult( $result );
+               foreach ( $users as $user ) {
+                       $user->setOption( BETA_FEATURE_FLOW_USER_TALK_PAGE, 1 );
+                       $user->saveSettings();
+
+                       if ( ++$i % $this->mBatchSize === 0 ) {
+                               wfWaitForSlaves();
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Get the update key name to go in the update log table
+        *
+        * Returns a different key when the beta feature is enabled or disable
+        * so that enabling it would trigger this script
+        * to execute so it can correctly update users preferences.
+        *
+        * @return string
+        */
+       protected function getUpdateKey() {
+               global $wgFlowEnableOptInBetaFeature;
+               return $wgFlowEnableOptInBetaFeature ? 'FlowBetaFeatureEnable' 
: 'FlowBetaFeatureDisable';
+       }
+}
+
+$maintClass = 'FlowUpdateBetaFeaturePreference'; // Tells it to run the class
+require_once( RUN_MAINTENANCE_IF_MAIN );
diff --git a/tests/browser/features/opt_in.feature 
b/tests/browser/features/opt_in.feature
new file mode 100644
index 0000000..f8ddd0a
--- /dev/null
+++ b/tests/browser/features/opt_in.feature
@@ -0,0 +1,41 @@
+@chrome @firefox
+@clean @login
+@en.wikipedia.beta.wmflabs.org
+Feature: Opt-in Flow beta feature
+
+  Depends on having $wgFlowEnableOptInBetaFeature = true
+  and NS_USER_TALK not occupied by Flow.
+
+  Background:
+    Given I am logged in as a new user
+
+  Scenario: Opt-in: I don't have a talk page
+    When I enable Flow beta feature
+    Then my talk page is a Flow board
+    And a notification tells me about it
+
+  Scenario: Opt-in: I have a wikitext talk page
+    Given my talk page has wiktext content
+    When I enable Flow beta feature
+    Then my talk page is a Flow board
+    And my flow board contains a link to my archived talk page
+    And my previous talk page is archived
+
+  Scenario: Opt-out: I didn't have a talk page
+    Given I have Flow beta feature enabled
+    When I disable Flow beta feature
+    Then my Flow board is archived
+    And my talk page is deleted without redirect
+
+  Scenario: Opt-out: I had a wikitext talk page
+    Given my talk page has wiktext content
+    And I have Flow beta feature enabled
+    When I disable Flow beta feature
+    Then my wikitext talk page is restored
+    And my Flow board is archived
+
+  Scenario: Re-opt-in
+    Given I have used the Flow beta feature before
+    When I enable Flow beta feature
+    Then my talk page is my old Flow board
+    And my previous talk page is archived
diff --git a/tests/browser/features/step_definitions/opt_in_steps.rb 
b/tests/browser/features/step_definitions/opt_in_steps.rb
new file mode 100644
index 0000000..7f73af8
--- /dev/null
+++ b/tests/browser/features/step_definitions/opt_in_steps.rb
@@ -0,0 +1,106 @@
+
+Given(/^I am logged in as a new user$/) do
+  @username = @data_manager.get 'New_user'
+  puts "New user: #{@username}"
+  api.create_account @username, password
+  visit(LoginPage).login_with @username, password
+end
+
+When(/^I enable Flow beta feature$/) do
+  visit(SpecialPreferencesPage) do |page|
+    page.beta_features_element.when_present.click
+    page.check_flow_beta_feature
+    page.save_preferences
+    page.confirmation_element.when_present
+  end
+end
+
+Then(/^my talk page is a Flow board$/) do
+  visit(UserTalkPage, using_params: { username: @username }) do |page|
+    page.flow.board_element.when_present
+  end
+end
+
+Given(/^my talk page has wiktext content$/) do
+  talk_page = "User_talk:#{@username}"
+  @talk_page_content = 'this is the content of my talk page'
+  api.create_page talk_page, @talk_page_content
+end
+
+Then(/^my previous talk page is archived$/) do
+  archive_name = "./User_talk:#{@username}/Archive_1"
+  archive_template = "User_talk:#{@username}".gsub '_', ' '
+  visit(WikiPage, using_params: { page: archive_name }) do |page|
+    expect(page.content_element.when_present.text).to match @talk_page_content
+    expect(page.content_element.when_present.text).to match archive_template
+  end
+end
+
+Given(/^I have Flow beta feature enabled$/) do
+  step 'I enable Flow beta feature'
+end
+
+When(/^I disable Flow beta feature$/) do
+  visit(SpecialPreferencesPage) do |page|
+    page.beta_features_element.when_present.click
+    page.uncheck_flow_beta_feature
+    page.save_preferences
+    page.confirmation_element.when_present
+  end
+end
+
+Then(/^my wikitext talk page is restored$/) do
+  talk_page_link = "User_talk:#{@username}".gsub '_', ' '
+  visit(UserTalkPage, using_params: { username: @username }) do |page|
+    page.content_element.when_present
+    expect(page.content).to match @talk_page_content
+    expect(page.content).to_not match talk_page_link
+  end
+end
+
+Then(/^my Flow board is archived$/) do
+  flow_archive_name = "./User_talk:#{@username}/Flow_Archive_1"
+  talk_page_link = "User_talk:#{@username}".gsub '_', ' '
+  visit(WikiPage, using_params: { page: flow_archive_name }) do |page|
+    page.flow.board_element.when_present
+    expect(page.flow.header).to_not match talk_page_link
+  end
+end
+
+Given(/^I have used the Flow beta feature before$/) do
+  step 'my talk page has wiktext content'
+  step 'I enable Flow beta feature'
+  @topic_title = @data_manager.get 'title'
+  api.action('flow', submodule: 'new-topic', page: "User_talk:#{@username}", 
nttopic: @topic_title, ntcontent: 'created via API')
+  step 'I disable Flow beta feature'
+end
+
+Then(/^my talk page is my old Flow board$/) do
+  archive_name = "User_talk:#{@username}/Archive_1".gsub '_', ' '
+  visit(UserTalkPage, using_params: { username: @username }) do |page|
+    expect(page.content_element.when_present.text).to match @topic_title
+    expect(page.flow.header).to match archive_name
+  end
+end
+
+Then(/^my flow board contains a link to my archived talk page$/) do
+  archive_name = "User_talk:#{@username}/Archive_1".gsub '_', ' '
+  visit(UserTalkPage, using_params: { username: @username }) do |page|
+    page.flow.board_element.when_present
+    expect(page.flow.header).to match archive_name
+  end
+end
+
+Then(/^a notification tells me about it$/) do
+  visit(SpecialNotificationsPage) do |page|
+    expect(page.first_notification_element.when_present.text).to match 'New 
discussion system'
+  end
+end
+
+Then(/^my talk page is deleted without redirect$/) do
+  visit(UserTalkPage, using_params: { username: @username }) do |page|
+    page.content_element.when_present
+    expect(page.content).to match 'This page has been deleted.'
+    expect(page.content).to match 'without leaving a redirect'
+  end
+end
diff --git a/tests/browser/features/support/data_manager.rb 
b/tests/browser/features/support/data_manager.rb
index 7ed73df..1cbbdf4 100644
--- a/tests/browser/features/support/data_manager.rb
+++ b/tests/browser/features/support/data_manager.rb
@@ -4,7 +4,7 @@
   end
 
   def get(part)
-    @data[part] = "#{part}-#{rand}" unless @data.key? part
+    @data[part] = "#{part}_#{Random.srand}" unless @data.key? part
     @data[part]
   end
 
diff --git a/tests/browser/features/support/pages/abstract_flow_page.rb 
b/tests/browser/features/support/pages/abstract_flow_page.rb
index 6a5ad0d..187d878 100644
--- a/tests/browser/features/support/pages/abstract_flow_page.rb
+++ b/tests/browser/features/support/pages/abstract_flow_page.rb
@@ -1,6 +1,5 @@
-require_relative 'wiki_page'
-
-class AbstractFlowPage < WikiPage
+class AbstractFlowPage
+  include PageObject
   include FlowEditor
 
   page_section(:description, BoardDescription, class: 'flow-board-header')
@@ -12,6 +11,8 @@
     option.when_present.click
   end
 
+  a(:logout, css: "#pt-logout a")
+
   # board component
   div(:flow_component, class: 'flow-component')
   div(:flow_board, class: 'flow-board')
diff --git a/tests/browser/features/support/pages/flow_component.rb 
b/tests/browser/features/support/pages/flow_component.rb
new file mode 100644
index 0000000..ac46bc4
--- /dev/null
+++ b/tests/browser/features/support/pages/flow_component.rb
@@ -0,0 +1,6 @@
+class FlowComponent
+  include PageObject
+
+  div(:board, class: 'flow-board')
+  div(:header, class: 'flow-ui-boardDescriptionWidget-content')
+end
diff --git a/tests/browser/features/support/pages/new_wiki_page.rb 
b/tests/browser/features/support/pages/new_wiki_page.rb
deleted file mode 100644
index f3fa990..0000000
--- a/tests/browser/features/support/pages/new_wiki_page.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-require_relative 'wiki_page'
-
-class NewWikiPage < WikiPage
-  # MEDIAWIKI_URL must have this in $wgFlowOccupyNamespaces.
-  page_url "<%=params[:page]%>"
-end
diff --git a/tests/browser/features/support/pages/special_notifications_page.rb 
b/tests/browser/features/support/pages/special_notifications_page.rb
new file mode 100644
index 0000000..42166e8
--- /dev/null
+++ b/tests/browser/features/support/pages/special_notifications_page.rb
@@ -0,0 +1,7 @@
+class SpecialNotificationsPage
+  include PageObject
+
+  page_url "Special:Notifications"
+
+  div(:first_notification, class: 'mw-echo-content', index: 0)
+end
diff --git a/tests/browser/features/support/pages/special_preferences_page.rb 
b/tests/browser/features/support/pages/special_preferences_page.rb
new file mode 100644
index 0000000..84445f1
--- /dev/null
+++ b/tests/browser/features/support/pages/special_preferences_page.rb
@@ -0,0 +1,13 @@
+class SpecialPreferencesPage
+  include PageObject
+
+  page_url "Special:Preferences"
+
+  link(:beta_features, id: 'preftab-betafeatures')
+
+  checkbox(:flow_beta_feature, id: 
'mw-input-wpbeta-feature-flow-user-talk-page')
+
+  button(:save_preferences, id: 'prefcontrol')
+
+  div(:confirmation, text: 'Your preferences have been saved.')
+end
diff --git a/tests/browser/features/support/pages/user_talk_page.rb 
b/tests/browser/features/support/pages/user_talk_page.rb
new file mode 100644
index 0000000..7ea50e1
--- /dev/null
+++ b/tests/browser/features/support/pages/user_talk_page.rb
@@ -0,0 +1,7 @@
+require_relative 'wiki_page'
+
+class UserTalkPage < WikiPage
+  include PageObject
+
+  page_url "./User_talk:<%= params[:username]%>"
+end
diff --git a/tests/browser/features/support/pages/wiki_page.rb 
b/tests/browser/features/support/pages/wiki_page.rb
index 3f0c5c9..42817fb 100644
--- a/tests/browser/features/support/pages/wiki_page.rb
+++ b/tests/browser/features/support/pages/wiki_page.rb
@@ -1,4 +1,9 @@
 class WikiPage
   include PageObject
+
+  page_url "<%=params[:page]%>"
+
   a(:logout, css: "#pt-logout a")
+  div(:content, id: 'mw-content-text')
+  page_section(:flow, FlowComponent, class: 'flow-component')
 end
diff --git a/tests/phpunit/Import/ArchiveNameHelperTest.php 
b/tests/phpunit/Import/ArchiveNameHelperTest.php
new file mode 100644
index 0000000..1ccef03
--- /dev/null
+++ b/tests/phpunit/Import/ArchiveNameHelperTest.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Flow\Tests\Import;
+use Title;
+use Flow\Import\ArchiveNameHelper;
+
+
+/**
+ * @group Flow
+ */
+class ArchiveNameHelperTest extends \MediaWikiTestCase {
+
+       public function decideArchiveTitleProvider() {
+               return array(
+                       array(
+                               'Selects the first pattern if n=1 does exist',
+                               // expect
+                               'Talk:Flow/Archive 1',
+                               // source title
+                               Title::newFromText( 'Talk:Flow' ),
+                               // formats
+                               array( '%s/Archive %d', '%s/Archive%d' ),
+                               // existing titles
+                               array(),
+                       ),
+
+                       array(
+                               'Selects n=2 when n=1 exists',
+                               // expect
+                               'Talk:Flow/Archive 2',
+                               // source title
+                               Title::newFromText( 'Talk:Flow' ),
+                               // formats
+                               array( '%s/Archive %d' ),
+                               // existing titles
+                               array( 'Talk:Flow/Archive 1' ),
+                       ),
+
+                       array(
+                               'Selects the second pattern if n=1 exists',
+                               // expect
+                               'Talk:Flow/Archive2',
+                               // source title
+                               Title::newFromText( 'Talk:Flow' ),
+                               // formats
+                               array( '%s/Archive %d', '%s/Archive%d' ),
+                               // existing titles
+                               array( 'Talk:Flow/Archive1' ),
+                       ),
+               );
+       }
+       /**
+        * @dataProvider decideArchiveTitleProvider
+        */
+       public function testDecideArchiveTitle( $message, $expect, Title 
$source, array $formats, array $exists ) {
+               // flip so we can use isset
+               $existsByKey = array_flip( $exists );
+
+               $titleRepo = $this->getMock( 'Flow\Repository\TitleRepository' 
);
+               $titleRepo->expects( $this->any() )
+                       ->method( 'exists' )
+                       ->will( $this->returnCallback( function( Title $title ) 
use ( $existsByKey ) {
+                               return isset( 
$existsByKey[$title->getPrefixedText()] );
+                       } ) );
+
+               $archiveNameHelper = new ArchiveNameHelper();
+               $result = $archiveNameHelper->decideArchiveTitle( $source, 
$formats, $titleRepo );
+               $this->assertEquals( $expect, $result, $message );
+       }
+
+       public function findLatestArchiveTitleProvider() {
+               return array(
+                       array(
+                               'Returns false if no archive exist',
+                               // expect
+                               false,
+                               // source title
+                               Title::newFromText( 'Talk:Flow' ),
+                               // formats
+                               array( '%s/Archive %d', '%s/Archive%d' ),
+                               // existing titles
+                               array(),
+                       ),
+
+                       array(
+                               'Selects n=2 when n=2 exists',
+                               // expect
+                               'Talk:Flow/Archive 2',
+                               // source title
+                               Title::newFromText( 'Talk:Flow' ),
+                               // formats
+                               array( '%s/Archive %d' ),
+                               // existing titles
+                               array( 'Talk:Flow/Archive 1', 
'Talk:Flow/Archive 2' ),
+                       ),
+
+               );
+       }
+       /**
+        * @dataProvider findLatestArchiveTitleProvider
+        */
+       public function testFindLatestArchiveTitle( $message, $expect, Title 
$source, array $formats, array $exists ) {
+               // flip so we can use isset
+               $existsByKey = array_flip( $exists );
+
+               $titleRepo = $this->getMock( 'Flow\Repository\TitleRepository' 
);
+               $titleRepo->expects( $this->any() )
+                       ->method( 'exists' )
+                       ->will( $this->returnCallback( function( Title $title ) 
use ( $existsByKey ) {
+                               return isset( 
$existsByKey[$title->getPrefixedText()] );
+                       } ) );
+
+               $archiveNameHelper = new ArchiveNameHelper();
+               $result = $archiveNameHelper->findLatestArchiveTitle( $source, 
$formats, $titleRepo );
+               $this->assertEquals( $expect, $result, $message );
+       }
+
+}
diff --git a/tests/phpunit/Import/ConverterTest.php 
b/tests/phpunit/Import/ConverterTest.php
index 005a980..af1ad90 100644
--- a/tests/phpunit/Import/ConverterTest.php
+++ b/tests/phpunit/Import/ConverterTest.php
@@ -8,7 +8,6 @@
 use Flow\Import\Importer;
 use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
-use Title;
 use User;
 
 /**
@@ -20,63 +19,6 @@
                        'Flow\Import\Converter',
                        $this->createConverter()
                );
-       }
-
-       public function decideArchiveTitleProvider() {
-               return array(
-                       array(
-                               'Selects the first pattern if n=1 does exist',
-                               // expect
-                               'Talk:Flow/Archive 1',
-                               // source title
-                               Title::newFromText( 'Talk:Flow' ),
-                               // formats
-                               array( '%s/Archive %d', '%s/Archive%d' ),
-                               // existing titles
-                               array(),
-                       ),
-
-                       array(
-                               'Selects n=2 when n=1 exists',
-                               // expect
-                               'Talk:Flow/Archive 2',
-                               // source title
-                               Title::newFromText( 'Talk:Flow' ),
-                               // formats
-                               array( '%s/Archive %d' ),
-                               // existing titles
-                               array( 'Talk:Flow/Archive 1' ),
-                       ),
-
-                       array(
-                               'Selects the second pattern if n=1 exists',
-                               // expect
-                               'Talk:Flow/Archive2',
-                               // source title
-                               Title::newFromText( 'Talk:Flow' ),
-                               // formats
-                               array( '%s/Archive %d', '%s/Archive%d' ),
-                               // existing titles
-                               array( 'Talk:Flow/Archive1' ),
-                       ),
-               );
-       }
-       /**
-        * @dataProvider decideArchiveTitleProvider
-        */
-       public function testDecideArchiveTitle( $message, $expect, Title 
$source, array $formats, array $exists ) {
-               // flip so we can use isset
-               $existsByKey = array_flip( $exists );
-
-               $titleRepo = $this->getMock( 'Flow\Repository\TitleRepository' 
);
-               $titleRepo->expects( $this->any() )
-                       ->method( 'exists' )
-                       ->will( $this->returnCallback( function( Title $title ) 
use ( $existsByKey ) {
-                               return isset( 
$existsByKey[$title->getPrefixedText()] );
-                       } ) );
-
-               $result = Converter::decideArchiveTitle( $source, $formats, 
$titleRepo );
-               $this->assertEquals( $expect, $result, $message );
        }
 
        protected function createConverter(
diff --git a/tests/phpunit/Import/TemplateHelperTest.php 
b/tests/phpunit/Import/TemplateHelperTest.php
new file mode 100644
index 0000000..f6a9ff4
--- /dev/null
+++ b/tests/phpunit/Import/TemplateHelperTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Flow\Tests\Import;
+
+use Flow\Import\TemplateHelper;
+
+class TemplateHelperTest extends \MediaWikiTestCase {
+
+       public function removeFromHtmlDataProvider() {
+               return array(
+                       array( // the template is NOT in the html
+                               '<body data-parsoid="{stuff}"><p name="asdf">hi 
there</p></body>',
+                               'I am not a real template',
+                               '<body data-parsoid="{stuff}"><p name="asdf">hi 
there</p></body>'
+                       ),
+                       array( // the template IS in the html ONCE
+                               '<body data-parsoid="{stuff}"><p>hi there<span 
typeof="mw:Transclusion" data-mw=\'{"parts":[{"template":{"target":{"wt":"I am 
a template","href":"./Template:I_am_a_template"}}}]}\'></span></p></body>',
+                               'I am a template',
+                               '<body data-parsoid="{stuff}"><p>hi 
there</p></body>'
+                       ),
+                       array( // the template IS in the html MANY TIMES
+                               '<body data-parsoid="{stuff}"><p>a<span 
typeof="mw:Transclusion" data-mw=\'{"parts":[{"template":{"target":{"wt":"I am 
a template","href":"./Template:I_am_a_template"}}}]}\'></span>b<span 
typeof="mw:Transclusion" data-mw=\'{"parts":[{"template":{"target":{"wt":"I am 
a template","href":"./Template:I_am_a_template"}}}]}\'></span>c</p></body>',
+                               'I am a template',
+                               '<body data-parsoid="{stuff}"><p>abc</p></body>'
+                       ),
+                       array( // somewhat malformed data-mw
+                               '<body data-parsoid="{stuff}"><p>hi there<span 
typeof="mw:Transclusion" data-mw=\'{"parts":[{}]}\'></span></p></body>',
+                               'Template name',
+                               '<body data-parsoid="{stuff}"><p>hi there<span 
typeof="mw:Transclusion" data-mw=\'{"parts":[{}]}\'></span></p></body>'
+                       ),
+                       array( // multinode template using 'about' attribute
+                               '<body data-parsoid="{stuff}">' .
+                                       '<p>hi there</p>' .
+                                       '<span typeof="mw:Transclusion" 
about="#mwt5" data-mw=\'{"parts":[{"template":{"target":{"wt":"I am a 
template","href":"./Template:I_am_a_template"}}}]}\'></span>' .
+                                       '<span about="#mwt5">random sibling 
node</span>' .
+                                       '<span about="#mwt5">and then another 
one</span>' .
+                               '</body>',
+                               'I am a template',
+                               '<body data-parsoid="{stuff}"><p>hi 
there</p></body>'
+                       )
+               );
+       }
+
+       /**
+        * @dataProvider removeFromHtmlDataProvider
+        */
+       public function testRemoveFromHtml( $originalHtml, $templateToRemove, 
$expectedHtml ) {
+               $actualHTML = TemplateHelper::removeFromHtml( $originalHtml, 
$templateToRemove );
+               $this->assertEquals(
+                       $expectedHtml,
+                       $actualHTML );
+       }
+
+}
\ No newline at end of file

-- 
To view, visit https://gerrit.wikimedia.org/r/230648
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ia9950c4eb1c0e5f912761a65c76c5a2b3b99c8ee
Gerrit-PatchSet: 37
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Sbisson <sbis...@wikimedia.org>
Gerrit-Reviewer: Catrope <roan.katt...@gmail.com>
Gerrit-Reviewer: Mattflaschen <mflasc...@wikimedia.org>
Gerrit-Reviewer: Matthias Mullie <mmul...@wikimedia.org>
Gerrit-Reviewer: Mooeypoo <mor...@gmail.com>
Gerrit-Reviewer: Sbisson <sbis...@wikimedia.org>
Gerrit-Reviewer: Siebrand <siebr...@kitano.nl>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to