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 BatchRowIterator;
6use Flow\BoardMover;
7use Flow\Container;
8use Flow\Content\BoardContent;
9use Flow\Data\ManagerGroup;
10use Flow\DbFactory;
11use Flow\Exception\UnknownWorkflowIdException;
12use Flow\WorkflowLoaderFactory;
13use Maintenance;
14use MediaWiki\Revision\RevisionRecord;
15use MediaWiki\Revision\SlotRecord;
16use MediaWiki\Title\Title;
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        global $wgLang;
76
77        $this->dbFactory = Container::get( 'db.factory' );
78        $this->workflowLoaderFactory = Container::get( 'factory.loader.workflow' );
79        $this->boardMover = Container::get( 'board_mover' );
80        $this->storage = Container::get( 'storage' );
81
82        $dryRun = $this->hasOption( 'dry-run' );
83
84        $limit = $this->getOption( 'limit' );
85
86        $wikiDbw = $this->dbFactory->getWikiDB( DB_PRIMARY );
87
88        $iterator = new BatchRowIterator( $wikiDbw, 'page', 'page_id', $this->getBatchSize() );
89        $iterator->setFetchColumns( [ 'page_namespace', 'page_title', 'page_latest' ] );
90        $iterator->addConditions( [
91            'page_content_model' => CONTENT_MODEL_FLOW_BOARD,
92        ] );
93        $iterator->setCaller( __METHOD__ );
94
95        if ( $this->hasOption( 'namespaceName' ) ) {
96            $namespaceName = $this->getOption( 'namespaceName' );
97            $namespaceId = $wgLang->getNsIndex( $namespaceName );
98
99            if ( !$namespaceId ) {
100                $this->error( "'$namespaceName' is not a valid namespace name" );
101                return false;
102            }
103
104            if ( $namespaceId == NS_TOPIC ) {
105                $this->error( 'This script can not be run on the Flow topic namespace' );
106                return false;
107            }
108
109            $iterator->addConditions( [
110                'page_namespace' => $namespaceId,
111            ] );
112        } else {
113            $iterator->addConditions( [
114                $wikiDbw->expr( 'page_namespace', '!=', NS_TOPIC ),
115            ] );
116        }
117
118        $checkedCount = 0;
119        $inconsistentCount = 0;
120
121        // Not all of $inconsistentCount are fixable by the current script.
122        $fixableInconsistentCount = 0;
123
124        foreach ( $iterator as $rows ) {
125            foreach ( $rows as $row ) {
126                $checkedCount++;
127                $coreTitle = Title::makeTitle( $row->page_namespace, $row->page_title );
128                $revision = $this->getServiceContainer()->getRevisionLookup()->getRevisionById( $row->page_latest );
129                $content = $revision->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
130                if ( !$content instanceof BoardContent ) {
131                    $actualClass = get_debug_type( $content );
132                    $this->error( "ERROR: '$coreTitle' content is a '$actualClass', but should be '"
133                        . BoardContent::class . "'." );
134                    continue;
135                }
136                $workflowId = $content->getWorkflowId();
137                if ( $workflowId === null ) {
138                    // See T153320.  If the workflow exists, it could
139                    // be looked up by title/page ID and the JSON could
140                    // be fixed with an edit.
141                    // Otherwise, the core revision has to be deleted.  This
142                    // script does not do either of these things.
143                    $this->error( "ERROR: '$coreTitle' JSON content does not have a valid workflow ID." );
144                    continue;
145                }
146
147                $workflowIdAlphadecimal = $workflowId->getAlphadecimal();
148
149                try {
150                    $workflow = $this->workflowLoaderFactory->loadWorkflowById( false, $workflowId );
151                } catch ( UnknownWorkflowIdException $ex ) {
152                    // This is a different error (a core page refers to
153                    // a non-existent workflow), which this script can not fix.
154                    $this->error( "ERROR: '$coreTitle' refers to workflow ID " .
155                        "'$workflowIdAlphadecimal', which could not be found." );
156                    continue;
157                }
158
159                if ( !$workflow->matchesTitle( $coreTitle ) ) {
160                    $pageId = (int)$row->page_id;
161
162                    $workflowTitle = $workflow->getOwnerTitle();
163                    $this->output( "INCONSISTENT: Core title for '$workflowIdAlphadecimal' is " .
164                        "'$coreTitle', but Flow title is '$workflowTitle'\n" );
165
166                    $inconsistentCount++;
167
168                    // Sanity check, or this will fail in BoardMover
169                    $workflowByPageId = $this->storage->find( 'Workflow', [
170                        'workflow_wiki' => WikiMap::getCurrentWikiId(),
171                        'workflow_page_id' => $pageId,
172                    ] );
173
174                    if ( !$workflowByPageId ) {
175                        $this->error( "ERROR: '$coreTitle' has page ID '$pageId', but no workflow " .
176                            "is linked to this page ID" );
177                        continue;
178                    }
179
180                    if ( !$dryRun ) {
181                        $this->boardMover->move( $pageId, $coreTitle );
182                        $this->boardMover->commit();
183                        $this->output( "FIXED: Updated '$workflowIdAlphadecimal' to match core " .
184                            "title, '$coreTitle'\n" );
185                    }
186
187                    $fixableInconsistentCount++;
188
189                    if ( $limit !== null && $fixableInconsistentCount >= $limit ) {
190                        break;
191                    }
192                }
193            }
194
195            $action = $dryRun ? 'identified as fixable' : 'fixed';
196            $this->output( "\nChecked a total of $checkedCount Flow boards.  Of those, " .
197                "$inconsistentCount boards had an inconsistent title; $fixableInconsistentCount " .
198                "were $action.\n" );
199            if ( $limit !== null && $fixableInconsistentCount >= $limit ) {
200                break;
201            }
202        }
203    }
204}
205
206$maintClass = FlowFixInconsistentBoards::class;
207require_once RUN_MAINTENANCE_IF_MAIN;