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