Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
72.83% |
67 / 92 |
|
10.00% |
1 / 10 |
CRAP | |
0.00% |
0 / 1 |
GadgetRepo | |
72.83% |
67 / 92 |
|
10.00% |
1 / 10 |
43.73 | |
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% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGadgetDefinitionTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getStructuredList | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
4.68 | |||
titleWithoutPrefix | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
validationWarnings | |
75.00% |
18 / 24 |
|
0.00% |
0 / 1 |
4.25 | |||
checkTitles | |
76.92% |
10 / 13 |
|
0.00% |
0 / 1 |
5.31 | |||
checkInvalidLoadConditions | |
93.33% |
28 / 30 |
|
0.00% |
0 / 1 |
6.01 | |||
maybeAddWarnings | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
singleton | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
setSingleton | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Gadgets; |
4 | |
5 | use InvalidArgumentException; |
6 | use MediaWiki\Linker\LinkTarget; |
7 | use MediaWiki\MediaWikiServices; |
8 | use MediaWiki\Message\Message; |
9 | use MediaWiki\Title\Title; |
10 | |
11 | abstract 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 | } |