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