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