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