Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
51.72% covered (warning)
51.72%
60 / 116
16.67% covered (danger)
16.67%
2 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
51.72% covered (warning)
51.72%
60 / 116
16.67% covered (danger)
16.67%
2 / 12
284.07
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onUserGetDefaultOptions
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 onGetPreferences
62.00% covered (warning)
62.00%
31 / 50
0.00% covered (danger)
0.00%
0 / 1
19.90
 onPreferencesGetLegend
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onPreferencesGetIcon
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onResourceLoaderRegisterModules
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 onBeforePageDisplay
67.86% covered (warning)
67.86%
19 / 28
0.00% covered (danger)
0.00%
0 / 1
18.61
 makeLegacyWarning
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 onContentHandlerDefaultModelFor
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 onWgQueryPages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onDeleteUnknownPreferences
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 onGetUserPermissionsErrors
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
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
23namespace MediaWiki\Extension\Gadgets;
24
25use InvalidArgumentException;
26use MediaWiki\Api\ApiMessage;
27use MediaWiki\Context\RequestContext;
28use MediaWiki\Extension\Gadgets\Special\SpecialGadgetUsage;
29use MediaWiki\Hook\DeleteUnknownPreferencesHook;
30use MediaWiki\Hook\PreferencesGetIconHook;
31use MediaWiki\Hook\PreferencesGetLegendHook;
32use MediaWiki\Html\Html;
33use MediaWiki\HTMLForm\HTMLForm;
34use MediaWiki\Output\Hook\BeforePageDisplayHook;
35use MediaWiki\Output\OutputPage;
36use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook;
37use MediaWiki\Preferences\Hook\GetPreferencesHook;
38use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook;
39use MediaWiki\ResourceLoader\ResourceLoader;
40use MediaWiki\Revision\Hook\ContentHandlerDefaultModelForHook;
41use MediaWiki\Skin\Skin;
42use MediaWiki\SpecialPage\Hook\WgQueryPagesHook;
43use MediaWiki\SpecialPage\SpecialPage;
44use MediaWiki\Title\Title;
45use MediaWiki\User\Hook\UserGetDefaultOptionsHook;
46use MediaWiki\User\Options\UserOptionsLookup;
47use MediaWiki\User\User;
48use OOUI\FieldLayout;
49use OOUI\HtmlSnippet;
50use OOUI\MessageWidget;
51use Wikimedia\Message\MessageSpecifier;
52use Wikimedia\Rdbms\IExpression;
53use Wikimedia\Rdbms\IReadableDatabase;
54use Wikimedia\Rdbms\LikeValue;
55use Wikimedia\WrappedString;
56use Wikimedia\WrappedStringList;
57
58class Hooks implements
59    UserGetDefaultOptionsHook,
60    GetPreferencesHook,
61    PreferencesGetIconHook,
62    PreferencesGetLegendHook,
63    ResourceLoaderRegisterModulesHook,
64    BeforePageDisplayHook,
65    ContentHandlerDefaultModelForHook,
66    WgQueryPagesHook,
67    DeleteUnknownPreferencesHook,
68    GetUserPermissionsErrorsHook
69{
70    public function __construct(
71        private readonly GadgetRepo $gadgetRepo,
72        private readonly UserOptionsLookup $userOptionsLookup,
73    ) {
74    }
75
76    /**
77     * UserGetDefaultOptions hook handler
78     * @param array &$defaultOptions Array of default preference keys and values
79     */
80    public function onUserGetDefaultOptions( &$defaultOptions ) {
81        $gadgets = $this->gadgetRepo->getStructuredList();
82        if ( !$gadgets ) {
83            return;
84        }
85
86        foreach ( $gadgets as $thisSection ) {
87            foreach ( $thisSection as $gadgetId => $gadget ) {
88                // Hidden gadgets don't need to be added here, T299071
89                if ( !$gadget->isHidden() ) {
90                    $defaultOptions['gadget-' . $gadgetId] = $gadget->isOnByDefault() ? 1 : 0;
91                }
92            }
93        }
94    }
95
96    /**
97     * GetPreferences hook handler.
98     * @param User $user
99     * @param array &$preferences Preference descriptions
100     */
101    public function onGetPreferences( $user, &$preferences ) {
102        $gadgets = $this->gadgetRepo->getStructuredList();
103        if ( !$gadgets ) {
104            return;
105        }
106
107        $preferences['gadgets-intro'] = [
108            'type' => 'info',
109            'default' => wfMessage( 'gadgets-prefstext' )->parseAsBlock(),
110            'section' => 'gadgets',
111            'raw' => true,
112        ];
113
114        $safeMode = $this->userOptionsLookup->getOption( $user, 'forcesafemode' );
115        if ( $safeMode ) {
116            $preferences['gadgets-safemode'] = [
117                'type' => 'info',
118                'section' => 'gadgets',
119                'raw' => true,
120                'rawrow' => true,
121                'default' => new FieldLayout(
122                    new MessageWidget( [
123                        'label' => new HtmlSnippet( wfMessage( 'gadgets-prefstext-safemode' )->parse() ),
124                        'type' => 'warning',
125                    ] )
126                ),
127            ];
128        }
129
130        $skin = RequestContext::getMain()->getSkin();
131        foreach ( $gadgets as $section => $thisSection ) {
132            if ( $section !== '' ) {
133                $sectionInfoMsg = wfMessage( "gadget-section-info-$section" );
134                if ( !$sectionInfoMsg->isDisabled() ) {
135                    $preferences['gadget-section-info-' . $section] = [
136                        'type' => 'info',
137                        'default' => $sectionInfoMsg->parse(),
138                        'section' => "gadgets/gadget-section-$section",
139                        'raw' => true,
140                    ];
141                }
142            }
143
144            foreach ( $thisSection as $gadget ) {
145                // Only show option to enable gadget if it can be enabled
146                $type = 'api';
147                if (
148                    !$safeMode
149                    && !$gadget->isHidden()
150                    && $gadget->isAllowed( $user )
151                    && $gadget->isSkinSupported( $skin )
152                ) {
153                    $type = 'check';
154                }
155                $gname = $gadget->getName();
156                $sectionLabelMsg = "gadget-section-$section";
157
158                $preferences["gadget-$gname"] = [
159                    'type' => $type,
160                    'label-message' => $gadget->getDescriptionMessageKey(),
161                    'section' => $section !== '' ? "gadgets/$sectionLabelMsg" : 'gadgets',
162                    'default' => $gadget->isEnabled( $user ),
163                    'noglobal' => true,
164                ];
165            }
166        }
167    }
168
169    /**
170     * PreferencesGetLegend hook handler.
171     *
172     * Used to override the subsection heading labels for the gadget groups. The default message would
173     * be "prefs-$key", but we've previously used different messages, and they have on-wiki overrides
174     * that would have to be moved if the message keys changed.
175     *
176     * @param HTMLForm $form the HTMLForm object. This is a ContextSource as well
177     * @param string $key the section name
178     * @param string &$legend the legend text. Defaults to wfMessage( "prefs-$key" )->text() but may
179     *   be overridden
180     * @return bool|void True or no return value to continue or false to abort
181     */
182    public function onPreferencesGetLegend( $form, $key, &$legend ) {
183        if ( str_starts_with( $key, 'gadget-section-' ) ) {
184            $legend = new HtmlSnippet( $form->msg( $key )->parse() );
185        }
186    }
187
188    /**
189     * Add icon for Special:Preferences mobile layout
190     *
191     * @param array &$iconNames Array of icon names for their respective sections.
192     */
193    public function onPreferencesGetIcon( &$iconNames ) {
194        $iconNames[ 'gadgets' ] = 'puzzle';
195    }
196
197    /**
198     * ResourceLoaderRegisterModules hook handler.
199     */
200    public function onResourceLoaderRegisterModules( ResourceLoader $resourceLoader ): void {
201        foreach ( $this->gadgetRepo->getGadgetIds() as $id ) {
202            $resourceLoader->register( Gadget::getModuleName( $id ), [
203                'class' => GadgetResourceLoaderModule::class,
204                'id' => $id,
205            ] );
206        }
207    }
208
209    /**
210     * BeforePageDisplay hook handler.
211     * @param OutputPage $out
212     * @param Skin $skin
213     */
214    public function onBeforePageDisplay( $out, $skin ): void {
215        $repo = $this->gadgetRepo;
216        $ids = $repo->getGadgetIds();
217        if ( !$ids ) {
218            return;
219        }
220
221        $enabledLegacyGadgets = [];
222        $conditions = new GadgetLoadConditions( $out );
223
224        foreach ( $ids as $id ) {
225            try {
226                $gadget = $repo->getGadget( $id );
227            } catch ( InvalidArgumentException ) {
228                continue;
229            }
230
231            if ( $conditions->check( $gadget ) ) {
232                if ( $gadget->hasModule() ) {
233                    if ( $gadget->getType() === 'styles' ) {
234                        $out->addModuleStyles( Gadget::getModuleName( $gadget->getName() ) );
235                    } else {
236                        $out->addModules( Gadget::getModuleName( $gadget->getName() ) );
237
238                        $peers = [];
239                        foreach ( $gadget->getPeers() as $peerName ) {
240                            try {
241                                $peers[] = $repo->getGadget( $peerName );
242                            } catch ( InvalidArgumentException ) {
243                                // Ignore, warning is emitted on Special:Gadgets
244                            }
245                        }
246                        // Load peer modules
247                        foreach ( $peers as $peer ) {
248                            if ( $peer->getType() === 'styles' ) {
249                                $out->addModuleStyles( Gadget::getModuleName( $peer->getName() ) );
250                            }
251                            // Else, if not type=styles: Use dependencies instead.
252                            // Note: No need for recursion as styles modules don't support
253                            // either of 'dependencies' and 'peers'.
254                        }
255                    }
256                }
257
258                if ( $gadget->getLegacyScripts() ) {
259                    $enabledLegacyGadgets[] = $id;
260                }
261            }
262        }
263
264        $strings = [];
265        foreach ( $enabledLegacyGadgets as $id ) {
266            $strings[] = $this->makeLegacyWarning( $id );
267        }
268        $out->addHTML( WrappedStringList::join( "\n", $strings ) );
269    }
270
271    /**
272     * @param string $id
273     * @return string|WrappedString HTML
274     */
275    private function makeLegacyWarning( $id ) {
276        $special = SpecialPage::getTitleFor( 'Gadgets' );
277
278        return ResourceLoader::makeInlineScript(
279            Html::encodeJsCall( 'mw.log.warn', [
280                "Gadget \"$id\" was not loaded. Please migrate it to use ResourceLoader. " .
281                'See <' . $special->getCanonicalURL() . '>.'
282            ] )
283        );
284    }
285
286    /**
287     * Create "MediaWiki:Gadgets/<id>.json" pages with GadgetDefinitionContent
288     *
289     * @param Title $title
290     * @param string &$model
291     * @return bool
292     */
293    public function onContentHandlerDefaultModelFor( $title, &$model ) {
294        if ( MediaWikiGadgetsJsonRepo::isGadgetDefinitionTitle( $title ) ) {
295            $model = 'GadgetDefinition';
296            return false;
297        }
298
299        return true;
300    }
301
302    /**
303     * Add the GadgetUsage special page to the list of QueryPages.
304     * @param array &$queryPages
305     */
306    public function onWgQueryPages( &$queryPages ) {
307        $queryPages[] = [ SpecialGadgetUsage::class, 'GadgetUsage' ];
308    }
309
310    /**
311     * Prevent gadget preferences from being deleted.
312     * @link https://www.mediawiki.org/wiki/Manual:Hooks/DeleteUnknownPreferences
313     * @param string[] &$where Array of where clause conditions to add to.
314     * @param IReadableDatabase $db
315     */
316    public function onDeleteUnknownPreferences( &$where, $db ) {
317        $where[] = $db->expr(
318            'up_property',
319            IExpression::NOT_LIKE,
320            new LikeValue( 'gadget-', $db->anyString() )
321        );
322    }
323
324    /**
325     * @param Title $title Title being checked against
326     * @param User $user Current user
327     * @param string $action Action being checked
328     * @param array|string|MessageSpecifier &$result User permissions error to add. If none, return true.
329     *   For consistency, error messages should be plain text with no special coloring,
330     *   bolding, etc. to show that they're errors; presenting them properly to the
331     *   user as errors is done by the caller.
332     * @return bool|void
333     */
334    public function onGetUserPermissionsErrors( $title, $user, $action, &$result ) {
335        if ( $action === 'edit'
336            && MediaWikiGadgetsJsonRepo::isGadgetDefinitionTitle( $title )
337        ) {
338            if ( !$user->isAllowed( 'editsitejs' ) ) {
339                $result = ApiMessage::create( wfMessage( 'sitejsprotected' ), 'sitejsprotected' );
340                return false;
341            }
342        }
343    }
344}