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