Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 139
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
FlowMoveBoardsToSubpages
0.00% covered (danger)
0.00%
0 / 133
0.00% covered (danger)
0.00%
0 / 4
1122
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 95
0.00% covered (danger)
0.00%
0 / 1
702
 findValidSubpage
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 createStubPage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Flow\Maintenance;
4
5use BatchRowIterator;
6use Flow\Container;
7use Flow\DbFactory;
8use MediaWiki\Content\WikitextContent;
9use MediaWiki\Maintenance\Maintenance;
10use MediaWiki\MediaWikiServices;
11use MediaWiki\Status\Status;
12use MediaWiki\Title\Title;
13use MediaWiki\User\User;
14use Wikimedia\Rdbms\IDBAccessObject;
15
16$IP = getenv( 'MW_INSTALL_PATH' );
17if ( $IP === false ) {
18    $IP = __DIR__ . '/../../..';
19}
20
21require_once "$IP/maintenance/Maintenance.php";
22
23/**
24 * Moves pages that contain Flow boards to a subpage of their current location
25 *
26 * There is a dry run available.
27 *
28 * @ingroup Maintenance
29 */
30class FlowMoveBoardsToSubpages extends Maintenance {
31    protected DbFactory $dbFactory;
32
33    public function __construct() {
34        parent::__construct();
35
36        $this->addDescription( 'Moves pages that contain Flow boards to a subpage of their current location. ' .
37            'Must be run separately for each affected wiki.' );
38
39        $this->addOption( 'dry-run', 'Only prints the board names, without changing anything.' );
40        $this->addOption( 'namespaceName', 'Name of namespace to check, otherwise all', false, true );
41        $this->addOption( 'limit', 'Limit of inconsistent pages to identify (and fix if not a dry ' .
42            'run). Defaults to no limit', false, true );
43        $this->addOption( 'subpage', 'Name of subpage to create. Defaults to "Flow"', false, true );
44        $this->addOption( 'always-number', 'Always number an archive page, even when it\'s the first one' );
45        $this->addOption( 'title', 'Title of a specific page to move', false, true );
46
47        $this->setBatchSize( 300 );
48
49        $this->requireExtension( 'Flow' );
50    }
51
52    /**
53     * @return false|void
54     */
55    public function execute() {
56        global $wgLang;
57
58        $this->dbFactory = Container::get( 'db.factory' );
59
60        $occupationController = MediaWikiServices::getInstance()->getService( 'FlowTalkpageManager' );
61        $movePageFactory = MediaWikiServices::getInstance()->getMovePageFactory();
62        $moveUser = $occupationController->getTalkpageManager();
63
64        $dryRun = $this->hasOption( 'dry-run' );
65
66        $limit = $this->getOption( 'limit' );
67
68        $subpage = $this->getOption( 'subpage', 'Flow' );
69
70        $wikiDbw = $this->dbFactory->getWikiDB( DB_PRIMARY );
71
72        $iterator = new BatchRowIterator( $wikiDbw, 'page', 'page_id', $this->getBatchSize() );
73        $iterator->setFetchColumns( [ 'page_namespace', 'page_title', 'page_latest' ] );
74        $iterator->addConditions( [
75            'page_content_model' => CONTENT_MODEL_FLOW_BOARD,
76        ] );
77        $iterator->setCaller( __METHOD__ );
78
79        $useSingleTitle = $this->hasOption( 'title' );
80        $titleText = null;
81
82        if ( $useSingleTitle ) {
83            $titleText = $this->getOption( 'title' );
84            $title = Title::newFromText( $titleText );
85            if ( !$title ) {
86                $this->error( "Invalid title: " . $titleText . "\n" );
87                return false;
88            }
89            $iterator->addConditions( [
90                'page_title' => $title->getDBkey(),
91                'page_namespace' => $title->getNamespace(),
92            ] );
93        } else {
94            if ( $this->hasOption( 'namespaceName' ) ) {
95                $namespaceName = $this->getOption( 'namespaceName' );
96                $namespaceId = $wgLang->getNsIndex( $namespaceName );
97
98                if ( !$namespaceId ) {
99                    $this->error( "'$namespaceName' is not a valid namespace name" );
100                    return false;
101                }
102
103                if ( $namespaceId == NS_TOPIC ) {
104                    $this->error( 'This script can not be run on the Flow topic namespace' );
105                    return false;
106                }
107
108                $iterator->addConditions( [
109                    'page_namespace' => $namespaceId,
110                ] );
111            } else {
112                $iterator->addConditions( [
113                    $wikiDbw->expr( 'page_namespace', '!=', NS_TOPIC ),
114                ] );
115            }
116        }
117
118        $checkedCount = 0;
119        $moveCount = 0;
120
121        foreach ( $iterator as $rows ) {
122            foreach ( $rows as $row ) {
123                $checkedCount++;
124                $coreTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
125
126                if ( !$useSingleTitle ) {
127                    if ( preg_match( "/\/([Ff]low|[Aa]rchive|StructuredDiscussions).*/", $coreTitle->getText() ) ) {
128                        // Don't try to act on subpages
129                        // $coreTitle->isSubpage() only works on namespaces with subpages enabled, which
130                        // we don't care about for this check.
131                        $this->output( "Skipped '$coreTitle' as it is already an archived page\n" );
132                        continue;
133                    }
134                    // $row / $coreTitle is a page with the flow board content model, and isn't an archived page
135                    if ( !$coreTitle->inNamespace( NS_USER_TALK ) ) {
136                        // Skip pages whose non-talk namespace counterpart doesn't exist, assuming they're archives
137                        // but don't skip user talk pages, since having a redlinked user page isn't indicative of anything
138                        $subject = $coreTitle->getSubjectPage();
139                        if ( !$subject || !$subject->exists() ) {
140                            $this->output( "Skipped '$coreTitle' as it has no associated subject page\n" );
141                            continue;
142                        }
143
144                        if ( $coreTitle->equals( $subject ) ) {
145                            $this->output( "Skipped '$coreTitle' as it is a standalone Flow page\n" );
146                            continue;
147                        }
148                    }
149                }
150
151                $creationStatus = $this->findValidSubpage( $coreTitle, $subpage, $moveUser );
152
153                if ( !$creationStatus->isOk() ) {
154                    $this->error( "Cannot move '$coreTitle': " . $creationStatus->getMessage()->text() . "\n" );
155                    continue;
156                }
157
158                $subpageTitle = $creationStatus->getValue();
159
160                if ( $dryRun ) {
161                    $moveCount++;
162                    $this->output( "Would move '$coreTitle' to '$subpageTitle'\n" );
163                } else {
164                    $mp = $movePageFactory->newMovePage( $coreTitle, $subpageTitle );
165                    try {
166                        $status = $mp->move(
167                            /* user */ $moveUser,
168                            /* reason */ "Flow archival",
169                            /* create redirect */ false
170                        );
171                    } catch ( \Throwable $e ) {
172                        $this->error( "Exception while moving '$coreTitle' to '$subpageTitle': " . $e->getMessage() . "\n" );
173                        $this->error( $e->getTraceAsString() . "\n" );
174                        continue;
175                    }
176
177                    if ( $status->isGood() ) {
178                        $moveCount++;
179                        $this->output( "Moved '$coreTitle' to '$subpageTitle'\n" );
180                        $stubStatus = $this->createStubPage( $coreTitle, $subpageTitle, $moveUser );
181                        if ( $stubStatus->isGood() ) {
182                            $this->output( "Created stub at '$coreTitle'\n" );
183                        } else {
184                            $this->error( "Failed to create stub at '$coreTitle': " . $status->getMessage()->text() . "\n" );
185                        }
186                    } else {
187                        $this->error( "Failed to move '$coreTitle' to '$subpageTitle': " . $status->getMessage()->text() . "\n" );
188                    }
189                }
190
191                if ( $limit !== null && $moveCount >= $limit ) {
192                    break;
193                }
194            }
195
196            $action = $dryRun ? 'would have been moved' : 'were moved';
197            $this->output( "\nChecked a total of $checkedCount pages. Of those, " .
198                "$moveCount pages $action.\n" );
199
200            if ( $limit !== null && $moveCount >= $limit ) {
201                break;
202            }
203        }
204        if ( $useSingleTitle && $moveCount === 0 ) {
205            $this->output( "No Flow board found for '" . $titleText . "'\n" );
206        }
207    }
208
209    /**
210     * Checks for a valid subpage to move a board into
211     *
212     * @param Title $coreTitle Previous location of the page, before moving
213     * @param string $subpage Name stem for the subpages
214     * @param User $moveUser User to make the move
215     * @return Status Contains subpage as value
216     */
217    protected function findValidSubpage( Title $coreTitle, string $subpage, User $moveUser ) {
218        $occupationController = MediaWikiServices::getInstance()->getService( 'FlowTalkpageManager' );
219
220        $status = Status::newGood();
221
222        $alwaysNumber = $this->getOption( 'always-number' );
223
224        for ( $i = 1; $i <= 9; $i++ ) {
225            $suffix = ( $alwaysNumber || $i > 1 ) ? $i : '';
226            $subpageTitle = $coreTitle->getSubpage( $subpage . $suffix );
227
228            $creationStatus = $occupationController->safeAllowCreation(
229                $subpageTitle,
230                $moveUser,
231                /* $mustNotExist = */ true,
232                /* $forWrite = */ true
233            );
234
235            if ( $creationStatus->isGood() ) {
236                $status->setResult( true, $subpageTitle );
237                return $status;
238            }
239
240            $status->merge( $creationStatus );
241        }
242
243        return $status;
244    }
245
246    /**
247     * Creates a new revision of the archived page with strategy-specific changes.
248     *
249     * @param Title $title Previous location of the page, before moving
250     * @param Title $archiveTitle Current location of the page, after moving
251     * @param User $user
252     * @return Status
253     */
254    protected function createStubPage( Title $title, Title $archiveTitle, User $user ) {
255        $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title );
256        // doUserEditContent will do this anyway, but we need to now for the revision.
257        $page->loadPageData( IDBAccessObject::READ_LATEST );
258
259        $status = $page->doUserEditContent(
260            new WikitextContent( "* [[{$archiveTitle->getPrefixedText()}]]" ),
261            $user,
262            "Flow archival",
263            EDIT_FORCE_BOT | EDIT_SUPPRESS_RC
264        );
265
266        return $status;
267    }
268}
269
270$maintClass = FlowMoveBoardsToSubpages::class;
271require_once RUN_MAINTENANCE_IF_MAIN;