Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
55.36% |
62 / 112 |
|
26.67% |
4 / 15 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
55.36% |
62 / 112 |
|
26.67% |
4 / 15 |
262.62 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
onPageSaveComplete | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
onPageDeleteComplete | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
onUserGetDefaultOptions | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
6 | |||
onGetPreferences | |
80.00% |
28 / 35 |
|
0.00% |
0 / 1 |
10.80 | |||
onPreferencesGetLegend | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
onPreferencesGetIcon | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onResourceLoaderRegisterModules | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onBeforePageDisplay | |
67.86% |
19 / 28 |
|
0.00% |
0 / 1 |
18.61 | |||
makeLegacyWarning | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
onEditFilterMergedContent | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
onContentHandlerDefaultModelFor | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
onWgQueryPages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onDeleteUnknownPreferences | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
onGetUserPermissionsErrors | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | /** |
3 | * Copyright © 2007 Daniel Kinzler |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | namespace MediaWiki\Extension\Gadgets; |
24 | |
25 | use ApiMessage; |
26 | use Content; |
27 | use Exception; |
28 | use HTMLForm; |
29 | use IContextSource; |
30 | use InvalidArgumentException; |
31 | use ManualLogEntry; |
32 | use MediaWiki\Extension\Gadgets\Content\GadgetDefinitionContent; |
33 | use MediaWiki\Extension\Gadgets\Special\SpecialGadgetUsage; |
34 | use MediaWiki\Hook\BeforePageDisplayHook; |
35 | use MediaWiki\Hook\DeleteUnknownPreferencesHook; |
36 | use MediaWiki\Hook\EditFilterMergedContentHook; |
37 | use MediaWiki\Hook\PreferencesGetIconHook; |
38 | use MediaWiki\Hook\PreferencesGetLegendHook; |
39 | use MediaWiki\Html\Html; |
40 | use MediaWiki\Output\OutputPage; |
41 | use MediaWiki\Page\Hook\PageDeleteCompleteHook; |
42 | use MediaWiki\Page\ProperPageIdentity; |
43 | use MediaWiki\Permissions\Authority; |
44 | use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook; |
45 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
46 | use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook; |
47 | use MediaWiki\ResourceLoader\ResourceLoader; |
48 | use MediaWiki\Revision\Hook\ContentHandlerDefaultModelForHook; |
49 | use MediaWiki\Revision\RevisionRecord; |
50 | use MediaWiki\SpecialPage\Hook\WgQueryPagesHook; |
51 | use MediaWiki\SpecialPage\SpecialPage; |
52 | use MediaWiki\Status\Status; |
53 | use MediaWiki\Storage\Hook\PageSaveCompleteHook; |
54 | use MediaWiki\Title\Title; |
55 | use MediaWiki\Title\TitleValue; |
56 | use MediaWiki\User\Hook\UserGetDefaultOptionsHook; |
57 | use MediaWiki\User\Options\UserOptionsLookup; |
58 | use MediaWiki\User\User; |
59 | use MessageSpecifier; |
60 | use OOUI\HtmlSnippet; |
61 | use RequestContext; |
62 | use Skin; |
63 | use Wikimedia\Rdbms\IExpression; |
64 | use Wikimedia\Rdbms\IReadableDatabase; |
65 | use Wikimedia\Rdbms\LikeValue; |
66 | use Wikimedia\WrappedString; |
67 | use WikiPage; |
68 | use Xml; |
69 | |
70 | class Hooks implements |
71 | PageDeleteCompleteHook, |
72 | PageSaveCompleteHook, |
73 | UserGetDefaultOptionsHook, |
74 | GetPreferencesHook, |
75 | PreferencesGetIconHook, |
76 | PreferencesGetLegendHook, |
77 | ResourceLoaderRegisterModulesHook, |
78 | BeforePageDisplayHook, |
79 | EditFilterMergedContentHook, |
80 | ContentHandlerDefaultModelForHook, |
81 | WgQueryPagesHook, |
82 | DeleteUnknownPreferencesHook, |
83 | GetUserPermissionsErrorsHook |
84 | { |
85 | private GadgetRepo $gadgetRepo; |
86 | private UserOptionsLookup $userOptionsLookup; |
87 | |
88 | public function __construct( |
89 | GadgetRepo $gadgetRepo, |
90 | UserOptionsLookup $userOptionsLookup |
91 | ) { |
92 | $this->gadgetRepo = $gadgetRepo; |
93 | $this->userOptionsLookup = $userOptionsLookup; |
94 | } |
95 | |
96 | /** |
97 | * Handle MediaWiki\Page\Hook\PageSaveCompleteHook |
98 | * |
99 | * @param WikiPage $wikiPage |
100 | * @param mixed $userIdentity unused |
101 | * @param string $summary |
102 | * @param int $flags |
103 | * @param mixed $revisionRecord unused |
104 | * @param mixed $editResult unused |
105 | */ |
106 | public function onPageSaveComplete( |
107 | $wikiPage, |
108 | $userIdentity, |
109 | $summary, |
110 | $flags, |
111 | $revisionRecord, |
112 | $editResult |
113 | ): void { |
114 | $title = $wikiPage->getTitle(); |
115 | $this->gadgetRepo->handlePageUpdate( $title ); |
116 | } |
117 | |
118 | /** |
119 | * Handle MediaWiki\Page\Hook\PageDeleteCompleteHook |
120 | * |
121 | * @param ProperPageIdentity $page |
122 | * @param Authority $deleter |
123 | * @param string $reason |
124 | * @param int $pageID |
125 | * @param RevisionRecord $deletedRev Last revision |
126 | * @param ManualLogEntry $logEntry |
127 | * @param int $archivedRevisionCount Number of revisions deleted |
128 | */ |
129 | public function onPageDeleteComplete( |
130 | ProperPageIdentity $page, |
131 | Authority $deleter, |
132 | string $reason, |
133 | int $pageID, |
134 | RevisionRecord $deletedRev, |
135 | ManualLogEntry $logEntry, |
136 | int $archivedRevisionCount |
137 | ): void { |
138 | $title = TitleValue::newFromPage( $page ); |
139 | $this->gadgetRepo->handlePageUpdate( $title ); |
140 | } |
141 | |
142 | /** |
143 | * UserGetDefaultOptions hook handler |
144 | * @param array &$defaultOptions Array of default preference keys and values |
145 | */ |
146 | public function onUserGetDefaultOptions( &$defaultOptions ) { |
147 | $gadgets = $this->gadgetRepo->getStructuredList(); |
148 | if ( !$gadgets ) { |
149 | return; |
150 | } |
151 | |
152 | /** |
153 | * @var $gadget Gadget |
154 | */ |
155 | foreach ( $gadgets as $thisSection ) { |
156 | foreach ( $thisSection as $gadgetId => $gadget ) { |
157 | // Hidden gadgets don't need to be added here, T299071 |
158 | if ( !$gadget->isHidden() ) { |
159 | $defaultOptions['gadget-' . $gadgetId] = $gadget->isOnByDefault() ? 1 : 0; |
160 | } |
161 | } |
162 | } |
163 | } |
164 | |
165 | /** |
166 | * GetPreferences hook handler. |
167 | * @param User $user |
168 | * @param array &$preferences Preference descriptions |
169 | */ |
170 | public function onGetPreferences( $user, &$preferences ) { |
171 | $gadgets = $this->gadgetRepo->getStructuredList(); |
172 | if ( !$gadgets ) { |
173 | return; |
174 | } |
175 | |
176 | $preferences['gadgets-intro'] = [ |
177 | 'type' => 'info', |
178 | 'default' => wfMessage( 'gadgets-prefstext' )->parseAsBlock(), |
179 | 'section' => 'gadgets', |
180 | 'raw' => true, |
181 | ]; |
182 | |
183 | $safeMode = $this->userOptionsLookup->getOption( $user, 'forcesafemode' ); |
184 | if ( $safeMode ) { |
185 | $preferences['gadgets-safemode'] = [ |
186 | 'type' => 'info', |
187 | 'default' => Html::warningBox( wfMessage( 'gadgets-prefstext-safemode' )->parse() ), |
188 | 'section' => 'gadgets', |
189 | 'raw' => true, |
190 | ]; |
191 | } |
192 | |
193 | $skin = RequestContext::getMain()->getSkin(); |
194 | foreach ( $gadgets as $section => $thisSection ) { |
195 | foreach ( $thisSection as $gadget ) { |
196 | // Only show option to enable gadget if it can be enabled |
197 | $type = 'api'; |
198 | if ( |
199 | !$safeMode |
200 | && !$gadget->isHidden() |
201 | && $gadget->isAllowed( $user ) |
202 | && $gadget->isSkinSupported( $skin ) |
203 | ) { |
204 | $type = 'check'; |
205 | } |
206 | $gname = $gadget->getName(); |
207 | $sectionLabelMsg = "gadget-section-$section"; |
208 | |
209 | $preferences["gadget-$gname"] = [ |
210 | 'type' => $type, |
211 | 'label-message' => $gadget->getDescriptionMessageKey(), |
212 | 'section' => $section !== '' ? "gadgets/$sectionLabelMsg" : 'gadgets', |
213 | 'default' => $gadget->isEnabled( $user ), |
214 | 'noglobal' => true, |
215 | ]; |
216 | } |
217 | } |
218 | } |
219 | |
220 | /** |
221 | * PreferencesGetLegend hook handler. |
222 | * |
223 | * Used to override the subsection heading labels for the gadget groups. The default message would |
224 | * be "prefs-$key", but we've previously used different messages, and they have on-wiki overrides |
225 | * that would have to be moved if the message keys changed. |
226 | * |
227 | * @param HTMLForm $form the HTMLForm object. This is a ContextSource as well |
228 | * @param string $key the section name |
229 | * @param string &$legend the legend text. Defaults to wfMessage( "prefs-$key" )->text() but may |
230 | * be overridden |
231 | * @return bool|void True or no return value to continue or false to abort |
232 | */ |
233 | public function onPreferencesGetLegend( $form, $key, &$legend ) { |
234 | if ( str_starts_with( $key, 'gadget-section-' ) ) { |
235 | $legend = new HtmlSnippet( $form->msg( $key )->parse() ); |
236 | } |
237 | } |
238 | |
239 | /** |
240 | * Add icon for Special:Preferences mobile layout |
241 | * |
242 | * @param array &$iconNames Array of icon names for their respective sections. |
243 | */ |
244 | public function onPreferencesGetIcon( &$iconNames ) { |
245 | $iconNames[ 'gadgets' ] = 'puzzle'; |
246 | } |
247 | |
248 | /** |
249 | * ResourceLoaderRegisterModules hook handler. |
250 | * @param ResourceLoader $resourceLoader |
251 | */ |
252 | public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void { |
253 | foreach ( $this->gadgetRepo->getGadgetIds() as $id ) { |
254 | $resourceLoader->register( Gadget::getModuleName( $id ), [ |
255 | 'class' => GadgetResourceLoaderModule::class, |
256 | 'id' => $id, |
257 | ] ); |
258 | } |
259 | } |
260 | |
261 | /** |
262 | * BeforePageDisplay hook handler. |
263 | * @param OutputPage $out |
264 | * @param Skin $skin |
265 | */ |
266 | public function onBeforePageDisplay( $out, $skin ): void { |
267 | $repo = $this->gadgetRepo; |
268 | $ids = $repo->getGadgetIds(); |
269 | if ( !$ids ) { |
270 | return; |
271 | } |
272 | |
273 | $enabledLegacyGadgets = []; |
274 | $conditions = new GadgetLoadConditions( $out ); |
275 | |
276 | /** |
277 | * @var $gadget Gadget |
278 | */ |
279 | foreach ( $ids as $id ) { |
280 | try { |
281 | $gadget = $repo->getGadget( $id ); |
282 | } catch ( InvalidArgumentException $e ) { |
283 | continue; |
284 | } |
285 | |
286 | if ( $conditions->check( $gadget ) ) { |
287 | if ( $gadget->hasModule() ) { |
288 | if ( $gadget->getType() === 'styles' ) { |
289 | $out->addModuleStyles( Gadget::getModuleName( $gadget->getName() ) ); |
290 | } else { |
291 | $out->addModules( Gadget::getModuleName( $gadget->getName() ) ); |
292 | |
293 | $peers = []; |
294 | foreach ( $gadget->getPeers() as $peerName ) { |
295 | try { |
296 | $peers[] = $repo->getGadget( $peerName ); |
297 | } catch ( InvalidArgumentException $e ) { |
298 | // Ignore, warning is emitted on Special:Gadgets |
299 | } |
300 | } |
301 | // Load peer modules |
302 | foreach ( $peers as $peer ) { |
303 | if ( $peer->getType() === 'styles' ) { |
304 | $out->addModuleStyles( Gadget::getModuleName( $peer->getName() ) ); |
305 | } |
306 | // Else, if not type=styles: Use dependencies instead. |
307 | // Note: No need for recursion as styles modules don't support |
308 | // either of 'dependencies' and 'peers'. |
309 | } |
310 | } |
311 | } |
312 | |
313 | if ( $gadget->getLegacyScripts() ) { |
314 | $enabledLegacyGadgets[] = $id; |
315 | } |
316 | } |
317 | } |
318 | |
319 | $strings = []; |
320 | foreach ( $enabledLegacyGadgets as $id ) { |
321 | $strings[] = $this->makeLegacyWarning( $id ); |
322 | } |
323 | $out->addHTML( WrappedString::join( "\n", $strings ) ); |
324 | } |
325 | |
326 | /** |
327 | * @param string $id |
328 | * @return string|WrappedString HTML |
329 | */ |
330 | private function makeLegacyWarning( $id ) { |
331 | $special = SpecialPage::getTitleFor( 'Gadgets' ); |
332 | |
333 | return ResourceLoader::makeInlineScript( |
334 | Xml::encodeJsCall( 'mw.log.warn', [ |
335 | "Gadget \"$id\" was not loaded. Please migrate it to use ResourceLoader. " . |
336 | 'See <' . $special->getCanonicalURL() . '>.' |
337 | ] ) |
338 | ); |
339 | } |
340 | |
341 | /** |
342 | * Valid gadget definition page after content is modified |
343 | * |
344 | * @param IContextSource $context |
345 | * @param Content $content |
346 | * @param Status $status |
347 | * @param string $summary |
348 | * @param User $user |
349 | * @param bool $minoredit |
350 | * @throws Exception |
351 | * @return bool |
352 | */ |
353 | public function onEditFilterMergedContent( |
354 | IContextSource $context, |
355 | Content $content, |
356 | Status $status, |
357 | $summary, |
358 | User $user, |
359 | $minoredit |
360 | ) { |
361 | if ( $content instanceof GadgetDefinitionContent ) { |
362 | $validateStatus = $content->validate(); |
363 | if ( !$validateStatus->isGood() ) { |
364 | $status->merge( $validateStatus ); |
365 | return false; |
366 | } |
367 | } |
368 | |
369 | return true; |
370 | } |
371 | |
372 | /** |
373 | * Create "MediaWiki:Gadgets/<id>.json" pages with GadgetDefinitionContent |
374 | * |
375 | * @param Title $title |
376 | * @param string &$model |
377 | * @return bool |
378 | */ |
379 | public function onContentHandlerDefaultModelFor( $title, &$model ) { |
380 | if ( MediaWikiGadgetsJsonRepo::isGadgetDefinitionTitle( $title ) ) { |
381 | $model = 'GadgetDefinition'; |
382 | return false; |
383 | } |
384 | |
385 | return true; |
386 | } |
387 | |
388 | /** |
389 | * Add the GadgetUsage special page to the list of QueryPages. |
390 | * @param array &$queryPages |
391 | */ |
392 | public function onWgQueryPages( &$queryPages ) { |
393 | $queryPages[] = [ SpecialGadgetUsage::class, 'GadgetUsage' ]; |
394 | } |
395 | |
396 | /** |
397 | * Prevent gadget preferences from being deleted. |
398 | * @link https://www.mediawiki.org/wiki/Manual:Hooks/DeleteUnknownPreferences |
399 | * @param string[] &$where Array of where clause conditions to add to. |
400 | * @param IReadableDatabase $db |
401 | */ |
402 | public function onDeleteUnknownPreferences( &$where, $db ) { |
403 | $where[] = $db->expr( |
404 | 'up_property', |
405 | IExpression::NOT_LIKE, |
406 | new LikeValue( 'gadget-', $db->anyString() ) |
407 | ); |
408 | } |
409 | |
410 | /** |
411 | * @param Title $title Title being checked against |
412 | * @param User $user Current user |
413 | * @param string $action Action being checked |
414 | * @param array|string|MessageSpecifier &$result User permissions error to add. If none, return true. |
415 | * For consistency, error messages should be plain text with no special coloring, |
416 | * bolding, etc. to show that they're errors; presenting them properly to the |
417 | * user as errors is done by the caller. |
418 | * @return bool|void |
419 | */ |
420 | public function onGetUserPermissionsErrors( $title, $user, $action, &$result ) { |
421 | if ( $action === 'edit' |
422 | && MediaWikiGadgetsJsonRepo::isGadgetDefinitionTitle( $title ) |
423 | ) { |
424 | if ( !$user->isAllowed( 'editsitejs' ) ) { |
425 | $result = ApiMessage::create( wfMessage( 'sitejsprotected' ), 'sitejsprotected' ); |
426 | return false; |
427 | } |
428 | } |
429 | } |
430 | } |