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