Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 10
992
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 onSkinTemplateNavigation__Universal
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 skinConfigViewsLinks
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 showIcon
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getUserTalkPage
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
110
 onListDefinedTags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onChangeTagsListActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStartupData
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\WikiLove;
4
5use MediaWiki\Api\ApiMessage;
6use MediaWiki\Api\IApiMessage;
7use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
8use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
9use MediaWiki\Config\Config;
10use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook;
11use MediaWiki\Output\Hook\BeforePageDisplayHook;
12use MediaWiki\Output\OutputPage;
13use MediaWiki\Permissions\PermissionManager;
14use MediaWiki\Preferences\Hook\GetPreferencesHook;
15use MediaWiki\ResourceLoader\Context;
16use MediaWiki\Skin\Skin;
17use MediaWiki\Skin\SkinTemplate;
18use MediaWiki\Title\Title;
19use MediaWiki\User\Options\UserOptionsLookup;
20use MediaWiki\User\User;
21
22/**
23 * Hooks for WikiLove extension
24 *
25 * @ingroup Extensions
26 */
27class Hooks implements
28    GetPreferencesHook,
29    SkinTemplateNavigation__UniversalHook,
30    BeforePageDisplayHook,
31    ListDefinedTagsHook,
32    ChangeTagsListActiveHook
33{
34    public function __construct(
35        private readonly Config $config,
36        private readonly PermissionManager $permissionManager,
37        private readonly UserOptionsLookup $userOptionsLookup,
38    ) {
39    }
40
41    /**
42     * Add the preference in the user preferences with the GetPreferences hook.
43     *
44     * @param User $user
45     * @param array &$preferences
46     */
47    public function onGetPreferences( $user, &$preferences ) {
48        if ( !$this->config->get( 'WikiLoveGlobal' ) ) {
49            $preferences['wikilove-enabled'] = [
50                'type' => 'check',
51                'section' => 'editing/advancedediting',
52                'label-message' => 'wikilove-enable-preference',
53            ];
54        }
55    }
56
57    /**
58     * Adds the required module if we are on a user (talk) page.
59     *
60     * @param OutputPage $out
61     * @param Skin $skin
62     */
63    public function onBeforePageDisplay( $out, $skin ): void {
64        // WikiLove currently doesn't support Minerva.
65        if ( $skin->getSkinName() === 'minerva' ) {
66            return;
67        }
68        if (
69            !$this->config->get( 'WikiLoveGlobal' ) &&
70            !$this->userOptionsLookup->getOption( $out->getUser(), 'wikilove-enabled' )
71        ) {
72            return;
73        }
74
75        $title = self::getUserTalkPage(
76            $this->permissionManager,
77            $skin->getTitle(),
78            $skin->getUser()
79        );
80        // getUserTalkPage() returns an ApiMessage on error
81        if ( !$title instanceof ApiMessage ) {
82            $recipient = $title->getBaseText();
83
84            $out->addJsConfigVars( [ 'wikilove-recipient' => $recipient ] );
85
86            $out->addModules( 'ext.wikiLove.init' );
87            $out->addModuleStyles( 'ext.wikiLove.icon' );
88        }
89    }
90
91    /**
92     * Add a tab or an icon the new way (MediaWiki 1.18+)
93     *
94     * @param SkinTemplate $skin
95     * @param array &$links Navigation links
96     */
97    public function onSkinTemplateNavigation__Universal( $skin, &$links ): void {
98        // WikiLove currently doesn't support Minerva.
99        if ( $skin->getSkinName() === 'minerva' ) {
100            return;
101        }
102        if ( $this->showIcon( $skin ) ) {
103            $this->skinConfigViewsLinks( $skin, $links['views'] );
104        } else {
105            $this->skinConfigViewsLinks( $skin, $links['actions'] );
106        }
107    }
108
109    /**
110     * Configure views links.
111     *
112     * Helper function for SkinTemplateNavigation hooks
113     * to configure views links.
114     */
115    private function skinConfigViewsLinks( Skin $skin, array &$views ): void {
116        // If WikiLove is turned off for this user, don't display tab.
117        if (
118            !$this->config->get( 'WikiLoveGlobal' ) &&
119            !$this->userOptionsLookup->getOption( $skin->getUser(), 'wikilove-enabled' )
120        ) {
121            return;
122        }
123
124        // getUserTalkPage() returns an ApiMessage on error
125        if ( !self::getUserTalkPage(
126                $this->permissionManager,
127                $skin->getTitle(),
128                $skin->getUser()
129            ) instanceof ApiMessage
130        ) {
131            $views['wikilove'] = [
132                'text' => $skin->msg( 'wikilove-tab-text' )->text(),
133                'href' => '#',
134            ];
135            if ( $this->showIcon( $skin ) ) {
136                $views['wikilove']['icon'] = 'heart';
137                $views['wikilove']['button'] = true;
138                $views['wikilove']['primary'] = true;
139            }
140        }
141    }
142
143    /**
144     * Only show an icon when the global preference is enabled and the current skin isn't CologneBlue.
145     */
146    private function showIcon( Skin $skin ): bool {
147        return $this->config->get( 'WikiLoveTabIcon' ) &&
148            $skin->getSkinName() !== 'cologneblue';
149    }
150
151    /**
152     * Find the editable talk page of the user we want to send WikiLove to. This
153     * function also does some sense-checking to make sure we will actually
154     * be able to send WikiLove to the target.
155     *
156     * Phan false positives are suppressed
157     *
158     * @param PermissionManager $permissionManager
159     * @param Title $title The title of a user page or user talk page
160     * @param User $user the current user
161     * @return Title|IApiMessage Returns either the Title object for the talk page or an error message
162     * @suppress PhanPossiblyUndeclaredVariable
163     */
164    public static function getUserTalkPage( PermissionManager $permissionManager, Title $title, User $user ) {
165        // Exit early if the sending user isn't logged in
166        if ( !$user->isRegistered() || $user->isTemp() ) {
167            return ApiMessage::create( 'wikilove-err-not-logged-in', 'notloggedin' );
168        }
169
170        // Exit early if the page is in the wrong namespace
171        $ns = $title->getNamespace();
172        if ( $ns !== NS_USER && $ns !== NS_USER_TALK ) {
173            return ApiMessage::create( 'wikilove-err-invalid-username', 'invalidusername' );
174        }
175
176        // If we're on a subpage, get the root page title
177        $baseTitle = $title->getRootTitle();
178
179        // Users can't send WikiLove to themselves
180        if ( $user->getName() === $baseTitle->getText() ) {
181            return ApiMessage::create( 'wikilove-err-no-self-wikilove', 'no-self-wikilove' );
182        }
183
184        // Get the user talk page
185        if ( $ns === NS_USER_TALK ) {
186            // We're already on the user talk page
187            $talkTitle = $baseTitle;
188        } elseif ( $ns === NS_USER ) {
189            // We're on the user page, so retrieve the user talk page instead
190            $talkTitle = $baseTitle->getTalkPage();
191        }
192
193        // If it's a redirect, exit. We don't follow redirects since it might confuse the user or
194        // lead to an endless loop (like if the talk page redirects to the user page or a subpage).
195        // This means that the WikiLove tab will not appear on user pages or user talk pages if
196        // the user talk page is a redirect.
197        if ( $talkTitle->isRedirect() ) {
198            return ApiMessage::create( 'wikilove-err-redirect', 'isredirect' );
199        }
200
201        // Make sure we can edit the page
202        if ( !$permissionManager->quickUserCan( 'edit', $user, $talkTitle ) ) {
203            return ApiMessage::create( 'wikilove-err-cannot-edit', 'cannotedit' );
204        }
205
206        return $talkTitle;
207    }
208
209    /**
210     * ListDefinedTags hook handler
211     *
212     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ListDefinedTags
213     * @param array &$tags
214     */
215    public function onListDefinedTags( &$tags ) {
216        $tags[] = 'wikilove';
217    }
218
219    /**
220     * ChangeTagsListActive hook handler
221     *
222     * @see https://www.mediawiki.org/wiki/Manual:Hooks/ChangeTagsListActive
223     * @param array &$tags
224     */
225    public function onChangeTagsListActive( &$tags ) {
226        $tags[] = 'wikilove';
227    }
228
229    /**
230     * Generate the content of the virtual `data.json` file in the `ext.wikiLove.startup`
231     * ResourceLoader module.
232     * @param Context $context
233     * @return array JSON data
234     */
235    public static function getStartupData( Context $context ) {
236        return [
237            'whatIsThisLink' => Skin::makeInternalOrExternalUrl(
238                $context->msg( 'wikilove-what-is-this-link' )->text()
239            ),
240        ];
241    }
242
243}