Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.33% covered (warning)
73.33%
77 / 105
10.00% covered (danger)
10.00%
1 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
GadgetRepo
73.33% covered (warning)
73.33%
77 / 105
10.00% covered (danger)
10.00%
1 / 10
47.07
0.00% covered (danger)
0.00%
0 / 1
 getGadgetIds
n/a
0 / 0
n/a
0 / 0
0
 getGadget
n/a
0 / 0
n/a
0 / 0
0
 handlePageUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGadgetDefinitionTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStructuredList
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
4.68
 titleWithoutPrefix
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 validationWarnings
76.00% covered (warning)
76.00%
19 / 25
0.00% covered (danger)
0.00%
0 / 1
4.22
 checkTitles
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
5.31
 checkProperty
88.10% covered (warning)
88.10%
37 / 42
0.00% covered (danger)
0.00%
0 / 1
8.11
 maybeAddWarnings
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 singleton
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setSingleton
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Gadgets;
4
5use InvalidArgumentException;
6use MediaWiki\Linker\LinkTarget;
7use MediaWiki\MediaWikiServices;
8use MediaWiki\Message\Message;
9use MediaWiki\Title\Title;
10
11abstract class GadgetRepo {
12
13    /**
14     * @var GadgetRepo|null
15     */
16    private static $instance;
17
18    /** @internal */
19    public const RESOURCE_TITLE_PREFIX = 'MediaWiki:Gadget-';
20
21    /**
22     * Get the ids of the gadgets provided by this repository
23     *
24     * It's possible this could be out of sync with what
25     * getGadget() will return due to caching
26     *
27     * @return string[]
28     */
29    abstract public function getGadgetIds(): array;
30
31    /**
32     * Get the Gadget object for a given gadget ID
33     *
34     * @param string $id
35     * @return Gadget
36     * @throws InvalidArgumentException For unregistered ID, used by getStructuredList()
37     */
38    abstract public function getGadget( string $id ): Gadget;
39
40    /**
41     * Invalidate any caches based on the provided page (after create, edit, or delete).
42     *
43     * This must be called on create and delete as well (T39228).
44     *
45     * @param LinkTarget $target
46     * @return void
47     */
48    public function handlePageUpdate( LinkTarget $target ): void {
49    }
50
51    /**
52     * Given a gadget ID, return the title of the page where the gadget is
53     * defined (or null if the given repo does not have per-gadget definition
54     * pages).
55     *
56     * @param string $id
57     * @return Title|null
58     */
59    public function getGadgetDefinitionTitle( string $id ): ?Title {
60        return null;
61    }
62
63    /**
64     * Get a lists of Gadget objects by section
65     *
66     * @return array<string,Gadget[]> `[ 'section' => [ 'name' => $gadget ] ]`
67     */
68    public function getStructuredList() {
69        $list = [];
70        foreach ( $this->getGadgetIds() as $id ) {
71            try {
72                $gadget = $this->getGadget( $id );
73            } catch ( InvalidArgumentException $e ) {
74                continue;
75            }
76            $list[$gadget->getSection()][$gadget->getName()] = $gadget;
77        }
78
79        return $list;
80    }
81
82    /**
83     * Get the page name without "MediaWiki:Gadget-" prefix.
84     *
85     * This name is used by `mw.loader.require()` so that `require("./example.json")` resolves
86     * to `MediaWiki:Gadget-example.json`.
87     *
88     * @param string $titleText
89     * @param string $gadgetId
90     * @return string
91     */
92    public function titleWithoutPrefix( string $titleText, string $gadgetId ): string {
93        // there is only one occurrence of the prefix
94        $numReplaces = 1;
95        return str_replace( self::RESOURCE_TITLE_PREFIX, '', $titleText, $numReplaces );
96    }
97
98    /**
99     * @param Gadget $gadget
100     * @return Message[]
101     */
102    public function validationWarnings( Gadget $gadget ): array {
103        // Basic checks local to the gadget definition
104        $warningMsgKeys = $gadget->getValidationWarnings();
105        $warnings = array_map( static function ( $warningMsgKey ) {
106            return wfMessage( $warningMsgKey );
107        }, $warningMsgKeys );
108
109        // Check for invalid values in skins, rights, namespaces, contentModels, and dependencies
110        $this->checkProperty( $gadget, 'skins', $warnings );
111        $this->checkProperty( $gadget, 'rights', $warnings );
112        $this->checkProperty( $gadget, 'namespaces', $warnings );
113        $this->checkProperty( $gadget, 'contentModels', $warnings );
114        $this->checkProperty( $gadget, 'dependencies', $warnings );
115
116        // Peer gadgets not being styles-only gadgets, or not being defined at all
117        foreach ( $gadget->getPeers() as $peer ) {
118            try {
119                $peerGadget = $this->getGadget( $peer );
120                if ( $peerGadget->getType() !== 'styles' ) {
121                    $warnings[] = wfMessage( "gadgets-validate-invalidpeer", $peer );
122                }
123            } catch ( InvalidArgumentException $ex ) {
124                $warnings[] = wfMessage( "gadgets-validate-nopeer", $peer );
125            }
126        }
127
128        // Check that the gadget pages exist and are of the right content model
129        $warnings = array_merge(
130            $warnings,
131            $this->checkTitles( $gadget->getScripts(), CONTENT_MODEL_JAVASCRIPT,
132                "gadgets-validate-invalidjs" ),
133            $this->checkTitles( $gadget->getStyles(), CONTENT_MODEL_CSS,
134                "gadgets-validate-invalidcss" ),
135            $this->checkTitles( $gadget->getJSONs(), CONTENT_MODEL_JSON,
136                "gadgets-validate-invalidjson" )
137        );
138
139        return $warnings;
140    }
141
142    /**
143     * Verify gadget resource pages exist and use the correct content model.
144     *
145     * @param string[] $pages Full page names
146     * @param string $expectedContentModel
147     * @param string $msg Interface message key
148     * @return Message[]
149     */
150    private function checkTitles( array $pages, string $expectedContentModel, string $msg ): array {
151        $warnings = [];
152        foreach ( $pages as $pageName ) {
153            $title = Title::newFromText( $pageName );
154            if ( !$title ) {
155                $warnings[] = wfMessage( "gadgets-validate-invalidtitle", $pageName );
156                continue;
157            }
158            if ( !$title->exists() ) {
159                $warnings[] = wfMessage( "gadgets-validate-nopage", $pageName );
160                continue;
161            }
162            $contentModel = $title->getContentModel();
163            if ( $contentModel !== $expectedContentModel ) {
164                $warnings[] = wfMessage( $msg, $pageName, $contentModel );
165            }
166        }
167        return $warnings;
168    }
169
170    /**
171     * @param Gadget $gadget
172     * @param string $property
173     * @param Message[] &$warnings
174     */
175    private function checkProperty( Gadget $gadget, string $property, array &$warnings ) {
176        switch ( $property ) {
177            case 'dependencies':
178                $rl = MediaWikiServices::getInstance()->getResourceLoader();
179                $this->maybeAddWarnings( $gadget->getDependencies(),
180                    static function ( $dep ) use ( $rl ) {
181                        return $rl->getModule( $dep ) === null;
182                    }, $warnings, "gadgets-validate-invaliddependencies" );
183                $this->maybeAddWarnings( $gadget->getDependencies(),
184                    static function ( $dep ) use ( $rl ) {
185                        $depModule = $rl->getModule( $dep );
186                        return $depModule !== null && $depModule->getDeprecationWarning() !== null;
187                    }, $warnings, "gadgets-validate-deprecateddependencies" );
188                break;
189
190            case 'skins':
191                $allSkins = array_keys( MediaWikiServices::getInstance()->getSkinFactory()->getInstalledSkins() );
192                $this->maybeAddWarnings( $gadget->getRequiredSkins(),
193                    static function ( $skin ) use ( $allSkins ) {
194                        return !in_array( $skin, $allSkins, true );
195                    }, $warnings, "gadgets-validate-invalidskins" );
196                break;
197
198            case 'rights':
199                $allPerms = MediaWikiServices::getInstance()->getPermissionManager()->getAllPermissions();
200                $this->maybeAddWarnings( $gadget->getRequiredRights(),
201                    static function ( $right ) use ( $allPerms ) {
202                        return !in_array( $right, $allPerms, true );
203                    }, $warnings, "gadgets-validate-invalidrights" );
204                break;
205
206            case 'namespaces':
207                $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
208                $this->maybeAddWarnings( $gadget->getRequiredNamespaces(),
209                    static function ( $ns ) use ( $nsInfo ) {
210                        return !$nsInfo->exists( $ns );
211                    }, $warnings, "gadgets-validate-invalidnamespaces"
212                );
213                break;
214
215            case 'contentModels':
216                $contentHandlerFactory = MediaWikiServices::getInstance()->getContentHandlerFactory();
217                $this->maybeAddWarnings( $gadget->getRequiredContentModels(),
218                    static function ( $model ) use ( $contentHandlerFactory ) {
219                        return !$contentHandlerFactory->isDefinedModel( $model );
220                    }, $warnings, "gadgets-validate-invalidcontentmodels"
221                );
222                break;
223            default:
224        }
225    }
226
227    /**
228     * Iterate over the given $entries, for each check if it is invalid using $isInvalid predicate,
229     * and if so add the $message to $warnings.
230     *
231     * @param array $entries
232     * @param callable $isInvalid
233     * @param array &$warnings
234     * @param string $message
235     */
236    private function maybeAddWarnings( array $entries, callable $isInvalid, array &$warnings, string $message ) {
237        $invalidEntries = [];
238        foreach ( $entries as $entry ) {
239            if ( $isInvalid( $entry ) ) {
240                $invalidEntries[] = $entry;
241            }
242        }
243        if ( $invalidEntries ) {
244            $warnings[] = wfMessage( $message,
245                Message::listParam( $invalidEntries, 'comma' ),
246                count( $invalidEntries ) );
247        }
248    }
249
250    /**
251     * Get the configured default GadgetRepo.
252     *
253     * @deprecated Use the GadgetsRepo service instead
254     * @return GadgetRepo
255     */
256    public static function singleton() {
257        wfDeprecated( __METHOD__, '1.42' );
258        if ( self::$instance === null ) {
259            return MediaWikiServices::getInstance()->getService( 'GadgetsRepo' );
260        }
261        return self::$instance;
262    }
263
264    /**
265     * Should only be used by unit tests
266     *
267     * @deprecated Use the GadgetsRepo service instead
268     * @param GadgetRepo|null $repo
269     */
270    public static function setSingleton( $repo = null ) {
271        wfDeprecated( __METHOD__, '1.42' );
272        self::$instance = $repo;
273    }
274}