Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
49.33% covered (danger)
49.33%
37 / 75
42.86% covered (danger)
42.86%
12 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
Action
49.33% covered (danger)
49.33%
37 / 75
42.86% covered (danger)
42.86%
12 / 28
271.44
0.00% covered (danger)
0.00%
0 / 1
 factory
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getActionName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContext
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 getRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOutput
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAuthority
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSkin
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWikiPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getArticle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 msg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setHookContainer
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getHookContainer
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getHookRunner
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getName
n/a
0 / 0
n/a
0 / 0
0
 getRestriction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 needsReadRights
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkCanExecute
90.48% covered (success)
90.48%
19 / 21
0.00% covered (danger)
0.00%
0 / 1
8.06
 requiresWrite
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requiresUnblock
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHeaders
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getPageTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescription
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addHelpLink
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 show
n/a
0 / 0
n/a
0 / 0
0
 useTransactionalTimeLimit
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Base classes for actions done on pages.
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
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
18 *
19 * @file
20 */
21
22use MediaWiki\Context\IContextSource;
23use MediaWiki\HookContainer\HookContainer;
24use MediaWiki\HookContainer\HookRunner;
25use MediaWiki\Language\RawMessage;
26use MediaWiki\MediaWikiServices;
27use MediaWiki\Message\Message;
28use MediaWiki\Output\OutputPage;
29use MediaWiki\Permissions\Authority;
30use MediaWiki\Request\WebRequest;
31use MediaWiki\Title\Title;
32use MediaWiki\User\User;
33
34/**
35 * @defgroup Actions Actions
36 */
37
38/**
39 * Actions are things which can be done to pages (edit, delete, rollback, etc).  They
40 * are distinct from Special Pages because an action must apply to exactly one page.
41 *
42 * To add an action in an extension, create a subclass of Action, and add the key to
43 * $wgActions.
44 *
45 * Actions generally fall into two groups: the show-a-form-then-do-something-with-the-input
46 * format (protect, delete, move, etc), and the just-do-something format (watch, rollback,
47 * patrol, etc). The FormAction and FormlessAction classes represent these two groups.
48 *
49 * @stable to extend
50 */
51abstract class Action implements MessageLocalizer {
52
53    /**
54     * @var Article
55     * @since 1.35
56     */
57    private $article;
58
59    /**
60     * IContextSource if specified; otherwise we'll use the Context from the Page
61     * @since 1.17
62     * @var IContextSource|null
63     */
64    protected $context;
65
66    /**
67     * The fields used to create the HTMLForm
68     * @since 1.17
69     * @var array
70     */
71    protected $fields;
72
73    /** @var HookContainer|null */
74    private $hookContainer;
75    /** @var HookRunner|null */
76    private $hookRunner;
77
78    /**
79     * Get an appropriate Action subclass for the given action
80     * @since 1.17
81     *
82     * @param string $action
83     * @param Article $article
84     * @param IContextSource|null $context Falls back to article's context
85     * @return Action|false|null False if the action is disabled, null
86     *     if it is not recognised
87     */
88    final public static function factory(
89        string $action,
90        Article $article,
91        IContextSource $context = null
92    ) {
93        return MediaWikiServices::getInstance()
94            ->getActionFactory()
95            ->getAction( $action, $article, $context ?? $article->getContext() );
96    }
97
98    /**
99     * Get the action that will be executed, not necessarily the one passed
100     * passed through the "action" request parameter. Actions disabled in
101     * $wgActions will be replaced by "nosuchaction".
102     *
103     * @since 1.19
104     * @param IContextSource $context
105     * @return string Action name
106     */
107    final public static function getActionName( IContextSource $context ) {
108        // Optimisation: Reuse/prime the cached value of RequestContext
109        return $context->getActionName();
110    }
111
112    /**
113     * Get the IContextSource in use here
114     * @since 1.17
115     * @return IContextSource
116     */
117    final public function getContext() {
118        if ( $this->context instanceof IContextSource ) {
119            return $this->context;
120        }
121        wfDebug( __METHOD__ . ": no context known, falling back to Article's context." );
122        return $this->getArticle()->getContext();
123    }
124
125    /**
126     * Get the WebRequest being used for this instance
127     * @since 1.17
128     *
129     * @return WebRequest
130     */
131    final public function getRequest() {
132        return $this->getContext()->getRequest();
133    }
134
135    /**
136     * Get the OutputPage being used for this instance
137     * @since 1.17
138     *
139     * @return OutputPage
140     */
141    final public function getOutput() {
142        return $this->getContext()->getOutput();
143    }
144
145    /**
146     * Shortcut to get the User being used for this instance
147     * @since 1.17
148     *
149     * @return User
150     */
151    final public function getUser() {
152        return $this->getContext()->getUser();
153    }
154
155    /**
156     * Shortcut to get the Authority executing this instance
157     *
158     * @return Authority
159     * @since 1.39
160     */
161    final public function getAuthority(): Authority {
162        return $this->getContext()->getAuthority();
163    }
164
165    /**
166     * Shortcut to get the Skin being used for this instance
167     * @since 1.17
168     *
169     * @return Skin
170     */
171    final public function getSkin() {
172        return $this->getContext()->getSkin();
173    }
174
175    /**
176     * Shortcut to get the user Language being used for this instance
177     *
178     * @return Language
179     */
180    final public function getLanguage() {
181        return $this->getContext()->getLanguage();
182    }
183
184    /**
185     * Get a WikiPage object
186     * @since 1.35
187     *
188     * @return WikiPage
189     */
190    final public function getWikiPage(): WikiPage {
191        return $this->getArticle()->getPage();
192    }
193
194    /**
195     * Get a Article object
196     * @since 1.35
197     * Overriding this method is deprecated since 1.35
198     *
199     * @return Article|ImagePage|CategoryPage
200     */
201    public function getArticle() {
202        return $this->article;
203    }
204
205    /**
206     * Shortcut to get the Title object from the page
207     * @since 1.17
208     *
209     * @return Title
210     */
211    final public function getTitle() {
212        return $this->getWikiPage()->getTitle();
213    }
214
215    /**
216     * Get a Message object with context set
217     * Parameters are the same as wfMessage()
218     *
219     * @param string|string[]|MessageSpecifier $key
220     * @param mixed ...$params
221     * @return Message
222     */
223    final public function msg( $key, ...$params ) {
224        return $this->getContext()->msg( $key, ...$params );
225    }
226
227    /**
228     * @since 1.40
229     * @internal For use by ActionFactory
230     * @param HookContainer $hookContainer
231     */
232    public function setHookContainer( HookContainer $hookContainer ) {
233        $this->hookContainer = $hookContainer;
234        $this->hookRunner = new HookRunner( $hookContainer );
235    }
236
237    /**
238     * @since 1.35
239     * @internal since 1.37
240     * @return HookContainer
241     */
242    protected function getHookContainer() {
243        if ( !$this->hookContainer ) {
244            $this->hookContainer = MediaWikiServices::getInstance()->getHookContainer();
245        }
246        return $this->hookContainer;
247    }
248
249    /**
250     * @since 1.35
251     * @internal This is for use by core only. Hook interfaces may be removed
252     *   without notice.
253     * @return HookRunner
254     */
255    protected function getHookRunner() {
256        if ( !$this->hookRunner ) {
257            $this->hookRunner = new HookRunner( $this->getHookContainer() );
258        }
259        return $this->hookRunner;
260    }
261
262    /**
263     * Only public since 1.21
264     *
265     * @stable to call
266     *
267     * @param Article $article
268     * @param IContextSource $context
269     */
270    public function __construct( Article $article, IContextSource $context ) {
271        $this->article = $article;
272        $this->context = $context;
273    }
274
275    /**
276     * Return the name of the action this object responds to
277     * @since 1.17
278     *
279     * @return string Lowercase name
280     */
281    abstract public function getName();
282
283    /**
284     * Get the permission required to perform this action.  Often, but not always,
285     * the same as the action name
286     *
287     * Implementations of this methods must always return the same value, regardless
288     * of parameters passed to the constructor or system state.
289     *
290     * @since 1.17
291     * @stable to override
292     *
293     * @return string|null
294     */
295    public function getRestriction() {
296        return null;
297    }
298
299    /**
300     * Indicates whether this action requires read rights
301     *
302     * Implementations of this methods must always return the same value, regardless
303     * of parameters passed to the constructor or system state.
304     *
305     * @since 1.38
306     * @stable to override
307     * @return bool
308     */
309    public function needsReadRights() {
310        return true;
311    }
312
313    /**
314     * Checks if the given user (identified by an object) can perform this action.  Can be
315     * overridden by sub-classes with more complicated permissions schemes.  Failures here
316     * must throw subclasses of ErrorPageError
317     * @since 1.17
318     * @stable to override
319     *
320     * @param User $user
321     * @throws UserBlockedError|ReadOnlyError|PermissionsError
322     */
323    protected function checkCanExecute( User $user ) {
324        $right = $this->getRestriction();
325        $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
326        if ( $right !== null ) {
327            $errors = $permissionManager->getPermissionErrors( $right, $user, $this->getTitle() );
328            if ( count( $errors ) ) {
329                throw new PermissionsError( $right, $errors );
330            }
331        }
332
333        // If the action requires an unblock, explicitly check the user's block.
334        $checkReplica = !$this->getRequest()->wasPosted();
335        if (
336            $this->requiresUnblock() &&
337            $permissionManager->isBlockedFrom( $user, $this->getTitle(), $checkReplica )
338        ) {
339            $block = $user->getBlock();
340            if ( $block ) {
341                throw new UserBlockedError(
342                    $block,
343                    $user,
344                    $this->getLanguage(),
345                    $this->getRequest()->getIP()
346                );
347            }
348
349            throw new PermissionsError( $this->getName(), [ 'badaccess-group0' ] );
350        }
351
352        // This should be checked at the end so that the user won't think the
353        // error is only temporary when he also don't have the rights to execute
354        // this action
355        $readOnlyMode = MediaWikiServices::getInstance()->getReadOnlyMode();
356        if ( $this->requiresWrite() && $readOnlyMode->isReadOnly() ) {
357            throw new ReadOnlyError();
358        }
359    }
360
361    /**
362     * Whether this action requires the wiki not to be locked
363     *
364     * Implementations of this methods must always return the same value, regardless
365     * of parameters passed to the constructor or system state.
366     *
367     * @since 1.17
368     * @stable to override
369     *
370     * @return bool
371     */
372    public function requiresWrite() {
373        return true;
374    }
375
376    /**
377     * Whether this action can still be executed by a blocked user.
378     *
379     * Implementations of this methods must always return the same value, regardless
380     * of parameters passed to the constructor or system state.
381     *
382     * @since 1.17
383     * @stable to override
384     *
385     * @return bool
386     */
387    public function requiresUnblock() {
388        return true;
389    }
390
391    /**
392     * Set output headers for noindexing etc.  This function will not be called through
393     * the execute() entry point, so only put UI-related stuff in here.
394     * @stable to override
395     * @since 1.17
396     */
397    protected function setHeaders() {
398        $out = $this->getOutput();
399        $out->setRobotPolicy( 'noindex,nofollow' );
400        $title = $this->getPageTitle();
401        if ( is_string( $title ) ) {
402            // T343849: deprecated
403            wfDeprecated( 'string return from Action::getPageTitle()', '1.41' );
404            $title = ( new RawMessage( '$1' ) )->rawParams( $title );
405        }
406        $out->setPageTitleMsg( $title );
407        $out->setSubtitle( $this->getDescription() );
408        $out->setArticleRelated( true );
409    }
410
411    /**
412     * Returns the name that goes in the `<h1>` page title.
413     *
414     * Since 1.41, returning a string from this method has been deprecated.
415     *
416     * @stable to override
417     * @return string|Message
418     */
419    protected function getPageTitle() {
420        return ( new RawMessage( '$1' ) )->plaintextParams( $this->getTitle()->getPrefixedText() );
421    }
422
423    /**
424     * Returns the description that goes below the `<h1>` element.
425     *
426     * @since 1.17
427     * @stable to override
428     * @return string HTML
429     */
430    protected function getDescription() {
431        return $this->msg( strtolower( $this->getName() ) )->escaped();
432    }
433
434    /**
435     * Adds help link with an icon via page indicators.
436     * Link target can be overridden by a local message containing a wikilink:
437     * the message key is: lowercase action name + '-helppage'.
438     * @param string $to Target MediaWiki.org page title or encoded URL.
439     * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
440     * @since 1.25
441     */
442    public function addHelpLink( $to, $overrideBaseUrl = false ) {
443        $lang = MediaWikiServices::getInstance()->getContentLanguage();
444        $target = $lang->lc( $this->getName() . '-helppage' );
445        $msg = $this->msg( $target );
446
447        if ( !$msg->isDisabled() ) {
448            $title = Title::newFromText( $msg->plain() );
449            if ( $title instanceof Title ) {
450                $this->getOutput()->addHelpLink( $title->getLocalURL(), true );
451            }
452        } else {
453            $this->getOutput()->addHelpLink( $to, $overrideBaseUrl );
454        }
455    }
456
457    /**
458     * The main action entry point.  Do all output for display and send it to the context
459     * output.  Do not use globals $wgOut, $wgRequest, etc, in implementations; use
460     * $this->getOutput(), etc.
461     * @since 1.17
462     *
463     * @throws ErrorPageError
464     */
465    abstract public function show();
466
467    /**
468     * Call wfTransactionalTimeLimit() if this request was POSTed
469     * @since 1.26
470     */
471    protected function useTransactionalTimeLimit() {
472        if ( $this->getRequest()->wasPosted() ) {
473            wfTransactionalTimeLimit();
474        }
475    }
476
477    /**
478     * Indicates whether this action may perform database writes
479     * @return bool
480     * @since 1.27
481     * @stable to override
482     */
483    public function doesWrites() {
484        return false;
485    }
486}