Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 217 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
InitWikiConfig | |
0.00% |
0 / 211 |
|
0.00% |
0 / 14 |
2970 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
2 | |||
initServices | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getWikidataWikiId | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getEditSummary | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getWikidataData | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getRawTitleFromWikidata | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getGEConfigVariables | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
12 | |||
validateGEConfigVariables | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
getHelpPanelLinkId | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getHelpPanelLink | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
getSuggestedEditsVariables | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
110 | |||
execute | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
initGEConfig | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
56 | |||
initSuggestedEditsConfig | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
56 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\Maintenance; |
4 | |
5 | use GrowthExperiments\Config\WikiPageConfigWriterFactory; |
6 | use GrowthExperiments\GrowthExperimentsServices; |
7 | use GrowthExperiments\Specials\SpecialEditGrowthConfig; |
8 | use GrowthExperiments\Util; |
9 | use Maintenance; |
10 | use MediaWiki\Http\HttpRequestFactory; |
11 | use MediaWiki\MediaWikiServices; |
12 | use MediaWiki\Page\PageProps; |
13 | use MediaWiki\Title\Title; |
14 | use MediaWiki\Title\TitleFactory; |
15 | use MediaWiki\WikiMap\WikiMap; |
16 | |
17 | $IP = getenv( 'MW_INSTALL_PATH' ); |
18 | if ( $IP === false ) { |
19 | $IP = __DIR__ . '/../../..'; |
20 | } |
21 | require_once "$IP/maintenance/Maintenance.php"; |
22 | |
23 | class InitWikiConfig extends Maintenance { |
24 | /** @var TitleFactory */ |
25 | private $titleFactory; |
26 | |
27 | /** @var PageProps */ |
28 | private $pageProps; |
29 | |
30 | /** @var WikiPageConfigWriterFactory */ |
31 | private $wikiPageConfigWriterFactory; |
32 | |
33 | /** @var HttpRequestFactory */ |
34 | private $httpRequestFactory; |
35 | |
36 | /** @var SpecialEditGrowthConfig|null */ |
37 | private $specialEditGrowthConfig; |
38 | |
39 | public function __construct() { |
40 | parent::__construct(); |
41 | $this->requireExtension( 'GrowthExperiments' ); |
42 | $this->addDescription( 'Initialize wiki configuration of GrowthExperiments based on Wikidata' ); |
43 | |
44 | $this->addOption( 'dry-run', 'Print the configuration that would be saved on-wiki.' ); |
45 | $this->addOption( 'override', 'Override existing config files' ); |
46 | $this->addOption( 'skip-validation', 'Skip validation (you should check the resulting config)' ); |
47 | $this->addOption( |
48 | 'phab', |
49 | 'ID of a Phabricator task about configuration of the wiki (e.q. T274646).' . |
50 | 'Will be linked in an edit summary', |
51 | false, |
52 | true |
53 | ); |
54 | $this->addOption( |
55 | 'wikidata-wikiid', |
56 | 'Force wiki ID to be used by Wikidata (useful for localhost testing)', |
57 | false, |
58 | true |
59 | ); |
60 | } |
61 | |
62 | private function initServices() { |
63 | $services = MediaWikiServices::getInstance(); |
64 | |
65 | $this->wikiPageConfigWriterFactory = GrowthExperimentsServices::wrap( $services ) |
66 | ->getWikiPageConfigWriterFactory(); |
67 | $this->titleFactory = $services->getTitleFactory(); |
68 | $this->pageProps = $services->getPageProps(); |
69 | $this->httpRequestFactory = $services->getHttpRequestFactory(); |
70 | $this->specialEditGrowthConfig = $services->getSpecialPageFactory()->getPage( 'EditGrowthConfig' ); |
71 | } |
72 | |
73 | /** |
74 | * @return string |
75 | */ |
76 | private function getWikidataWikiId(): string { |
77 | return $this->hasOption( 'wikidata-wikiid' ) ? |
78 | $this->getOption( 'wikidata-wikiid' ) : |
79 | WikiMap::getCurrentWikiId(); |
80 | } |
81 | |
82 | private function getEditSummary() { |
83 | $summary = 'Configuration for [[mw:Growth/Personalized first day/Newcomer homepage]].'; |
84 | if ( $this->hasOption( 'phab' ) ) { |
85 | $summary .= ' See [[phab:' . $this->getOption( 'phab' ) . ']] for more information.'; |
86 | } |
87 | return $summary; |
88 | } |
89 | |
90 | /** |
91 | * Retreive entity data from Wikidata |
92 | * |
93 | * @param string $qid |
94 | * @return array|null |
95 | */ |
96 | private function getWikidataData( string $qid ): ?array { |
97 | $url = "https://www.wikidata.org/wiki/Special:EntityData/$qid.json"; |
98 | $status = Util::getJsonUrl( $this->httpRequestFactory, $url ); |
99 | if ( !$status->isOK() ) { |
100 | $this->fatalError( 'Failed to download ' . $url . "\n" ); |
101 | } |
102 | return $status->getValue(); |
103 | } |
104 | |
105 | /** |
106 | * @param string $primaryQid |
107 | * @param string[] $backupQids |
108 | * @return string|null String on success, null on failure |
109 | */ |
110 | private function getRawTitleFromWikidata( |
111 | string $primaryQid, |
112 | array $backupQids = [] |
113 | ): ?string { |
114 | $qids = array_merge( [ $primaryQid ], $backupQids ); |
115 | foreach ( $qids as $qid ) { |
116 | $data = $this->getWikidataData( $qid ); |
117 | if ( $data == null ) { |
118 | $this->fatalError( "Wikidata returned an invalid JSON\n" ); |
119 | } |
120 | $sitelinks = $data['entities'][$qid]['sitelinks']; |
121 | if ( array_key_exists( $this->getWikidataWikiId(), $sitelinks ) ) { |
122 | return $sitelinks[$this->getWikidataWikiId()]['title']; |
123 | } |
124 | } |
125 | |
126 | return null; |
127 | } |
128 | |
129 | /** |
130 | * @return array |
131 | */ |
132 | private function getGEConfigVariables(): array { |
133 | // Init list of variables |
134 | $variables = []; |
135 | |
136 | // Set help panel variables |
137 | $variables['GEHelpPanelHelpDeskTitle'] = $this->getRawTitleFromWikidata( 'Q4026300' ); |
138 | $variables['GEHelpPanelLinks'] = array_values( array_filter( [ |
139 | // Manual of style |
140 | $this->getHelpPanelLink( 'Q4994848' ), |
141 | // Help:Editing |
142 | $this->getHelpPanelLink( |
143 | 'Q151637', |
144 | [], |
145 | 'mw:Special:MyLanguage/Help:VisualEditor/User_guide' |
146 | ), |
147 | // Help:Introduction to images with VisualEditor |
148 | $this->getHelpPanelLink( |
149 | 'Q27919584', |
150 | [], |
151 | 'mw:Special:MyLanguage/Help:VisualEditor/User_guide#Images' ), |
152 | // Help:Introduction to referencing with VisualEditor |
153 | $this->getHelpPanelLink( |
154 | 'Q24238629', |
155 | [], |
156 | 'mw:Special:MyLanguage/Help:VisualEditor/User_guide#Editing_references' |
157 | ), |
158 | // Wikipedia:Article wizard |
159 | $this->getHelpPanelLink( 'Q10968373', [ 'Q4966605' ] ), |
160 | ] ) ); |
161 | |
162 | // Help:Contents |
163 | $variables['GEHelpPanelViewMoreTitle'] = $this->getRawTitleFromWikidata( 'Q914807' ); |
164 | |
165 | // Set suggested edits learn more links |
166 | $variables['GEHomepageSuggestedEditsIntroLinks'] = array_filter( [ |
167 | // Wikipedia:Article wizard (preferred) or Help:How to start a new page |
168 | 'create' => $this->getRawTitleFromWikidata( |
169 | 'Q10968373', |
170 | [ 'Q4966605' ] |
171 | ) ?? 'mw:Special:MyLanguage/Help:VisualEditor/User_guide', |
172 | // Help:Introduction to images with VisualEditor |
173 | 'image' => $this->getRawTitleFromWikidata( |
174 | 'Q27919584' |
175 | ) ?? 'mw:Special:MyLanguage/Help:VisualEditor/User_guide#Images', |
176 | ] ); |
177 | |
178 | // Remove null variables (array_filter will remove all variables which are not on Wikidata |
179 | // as getRawTitleFromWikidata would return null in that case) |
180 | $variables = array_filter( $variables ); |
181 | |
182 | // Validate variables if --skip-validation was not used |
183 | if ( !$this->hasOption( 'skip-validation' ) ) { |
184 | $validationRes = $this->validateGEConfigVariables( $variables ); |
185 | if ( is_string( $validationRes ) ) { |
186 | $this->fatalError( $validationRes . "\n" ); |
187 | } |
188 | } |
189 | |
190 | return $variables; |
191 | } |
192 | |
193 | /** |
194 | * @param array $variables |
195 | * @return true|string True on success, error message otherwise |
196 | */ |
197 | private function validateGEConfigVariables( array $variables ) { |
198 | if ( !array_key_exists( 'GEHomepageSuggestedEditsIntroLinks', $variables ) ) { |
199 | return 'GEHomepageSuggestedEditsIntroLinks was not provided, please edit config manually'; |
200 | } |
201 | |
202 | foreach ( [ 'create', 'image' ] as $type ) { |
203 | if ( !array_key_exists( $type, $variables['GEHomepageSuggestedEditsIntroLinks'] ) ) { |
204 | return 'GEHomepageSuggestedEditsIntroLinks does not have one of mandatory links'; |
205 | } |
206 | } |
207 | |
208 | return true; |
209 | } |
210 | |
211 | /** |
212 | * Get help panel link ID to be used for given Title |
213 | * |
214 | * @note Similar code is used in SpecialEditGrowthConfig::normalizeHelpPanelLinks. |
215 | * @param Title $link |
216 | * @return string |
217 | */ |
218 | private function getHelpPanelLinkId( Title $link ) { |
219 | $wdLinkId = null; |
220 | if ( $link->exists() && !$link->isExternal() ) { |
221 | $props = $this->pageProps->getProperties( $link, 'wikibase_item' ); |
222 | $pageId = $link->getId(); |
223 | if ( array_key_exists( $pageId, $props ) ) { |
224 | $wdLinkId = $props[$pageId]; |
225 | } |
226 | } |
227 | return $wdLinkId ?? $link->getPrefixedDBkey(); |
228 | } |
229 | |
230 | /** |
231 | * @param string $primaryQid |
232 | * @param array $backupQids |
233 | * @param string|null $backupExternal Interwiki link to be used as link of last resort |
234 | * @return array|null |
235 | */ |
236 | private function getHelpPanelLink( |
237 | string $primaryQid, |
238 | array $backupQids = [], |
239 | ?string $backupExternal = null |
240 | ): ?array { |
241 | $rawTitle = $this->getRawTitleFromWikidata( $primaryQid, $backupQids ); |
242 | if ( $rawTitle === null ) { |
243 | if ( $backupExternal === null ) { |
244 | return null; |
245 | } |
246 | $rawTitle = $backupExternal; |
247 | } |
248 | $title = $this->titleFactory->newFromText( $rawTitle ); |
249 | if ( $title === null ) { |
250 | return null; |
251 | } |
252 | return [ |
253 | 'title' => $title->getFullText(), |
254 | 'text' => $title->getText(), |
255 | 'id' => $this->getHelpPanelLinkId( $title ) |
256 | ]; |
257 | } |
258 | |
259 | /** |
260 | * @return array |
261 | */ |
262 | private function getSuggestedEditsVariables(): array { |
263 | $taskTemplatesQIDs = [ |
264 | 'copyedit' => [ 'Q6292692', 'Q6706206', 'Q6931087', 'Q7656698', 'Q6931386' ], |
265 | 'links' => [ 'Q13107723', 'Q5849007', 'Q5621858' ], |
266 | 'references' => [ 'Q5962027', 'Q6192879' ], |
267 | 'update' => [ 'Q5617874', 'Q14337093' ], |
268 | 'expand' => [ 'Q5529697', 'Q5623589', 'Q5866533' ], |
269 | ]; |
270 | $taskLearnMoreQIDs = [ |
271 | 'copyedit' => [ 'Q10953805' ], |
272 | 'links' => [ 'Q27919580', 'Q75275496' ], |
273 | 'references' => [ 'Q79951', 'Q642335' ], |
274 | 'update' => [ 'Q4664141' ], |
275 | 'expand' => [ 'Q10973854', 'Q4663261' ], |
276 | ]; |
277 | $taskLearnMoreBackup = [ |
278 | 'links' => 'mw:Special:MyLanguage/Help:VisualEditor/User_guide#Editing_links', |
279 | ]; |
280 | |
281 | $variables = []; |
282 | |
283 | $defaultTaskTypeData = $this->specialEditGrowthConfig->getDefaultDataForEnabledTaskTypes(); |
284 | foreach ( $defaultTaskTypeData as $taskType => $taskData ) { |
285 | if ( |
286 | !array_key_exists( $taskType, $taskTemplatesQIDs ) || |
287 | !array_key_exists( $taskType, $taskLearnMoreQIDs ) |
288 | ) { |
289 | continue; |
290 | } |
291 | |
292 | $templates = []; |
293 | foreach ( $taskTemplatesQIDs[$taskType] as $qid ) { |
294 | $candidateTitle = $this->getRawTitleFromWikidata( $qid ); |
295 | if ( $candidateTitle === null ) { |
296 | continue; |
297 | } |
298 | $templates[] = $this->titleFactory->newFromText( $candidateTitle )->getText(); |
299 | } |
300 | |
301 | if ( $templates === [] ) { |
302 | continue; |
303 | } |
304 | |
305 | $variables[$taskType] = [ |
306 | 'group' => $taskData['difficulty'], |
307 | 'templates' => $templates, |
308 | ]; |
309 | |
310 | $learnmoreLink = $this->getRawTitleFromWikidata( |
311 | $taskLearnMoreQIDs[$taskType][0], |
312 | array_slice( $taskLearnMoreQIDs[$taskType], 1 ) |
313 | ); |
314 | if ( $learnmoreLink === null && array_key_exists( $taskType, $taskLearnMoreBackup ) ) { |
315 | $learnmoreLink = $taskLearnMoreBackup[$taskType]; |
316 | } |
317 | if ( $learnmoreLink !== null ) { |
318 | $variables[$taskType]['learnmore'] = $learnmoreLink; |
319 | } |
320 | } |
321 | return $variables; |
322 | } |
323 | |
324 | /** |
325 | * @inheritDoc |
326 | */ |
327 | public function execute() { |
328 | $this->initServices(); |
329 | $dryRun = $this->hasOption( 'dry-run' ); |
330 | |
331 | $status = $this->initGEConfig( $dryRun ); |
332 | if ( !$status ) { |
333 | return false; |
334 | } |
335 | |
336 | $status = $this->initSuggestedEditsConfig( $dryRun ); |
337 | if ( !$status ) { |
338 | return false; |
339 | } |
340 | |
341 | return true; |
342 | } |
343 | |
344 | /** |
345 | * @param bool $dryRun |
346 | * @return bool |
347 | */ |
348 | private function initGEConfig( $dryRun ) { |
349 | $title = $this->titleFactory->newFromText( |
350 | $this->getConfig()->get( 'GEWikiConfigPageTitle' ) |
351 | ); |
352 | if ( $title === null ) { |
353 | $this->fatalError( "Invalid GEWikiConfigPageTitle!\n" ); |
354 | } |
355 | if ( |
356 | !$this->hasOption( 'override' ) && |
357 | !$this->hasOption( 'dry-run' ) && |
358 | $title->exists() |
359 | ) { |
360 | $this->fatalError( |
361 | "On-wiki config already exists ({$title->getPrefixedText()}). " . |
362 | "You can skip the validation using --override." |
363 | ); |
364 | } |
365 | |
366 | // @phan-suppress-next-next-line PhanTypeMismatchArgumentNullable Still T240141? |
367 | $wikiPageConfigWriter = $this->wikiPageConfigWriterFactory |
368 | ->newWikiPageConfigWriter( $title ); |
369 | |
370 | $variables = $this->getGEConfigVariables(); |
371 | if ( !$dryRun ) { |
372 | $wikiPageConfigWriter->setVariables( $variables ); |
373 | $status = $wikiPageConfigWriter->save( $this->getEditSummary() ); |
374 | if ( !$status->isOK() ) { |
375 | $this->fatalError( $status->getWikiText( false, false, 'en' ) ); |
376 | } |
377 | } else { |
378 | $this->output( $title->getPrefixedText() . ":\n" ); |
379 | $this->output( json_encode( $variables, JSON_PRETTY_PRINT ) . "\n" ); |
380 | } |
381 | |
382 | return true; |
383 | } |
384 | |
385 | /** |
386 | * @param bool $dryRun |
387 | * @return bool |
388 | */ |
389 | private function initSuggestedEditsConfig( bool $dryRun ): bool { |
390 | $title = $this->titleFactory->newFromText( |
391 | $this->getConfig()->get( 'GENewcomerTasksConfigTitle' ) |
392 | ); |
393 | if ( $title === null ) { |
394 | $this->fatalError( "Invalid GENewcomerTasksConfigTitle!\n" ); |
395 | } |
396 | if ( |
397 | !$this->hasOption( 'override' ) && |
398 | !$this->hasOption( 'dry-run' ) && |
399 | $title->exists() |
400 | ) { |
401 | $this->fatalError( |
402 | "On-wiki config already exists ({$title->getPrefixedText()}). " . |
403 | "You can skip the validation using --override." |
404 | ); |
405 | } |
406 | |
407 | // @phan-suppress-next-next-line PhanTypeMismatchArgumentNullable Still T240141? |
408 | $wikiPageConfigWriter = $this->wikiPageConfigWriterFactory |
409 | ->newWikiPageConfigWriter( $title ); |
410 | |
411 | $variables = $this->getSuggestedEditsVariables(); |
412 | |
413 | if ( !$dryRun ) { |
414 | $wikiPageConfigWriter->setVariables( $variables ); |
415 | $status = $wikiPageConfigWriter->save( $this->getEditSummary() ); |
416 | if ( !$status->isOK() ) { |
417 | $this->fatalError( $status->getWikiText( false, false, 'en' ) ); |
418 | } |
419 | return true; |
420 | } else { |
421 | $this->output( $title->getPrefixedText() . ":\n" ); |
422 | $this->output( json_encode( $variables, JSON_PRETTY_PRINT ) . "\n" ); |
423 | return false; |
424 | } |
425 | } |
426 | } |
427 | |
428 | $maintClass = InitWikiConfig::class; |
429 | require_once RUN_MAINTENANCE_IF_MAIN; |