Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
37.50% covered (danger)
37.50%
33 / 88
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
MinervaPagePermissions
37.50% covered (danger)
37.50%
33 / 88
0.00% covered (danger)
0.00%
0 / 9
610.50
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 setContext
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 isAllowed
86.84% covered (warning)
86.84%
33 / 38
0.00% covered (danger)
0.00%
0 / 1
24.21
 isTalkAllowed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isCurrentPageContentModelEditable
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 canEditOrCreate
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 canMove
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 canDelete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 canProtect
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20namespace MediaWiki\Minerva\Permissions;
21
22use ContentHandler;
23use IContextSource;
24use MediaWiki\Config\Config;
25use MediaWiki\Config\ConfigException;
26use MediaWiki\Content\IContentHandlerFactory;
27use MediaWiki\MainConfigNames;
28use MediaWiki\Minerva\LanguagesHelper;
29use MediaWiki\Minerva\SkinOptions;
30use MediaWiki\Output\OutputPage;
31use MediaWiki\Permissions\Authority;
32use MediaWiki\Permissions\PermissionManager;
33use MediaWiki\Title\Title;
34use MediaWiki\User\UserFactory;
35use MediaWiki\Watchlist\WatchlistManager;
36
37/**
38 * A wrapper for all available Minerva permissions.
39 */
40final class MinervaPagePermissions implements IMinervaPagePermissions {
41    /**
42     * @var Title Current page title
43     */
44    private $title;
45    /**
46     * @var Config Extension config
47     */
48    private $config;
49
50    /**
51     * @var Authority
52     */
53    private $performer;
54
55    private OutputPage $out;
56
57    /**
58     * @var ContentHandler
59     */
60    private $contentHandler;
61
62    /**
63     * @var SkinOptions Minerva skin options
64     */
65    private $skinOptions;
66
67    /**
68     * @var LanguagesHelper
69     */
70    private $languagesHelper;
71
72    /**
73     * @var PermissionManager
74     */
75    private $permissionManager;
76
77    /**
78     * @var IContentHandlerFactory
79     */
80    private $contentHandlerFactory;
81
82    private UserFactory $userFactory;
83
84    private WatchlistManager $watchlistManager;
85
86    /**
87     * Initialize internal Minerva Permissions system
88     * @param SkinOptions $skinOptions
89     * @param LanguagesHelper $languagesHelper
90     * @param PermissionManager $permissionManager
91     * @param IContentHandlerFactory $contentHandlerFactory
92     * @param UserFactory $userFactory
93     * @param WatchlistManager $watchlistManager
94     */
95    public function __construct(
96        SkinOptions $skinOptions,
97        LanguagesHelper $languagesHelper,
98        PermissionManager $permissionManager,
99        IContentHandlerFactory $contentHandlerFactory,
100        UserFactory $userFactory,
101        WatchlistManager $watchlistManager
102    ) {
103        $this->skinOptions = $skinOptions;
104        $this->languagesHelper = $languagesHelper;
105        $this->permissionManager = $permissionManager;
106        $this->contentHandlerFactory = $contentHandlerFactory;
107        $this->userFactory = $userFactory;
108        $this->watchlistManager = $watchlistManager;
109    }
110
111    /**
112     * @param IContextSource $context
113     * @return $this
114     */
115    public function setContext( IContextSource $context ) {
116        $this->title = $context->getTitle();
117        $this->config = $context->getConfig();
118        $this->performer = $context->getAuthority();
119        $this->out = $context->getOutput();
120        // Title may be undefined in certain contexts (T179833)
121        // TODO: Check if this is still true if we always pass a context instead of using global one
122        if ( $this->title ) {
123            $this->contentHandler = $this->contentHandlerFactory->getContentHandler(
124                $this->title->getContentModel()
125            );
126        }
127        return $this;
128    }
129
130    /**
131     * Gets whether or not the action is allowed.
132     *
133     * Actions isn't allowed when:
134     * <ul>
135     *   <li>the user is on the main page</li>
136     * </ul>
137     *
138     * The "edit" action is not allowed if editing is not possible on the page
139     * @see method isCurrentPageContentModelEditable
140     *
141     * The "switch-language" is allowed if there are interlanguage links on the page,
142     * or <code>$wgMinervaAlwaysShowLanguageButton</code> is truthy.
143     *
144     * @inheritDoc
145     * @throws ConfigException
146     */
147    public function isAllowed( $action ) {
148        if ( !$this->title ) {
149            return false;
150        }
151
152        // T206406: Enable "Talk" or "Discussion" button on Main page, also, not forgetting
153        // the "switch-language" button. But disable "edit" and "watch" actions.
154        if ( $this->title->isMainPage() ) {
155            if ( $action === self::SWITCH_LANGUAGE ) {
156                return !$this->config->get( MainConfigNames::HideInterlanguageLinks );
157            }
158            // Only the talk page is allowed on the main page provided user is registered.
159            // talk page permission is disabled on mobile for anons
160            // https://phabricator.wikimedia.org/T54165
161            return $action === self::TALK && $this->performer->isRegistered();
162        }
163
164        if ( $action === self::TALK ) {
165            return (
166                $this->title->isTalkPage() ||
167                $this->title->canHaveTalkPage()
168            );
169        }
170
171        if ( $action === self::HISTORY && $this->title->exists() ) {
172            return $this->skinOptions->get( SkinOptions::HISTORY_IN_PAGE_ACTIONS );
173        }
174
175        if ( $action === SkinOptions::TOOLBAR_SUBMENU ) {
176            return $this->skinOptions->get( SkinOptions::TOOLBAR_SUBMENU );
177        }
178
179        if ( $action === self::EDIT_OR_CREATE ) {
180            return $this->canEditOrCreate();
181        }
182
183        if ( $action === self::CONTENT_EDIT ) {
184            return $this->isCurrentPageContentModelEditable();
185        }
186
187        if ( $action === self::WATCHABLE || $action === self::WATCH ) {
188            $isWatchable = $this->watchlistManager->isWatchable( $this->title );
189
190            if ( $action === self::WATCHABLE ) {
191                return $isWatchable;
192            }
193            if ( $action === self::WATCH ) {
194                return $isWatchable &&
195                    $this->performer->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' );
196            }
197        }
198
199        if ( $action === self::SWITCH_LANGUAGE ) {
200            if ( $this->config->get( MainConfigNames::HideInterlanguageLinks ) ) {
201                return false;
202            }
203            return $this->languagesHelper->doesTitleHasLanguagesOrVariants( $this->out, $this->title ) ||
204                $this->config->get( 'MinervaAlwaysShowLanguageButton' );
205        }
206
207        if ( $action === self::MOVE ) {
208            return $this->canMove();
209        }
210
211        if ( $action === self::DELETE ) {
212            return $this->canDelete();
213        }
214
215        if ( $action === self::PROTECT ) {
216            return $this->canProtect();
217        }
218
219        // Unknown action has been passed.
220        return false;
221    }
222
223    /**
224     * @inheritDoc
225     */
226    public function isTalkAllowed() {
227        return $this->isAllowed( self::TALK );
228    }
229
230    /**
231     * Checks whether the editor can handle the existing content handler type.
232     *
233     * @return bool
234     */
235    protected function isCurrentPageContentModelEditable() {
236        if ( !$this->contentHandler ) {
237            return false;
238        }
239
240        if (
241            $this->contentHandler->supportsDirectEditing() &&
242            $this->contentHandler->supportsDirectApiEditing()
243        ) {
244            return true;
245        }
246
247        // For content types with custom action=edit handlers, let them do their thing
248        if ( array_key_exists( 'edit', $this->contentHandler->getActionOverrides() ?? [] ) ) {
249            return true;
250        }
251
252        return false;
253    }
254
255    /**
256     * Returns true if $title page exists and is editable or is creatable by $user as determined by
257     * quick checks.
258     * @return bool
259     */
260    private function canEditOrCreate() {
261        if ( !$this->title ) {
262            return false;
263        }
264
265        $userQuickEditCheck =
266            $this->performer->probablyCan( 'edit', $this->title ) && (
267                $this->title->exists() ||
268                $this->performer->probablyCan( 'create', $this->title )
269            );
270        if ( $this->performer->isRegistered() ) {
271            $legacyUser = $this->userFactory->newFromAuthority( $this->performer );
272            $blocked = $this->permissionManager->isBlockedFrom(
273                $legacyUser, $this->title, true
274            );
275        } else {
276            $blocked = false;
277        }
278        return $this->isCurrentPageContentModelEditable() && $userQuickEditCheck && !$blocked;
279    }
280
281    /**
282     * Checks whether the user has the permissions to move the current page.
283     *
284     * @return bool
285     */
286    private function canMove() {
287        if ( !$this->title ) {
288            return false;
289        }
290
291        return $this->performer->probablyCan( 'move', $this->title )
292            && $this->title->exists();
293    }
294
295    /**
296     * Checks whether the user has the permissions to delete the current page.
297     *
298     * @return bool
299     */
300    private function canDelete() {
301        if ( !$this->title ) {
302            return false;
303        }
304
305        return $this->performer->probablyCan( 'delete', $this->title )
306            && $this->title->exists();
307    }
308
309    /**
310     * Checks whether the user has the permissions to change the protections status of the current page.
311     *
312     * @return bool
313     */
314    private function canProtect() {
315        if ( !$this->title ) {
316            return false;
317        }
318
319        return $this->performer->probablyCan( 'protect', $this->title )
320            && $this->title->exists();
321    }
322}