Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.83% covered (warning)
72.83%
67 / 92
10.00% covered (danger)
10.00%
1 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
GadgetRepo
72.83% covered (warning)
72.83%
67 / 92
10.00% covered (danger)
10.00%
1 / 10
43.73
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
75.00% covered (warning)
75.00%
18 / 24
0.00% covered (danger)
0.00%
0 / 1
4.25
 checkTitles
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
5.31
 checkInvalidLoadConditions
93.33% covered (success)
93.33%
28 / 30
0.00% covered (danger)
0.00%
0 / 1
6.01
 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 category
65     *
66     * @return array<string,Gadget[]> `[ 'category' => [ '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->getCategory()][$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, and contentModels
110        $this->checkInvalidLoadConditions( $gadget, 'skins', $warnings );
111        $this->checkInvalidLoadConditions( $gadget, 'rights', $warnings );
112        $this->checkInvalidLoadConditions( $gadget, 'namespaces', $warnings );
113        $this->checkInvalidLoadConditions( $gadget, 'contentModels', $warnings );
114
115        // Peer gadgets not being styles-only gadgets, or not being defined at all
116        foreach ( $gadget->getPeers() as $peer ) {
117            try {
118                $peerGadget = $this->getGadget( $peer );
119                if ( $peerGadget->getType() !== 'styles' ) {
120                    $warnings[] = wfMessage( "gadgets-validate-invalidpeer", $peer );
121                }
122            } catch ( InvalidArgumentException $ex ) {
123                $warnings[] = wfMessage( "gadgets-validate-nopeer", $peer );
124            }
125        }
126
127        // Check that the gadget pages exist and are of the right content model
128        $warnings = array_merge(
129            $warnings,
130            $this->checkTitles( $gadget->getScripts(), CONTENT_MODEL_JAVASCRIPT,
131                "gadgets-validate-invalidjs" ),
132            $this->checkTitles( $gadget->getStyles(), CONTENT_MODEL_CSS,
133                "gadgets-validate-invalidcss" ),
134            $this->checkTitles( $gadget->getJSONs(), CONTENT_MODEL_JSON,
135                "gadgets-validate-invalidjson" )
136        );
137
138        return $warnings;
139    }
140
141    /**
142     * Verify gadget resource pages exist and use the correct content model.
143     *
144     * @param string[] $pages Full page names
145     * @param string $expectedContentModel
146     * @param string $msg Interface message key
147     * @return Message[]
148     */
149    private function checkTitles( array $pages, string $expectedContentModel, string $msg ): array {
150        $warnings = [];
151        foreach ( $pages as $pageName ) {
152            $title = Title::newFromText( $pageName );
153            if ( !$title ) {
154                $warnings[] = wfMessage( "gadgets-validate-invalidtitle", $pageName );
155                continue;
156            }
157            if ( !$title->exists() ) {
158                $warnings[] = wfMessage( "gadgets-validate-nopage", $pageName );
159                continue;
160            }
161            $contentModel = $title->getContentModel();
162            if ( $contentModel !== $expectedContentModel ) {
163                $warnings[] = wfMessage( $msg, $pageName, $contentModel );
164            }
165        }
166        return $warnings;
167    }
168
169    /**
170     * @param Gadget $gadget
171     * @param string $condition
172     * @param Message[] &$warnings
173     */
174    private function checkInvalidLoadConditions( Gadget $gadget, string $condition, array &$warnings ) {
175        switch ( $condition ) {
176            case 'skins':
177                $allSkins = array_keys( MediaWikiServices::getInstance()->getSkinFactory()->getInstalledSkins() );
178                $this->maybeAddWarnings( $gadget->getRequiredSkins(),
179                    static function ( $skin ) use ( $allSkins ) {
180                        return !in_array( $skin, $allSkins, true );
181                    }, $warnings, "gadgets-validate-invalidskins" );
182                break;
183
184            case 'rights':
185                $allPerms = MediaWikiServices::getInstance()->getPermissionManager()->getAllPermissions();
186                $this->maybeAddWarnings( $gadget->getRequiredRights(),
187                    static function ( $right ) use ( $allPerms ) {
188                        return !in_array( $right, $allPerms, true );
189                    }, $warnings, "gadgets-validate-invalidrights" );
190                break;
191
192            case 'namespaces':
193                $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
194                $this->maybeAddWarnings( $gadget->getRequiredNamespaces(),
195                    static function ( $ns ) use ( $nsInfo ) {
196                        return !$nsInfo->exists( $ns );
197                    }, $warnings, "gadgets-validate-invalidnamespaces"
198                );
199                break;
200
201            case 'contentModels':
202                $contentHandlerFactory = MediaWikiServices::getInstance()->getContentHandlerFactory();
203                $this->maybeAddWarnings( $gadget->getRequiredContentModels(),
204                    static function ( $model ) use ( $contentHandlerFactory ) {
205                        return !$contentHandlerFactory->isDefinedModel( $model );
206                    }, $warnings, "gadgets-validate-invalidcontentmodels"
207                );
208                break;
209            default:
210        }
211    }
212
213    /**
214     * Iterate over the given $entries, for each check if it is invalid using $isInvalid predicate,
215     * and if so add the $message to $warnings.
216     *
217     * @param array $entries
218     * @param callable $isInvalid
219     * @param array &$warnings
220     * @param string $message
221     */
222    private function maybeAddWarnings( array $entries, callable $isInvalid, array &$warnings, string $message ) {
223        $invalidEntries = [];
224        foreach ( $entries as $entry ) {
225            if ( $isInvalid( $entry ) ) {
226                $invalidEntries[] = $entry;
227            }
228        }
229        if ( count( $invalidEntries ) ) {
230            $warnings[] = wfMessage( $message,
231                Message::listParam( $invalidEntries, 'comma' ),
232                count( $invalidEntries ) );
233        }
234    }
235
236    /**
237     * Get the configured default GadgetRepo.
238     *
239     * @deprecated Use the GadgetsRepo service instead
240     * @return GadgetRepo
241     */
242    public static function singleton() {
243        wfDeprecated( __METHOD__, '1.42' );
244        if ( self::$instance === null ) {
245            return MediaWikiServices::getInstance()->getService( 'GadgetsRepo' );
246        }
247        return self::$instance;
248    }
249
250    /**
251     * Should only be used by unit tests
252     *
253     * @deprecated Use the GadgetsRepo service instead
254     * @param GadgetRepo|null $repo
255     */
256    public static function setSingleton( $repo = null ) {
257        wfDeprecated( __METHOD__, '1.42' );
258        self::$instance = $repo;
259    }
260}