Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 294
0.00% covered (danger)
0.00%
0 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
OptInController
0.00% covered (danger)
0.00%
0 / 294
0.00% covered (danger)
0.00%
0 / 29
3422
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 initiateChange
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 enable
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 disable
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 hasFlowBoardArchive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isFlowBoard
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 movePage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 fatal
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 fromNewlineSeparated
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 findLatestArchive
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 findNextArchive
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 findLatestFlowArchive
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 findNextFlowArchive
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 createRevision
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 createFlowBoard
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 archiveExistingTalkpage
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 restoreExistingFlowBoard
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 getContent
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getFormattedCurrentTemplate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 formatTemplate
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 editBoardDescription
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
42
 getFormattedArchiveTemplate
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 removeArchiveTemplateFromWikitextTalkpage
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 removeCurrentTemplateFromWikitext
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 extractTemplatesAboveFirstSection
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 editWikitextContent
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 addCurrentTemplate
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 archiveFlowBoard
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 logBlockErrors
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace Flow\Import;
4
5use DateTime;
6use DateTimeZone;
7use Exception;
8use Flow\Block\AbstractBlock;
9use Flow\Collection\HeaderCollection;
10use Flow\Container;
11use Flow\Content\BoardContent;
12use Flow\Conversion\Utils;
13use Flow\Exception\InvalidDataException;
14use Flow\Notifications\Controller;
15use Flow\OccupationController;
16use Flow\WorkflowLoader;
17use Flow\WorkflowLoaderFactory;
18use MediaWiki\Content\WikitextContent;
19use MediaWiki\Context\DerivativeContext;
20use MediaWiki\Context\IContextSource;
21use MediaWiki\Context\RequestContext;
22use MediaWiki\Deferred\DeferredUpdates;
23use MediaWiki\Json\FormatJson;
24use MediaWiki\Logger\LoggerFactory;
25use MediaWiki\MediaWikiServices;
26use MediaWiki\Parser\ParserOptions;
27use MediaWiki\Revision\RevisionRecord;
28use MediaWiki\Revision\SlotRecord;
29use MediaWiki\Title\Title;
30use MediaWiki\User\User;
31use Psr\Log\LoggerInterface;
32use Wikimedia\Rdbms\IDBAccessObject;
33
34/**
35 * Entry point for enabling Flow on a page.
36 */
37class OptInController {
38    public const ENABLE = 'enable';
39    public const DISABLE = 'disable';
40
41    /**
42     * @var OccupationController
43     */
44    private $occupationController;
45
46    /**
47     * @var Controller
48     */
49    private $notificationController;
50
51    /**
52     * @var ArchiveNameHelper
53     */
54    private $archiveNameHelper;
55
56    /**
57     * @var LoggerInterface
58     */
59    private $logger;
60
61    /**
62     * @var IContextSource
63     */
64    private $context;
65
66    /**
67     * @var User
68     */
69    private $user;
70
71    /**
72     * @param OccupationController $occupationController
73     * @param Controller $notificationController
74     * @param ArchiveNameHelper $archiveNameHelper
75     * @param LoggerInterface $logger Logger for errors and exceptions
76     * @param User $scriptUser User that takes actions, such as creating the board or
77     *   editing descriptions
78     */
79    public function __construct(
80        OccupationController $occupationController,
81        Controller $notificationController,
82        ArchiveNameHelper $archiveNameHelper,
83        LoggerInterface $logger,
84        User $scriptUser
85    ) {
86        $this->occupationController = $occupationController;
87        $this->notificationController = $notificationController;
88        $this->archiveNameHelper = $archiveNameHelper;
89        $this->logger = $logger;
90        $this->user = $scriptUser;
91        $this->context = new DerivativeContext( RequestContext::getMain() );
92        $this->context->setUser( $this->user );
93    }
94
95    /**
96     * @param string $action Action to take, self::ENABLE or self::DISABLE
97     * @param Title $talkpage Title of user's talk page
98     * @param User $user User that owns the talk page
99     */
100    public function initiateChange( $action, Title $talkpage, User $user ) {
101        $outerMethod = __METHOD__;
102        $logger = $this->logger;
103
104        DeferredUpdates::addCallableUpdate(
105            function () use ( $logger, $outerMethod, $action, $talkpage, $user ) {
106                try {
107                    if ( $action === self::ENABLE ) {
108                        $this->enable( $talkpage, $user );
109                    } elseif ( $action === self::DISABLE ) {
110                        $this->disable( $talkpage );
111                    } else {
112                        $logger->error( $outerMethod . ': unrecognized action: ' . $action );
113                    }
114                } catch ( Exception $exception ) {
115                    $logger->error(
116                        $outerMethod . ' failed to {action} Flow on \'{talkpage}\' for user \'{user}\'. {message} {trace}',
117                        [
118                            'action' => $action,
119                            'talkpage' => $talkpage->getPrefixedText(),
120                            'user' => $user->getName(),
121                            'message' => $exception->getMessage(),
122                            'exception' => $exception,
123                        ]
124                    );
125
126                    // Rollback both Flow and Core DBs.
127                    MediaWikiServices::getInstance()->getDBLoadBalancerFactory()
128                        ->rollbackPrimaryChanges( $outerMethod );
129                }
130            },
131            DeferredUpdates::POSTSEND
132        );
133    }
134
135    public function enable( Title $title, User $user ) {
136        if ( $this->isFlowBoard( $title ) ) {
137            // already a Flow board
138            return;
139        }
140
141        // archive existing wikitext talk page
142        $currentTemplate = null;
143        $templatesFromTalkpage = null;
144        if ( $title->exists( IDBAccessObject::READ_LATEST ) ) {
145            $templatesFromTalkpage = $this->extractTemplatesAboveFirstSection( $title );
146            $wikitextTalkpageArchiveTitle = $this->archiveExistingTalkpage( $title );
147            $currentTemplate = $this->getFormattedCurrentTemplate( $wikitextTalkpageArchiveTitle );
148        }
149
150        // create or restore flow board
151        $archivedFlowPage = $this->findLatestFlowArchive( $title );
152        if ( $archivedFlowPage ) {
153            $this->restoreExistingFlowBoard( $archivedFlowPage, $title, $currentTemplate );
154        } else {
155            $this->createFlowBoard( $title, $templatesFromTalkpage . "\n\n" . $currentTemplate );
156            $this->notificationController->notifyFlowEnabledOnTalkpage( $user );
157        }
158    }
159
160    public function disable( Title $title ) {
161        if ( !$this->isFlowBoard( $title ) ) {
162            return;
163        }
164
165        // archive the flow board
166        $flowArchiveTitle = $this->archiveFlowBoard( $title );
167
168        // restore the original wikitext talk page
169        $archivedTalkpage = $this->findLatestArchive( $title );
170        if ( $archivedTalkpage ) {
171            $this->removeArchiveTemplateFromWikitextTalkpage( $archivedTalkpage );
172            $this->addCurrentTemplate( $archivedTalkpage, $flowArchiveTitle );
173            $restoreReason = wfMessage( 'flow-optin-restore-wikitext' )->inContentLanguage()->text();
174            $this->movePage( $archivedTalkpage, $title, $restoreReason );
175        }
176    }
177
178    /**
179     * Check whether the current user has a flow board archived already.
180     *
181     * @param User $user
182     * @return bool Flow board archive exists
183     */
184    public function hasFlowBoardArchive( User $user ) {
185        return $this->findLatestFlowArchive( $user->getTalkPage() ) !== false;
186    }
187
188    /**
189     * @param Title $title
190     * @return bool
191     */
192    private function isFlowBoard( Title $title ) {
193        return $title->getContentModel( IDBAccessObject::READ_LATEST ) === CONTENT_MODEL_FLOW_BOARD;
194    }
195
196    /**
197     * @param Title $from
198     * @param Title $to
199     * @param string $reason
200     */
201    private function movePage( Title $from, Title $to, $reason = '' ) {
202        $this->occupationController->forceAllowCreation( $to );
203
204        $mp = MediaWikiServices::getInstance()
205            ->getMovePageFactory()
206            ->newMovePage( $from, $to );
207        $mp->move( $this->user, $reason, false );
208
209        /*
210         * Article IDs are cached inside title objects. Since we'll be
211         * reusing these objects, we have to make sure they reflect the
212         * correct IDs.
213         *
214         * We could just IDBAccessObject::READ_LATEST everywhere, but that would
215         * result in a lot of unneeded calls to primary database.
216         *
217         * If these IDs are wrong, we could end up associating workflows
218         * with an incorrect page (that was just moved)
219         *
220         * Anyway, the page has just been moved without redirect, so that
221         * page is no longer valid.
222         */
223        $from->resetArticleID( 0 );
224        $linkCache = MediaWikiServices::getInstance()->getLinkCache();
225        $linkCache->addBadLinkObj( $from );
226
227        /*
228         * Force id cached inside $title to be updated, as well as info
229         * inside LinkCache.
230         */
231        $to->getArticleID( IDBAccessObject::READ_LATEST );
232    }
233
234    /**
235     * @param string $msgKey
236     * @param mixed $args
237     * @throws ImportException
238     * @return never
239     */
240    private function fatal( $msgKey, $args = [] ) {
241        throw new ImportException( wfMessage( $msgKey, $args )->inContentLanguage()->text() );
242    }
243
244    /**
245     * @param string $str
246     * @return string[]
247     */
248    private function fromNewlineSeparated( $str ) {
249        return explode( "\n", $str );
250    }
251
252    /**
253     * @param Title $title
254     * @return Title|false
255     */
256    private function findLatestArchive( Title $title ) {
257        $archiveFormats = $this->fromNewlineSeparated(
258            wfMessage( 'flow-conversion-archive-page-name-format' )->inContentLanguage()->plain() );
259        return $this->archiveNameHelper->findLatestArchiveTitle( $title, $archiveFormats );
260    }
261
262    /**
263     * @param Title $title
264     * @return Title
265     * @throws ImportException
266     */
267    private function findNextArchive( Title $title ) {
268        $archiveFormats = $this->fromNewlineSeparated(
269            wfMessage( 'flow-conversion-archive-page-name-format' )->inContentLanguage()->plain() );
270        return $this->archiveNameHelper->decideArchiveTitle( $title, $archiveFormats );
271    }
272
273    /**
274     * @param Title $title
275     * @return Title|false
276     */
277    private function findLatestFlowArchive( Title $title ) {
278        $archiveFormats = $this->fromNewlineSeparated(
279            wfMessage( 'flow-conversion-archive-flow-page-name-format' )->inContentLanguage()->plain() );
280        return $this->archiveNameHelper->findLatestArchiveTitle( $title, $archiveFormats );
281    }
282
283    /**
284     * @param Title $title
285     * @return Title
286     * @throws ImportException
287     */
288    private function findNextFlowArchive( Title $title ) {
289        $archiveFormats = $this->fromNewlineSeparated(
290            wfMessage( 'flow-conversion-archive-flow-page-name-format' )->inContentLanguage()->plain() );
291        return $this->archiveNameHelper->decideArchiveTitle( $title, $archiveFormats );
292    }
293
294    /**
295     * @param Title $title
296     * @param string $contentText
297     * @param string $summary
298     * @throws ImportException
299     * @param-taint $contentText escapes_escaped
300     */
301    private function createRevision( Title $title, $contentText, $summary ) {
302        $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
303        $newContent = new WikitextContent( $contentText );
304        $status = $page->doUserEditContent(
305            $newContent,
306            $this->user,
307            $summary,
308            EDIT_FORCE_BOT | EDIT_SUPPRESS_RC
309        );
310
311        if ( !$status->isGood() ) {
312            $statusFormatter = MediaWikiServices::getInstance()->getFormatterFactory()->getStatusFormatter(
313                $this->context
314            );
315            $this->logger->error( 'Failed creating revision at {title} because {status}', [
316                'title' => $title->getPrefixedText(),
317                'status' => $statusFormatter->getWikiText( $status, [ 'lang' => 'en' ] ),
318                'exception' => new \RuntimeException(),
319            ] );
320            throw new ImportException( "Failed creating revision at {$title}" );
321        }
322    }
323
324    /**
325     * @param Title $title
326     * @param string $boardDescription
327     * @throws ImportException
328     * @throws \Flow\Exception\CrossWikiException
329     * @throws \Flow\Exception\InvalidInputException
330     */
331    private function createFlowBoard( Title $title, $boardDescription ) {
332        /** @var WorkflowLoaderFactory $loaderFactory */
333        $loaderFactory = Container::get( 'factory.loader.workflow' );
334        $page = $title->getPrefixedText();
335
336        $creationStatus = $this->occupationController->safeAllowCreation( $title, $this->user, false );
337        if ( !$creationStatus->isGood() ) {
338            $this->fatal( 'flow-special-enableflow-board-creation-not-allowed', $page );
339        }
340
341        $loader = $loaderFactory->createWorkflowLoader( $title );
342        $blocks = $loader->getBlocks();
343        $this->logBlockErrors( $blocks );
344
345        if ( !$boardDescription ) {
346            $boardDescription = ' ';
347        }
348
349        $action = 'edit-header';
350        $params = [
351            'header' => [
352                'content' => $boardDescription,
353                'format' => 'wikitext',
354            ],
355        ];
356
357        $blocksToCommit = $loader->handleSubmit(
358            $this->context,
359            $action,
360            $params
361        );
362
363        foreach ( $blocks as $block ) {
364            if ( $block->hasErrors() ) {
365                $errors = $block->getErrors();
366
367                foreach ( $errors as $errorKey ) {
368                    $this->fatal( $block->getErrorMessage( $errorKey ) );
369                }
370            }
371        }
372
373        $loader->commit( $blocksToCommit );
374    }
375
376    /**
377     * @param Title $title
378     * @return Title
379     */
380    private function archiveExistingTalkpage( Title $title ) {
381        $archiveTitle = $this->findNextArchive( $title );
382        $archiveReason = wfMessage( 'flow-optin-archive-wikitext' )->inContentLanguage()->text();
383        $this->movePage( $title, $archiveTitle, $archiveReason );
384
385        $content = $this->getContent( $archiveTitle );
386        $content = $this->removeCurrentTemplateFromWikitext( $content, $archiveTitle );
387        $content = $this->getFormattedArchiveTemplate( $title ) . "\n\n" . $content;
388
389        $addTemplateReason = wfMessage( 'flow-beta-feature-add-archive-template-edit-summary' )->inContentLanguage()->plain();
390        $this->createRevision(
391            $archiveTitle,
392            $content,
393            $addTemplateReason
394        );
395
396        return $archiveTitle;
397    }
398
399    /**
400     * @param Title $archivedFlowPage
401     * @param Title $title
402     * @param string|null $currentTemplate
403     */
404    private function restoreExistingFlowBoard( Title $archivedFlowPage, Title $title, $currentTemplate = null ) {
405        $this->editBoardDescription(
406            $archivedFlowPage,
407            static function ( $content ) use ( $currentTemplate, $archivedFlowPage ) {
408                $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();
409                $content = TemplateHelper::removeFromHtml( $content, $templateName );
410                if ( $currentTemplate ) {
411                    $content = Utils::convert( 'wikitext', 'html', $currentTemplate, $archivedFlowPage ) .
412                        '<br/><br/>' . $content;
413                }
414                return $content;
415            },
416            'html'
417        );
418
419        $restoreReason = wfMessage( 'flow-optin-restore-flow-board' )->inContentLanguage()->text();
420        $this->movePage( $archivedFlowPage, $title, $restoreReason );
421    }
422
423    /**
424     * @param Title $title
425     * @return string
426     */
427    private function getContent( Title $title ) {
428        $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
429        $page->loadPageData( IDBAccessObject::READ_LATEST );
430        $revision = $page->getRevisionRecord();
431        if ( $revision ) {
432            $content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC );
433            if ( $content instanceof WikitextContent ) {
434                return $content->getText();
435            }
436        }
437
438        return '';
439    }
440
441    /**
442     * @param Title $archiveTitle
443     * @return string
444     */
445    private function getFormattedCurrentTemplate( Title $archiveTitle ) {
446        $now = new DateTime( "now", new DateTimeZone( "GMT" ) );
447        $arguments = [
448            'archive' => $archiveTitle->getPrefixedText(),
449            'date' => $now->format( 'Y-m-d' ),
450        ];
451        $template = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain();
452        return $this->formatTemplate( $template, $arguments );
453    }
454
455    /**
456     * @param string $name
457     * @param array $args
458     * @return string
459     */
460    private function formatTemplate( $name, array $args ) {
461        $arguments = implode( '|',
462            array_map(
463                static function ( $key, $value ) {
464                    return "$key=$value";
465                },
466                array_keys( $args ),
467                array_values( $args ) )
468        );
469        return "{{{$name}|$arguments}}";
470    }
471
472    /**
473     * @param Title $title
474     * @param callable $newDescriptionCallback
475     * @param string $format
476     * @throws ImportException
477     * @throws InvalidDataException
478     */
479    private function editBoardDescription( Title $title, callable $newDescriptionCallback, $format = 'html' ) {
480        /*
481         * We could use WorkflowLoaderFactory::createWorkflowLoader
482         * to get to the workflow ID, but that uses WikiPageFactory::newFromTitle
483         * to build the wikipage & get the content. For most requests,
484         * that'll be better (it reads from replicas), but we really
485         * need to read from primary database here.
486         * We'll need WorkflowLoader further down anyway, but we'll
487         * then have the correct workflow ID to initialize it with!
488         *
489         * $title->getLatestRevId() should be fine, it'll be read from
490         * LinkCache, which has been updated.
491         * RevisionLookup::getRevisionById will try replica first.
492         * If it can't find the id, it'll try to find it on primary database.
493         */
494        $revId = $title->getLatestRevID();
495        $revRecord = MediaWikiServices::getInstance()
496            ->getRevisionLookup()
497            ->getRevisionById( $revId );
498        $content = $revRecord ? $revRecord->getContent( SlotRecord::MAIN ) : null;
499        if ( !$content instanceof BoardContent ) {
500            throw new InvalidDataException(
501                'Could not find board page for ' . $title->getPrefixedDBkey() . ' (id: ' . $title->getArticleID() . ').' .
502                'Found content: ' . var_export( $content, true )
503            );
504        }
505        $workflowId = $content->getWorkflowId();
506
507        $collection = HeaderCollection::newFromId( $workflowId );
508        $revision = $collection->getLastRevision();
509
510        /*
511         * We could just do $revision->getContent( $format ), but that
512         * may need to find $title in order to convert.
513         * We already know $title (and don't want to risk it being used
514         * in a way it stores lagging replica data), so let's just
515         * manually convert the content.
516         */
517        $content = $revision->getContentRaw();
518        $content = Utils::convert( $revision->getContentFormat(), $format, $content, $title );
519
520        $newDescription = $newDescriptionCallback( $content );
521
522        $action = 'edit-header';
523        $params = [
524            'header' => [
525                'content' => $newDescription,
526                'format' => $format,
527                'prev_revision' => $revision->getRevisionId()->getAlphadecimal()
528            ],
529        ];
530
531        /** @var WorkflowLoaderFactory $factory */
532        $factory = Container::get( 'factory.loader.workflow' );
533
534        /** @var WorkflowLoader $loader */
535        $loader = $factory->createWorkflowLoader( $title, $workflowId );
536
537        $blocks = $loader->getBlocks();
538        $this->logBlockErrors( $blocks );
539
540        $blocksToCommit = $loader->handleSubmit(
541            $this->context,
542            $action,
543            $params
544        );
545
546        foreach ( $blocks as $block ) {
547            if ( $block->hasErrors() ) {
548                $errors = $block->getErrors();
549
550                foreach ( $errors as $errorKey ) {
551                    $this->fatal( $block->getErrorMessage( $errorKey ) );
552                }
553            }
554        }
555
556        $loader->commit( $blocksToCommit );
557    }
558
559    /**
560     * @param Title $current
561     * @return string
562     */
563    private function getFormattedArchiveTemplate( Title $current ) {
564        $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();
565        $now = new DateTime( "now", new DateTimeZone( "GMT" ) );
566        return $this->formatTemplate( $templateName, [
567            'from' => $current->getPrefixedText(),
568            'date' => $now->format( 'Y-m-d' ),
569        ] );
570    }
571
572    /**
573     * @param Title $title
574     * @throws ImportException
575     */
576    private function removeArchiveTemplateFromWikitextTalkpage( Title $title ) {
577        $wtContent = $this->getContent( $title );
578        if ( !$wtContent ) {
579            return;
580        }
581
582        $content = Utils::convert( 'wikitext', 'html', $wtContent, $title );
583        $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();
584
585        $newContent = TemplateHelper::removeFromHtml( $content, $templateName );
586
587        $this->createRevision(
588            $title,
589            Utils::convert( 'html', 'wikitext', $newContent, $title ),
590            wfMessage( 'flow-beta-feature-remove-archive-template-edit-summary' )->inContentLanguage()->plain() );
591    }
592
593    /**
594     * @param string $wikitextContent
595     * @param Title $title
596     * @return string
597     */
598    private function removeCurrentTemplateFromWikitext( $wikitextContent, Title $title ) {
599        $templateName = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain();
600        $contentAsHtml = Utils::convert( 'wikitext', 'html', $wikitextContent, $title );
601        $contentWithoutTemplate = TemplateHelper::removeFromHtml( $contentAsHtml, $templateName );
602        return Utils::convert( 'html', 'wikitext', $contentWithoutTemplate, $title );
603    }
604
605    /**
606     * @param Title $title
607     * @return string
608     */
609    private function extractTemplatesAboveFirstSection( Title $title ) {
610        $content = $this->getContent( $title );
611        if ( !$content ) {
612            return '';
613        }
614
615        $parser = MediaWikiServices::getInstance()->getParserFactory()->create();
616        $output = $parser->parse( $content, $title, new ParserOptions( $this->user ) );
617        $sections = $output->getSections();
618        if ( $sections ) {
619            # T319141: `byteoffset` is actually a *codepoint* offset.
620            $content = mb_substr( $content, 0, $sections[0]['byteoffset'] );
621        }
622        return TemplateHelper::extractTemplates( $content, $title );
623    }
624
625    /**
626     * @param Title $title
627     * @param string $reason
628     * @param callable $newDescriptionCallback
629     * @param string $format
630     * @throws ImportException
631     * @throws InvalidDataException
632     */
633    private function editWikitextContent( Title $title, $reason, callable $newDescriptionCallback, $format = 'html' ) {
634        $content = Utils::convert( 'wikitext', $format, $this->getContent( $title ), $title );
635        $newContent = $newDescriptionCallback( $content );
636        $this->createRevision(
637            $title,
638            Utils::convert( $format, 'wikitext', $newContent, $title ),
639            $reason
640        );
641    }
642
643    /**
644     * Add the "current" template to the page considered the current talkpage
645     * and link to the archived talkpage.
646     *
647     * @param Title $currentTalkpageTitle
648     * @param Title $archivedTalkpageTitle
649     */
650    private function addCurrentTemplate( Title $currentTalkpageTitle, Title $archivedTalkpageTitle ) {
651        $template = $this->getFormattedCurrentTemplate( $archivedTalkpageTitle );
652        $this->editWikitextContent(
653            $currentTalkpageTitle,
654            wfMessage( 'flow-beta-feature-add-current-template-edit-summary' )->inContentLanguage()->plain(),
655            static function ( $content ) use ( $template ) {
656                return $template . "\n\n" . $content;
657            },
658            'wikitext'
659        );
660    }
661
662    /**
663     * @param Title $title
664     * @return Title
665     * @throws InvalidDataException
666     */
667    private function archiveFlowBoard( Title $title ) {
668        $flowArchiveTitle = $this->findNextFlowArchive( $title );
669        $archiveReason = wfMessage( 'flow-optin-archive-flow-board' )->inContentLanguage()->text();
670        $this->movePage( $title, $flowArchiveTitle, $archiveReason );
671
672        $template = $this->getFormattedArchiveTemplate( $title );
673        $template = Utils::convert( 'wikitext', 'html', $template, $title );
674
675        $this->editBoardDescription(
676            $flowArchiveTitle,
677            static function ( $content ) use ( $template ) {
678                $templateName = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain();
679                $content = TemplateHelper::removeFromHtml( $content, $templateName );
680                return $template . "<br/><br/>" . $content;
681            },
682            'html' );
683
684        return $flowArchiveTitle;
685    }
686
687    private function logBlockErrors( array $blocks ) {
688        $errors = [];
689        /** @var AbstractBlock $block */
690        foreach ( $blocks as $block ) {
691            if ( $block->hasErrors() ) {
692                $blockErrors = $block->getErrors();
693                foreach ( $blockErrors as $blockErrorType ) {
694                    $errors[ $block->getName() ] = [
695                        'type' => $blockErrorType,
696                        'message' => $block->getErrorMessage( $blockErrorType ),
697                        'extra' => $block->getErrorExtra( $blockErrorType )
698                    ];
699                }
700            }
701        }
702        if ( $errors ) {
703            LoggerFactory::getInstance( 'Flow' )->error(
704                'Found {count} block errors for user {user_id}',
705                [
706                    'count' => count( $errors ),
707                    'user_id' => RequestContext::getMain()->getUser()->getId(),
708                    'errors' => FormatJson::encode( $errors )
709                ]
710            );
711        }
712    }
713}