Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
FlowFixInconsistentBoards
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 2
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 1
306
1<?php
2
3namespace Flow\Maintenance;
4
5use Flow\BoardMover;
6use Flow\Container;
7use Flow\Content\BoardContent;
8use Flow\Data\ManagerGroup;
9use Flow\DbFactory;
10use Flow\Exception\UnknownWorkflowIdException;
11use Flow\WorkflowLoaderFactory;
12use MediaWiki\Maintenance\Maintenance;
13use MediaWiki\Revision\RevisionRecord;
14use MediaWiki\Revision\SlotRecord;
15use MediaWiki\Title\Title;
16use MediaWiki\Utils\BatchRowIterator;
17use MediaWiki\WikiMap\WikiMap;
18
19$IP = getenv( 'MW_INSTALL_PATH' );
20if ( $IP === false ) {
21    $IP = __DIR__ . '/../../..';
22}
23
24require_once "$IP/maintenance/Maintenance.php";
25
26/**
27 * Changes Flow boards and their topics to be associated with their current title, based on the JSON content
28 * Fixes inconsistent bugs like T138310.
29 *
30 * There is a dry run available.
31 *
32 * @ingroup Maintenance
33 */
34class FlowFixInconsistentBoards extends Maintenance {
35    /**
36     * @var DbFactory
37     */
38    protected $dbFactory;
39
40    /**
41     * @var WorkflowLoaderFactory
42     */
43    protected $workflowLoaderFactory;
44
45    /**
46     * @var BoardMover
47     */
48    protected $boardMover;
49
50    /**
51     * @var ManagerGroup
52     */
53    protected $storage;
54
55    public function __construct() {
56        parent::__construct();
57
58        $this->addDescription( 'Changes Flow boards and their topics to be associated with their ' .
59            'current title, based on the JSON content. Must be run separately for each affected wiki.' );
60
61        $this->addOption( 'dry-run', 'Only prints the board names, without changing anything.' );
62        $this->addOption( 'namespaceName', 'Name of namespace to check, otherwise all', false, true );
63        $this->addOption( 'limit', 'Limit of inconsistent pages to identify (and fix if not a dry ' .
64            'run). Defaults to no limit', false, true );
65
66        $this->setBatchSize( 300 );
67
68        $this->requireExtension( 'Flow' );
69    }
70
71    /**
72     * @return false|void
73     */
74    public function execute() {
75        $this->dbFactory = Container::get( 'db.factory' );
76        $this->workflowLoaderFactory = Container::get( 'factory.loader.workflow' );
77        $this->boardMover = Container::get( 'board_mover' );
78        $this->storage = Container::get( 'storage' );
79
80        $dryRun = $this->hasOption( 'dry-run' );
81
82        $limit = $this->getOption( 'limit' );
83
84        $wikiDbw = $this->dbFactory->getWikiDB( DB_PRIMARY );
85
86        $iterator = new BatchRowIterator( $wikiDbw, 'page', 'page_id', $this->getBatchSize() );
87        $iterator->setFetchColumns( [ 'page_namespace', 'page_title', 'page_latest' ] );
88        $iterator->addConditions( [
89            'page_content_model' => CONTENT_MODEL_FLOW_BOARD,
90        ] );
91        $iterator->setCaller( __METHOD__ );
92
93        if ( $this->hasOption( 'namespaceName' ) ) {
94            $namespaceName = $this->getOption( 'namespaceName' );
95            $lang = $this->getServiceContainer()->getContentLanguage();
96            $namespaceId = $lang->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        $checkedCount = 0;
118        $inconsistentCount = 0;
119
120        // Not all of $inconsistentCount are fixable by the current script.
121        $fixableInconsistentCount = 0;
122
123        foreach ( $iterator as $rows ) {
124            foreach ( $rows as $row ) {
125                $checkedCount++;
126                $coreTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
127                $revision = $this->getServiceContainer()->getRevisionLookup()->getRevisionById( $row->page_latest );
128                $content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
129                if ( !$content instanceof BoardContent ) {
130                    $actualClass = get_debug_type( $content );
131                    $this->error( "ERROR: '$coreTitle' content is a '$actualClass', but should be '"
132                        . BoardContent::class . "'." );
133                    continue;
134                }
135                $workflowId = $content->getWorkflowId();
136                if ( $workflowId === null ) {
137                    // See T153320. If the workflow exists, it could
138                    // be looked up by title/page ID and the JSON could
139                    // be fixed with an edit.
140                    // Otherwise, the core revision has to be deleted. This
141                    // script does not do either of these things.
142                    $this->error( "ERROR: '$coreTitle' JSON content does not have a valid workflow ID." );
143                    continue;
144                }
145
146                $workflowIdAlphadecimal = $workflowId->getAlphadecimal();
147
148                try {
149                    $workflow = $this->workflowLoaderFactory->loadWorkflowById( false, $workflowId );
150                } catch ( UnknownWorkflowIdException ) {
151                    // This is a different error (a core page refers to
152                    // a non-existent workflow), which this script can not fix.
153                    $this->error( "ERROR: '$coreTitle' refers to workflow ID " .
154                        "'$workflowIdAlphadecimal', which could not be found." );
155                    continue;
156                }
157
158                if ( !$workflow->matchesTitle( $coreTitle ) ) {
159                    $pageId = (int)$row->page_id;
160
161                    $workflowTitle = $workflow->getOwnerTitle();
162                    $this->output( "INCONSISTENT: Core title for '$workflowIdAlphadecimal' is " .
163                        "'$coreTitle', but Flow title is '$workflowTitle'\n" );
164
165                    $inconsistentCount++;
166
167                    // Sanity check, or this will fail in BoardMover
168                    $workflowByPageId = $this->storage->find( 'Workflow', [
169                        'workflow_wiki' => WikiMap::getCurrentWikiId(),
170                        'workflow_page_id' => $pageId,
171                    ] );
172
173                    if ( !$workflowByPageId ) {
174                        $this->error( "ERROR: '$coreTitle' has page ID '$pageId', but no workflow " .
175                            "is linked to this page ID" );
176                        continue;
177                    }
178
179                    if ( !$dryRun ) {
180                        $this->boardMover->move( $pageId, $coreTitle );
181                        $this->boardMover->commit();
182                        $this->output( "FIXED: Updated '$workflowIdAlphadecimal' to match core " .
183                            "title, '$coreTitle'\n" );
184                    }
185
186                    $fixableInconsistentCount++;
187
188                    if ( $limit !== null && $fixableInconsistentCount >= $limit ) {
189                        break;
190                    }
191                }
192            }
193
194            $action = $dryRun ? 'identified as fixable' : 'fixed';
195            $this->output( "\nChecked a total of $checkedCount Flow boards. Of those, " .
196                "$inconsistentCount boards had an inconsistent title; $fixableInconsistentCount " .
197                "were $action.\n" );
198            if ( $limit !== null && $fixableInconsistentCount >= $limit ) {
199                break;
200            }
201        }
202    }
203}
204
205$maintClass = FlowFixInconsistentBoards::class;
206require_once RUN_MAINTENANCE_IF_MAIN;