Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 293
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 / 293
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 / 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 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    /**
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            $statusFormatter = MediaWikiServices::getInstance()->getFormatterFactory()->getStatusFormatter(
320                $this->context
321            );
322            $this->logger->error( 'Failed creating revision at {title} because {status}', [
323                'title' => $title->getPrefixedText(),
324                'status' => $statusFormatter->getWikiText( $status, [ 'lang' => 'en' ] ),
325                'exception' => new \RuntimeException(),
326            ] );
327            throw new ImportException( "Failed creating revision at {$title}" );
328        }
329    }
330
331    /**
332     * @param Title $title
333     * @param string $boardDescription
334     * @throws ImportException
335     * @throws \Flow\Exception\CrossWikiException
336     * @throws \Flow\Exception\InvalidInputException
337     */
338    private function createFlowBoard( Title $title, $boardDescription ) {
339        /** @var WorkflowLoaderFactory $loaderFactory */
340        $loaderFactory = Container::get( 'factory.loader.workflow' );
341        $page = $title->getPrefixedText();
342
343        $creationStatus = $this->occupationController->safeAllowCreation( $title, $this->user, false );
344        if ( !$creationStatus->isGood() ) {
345            $this->fatal( 'flow-special-enableflow-board-creation-not-allowed', $page );
346        }
347
348        $loader = $loaderFactory->createWorkflowLoader( $title );
349        $blocks = $loader->getBlocks();
350        $this->logBlockErrors( $blocks );
351
352        if ( !$boardDescription ) {
353            $boardDescription = ' ';
354        }
355
356        $action = 'edit-header';
357        $params = [
358            'header' => [
359                'content' => $boardDescription,
360                'format' => 'wikitext',
361            ],
362        ];
363
364        $blocksToCommit = $loader->handleSubmit(
365            $this->context,
366            $action,
367            $params
368        );
369
370        foreach ( $blocks as $block ) {
371            if ( $block->hasErrors() ) {
372                $errors = $block->getErrors();
373
374                foreach ( $errors as $errorKey ) {
375                    $this->fatal( $block->getErrorMessage( $errorKey ) );
376                }
377            }
378        }
379
380        $loader->commit( $blocksToCommit );
381    }
382
383    /**
384     * @param Title $title
385     * @return Title
386     */
387    private function archiveExistingTalkpage( Title $title ) {
388        $archiveTitle = $this->findNextArchive( $title );
389        $archiveReason = wfMessage( 'flow-optin-archive-wikitext' )->inContentLanguage()->text();
390        $this->movePage( $title, $archiveTitle, $archiveReason );
391
392        $content = $this->getContent( $archiveTitle );
393        $content = $this->removeCurrentTemplateFromWikitext( $content, $archiveTitle );
394        $content = $this->getFormattedArchiveTemplate( $title ) . "\n\n" . $content;
395
396        $addTemplateReason = wfMessage( 'flow-beta-feature-add-archive-template-edit-summary' )->inContentLanguage()->plain();
397        $this->createRevision(
398            $archiveTitle,
399            $content,
400            $addTemplateReason
401        );
402
403        return $archiveTitle;
404    }
405
406    /**
407     * @param Title $archivedFlowPage
408     * @param Title $title
409     * @param string|null $currentTemplate
410     */
411    private function restoreExistingFlowBoard( Title $archivedFlowPage, Title $title, $currentTemplate = null ) {
412        $this->editBoardDescription(
413            $archivedFlowPage,
414            static function ( $content ) use ( $currentTemplate, $archivedFlowPage ) {
415                $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();
416                $content = TemplateHelper::removeFromHtml( $content, $templateName );
417                if ( $currentTemplate ) {
418                    $content = Utils::convert( 'wikitext', 'html', $currentTemplate, $archivedFlowPage ) . "<br/><br/>" . $content;
419                }
420                return $content;
421            },
422            'html'
423        );
424
425        $restoreReason = wfMessage( 'flow-optin-restore-flow-board' )->inContentLanguage()->text();
426        $this->movePage( $archivedFlowPage, $title, $restoreReason );
427    }
428
429    /**
430     * @param Title $title
431     * @return string
432     */
433    private function getContent( Title $title ) {
434        $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
435        $page->loadPageData( IDBAccessObject::READ_LATEST );
436        $revision = $page->getRevisionRecord();
437        if ( $revision ) {
438            $content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::FOR_PUBLIC );
439            if ( $content instanceof WikitextContent ) {
440                return $content->getText();
441            }
442        }
443
444        return '';
445    }
446
447    /**
448     * @param Title $archiveTitle
449     * @return string
450     */
451    private function getFormattedCurrentTemplate( Title $archiveTitle ) {
452        $now = new DateTime( "now", new DateTimeZone( "GMT" ) );
453        $arguments = [
454            'archive' => $archiveTitle->getPrefixedText(),
455            'date' => $now->format( 'Y-m-d' ),
456        ];
457        $template = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain();
458        return $this->formatTemplate( $template, $arguments );
459    }
460
461    /**
462     * @param string $name
463     * @param array $args
464     * @return string
465     */
466    private function formatTemplate( $name, array $args ) {
467        $arguments = implode( '|',
468            array_map(
469                static function ( $key, $value ) {
470                    return "$key=$value";
471                },
472                array_keys( $args ),
473                array_values( $args ) )
474        );
475        return "{{{$name}|$arguments}}";
476    }
477
478    /**
479     * @param Title $title
480     * @param callable $newDescriptionCallback
481     * @param string $format
482     * @throws ImportException
483     * @throws InvalidDataException
484     */
485    private function editBoardDescription( Title $title, callable $newDescriptionCallback, $format = 'html' ) {
486        /*
487         * We could use WorkflowLoaderFactory::createWorkflowLoader
488         * to get to the workflow ID, but that uses WikiPageFactory::newFromTitle
489         * to build the wikipage & get the content. For most requests,
490         * that'll be better (it reads from replicas), but we really
491         * need to read from primary database here.
492         * We'll need WorkflowLoader further down anyway, but we'll
493         * then have the correct workflow ID to initialize it with!
494         *
495         * $title->getLatestRevId() should be fine, it'll be read from
496         * LinkCache, which has been updated.
497         * RevisionLookup::getRevisionById will try replica first.
498         * If it can't find the id, it'll try to find it on primary database.
499         */
500        $revId = $title->getLatestRevID();
501        $revRecord = MediaWikiServices::getInstance()
502            ->getRevisionLookup()
503            ->getRevisionById( $revId );
504        $content = $revRecord ? $revRecord->getContent( SlotRecord::MAIN ) : null;
505        if ( !$content instanceof BoardContent ) {
506            throw new InvalidDataException(
507                'Could not find board page for ' . $title->getPrefixedDBkey() . ' (id: ' . $title->getArticleID() . ').' .
508                'Found content: ' . var_export( $content, true )
509            );
510        }
511        $workflowId = $content->getWorkflowId();
512
513        $collection = HeaderCollection::newFromId( $workflowId );
514        $revision = $collection->getLastRevision();
515
516        /*
517         * We could just do $revision->getContent( $format ), but that
518         * may need to find $title in order to convert.
519         * We already know $title (and don't want to risk it being used
520         * in a way it stores lagging replica data), so let's just
521         * manually convert the content.
522         */
523        $content = $revision->getContentRaw();
524        $content = Utils::convert( $revision->getContentFormat(), $format, $content, $title );
525
526        $newDescription = $newDescriptionCallback( $content );
527
528        $action = 'edit-header';
529        $params = [
530            'header' => [
531                'content' => $newDescription,
532                'format' => $format,
533                'prev_revision' => $revision->getRevisionId()->getAlphadecimal()
534            ],
535        ];
536
537        /** @var WorkflowLoaderFactory $factory */
538        $factory = Container::get( 'factory.loader.workflow' );
539
540        /** @var WorkflowLoader $loader */
541        $loader = $factory->createWorkflowLoader( $title, $workflowId );
542
543        $blocks = $loader->getBlocks();
544        $this->logBlockErrors( $blocks );
545
546        $blocksToCommit = $loader->handleSubmit(
547            $this->context,
548            $action,
549            $params
550        );
551
552        foreach ( $blocks as $block ) {
553            if ( $block->hasErrors() ) {
554                $errors = $block->getErrors();
555
556                foreach ( $errors as $errorKey ) {
557                    $this->fatal( $block->getErrorMessage( $errorKey ) );
558                }
559            }
560        }
561
562        $loader->commit( $blocksToCommit );
563    }
564
565    /**
566     * @param Title $current
567     * @return string
568     */
569    private function getFormattedArchiveTemplate( Title $current ) {
570        $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();
571        $now = new DateTime( "now", new DateTimeZone( "GMT" ) );
572        return $this->formatTemplate( $templateName, [
573            'from' => $current->getPrefixedText(),
574            'date' => $now->format( 'Y-m-d' ),
575        ] );
576    }
577
578    /**
579     * @param Title $title
580     * @throws ImportException
581     */
582    private function removeArchiveTemplateFromWikitextTalkpage( Title $title ) {
583        $wtContent = $this->getContent( $title );
584        if ( !$wtContent ) {
585            return;
586        }
587
588        $content = Utils::convert( 'wikitext', 'html', $wtContent, $title );
589        $templateName = wfMessage( 'flow-importer-wt-converted-archive-template' )->inContentLanguage()->plain();
590
591        $newContent = TemplateHelper::removeFromHtml( $content, $templateName );
592
593        $this->createRevision(
594            $title,
595            Utils::convert( 'html', 'wikitext', $newContent, $title ),
596            wfMessage( 'flow-beta-feature-remove-archive-template-edit-summary' )->inContentLanguage()->plain() );
597    }
598
599    /**
600     * @param string $wikitextContent
601     * @param Title $title
602     * @return string
603     */
604    private function removeCurrentTemplateFromWikitext( $wikitextContent, Title $title ) {
605        $templateName = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain();
606        $contentAsHtml = Utils::convert( 'wikitext', 'html', $wikitextContent, $title );
607        $contentWithoutTemplate = TemplateHelper::removeFromHtml( $contentAsHtml, $templateName );
608        return Utils::convert( 'html', 'wikitext', $contentWithoutTemplate, $title );
609    }
610
611    /**
612     * @param Title $title
613     * @return string
614     */
615    private function extractTemplatesAboveFirstSection( Title $title ) {
616        $content = $this->getContent( $title );
617        if ( !$content ) {
618            return '';
619        }
620
621        $parser = MediaWikiServices::getInstance()->getParserFactory()->create();
622        $output = $parser->parse( $content, $title, new ParserOptions( $this->user ) );
623        $sections = $output->getSections();
624        if ( $sections ) {
625            # T319141: `byteoffset` is actually a *codepoint* offset.
626            $content = mb_substr( $content, 0, $sections[0]['byteoffset'] );
627        }
628        return TemplateHelper::extractTemplates( $content, $title );
629    }
630
631    /**
632     * @param Title $title
633     * @param string $reason
634     * @param callable $newDescriptionCallback
635     * @param string $format
636     * @throws ImportException
637     * @throws InvalidDataException
638     */
639    private function editWikitextContent( Title $title, $reason, callable $newDescriptionCallback, $format = 'html' ) {
640        $content = Utils::convert( 'wikitext', $format, $this->getContent( $title ), $title );
641        $newContent = $newDescriptionCallback( $content );
642        $this->createRevision(
643            $title,
644            Utils::convert( $format, 'wikitext', $newContent, $title ),
645            $reason
646        );
647    }
648
649    /**
650     * Add the "current" template to the page considered the current talkpage
651     * and link to the archived talkpage.
652     *
653     * @param Title $currentTalkpageTitle
654     * @param Title $archivedTalkpageTitle
655     */
656    private function addCurrentTemplate( Title $currentTalkpageTitle, Title $archivedTalkpageTitle ) {
657        $template = $this->getFormattedCurrentTemplate( $archivedTalkpageTitle );
658        $this->editWikitextContent(
659            $currentTalkpageTitle,
660            wfMessage( 'flow-beta-feature-add-current-template-edit-summary' )->inContentLanguage()->plain(),
661            static function ( $content ) use ( $template ) {
662                return $template . "\n\n" . $content;
663            },
664            'wikitext'
665        );
666    }
667
668    /**
669     * @param Title $title
670     * @return Title
671     * @throws InvalidDataException
672     */
673    private function archiveFlowBoard( Title $title ) {
674        $flowArchiveTitle = $this->findNextFlowArchive( $title );
675        $archiveReason = wfMessage( 'flow-optin-archive-flow-board' )->inContentLanguage()->text();
676        $this->movePage( $title, $flowArchiveTitle, $archiveReason );
677
678        $template = $this->getFormattedArchiveTemplate( $title );
679        $template = Utils::convert( 'wikitext', 'html', $template, $title );
680
681        $this->editBoardDescription(
682            $flowArchiveTitle,
683            static function ( $content ) use ( $template ) {
684                $templateName = wfMessage( 'flow-importer-wt-converted-template' )->inContentLanguage()->plain();
685                $content = TemplateHelper::removeFromHtml( $content, $templateName );
686                return $template . "<br/><br/>" . $content;
687            },
688            'html' );
689
690        return $flowArchiveTitle;
691    }
692
693    /**
694     * @param array $blocks
695     */
696    private function logBlockErrors( array $blocks ) {
697        $errors = [];
698        /** @var AbstractBlock $block */
699        foreach ( $blocks as $block ) {
700            if ( $block->hasErrors() ) {
701                $blockErrors = $block->getErrors();
702                foreach ( $blockErrors as $blockErrorType ) {
703                    $errors[ $block->getName() ] = [
704                        'type' => $blockErrorType,
705                        'message' => $block->getErrorMessage( $blockErrorType ),
706                        'extra' => $block->getErrorExtra( $blockErrorType )
707                    ];
708                }
709            }
710        }
711        if ( $errors ) {
712            LoggerFactory::getInstance( 'Flow' )->error(
713                'Found {count} block errors for user {user_id}',
714                [
715                    'count' => count( $errors ),
716                    'user_id' => RequestContext::getMain()->getUser()->getId(),
717                    'errors' => FormatJson::encode( $errors )
718                ]
719            );
720        }
721    }
722}