Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 94 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
FixSuggestedEditChangeTags | |
0.00% |
0 / 88 |
|
0.00% |
0 / 5 |
240 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
initialize | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
30 | |||
execute | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
getStructuredEditTagsQuery | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
2 | |||
processRow | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Maintenance; |
4 | |
5 | use ChangeTags; |
6 | use GrowthExperiments\GrowthExperimentsServices; |
7 | use GrowthExperiments\NewcomerTasks\TaskType\ImageRecommendationTaskTypeHandler; |
8 | use GrowthExperiments\NewcomerTasks\TaskType\LinkRecommendationTaskTypeHandler; |
9 | use GrowthExperiments\NewcomerTasks\TaskType\StructuredTaskTypeHandler; |
10 | use GrowthExperiments\NewcomerTasks\TaskType\TaskType; |
11 | use GrowthExperiments\NewcomerTasks\TaskType\TaskTypeHandler; |
12 | use Maintenance; |
13 | use MediaWiki\MediaWikiServices; |
14 | use MediaWiki\Revision\RevisionStore; |
15 | use MediaWiki\Status\Status; |
16 | use MediaWiki\User\User; |
17 | use StatusValue; |
18 | use stdClass; |
19 | use Wikimedia\Rdbms\IReadableDatabase; |
20 | use Wikimedia\Rdbms\SelectQueryBuilder; |
21 | |
22 | $IP = getenv( 'MW_INSTALL_PATH' ); |
23 | if ( $IP === false ) { |
24 | $IP = __DIR__ . '/../../..'; |
25 | } |
26 | require_once "$IP/maintenance/Maintenance.php"; |
27 | |
28 | class FixSuggestedEditChangeTags extends Maintenance { |
29 | |
30 | /** @var IReadableDatabase */ |
31 | private $dbr; |
32 | |
33 | /** @var RevisionStore */ |
34 | private $revisionStore; |
35 | |
36 | /** @var User User to perform the actions with. */ |
37 | private $user; |
38 | |
39 | /** @var TaskType The task type to work on. */ |
40 | private $taskType; |
41 | |
42 | /** @var TaskTypeHandler The handler of task type to work on. */ |
43 | private $taskTypeHandler; |
44 | |
45 | /** @var string The log_action value associated with this task type. */ |
46 | private $logAction; |
47 | |
48 | /** @var string The change tag name associated with this task type. */ |
49 | private $changeTagName; |
50 | |
51 | /** @var int The change tag ID associated with this task type. */ |
52 | private $changeTagId; |
53 | |
54 | public function __construct() { |
55 | parent::__construct(); |
56 | $this->requireExtension( 'GrowthExperiments' ); |
57 | $this->addDescription( 'Remove change tags which were added in error (T296818)' ); |
58 | $this->addOption( 'tasktype', 'Task type', true, true ); |
59 | $this->addOption( 'from', 'Revision ID to continue from', false, true ); |
60 | $this->addOption( 'fix', 'Make changes (default is dry run)' ); |
61 | $this->addOption( 'verbose', 'Verbose mode (list fixed titles)', false, false, 'v' ); |
62 | $this->setBatchSize( 100 ); |
63 | } |
64 | |
65 | private function initialize() { |
66 | $services = MediaWikiServices::getInstance(); |
67 | $growthServices = GrowthExperimentsServices::wrap( $services ); |
68 | |
69 | $this->dbr = $this->getDB( DB_REPLICA ); |
70 | $this->revisionStore = $services->getRevisionStore(); |
71 | $this->user = User::newSystemUser( User::MAINTENANCE_SCRIPT_USER, [ 'steal' => true ] ); |
72 | |
73 | $taskTypeId = $this->getOption( 'tasktype' ); |
74 | $configurationLoader = $growthServices->getNewcomerTasksConfigurationLoader(); |
75 | $taskTypes = $configurationLoader->getTaskTypes() ?: $configurationLoader->loadTaskTypes(); |
76 | if ( $taskTypes instanceof StatusValue ) { |
77 | $this->fatalError( Status::wrap( $taskTypes )->getWikiText( false, false, 'en' ) ); |
78 | } elseif ( !array_key_exists( $taskTypeId, $taskTypes ) ) { |
79 | $this->fatalError( "Invalid task type $taskTypeId" ); |
80 | } |
81 | $this->taskType = $taskTypes[$taskTypeId]; |
82 | $this->taskTypeHandler = $growthServices->getTaskTypeHandlerRegistry() |
83 | ->getByTaskType( $this->taskType ); |
84 | if ( !( $this->taskTypeHandler instanceof StructuredTaskTypeHandler ) ) { |
85 | $this->fatalError( "$taskTypeId is not a structured task type" ); |
86 | } |
87 | |
88 | $this->logAction = [ |
89 | 'link-recommendation' => 'addlink', |
90 | 'image-recommendation' => 'addimage', |
91 | ][$taskTypeId]; |
92 | $this->changeTagName = [ |
93 | 'link-recommendation' => LinkRecommendationTaskTypeHandler::CHANGE_TAG, |
94 | 'image-recommendation' => ImageRecommendationTaskTypeHandler::CHANGE_TAG, |
95 | ][$taskTypeId]; |
96 | $changeTagDefStore = $services->getChangeTagDefStore(); |
97 | $this->changeTagId = $changeTagDefStore->getId( $this->changeTagName ); |
98 | } |
99 | |
100 | /** @inheritDoc */ |
101 | public function execute() { |
102 | $this->initialize(); |
103 | $fromRevision = (int)$this->getOption( 'from', 0 ); |
104 | |
105 | $fixedRevisions = 0; |
106 | do { |
107 | $this->output( "Fetching batch from revision $fromRevision\n" ); |
108 | $queryBuilder = $this->getStructuredEditTagsQuery( $this->dbr, $this->getBatchSize(), |
109 | $fromRevision ); |
110 | $res = $queryBuilder->fetchResultSet(); |
111 | foreach ( $res as $row ) { |
112 | $this->processRow( $row, $fixedRevisions ); |
113 | $fromRevision = $row->rev_id + 1; |
114 | } |
115 | $this->waitForReplication(); |
116 | } while ( $res->numRows() ); |
117 | $this->output( |
118 | ( $this->hasOption( 'fix' ) ? 'Fixed' : 'Would have fixed' ) |
119 | . " $fixedRevisions revisions\n" |
120 | ); |
121 | } |
122 | |
123 | /** |
124 | * Get a query for (one page of) all revisions with structured edit change tags. |
125 | * @param IReadableDatabase $dbr |
126 | * @param int $limit Number of rows the query should return. |
127 | * @param int $fromRevision Revision to start from (ascending). |
128 | * @return SelectQueryBuilder |
129 | */ |
130 | private function getStructuredEditTagsQuery( |
131 | IReadableDatabase $dbr, |
132 | int $limit, |
133 | int $fromRevision |
134 | ): SelectQueryBuilder { |
135 | // Get a basic revision query. |
136 | $queryInfo = $this->revisionStore->getQueryInfo(); |
137 | $queryBuilder = $dbr->newSelectQueryBuilder()->queryInfo( $queryInfo ); |
138 | |
139 | // Join with change tags and filter to the selected tag. |
140 | // ChangeTags::modifyDisplayQuery() would make a complicated query to concatenate all |
141 | // tags; we don't need that so not worth using it. |
142 | $queryBuilder->join( 'change_tag', null, [ |
143 | 'rev_id = ct_rev_id', |
144 | 'ct_tag_id' => $this->changeTagId, |
145 | ] ); |
146 | |
147 | // Join with the relevant log events. |
148 | // We only associate the log event with the revision on edit, not rejection, so |
149 | // this will be a one-to-one relationship. |
150 | $queryBuilder->leftJoin( |
151 | $queryBuilder->newJoinGroup() |
152 | ->table( 'logging' ) |
153 | ->join( 'log_search', null, [ |
154 | 'ls_log_id = log_id', |
155 | 'ls_field' => 'associated_rev_id', |
156 | ] ), |
157 | null, |
158 | [ |
159 | 'ls_value = rev_id', |
160 | 'log_type' => 'growthexperiments', |
161 | 'log_action' => [ 'addlink', 'addimage' ], |
162 | ] |
163 | ); |
164 | $queryBuilder->field( 'log_action' ); |
165 | |
166 | // Handle paging. |
167 | // We have to make use of the rather unhelpful (ct_tag_id, ct_rc_id, ct_rev_id, ct_log_id) |
168 | // index, otherwise the query would have to go through all revisions. |
169 | // We can choose between paginating by revision ID, which is a filesort, or paginating |
170 | // by RC id, which makes the --from flag less useful. For now we go with the first |
171 | // as the number of tagged revisions is expected to be small. |
172 | $queryBuilder->conds( 'ct_rev_id >= ' . $fromRevision ); |
173 | $queryBuilder->orderBy( 'ct_tag_id ASC, ct_rev_id ASC' ); |
174 | $queryBuilder->limit( $limit ); |
175 | |
176 | return $queryBuilder; |
177 | } |
178 | |
179 | private function processRow( stdClass $row, int &$fixedRevisions ) { |
180 | if ( $row->log_action != $this->logAction ) { |
181 | // Revision with a structured edit change tag but no associated log entry. |
182 | // Log entries are way more reliable (and we couldn't retroactively change log entries |
183 | // anyway), remove the change tag. |
184 | if ( $this->hasOption( 'fix' ) ) { |
185 | ChangeTags::updateTags( null, $this->changeTagName, $rc_id, $row->rev_id, $log_id, |
186 | null, null, $this->user ); |
187 | } |
188 | if ( $this->hasOption( 'verbose' ) ) { |
189 | $diffLink = wfExpandUrl( wfAppendQuery( wfScript(), [ |
190 | 'oldid' => $row->rev_id, |
191 | 'diff' => 'prev', |
192 | ] ), PROTO_CANONICAL ); |
193 | $verb = $this->hasOption( 'fix' ) ? 'Removing' : 'Would remove'; |
194 | $this->output( "$verb tag for $diffLink\n" ); |
195 | } |
196 | $fixedRevisions++; |
197 | } |
198 | } |
199 | |
200 | } |
201 | |
202 | $maintClass = FixSuggestedEditChangeTags::class; |
203 | require_once RUN_MAINTENANCE_IF_MAIN; |