Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 111
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChangeWikiConfig
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 7
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
2
 initServices
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 initConfigWriter
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 execute
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 saveChange
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 rawPageTitleEquals
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 touchConfigPage
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace GrowthExperiments\Maintenance;
4
5use GrowthExperiments\Config\WikiPageConfigWriter;
6use GrowthExperiments\Config\WikiPageConfigWriterFactory;
7use GrowthExperiments\GrowthExperimentsServices;
8use InvalidArgumentException;
9use MediaWiki\Json\FormatJson;
10use MediaWiki\Maintenance\Maintenance;
11use MediaWiki\Status\Status;
12use MediaWiki\Title\TitleFactory;
13use MediaWiki\User\User;
14
15$IP = getenv( 'MW_INSTALL_PATH' );
16if ( $IP === false ) {
17    $IP = __DIR__ . '/../../..';
18}
19require_once "$IP/maintenance/Maintenance.php";
20
21class ChangeWikiConfig extends Maintenance {
22    /** @var TitleFactory */
23    private $titleFactory;
24
25    /** @var WikiPageConfigWriterFactory */
26    private $wikiPageConfigWriterFactory;
27
28    public function __construct() {
29        parent::__construct();
30        $this->requireExtension( 'GrowthExperiments' );
31        $this->addDescription( 'Update a config key in on-wiki config' );
32
33        $this->addOption(
34            'json',
35            'If true, input value will be treated as JSON, not as string'
36        );
37        $this->addOption(
38            'page',
39            'Page that will be changed (defaults to GEWikiConfigPageTitle)',
40            false,
41            true
42        );
43        $this->addOption(
44            'summary',
45            'Edit summary to use',
46            false,
47            true
48        );
49        $this->addOption(
50            'touch',
51            'Make a null edit to the page (useful to reflect config serialization changes); '
52            . 'not supported for all config pages'
53        );
54
55        $this->addArg(
56            'key',
57            'Config key that is updated (use . to separate keys in a multidimensional array)',
58            false
59        );
60        $this->addArg(
61            'value',
62            'New value of the config key',
63            false
64        );
65        $this->addOption(
66            'create-only',
67            'Create the field if it doesn\'t exist but do not overwrite it if it does'
68        );
69    }
70
71    private function initServices() {
72        $services = $this->getServiceContainer();
73
74        $this->wikiPageConfigWriterFactory = GrowthExperimentsServices::wrap( $services )
75            ->getWikiPageConfigWriterFactory();
76        $this->titleFactory = $services->getTitleFactory();
77    }
78
79    private function initConfigWriter(): WikiPageConfigWriter {
80        $rawConfigPage = $this->getOption(
81            'page',
82            $this->getConfig()->get( 'GEWikiConfigPageTitle' )
83        );
84        $configPage = $this->titleFactory->newFromText( $rawConfigPage );
85        if ( $configPage === null ) {
86            $this->fatalError( "$rawConfigPage is not a valid title." );
87        }
88
89        try {
90            '@phan-var \MediaWiki\Linker\LinkTarget $configPage';
91            return $this->wikiPageConfigWriterFactory->newWikiPageConfigWriter(
92                $configPage
93            );
94        } catch ( InvalidArgumentException $e ) {
95            $this->fatalError( "$rawConfigPage is not a supported config page" );
96        }
97    }
98
99    /**
100     * @inheritDoc
101     */
102    public function execute() {
103        $this->initServices();
104
105        $touch = $this->hasOption( 'touch' );
106        $key = $this->getArg( 0 );
107        $value = $this->getArg( 1 );
108
109        if ( !$touch && ( $key === null || $value === null ) ) {
110            $this->fatalError( 'Key and value are required when --touch is not used.' );
111        }
112
113        if ( !$touch ) {
114            $this->saveChange( $key, $value );
115        } else {
116            $this->touchConfigPage();
117        }
118    }
119
120    /**
121     * Make a change to the config page
122     *
123     * @param mixed $key
124     * @param mixed $value
125     * @return void
126     */
127    private function saveChange( $key, $value ) {
128        $configWriter = $this->initConfigWriter();
129
130        if ( strpos( $key, '.' ) !== false ) {
131            $key = explode( '.', $key );
132        }
133
134        if ( $this->hasOption( 'json' ) ) {
135            $status = FormatJson::parse( $value, FormatJson::FORCE_ASSOC );
136            if ( !$status->isGood() ) {
137                $this->fatalError(
138                    "Unable to decode JSON to use with $key$value. Error from FormatJson::parse: " .
139                    $status->getWikiText( false, false, 'en' )
140                );
141            }
142            $value = $status->getValue();
143        }
144        try {
145            if ( $this->hasOption( 'create-only' )
146                && $configWriter->variableExists( $key )
147            ) {
148                return;
149            }
150            $configWriter->setVariable( $key, $value );
151        } catch ( InvalidArgumentException $e ) {
152            $this->fatalError( $e->getMessage() );
153        }
154
155        $status = $configWriter->save( $this->getOption( 'summary', '' ) );
156        if ( !$status->isOK() ) {
157            $this->fatalError( $status->getWikiText() );
158        }
159
160        $this->output( "Saved!\n" );
161    }
162
163    /**
164     * @param string $rawPageA Raw page title
165     * @param string $rawPageB Raw page title
166     * @return bool
167     */
168    private function rawPageTitleEquals( string $rawPageA, string $rawPageB ): bool {
169        $pageA = $this->titleFactory->newFromText( $rawPageA );
170        $pageB = $this->titleFactory->newFromText( $rawPageB );
171        if ( $pageA === null || $pageB === null ) {
172            return false;
173        }
174        return $pageA->equals( $pageB );
175    }
176
177    /**
178     * If supported, make a no-op change
179     */
180    private function touchConfigPage() {
181        $rawConfigPage = $this->getOption(
182            'page',
183            $this->getConfig()->get( 'GEWikiConfigPageTitle' )
184        );
185
186        if ( $this->rawPageTitleEquals( $rawConfigPage, $this->getConfig()->get( 'GEStructuredMentorList' ) ) ) {
187            $statusValue = GrowthExperimentsServices::wrap( $this->getServiceContainer() )
188                ->getMentorWriter()
189                ->touchList(
190                    User::newSystemUser( 'Maintenance script', [ 'steal' => true ] ),
191                    $this->getOption( 'summary', '' )
192                );
193            if ( !$statusValue->isOK() ) {
194                $this->fatalError( Status::wrap( $statusValue )->getWikiText() );
195            }
196            $this->output( "Saved!\n" );
197        } else {
198            $this->fatalError( '--touch is not supported for ' . $rawConfigPage );
199        }
200    }
201}
202
203$maintClass = ChangeWikiConfig::class;
204require_once RUN_MAINTENANCE_IF_MAIN;