Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
8.57% covered (danger)
8.57%
6 / 70
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConfigHooks
8.57% covered (danger)
8.57%
6 / 70
0.00% covered (danger)
0.00%
0 / 6
464.22
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 onEditFilterMergedContent
54.55% covered (warning)
54.55%
6 / 11
0.00% covered (danger)
0.00%
0 / 1
7.35
 onJsonValidateSave
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 onPageSaveComplete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 onSkinTemplateNavigation__Universal
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 onSpecialPage_initList
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace GrowthExperiments\Config;
4
5use Content;
6use ExtensionRegistry;
7use FormatJson;
8use GrowthExperiments\Config\Validation\ConfigValidatorFactory;
9use GrowthExperiments\Specials\SpecialEditGrowthConfig;
10use GrowthExperiments\Specials\SpecialEditGrowthConfigRedirect;
11use IContextSource;
12use JsonContent;
13use MediaWiki\Config\Config;
14use MediaWiki\Content\Hook\JsonValidateSaveHook;
15use MediaWiki\Deferred\DeferredUpdates;
16use MediaWiki\Hook\EditFilterMergedContentHook;
17use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook;
18use MediaWiki\Page\PageIdentity;
19use MediaWiki\SpecialPage\Hook\SpecialPage_initListHook;
20use MediaWiki\SpecialPage\SpecialPage;
21use MediaWiki\Status\Status;
22use MediaWiki\Storage\Hook\PageSaveCompleteHook;
23use MediaWiki\Title\TitleFactory;
24use MediaWiki\Title\TitleValue;
25use MediaWiki\User\User;
26use StatusValue;
27use TextContent;
28
29class ConfigHooks implements
30    EditFilterMergedContentHook,
31    JsonValidateSaveHook,
32    PageSaveCompleteHook,
33    SkinTemplateNavigation__UniversalHook,
34    SpecialPage_initListHook
35{
36    private ConfigValidatorFactory $configValidatorFactory;
37    private WikiPageConfigLoader $configLoader;
38    private TitleFactory $titleFactory;
39    private Config $config;
40
41    /**
42     * @param ConfigValidatorFactory $configValidatorFactory
43     * @param WikiPageConfigLoader $configLoader
44     * @param TitleFactory $titleFactory
45     * @param Config $config
46     */
47    public function __construct(
48        ConfigValidatorFactory $configValidatorFactory,
49        WikiPageConfigLoader $configLoader,
50        TitleFactory $titleFactory,
51        Config $config
52    ) {
53        $this->configValidatorFactory = $configValidatorFactory;
54        $this->configLoader = $configLoader;
55        $this->titleFactory = $titleFactory;
56        $this->config = $config;
57    }
58
59    /**
60     * @inheritDoc
61     */
62    public function onEditFilterMergedContent(
63        IContextSource $context,
64        Content $content,
65        Status $status,
66        $summary,
67        User $user,
68        $minoredit
69    ) {
70        // Check whether this is a config page edited
71        $title = $context->getTitle();
72        foreach ( $this->configValidatorFactory->getSupportedConfigPages() as $configTitle ) {
73            if ( $title->equals( $configTitle ) ) {
74                // Check content model
75                if (
76                    $content->getModel() !== CONTENT_MODEL_JSON ||
77                    !( $content instanceof TextContent )
78                ) {
79                    $status->fatal(
80                        'growthexperiments-config-validator-contentmodel-mismatch',
81                        $content->getModel()
82                    );
83                    return false;
84                }
85            }
86        }
87        return true;
88    }
89
90    /**
91     * @inheritDoc
92     */
93    public function onJsonValidateSave(
94        JsonContent $content, PageIdentity $pageIdentity, StatusValue $status
95    ) {
96        foreach ( $this->configValidatorFactory->getSupportedConfigPages() as $configTitle ) {
97            if ( $pageIdentity->isSamePageAs( $configTitle ) ) {
98                $data = FormatJson::parse( $content->getText(), FormatJson::FORCE_ASSOC )->getValue();
99                $status->merge(
100                    $this->configValidatorFactory
101                        // @phan-suppress-next-line PhanTypeMismatchArgumentNullable
102                        ->newConfigValidator( TitleValue::castPageToLinkTarget( $pageIdentity ) )
103                        ->validate( $data )
104                );
105                if ( !$status->isGood() ) {
106                    // JsonValidateSave expects a fatal status on failure, but the validator uses isGood()
107                    $status->setOK( false );
108                }
109                return $status->isOK();
110            }
111        }
112    }
113
114    /**
115     * Invalidate configuration cache when needed.
116     * @inheritDoc
117     * @see https://www.mediawiki.org/wiki/Manual:Hooks/PageSaveComplete
118     */
119    public function onPageSaveComplete(
120        $wikiPage, $user, $summary, $flags, $revisionRecord, $editResult
121    ) {
122        DeferredUpdates::addCallableUpdate( function () use ( $wikiPage ) {
123            $title = $wikiPage->getTitle();
124            foreach ( $this->configValidatorFactory->getSupportedConfigPages() as $configTitle ) {
125                if ( $title->equals( $configTitle ) ) {
126                    $this->configLoader->invalidate( $configTitle );
127                }
128            }
129        } );
130    }
131
132    /**
133     * @inheritDoc
134     */
135    public function onSkinTemplateNavigation__Universal( $sktemplate, &$links ): void {
136        // Code inspired by MassMessageHooks::onSkinTemplateNavigation
137        $title = $sktemplate->getTitle();
138        $geConfigTitle = $this->titleFactory
139            ->newFromText(
140                $this->config->get( 'GEWikiConfigPageTitle' )
141            );
142        $newcomerTasksConfigTitle = $this->titleFactory
143            ->newFromText(
144                $this->config->get( 'GENewcomerTasksConfigTitle' )
145            );
146        if (
147            array_key_exists( 'edit', $links['views'] ) &&
148            (
149                ( $geConfigTitle !== null && $title->equals( $geConfigTitle ) ) ||
150                ( $newcomerTasksConfigTitle !== null && $title->equals( $newcomerTasksConfigTitle ) )
151            ) &&
152            $title->hasContentModel( CONTENT_MODEL_JSON )
153        ) {
154            // Get the revision being viewed, if applicable
155            $request = $sktemplate->getRequest();
156            // $oldid is guaranteed to be an integer, 0 if invalid
157            $oldid = $request->getInt( 'oldid' );
158
159            // Show normal JSON editor if the user is trying to edit old version
160            if ( $oldid == 0 ) {
161                $links['views']['edit']['href'] = SpecialPage::getTitleFor(
162                    'EditGrowthConfig'
163                )->getFullUrl();
164            }
165        }
166    }
167
168    /**
169     * @inheritDoc
170     */
171    public function onSpecialPage_initList( &$list ) {
172        if (
173            ExtensionRegistry::getInstance()->isLoaded( 'CommunityConfiguration' ) &&
174            $this->config->get( 'GEUseCommunityConfigurationExtension' )
175        ) {
176            $list['EditGrowthConfig'] = [
177                'class' => SpecialEditGrowthConfigRedirect::class,
178            ];
179        } else {
180            $list['EditGrowthConfig'] = [
181                'class' => SpecialEditGrowthConfig::class,
182                'services' => [
183                    'TitleFactory',
184                    'RevisionLookup',
185                    'PageProps',
186                    'DBLoadBalancer',
187                    'ReadOnlyMode',
188                    'GrowthExperimentsWikiPageConfigLoader',
189                    'GrowthExperimentsWikiPageConfigWriterFactory',
190                    'GrowthExperimentsCommunityConfig'
191                ]
192            ];
193        }
194    }
195}