Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 217
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
InitWikiConfig
0.00% covered (danger)
0.00%
0 / 211
0.00% covered (danger)
0.00%
0 / 14
2970
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
2
 initServices
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getWikidataWikiId
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getEditSummary
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getWikidataData
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getRawTitleFromWikidata
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getGEConfigVariables
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
12
 validateGEConfigVariables
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getHelpPanelLinkId
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getHelpPanelLink
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 getSuggestedEditsVariables
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
110
 execute
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 initGEConfig
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 initSuggestedEditsConfig
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2
3namespace GrowthExperiments\Maintenance;
4
5use GrowthExperiments\Config\WikiPageConfigWriterFactory;
6use GrowthExperiments\GrowthExperimentsServices;
7use GrowthExperiments\Specials\SpecialEditGrowthConfig;
8use GrowthExperiments\Util;
9use Maintenance;
10use MediaWiki\Http\HttpRequestFactory;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Page\PageProps;
13use MediaWiki\Title\Title;
14use MediaWiki\Title\TitleFactory;
15use MediaWiki\WikiMap\WikiMap;
16
17$IP = getenv( 'MW_INSTALL_PATH' );
18if ( $IP === false ) {
19    $IP = __DIR__ . '/../../..';
20}
21require_once "$IP/maintenance/Maintenance.php";
22
23class 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;
429require_once RUN_MAINTENANCE_IF_MAIN;