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