Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 96 |
|
0.00% |
0 / 2 |
CRAP | |
0.00% |
0 / 1 |
| FlowFixInconsistentBoards | |
0.00% |
0 / 90 |
|
0.00% |
0 / 2 |
342 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
0.00% |
0 / 81 |
|
0.00% |
0 / 1 |
306 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace Flow\Maintenance; |
| 4 | |
| 5 | use BatchRowIterator; |
| 6 | use Flow\BoardMover; |
| 7 | use Flow\Container; |
| 8 | use Flow\Content\BoardContent; |
| 9 | use Flow\Data\ManagerGroup; |
| 10 | use Flow\DbFactory; |
| 11 | use Flow\Exception\UnknownWorkflowIdException; |
| 12 | use Flow\WorkflowLoaderFactory; |
| 13 | use MediaWiki\Maintenance\Maintenance; |
| 14 | use MediaWiki\Revision\RevisionRecord; |
| 15 | use MediaWiki\Revision\SlotRecord; |
| 16 | use MediaWiki\Title\Title; |
| 17 | use MediaWiki\WikiMap\WikiMap; |
| 18 | |
| 19 | $IP = getenv( 'MW_INSTALL_PATH' ); |
| 20 | if ( $IP === false ) { |
| 21 | $IP = __DIR__ . '/../../..'; |
| 22 | } |
| 23 | |
| 24 | require_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 | */ |
| 34 | class 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 ) { |
| 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; |
| 207 | require_once RUN_MAINTENANCE_IF_MAIN; |