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