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