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