Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 171
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
FlowRestoreLQT
0.00% covered (danger)
0.00%
0 / 165
0.00% covered (danger)
0.00%
0 / 7
420
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 restoreLQTBoards
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
12
 restoreLQTThreads
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
12
 restoreLQTPage
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
42
 movePage
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 restorePageRevision
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace Flow\Maintenance;
4
5use Flow\Container;
6use Flow\DbFactory;
7use Flow\Hooks;
8use Flow\Import\ArchiveNameHelper;
9use Maintenance;
10use MediaWiki\Revision\RevisionRecord;
11use MediaWiki\Revision\SlotRecord;
12use MediaWiki\Status\Status;
13use MediaWiki\Title\Title;
14use MediaWiki\User\ActorMigration;
15use MediaWiki\User\User;
16
17$IP = getenv( 'MW_INSTALL_PATH' );
18if ( $IP === false ) {
19    $IP = __DIR__ . '/../../..';
20}
21
22require_once "$IP/maintenance/Maintenance.php";
23
24class FlowRestoreLQT extends Maintenance {
25    /**
26     * @var User
27     */
28    protected $talkpageManagerUser;
29
30    /**
31     * @var DbFactory
32     */
33    protected $dbFactory;
34
35    /**
36     * @var bool
37     */
38    protected $dryRun = false;
39
40    /**
41     * @var bool
42     */
43    protected $overwrite = false;
44
45    public function __construct() {
46        parent::__construct();
47
48        $this->addDescription( 'Restores LQT boards after a Flow conversion (revert LQT conversion ' .
49            'edits & move LQT boards back)' );
50
51        $this->addOption( 'dryrun', 'Simulate script run, without making actual changes' );
52        $this->addOption( 'overwrite-flow', 'Removes the Flow board entirely, restoring LQT to ' .
53            'its original location' );
54
55        $this->setBatchSize( 1 );
56
57        $this->requireExtension( 'Flow' );
58    }
59
60    public function execute() {
61        $this->talkpageManagerUser = Hooks::getOccupationController()->getTalkpageManager();
62        $this->dbFactory = Container::get( 'db.factory' );
63        $this->dryRun = $this->getOption( 'dryrun', false );
64        $this->overwrite = $this->getOption( 'overwrite-flow', false );
65
66        $this->output( "Restoring posts...\n" );
67        $this->restoreLQTThreads();
68
69        $this->output( "Restoring boards...\n" );
70        $this->restoreLQTBoards();
71    }
72
73    /**
74     * During an import, LQT boards are moved out of the way (archived) to make
75     * place for the Flow board.
76     * And after completing an import, LQT boards are disabled with
77     * {{#useliquidthreads:0}}
78     * That's all perfectly fine assuming the conversion goes well, but we'll
79     * want to go back to the original content with this script...
80     */
81    protected function restoreLQTBoards() {
82        $dbr = $this->dbFactory->getWikiDB( DB_REPLICA );
83        $batchSize = $this->getBatchSize();
84
85        $revWhere = ActorMigration::newMigration()
86            ->getWhere( $dbr, 'rev_user', $this->talkpageManagerUser );
87
88        $lbFactory = $this->getServiceContainer()->getDBLoadBalancerFactory();
89
90        foreach ( $revWhere['orconds'] as $revCond ) {
91            $startId = 0;
92            do {
93                // fetch all LQT boards that have been moved out of the way,
94                // with their original title & their current title
95                $rows = $dbr->select(
96                    [ 'logging', 'page', 'revision' ] + $revWhere['tables'],
97                    // log_namespace & log_title will be the original location
98                    // page_namespace & page_title will be the current location
99                    // rev_id is the first Flow talk page manager edit id
100                    // log_id is the log entry for when importer moved LQT page
101                    [ 'log_namespace', 'log_title', 'page_id', 'page_namespace', 'page_title',
102                        'rev_id' => 'MIN(rev_id)', 'log_id' ],
103                    [
104                        'log_actor' => $this->talkpageManagerUser->getActorId(),
105                        'log_type' => 'move',
106                        'page_content_model' => 'wikitext',
107                        'page_id > ' . $dbr->addQuotes( $startId ),
108                        $revCond,
109                    ],
110                    __METHOD__,
111                    [
112                        'GROUP BY' => 'rev_page',
113                        'LIMIT' => $batchSize,
114                        'ORDER BY' => 'log_id ASC',
115                    ],
116                    [
117                        'page' => [
118                            'INNER JOIN',
119                            [ 'page_id = log_page' ],
120                        ],
121                        'revision' => [
122                            'INNER JOIN',
123                            [ 'rev_page = log_page' ],
124                        ],
125                    ] + $revWhere['joins']
126                );
127
128                foreach ( $rows as $row ) {
129                    $from = Title::newFromText( $row->page_title, $row->page_namespace );
130                    $to = Title::newFromText( $row->log_title, $row->log_namespace );
131
132                    // undo {{#useliquidthreads:0}}
133                    $this->restorePageRevision( $row->page_id, $row->rev_id );
134                    // undo page move to archive location
135                    $this->restoreLQTPage( $from, $to, $row->log_id );
136
137                    $startId = $row->page_id;
138                }
139
140                $lbFactory->waitForReplication();
141            } while ( $rows->numRows() >= $batchSize );
142        }
143    }
144
145    /**
146     * After converting an LQT thread to Flow, it's content is altered to
147     * redirect to the new Flow topic.
148     * This finds all last original revisions & restores them.
149     */
150    protected function restoreLQTThreads() {
151        $dbr = $this->dbFactory->getWikiDB( DB_REPLICA );
152        $batchSize = $this->getBatchSize();
153
154        $revWhere = ActorMigration::newMigration()
155            ->getWhere( $dbr, 'rev_user', $this->talkpageManagerUser );
156
157        $lbFactory = $this->getServiceContainer()->getDBLoadBalancerFactory();
158
159        foreach ( $revWhere['orconds'] as $revCond ) {
160            $startId = 0;
161            do {
162                // for every LQT post, find the first edit by Flow talk page manager
163                // (to redirect to the new Flow copy)
164                $rows = $dbr->select(
165                    [ 'page', 'revision' ] + $revWhere['tables'],
166                    [ 'rev_page', 'rev_id' => ' MIN(rev_id)' ],
167                    [
168                        'page_namespace' => [ NS_LQT_THREAD, NS_LQT_SUMMARY ],
169                        $revCond,
170                        'page_id > ' . $dbr->addQuotes( $startId ),
171                    ],
172                    __METHOD__,
173                    [
174                        'GROUP BY' => 'page_id',
175                        'LIMIT' => $batchSize,
176                        'ORDER BY' => 'page_id ASC',
177                    ],
178                    [
179                        'revision' => [
180                            'INNER JOIN',
181                            [ 'rev_page = page_id' ],
182                        ],
183                    ] + $revWhere['joins']
184                );
185
186                foreach ( $rows as $row ) {
187                    // undo #REDIRECT edit
188                    $this->restorePageRevision( $row->rev_page, $row->rev_id );
189                    $startId = $row->rev_page;
190                }
191
192                $lbFactory->waitForReplication();
193            } while ( $rows->numRows() >= $batchSize );
194        }
195    }
196
197    /**
198     * @param Title $lqt Title of the LQT board
199     * @param Title $flow Title of the Flow board
200     * @param int $logId Log id for when LQT board was moved by import
201     * @return Status
202     */
203    protected function restoreLQTPage( Title $lqt, Title $flow, $logId ) {
204        if ( $lqt->equals( $flow ) ) {
205            // is at correct location already (probably a rerun of this script)
206            return Status::newGood();
207        }
208
209        $archiveNameHelper = new ArchiveNameHelper();
210
211        if ( !$flow->exists() ) {
212            $this->movePage( $lqt, $flow, '/* Restore LQT board to original location */' );
213        } else {
214            /*
215             * The importer will query the log table to find the LQT archive
216             * location. It will assume that Flow talk page manager moved the
217             * LQT board to its archive location, and will not recognize the
218             * board if it's been moved by someone else.
219             * Because of that feature (yes, that is intended), we need to make
220             * sure that - in order to enable LQT imports to be picked up again
221             * after this - the move from <original page> to <archive page>
222             * happens in 1 go, by Flow talk page manager.
223             */
224            if ( !$this->overwrite ) {
225                /*
226                 * Before we go moving pages around like crazy, let's see if we
227                 * actually need to. While it's certainly possible that the LQT
228                 * pages have been moved since the import and we need to fix
229                 * them, it's very likely that they haven't. In that case, we
230                 * won't have to do the complex moves.
231                 */
232                $dbr = $this->dbFactory->getWikiDB( DB_REPLICA );
233                $count = $dbr->selectRowCount(
234                    [ 'logging' ],
235                    '*',
236                    [
237                        'log_page' => $lqt->getArticleID(),
238                        'log_type' => 'move',
239                        'log_id > ' . $dbr->addQuotes( $logId ),
240                    ],
241                    __METHOD__
242                );
243
244                if ( $count > 0 ) {
245                    $this->output( "Ensuring LQT board '{$lqt->getPrefixedDBkey()}' is " .
246                        "recognized as archive of Flow board '{$flow->getPrefixedDBkey()}'.\n" );
247
248                    // 1: move Flow board out of the way so we can restore LQT to
249                    // its original location
250                    $archive = $archiveNameHelper->decideArchiveTitle( $flow, [ '%s/Flow Archive %d' ] );
251                    $this->movePage( $flow, $archive, '/* Make place to restore LQT board */' );
252
253                    // 2: move LQT board to the original location
254                    $this->movePage( $lqt, $flow, '/* Restore LQT board to original location */' );
255
256                    // 3: move LQT board back to archive location
257                    $this->movePage( $flow, $lqt, '/* Restore LQT board to archive location */' );
258
259                    // 4: move Flow board back to the original location
260                    $this->movePage( $archive, $flow, '/* Restore Flow board to correct location */' );
261                }
262            } else {
263                $this->output( "Deleting '{$flow->getPrefixedDBkey()}' & moving " .
264                    "'{$lqt->getPrefixedDBkey()}' there.\n" );
265
266                if ( !$this->dryRun ) {
267                    $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $flow );
268                    $page->doDeleteArticleReal(
269                        '/* Make place to restore LQT board */',
270                        $this->talkpageManagerUser,
271                        false,
272                        null,
273                        $error,
274                        null,
275                        [],
276                        'delete',
277                        true
278                    );
279                }
280
281                $this->movePage( $lqt, $flow, '/* Restore LQT board to original location */' );
282            }
283        }
284    }
285
286    /**
287     * @param Title $from
288     * @param Title $to
289     * @param string $reason
290     * @return Status
291     */
292    protected function movePage( Title $from, Title $to, $reason ) {
293        $this->output( "    Moving '{$from->getPrefixedDBkey()}' to '{$to->getPrefixedDBkey()}'.\n" );
294
295        $movePage = $this->getServiceContainer()
296            ->getMovePageFactory()
297            ->newMovePage( $from, $to );
298        $status = $movePage->isValidMove();
299        if ( !$status->isGood() ) {
300            return $status;
301        }
302
303        if ( $this->dryRun ) {
304            return Status::newGood();
305        }
306
307        return $movePage->move( $this->talkpageManagerUser, $reason, false );
308    }
309
310    /**
311     * @param int $pageId
312     * @param int $nextRevisionId Revision of the first *bad* revision
313     * @return Status
314     */
315    protected function restorePageRevision( $pageId, $nextRevisionId ) {
316        global $wgLang;
317
318        $page = $this->getServiceContainer()->getWikiPageFactory()->newFromID( $pageId );
319        $revisionLookup = $this->getServiceContainer()->getRevisionLookup();
320        $nextRevision = $revisionLookup->getRevisionById( $nextRevisionId );
321        $revision = $revisionLookup->getPreviousRevision( $nextRevision );
322        $mainContent = $revision->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
323        '@phan-var \Content $mainContent';
324
325        if ( $page->getContent()->equals( $mainContent ) ) {
326            // has correct content already (probably a rerun of this script)
327            return Status::newGood();
328        }
329
330        $content = $mainContent->serialize();
331        $content = $wgLang->truncateForVisual( $content, 150 );
332        $content = str_replace( "\n", '\n', $content );
333        $this->output( "Restoring revision {$revision->getId()} for LQT page {$pageId}{$content}\n" );
334
335        if ( $this->dryRun ) {
336            return Status::newGood();
337        } else {
338            return $page->doUserEditContent(
339                $mainContent,
340                $this->talkpageManagerUser,
341                '/* Restore LQT topic content */',
342                EDIT_UPDATE | EDIT_MINOR | EDIT_FORCE_BOT,
343                $revision->getId()
344            );
345        }
346    }
347}
348
349$maintClass = FlowRestoreLQT::class;
350require_once RUN_MAINTENANCE_IF_MAIN;