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 MediaWiki\Config\Config;
23use MediaWiki\Config\ConfigException;
24use MediaWiki\Content\ContentHandler;
25use MediaWiki\Content\IContentHandlerFactory;
26use MediaWiki\Context\IContextSource;
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    /** @var Title Current page title */
42    private ?Title $title;
43    /** @var Config Extension config */
44    private Config $config;
45    private Authority $performer;
46    private OutputPage $out;
47    private ContentHandler $contentHandler;
48    private SkinOptions $skinOptions;
49    private LanguagesHelper $languagesHelper;
50    private PermissionManager $permissionManager;
51    private IContentHandlerFactory $contentHandlerFactory;
52    private UserFactory $userFactory;
53    private WatchlistManager $watchlistManager;
54
55    /**
56     * Initialize internal Minerva Permissions system
57     * @param SkinOptions $skinOptions
58     * @param LanguagesHelper $languagesHelper
59     * @param PermissionManager $permissionManager
60     * @param IContentHandlerFactory $contentHandlerFactory
61     * @param UserFactory $userFactory
62     * @param WatchlistManager $watchlistManager
63     */
64    public function __construct(
65        SkinOptions $skinOptions,
66        LanguagesHelper $languagesHelper,
67        PermissionManager $permissionManager,
68        IContentHandlerFactory $contentHandlerFactory,
69        UserFactory $userFactory,
70        WatchlistManager $watchlistManager
71    ) {
72        $this->skinOptions = $skinOptions;
73        $this->languagesHelper = $languagesHelper;
74        $this->permissionManager = $permissionManager;
75        $this->contentHandlerFactory = $contentHandlerFactory;
76        $this->userFactory = $userFactory;
77        $this->watchlistManager = $watchlistManager;
78    }
79
80    /**
81     * @param IContextSource $context
82     * @return $this
83     */
84    public function setContext( IContextSource $context ): self {
85        $this->title = $context->getTitle();
86        $this->config = $context->getConfig();
87        $this->performer = $context->getAuthority();
88        $this->out = $context->getOutput();
89        // Title may be undefined in certain contexts (T179833)
90        // TODO: Check if this is still true if we always pass a context instead of using global one
91        if ( $this->title ) {
92            $this->contentHandler = $this->contentHandlerFactory->getContentHandler(
93                $this->title->getContentModel()
94            );
95        }
96        return $this;
97    }
98
99    /**
100     * Gets whether or not the action is allowed.
101     *
102     * Actions isn't allowed when:
103     * <ul>
104     *   <li>the user is on the main page</li>
105     * </ul>
106     *
107     * The "edit" action is not allowed if editing is not possible on the page
108     * @see method isCurrentPageContentModelEditable
109     *
110     * The "switch-language" is allowed if there are interlanguage links on the page,
111     * or <code>$wgMinervaAlwaysShowLanguageButton</code> is truthy.
112     *
113     * @inheritDoc
114     * @throws ConfigException
115     */
116    public function isAllowed( $action ): bool {
117        if ( !$this->title ) {
118            return false;
119        }
120
121        // T206406: Enable "Talk" or "Discussion" button on Main page, also, not forgetting
122        // the "switch-language" button. But disable "edit" and "watch" actions.
123        if ( $this->title->isMainPage() ) {
124            if ( $action === self::SWITCH_LANGUAGE ) {
125                return !$this->config->get( MainConfigNames::HideInterlanguageLinks );
126            }
127            // Only the talk page is allowed on the main page provided user is registered.
128            // talk page permission is disabled on mobile for anons
129            // https://phabricator.wikimedia.org/T54165
130            return $action === self::TALK && $this->performer->isRegistered();
131        }
132
133        if ( $action === self::TALK ) {
134            return (
135                $this->title->isTalkPage() ||
136                $this->title->canHaveTalkPage()
137            );
138        }
139
140        if ( $action === self::HISTORY && $this->title->exists() ) {
141            return $this->skinOptions->get( SkinOptions::HISTORY_IN_PAGE_ACTIONS );
142        }
143
144        if ( $action === SkinOptions::TOOLBAR_SUBMENU ) {
145            return $this->skinOptions->get( SkinOptions::TOOLBAR_SUBMENU );
146        }
147
148        if ( $action === self::EDIT_OR_CREATE ) {
149            return $this->canEditOrCreate();
150        }
151
152        if ( $action === self::CONTENT_EDIT ) {
153            return $this->isCurrentPageContentModelEditable();
154        }
155
156        if ( $action === self::WATCHABLE || $action === self::WATCH ) {
157            $isWatchable = $this->watchlistManager->isWatchable( $this->title );
158
159            if ( $action === self::WATCHABLE ) {
160                return $isWatchable;
161            }
162            if ( $action === self::WATCH ) {
163                return $isWatchable &&
164                    $this->performer->isAllowedAll( 'viewmywatchlist', 'editmywatchlist' );
165            }
166        }
167
168        if ( $action === self::SWITCH_LANGUAGE ) {
169            if ( $this->config->get( MainConfigNames::HideInterlanguageLinks ) ) {
170                return false;
171            }
172            return $this->languagesHelper->doesTitleHasLanguagesOrVariants( $this->out, $this->title ) ||
173                $this->config->get( 'MinervaAlwaysShowLanguageButton' );
174        }
175
176        if ( $action === self::MOVE ) {
177            return $this->canMove();
178        }
179
180        if ( $action === self::DELETE ) {
181            return $this->canDelete();
182        }
183
184        if ( $action === self::PROTECT ) {
185            return $this->canProtect();
186        }
187
188        // Unknown action has been passed.
189        return false;
190    }
191
192    /**
193     * @inheritDoc
194     */
195    public function isTalkAllowed(): bool {
196        return $this->isAllowed( self::TALK );
197    }
198
199    /**
200     * Checks whether the editor can handle the existing content handler type.
201     *
202     * @return bool
203     */
204    protected function isCurrentPageContentModelEditable(): bool {
205        if ( !$this->contentHandler ) {
206            return false;
207        }
208
209        if (
210            $this->contentHandler->supportsDirectEditing() &&
211            $this->contentHandler->supportsDirectApiEditing()
212        ) {
213            return true;
214        }
215
216        // For content types with custom action=edit handlers, let them do their thing
217        if ( array_key_exists( 'edit', $this->contentHandler->getActionOverrides() ?? [] ) ) {
218            return true;
219        }
220
221        return false;
222    }
223
224    /**
225     * Returns true if $title page exists and is editable or is creatable by $user as determined by
226     * quick checks.
227     * @return bool
228     */
229    private function canEditOrCreate(): bool {
230        if ( !$this->title ) {
231            return false;
232        }
233
234        $userQuickEditCheck =
235            $this->performer->probablyCan( 'edit', $this->title ) && (
236                $this->title->exists() ||
237                $this->performer->probablyCan( 'create', $this->title )
238            );
239        if ( $this->performer->isRegistered() ) {
240            $legacyUser = $this->userFactory->newFromAuthority( $this->performer );
241            $blocked = $this->permissionManager->isBlockedFrom(
242                $legacyUser, $this->title, true
243            );
244        } else {
245            $blocked = false;
246        }
247        return $this->isCurrentPageContentModelEditable() && $userQuickEditCheck && !$blocked;
248    }
249
250    /**
251     * Checks whether the user has the permissions to move the current page.
252     *
253     * @return bool
254     */
255    private function canMove(): bool {
256        if ( !$this->title ) {
257            return false;
258        }
259
260        return $this->performer->probablyCan( 'move', $this->title )
261            && $this->title->exists();
262    }
263
264    /**
265     * Checks whether the user has the permissions to delete the current page.
266     *
267     * @return bool
268     */
269    private function canDelete(): bool {
270        if ( !$this->title ) {
271            return false;
272        }
273
274        return $this->performer->probablyCan( 'delete', $this->title )
275            && $this->title->exists();
276    }
277
278    /**
279     * Checks whether the user has the permissions to change the protections status of the current page.
280     *
281     * @return bool
282     */
283    private function canProtect(): bool {
284        if ( !$this->title ) {
285            return false;
286        }
287
288        return $this->performer->probablyCan( 'protect', $this->title )
289            && $this->title->exists();
290    }
291}