Completed
Branch master (939199)
by
unknown
39:35
created

includes/actions/Action.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
22
/**
23
 * @defgroup Actions Action done on pages
24
 */
25
26
/**
27
 * Actions are things which can be done to pages (edit, delete, rollback, etc).  They
28
 * are distinct from Special Pages because an action must apply to exactly one page.
29
 *
30
 * To add an action in an extension, create a subclass of Action, and add the key to
31
 * $wgActions.  There is also the deprecated UnknownAction hook
32
 *
33
 * Actions generally fall into two groups: the show-a-form-then-do-something-with-the-input
34
 * format (protect, delete, move, etc), and the just-do-something format (watch, rollback,
35
 * patrol, etc). The FormAction and FormlessAction classes represent these two groups.
36
 */
37
abstract class Action {
38
39
	/**
40
	 * Page on which we're performing the action
41
	 * @since 1.17
42
	 * @var WikiPage|Article|ImagePage|CategoryPage|Page $page
43
	 */
44
	protected $page;
45
46
	/**
47
	 * IContextSource if specified; otherwise we'll use the Context from the Page
48
	 * @since 1.17
49
	 * @var IContextSource $context
50
	 */
51
	protected $context;
52
53
	/**
54
	 * The fields used to create the HTMLForm
55
	 * @since 1.17
56
	 * @var array $fields
57
	 */
58
	protected $fields;
59
60
	/**
61
	 * Get the Action subclass which should be used to handle this action, false if
62
	 * the action is disabled, or null if it's not recognised
63
	 * @param string $action
64
	 * @param array $overrides
65
	 * @return bool|null|string|callable|Action
66
	 */
67
	final private static function getClass( $action, array $overrides ) {
68
		global $wgActions;
69
		$action = strtolower( $action );
70
71
		if ( !isset( $wgActions[$action] ) ) {
72
			return null;
73
		}
74
75
		if ( $wgActions[$action] === false ) {
76
			return false;
77
		} elseif ( $wgActions[$action] === true && isset( $overrides[$action] ) ) {
78
			return $overrides[$action];
79
		} elseif ( $wgActions[$action] === true ) {
80
			return ucfirst( $action ) . 'Action';
81
		} else {
82
			return $wgActions[$action];
83
		}
84
	}
85
86
	/**
87
	 * Get an appropriate Action subclass for the given action
88
	 * @since 1.17
89
	 * @param string $action
90
	 * @param Page $page
91
	 * @param IContextSource|null $context
92
	 * @return Action|bool|null False if the action is disabled, null
93
	 *     if it is not recognised
94
	 */
95
	final public static function factory( $action, Page $page, IContextSource $context = null ) {
96
		$classOrCallable = self::getClass( $action, $page->getActionOverrides() );
97
98
		if ( is_string( $classOrCallable ) ) {
99
			if ( !class_exists( $classOrCallable ) ) {
100
				return false;
101
			}
102
			$obj = new $classOrCallable( $page, $context );
103
			return $obj;
104
		}
105
106
		if ( is_callable( $classOrCallable ) ) {
107
			return call_user_func_array( $classOrCallable, [ $page, $context ] );
108
		}
109
110
		return $classOrCallable;
111
	}
112
113
	/**
114
	 * Get the action that will be executed, not necessarily the one passed
115
	 * passed through the "action" request parameter. Actions disabled in
116
	 * $wgActions will be replaced by "nosuchaction".
117
	 *
118
	 * @since 1.19
119
	 * @param IContextSource $context
120
	 * @return string Action name
121
	 */
122
	final public static function getActionName( IContextSource $context ) {
123
		global $wgActions;
124
125
		$request = $context->getRequest();
126
		$actionName = $request->getVal( 'action', 'view' );
127
128
		// Check for disabled actions
129
		if ( isset( $wgActions[$actionName] ) && $wgActions[$actionName] === false ) {
130
			$actionName = 'nosuchaction';
131
		}
132
133
		// Workaround for bug #20966: inability of IE to provide an action dependent
134
		// on which submit button is clicked.
135
		if ( $actionName === 'historysubmit' ) {
136 View Code Duplication
			if ( $request->getBool( 'revisiondelete' ) ) {
137
				$actionName = 'revisiondelete';
138
			} elseif ( $request->getBool( 'editchangetags' ) ) {
139
				$actionName = 'editchangetags';
140
			} else {
141
				$actionName = 'view';
142
			}
143
		} elseif ( $actionName == 'editredlink' ) {
144
			$actionName = 'edit';
145
		}
146
147
		// Trying to get a WikiPage for NS_SPECIAL etc. will result
148
		// in WikiPage::factory throwing "Invalid or virtual namespace -1 given."
149
		// For SpecialPages et al, default to action=view.
150
		if ( !$context->canUseWikiPage() ) {
151
			return 'view';
152
		}
153
154
		$action = Action::factory( $actionName, $context->getWikiPage(), $context );
155
		if ( $action instanceof Action ) {
156
			return $action->getName();
157
		}
158
159
		return 'nosuchaction';
160
	}
161
162
	/**
163
	 * Check if a given action is recognised, even if it's disabled
164
	 * @since 1.17
165
	 *
166
	 * @param string $name Name of an action
167
	 * @return bool
168
	 */
169
	final public static function exists( $name ) {
170
		return self::getClass( $name, [] ) !== null;
171
	}
172
173
	/**
174
	 * Get the IContextSource in use here
175
	 * @since 1.17
176
	 * @return IContextSource
177
	 */
178
	final public function getContext() {
179
		if ( $this->context instanceof IContextSource ) {
180
			return $this->context;
181
		} elseif ( $this->page instanceof Article ) {
182
			// NOTE: $this->page can be a WikiPage, which does not have a context.
183
			wfDebug( __METHOD__ . ": no context known, falling back to Article's context.\n" );
184
			return $this->page->getContext();
185
		}
186
187
		wfWarn( __METHOD__ . ': no context known, falling back to RequestContext::getMain().' );
188
		return RequestContext::getMain();
189
	}
190
191
	/**
192
	 * Get the WebRequest being used for this instance
193
	 * @since 1.17
194
	 *
195
	 * @return WebRequest
196
	 */
197
	final public function getRequest() {
198
		return $this->getContext()->getRequest();
199
	}
200
201
	/**
202
	 * Get the OutputPage being used for this instance
203
	 * @since 1.17
204
	 *
205
	 * @return OutputPage
206
	 */
207
	final public function getOutput() {
208
		return $this->getContext()->getOutput();
209
	}
210
211
	/**
212
	 * Shortcut to get the User being used for this instance
213
	 * @since 1.17
214
	 *
215
	 * @return User
216
	 */
217
	final public function getUser() {
218
		return $this->getContext()->getUser();
219
	}
220
221
	/**
222
	 * Shortcut to get the Skin being used for this instance
223
	 * @since 1.17
224
	 *
225
	 * @return Skin
226
	 */
227
	final public function getSkin() {
228
		return $this->getContext()->getSkin();
229
	}
230
231
	/**
232
	 * Shortcut to get the user Language being used for this instance
233
	 *
234
	 * @return Language
235
	 */
236
	final public function getLanguage() {
237
		return $this->getContext()->getLanguage();
238
	}
239
240
	/**
241
	 * Shortcut to get the Title object from the page
242
	 * @since 1.17
243
	 *
244
	 * @return Title
245
	 */
246
	final public function getTitle() {
247
		return $this->page->getTitle();
248
	}
249
250
	/**
251
	 * Get a Message object with context set
252
	 * Parameters are the same as wfMessage()
253
	 *
254
	 * @return Message
255
	 */
256
	final public function msg() {
257
		$params = func_get_args();
258
		return call_user_func_array( [ $this->getContext(), 'msg' ], $params );
259
	}
260
261
	/**
262
	 * Constructor.
263
	 *
264
	 * Only public since 1.21
265
	 *
266
	 * @param Page $page
267
	 * @param IContextSource|null $context
268
	 */
269
	public function __construct( Page $page, IContextSource $context = null ) {
270
		if ( $context === null ) {
271
			wfWarn( __METHOD__ . ' called without providing a Context object.' );
272
			// NOTE: We could try to initialize $context using $page->getContext(),
273
			//      if $page is an Article. That however seems to not work seamlessly.
274
		}
275
276
		$this->page = $page;
277
		$this->context = $context;
278
	}
279
280
	/**
281
	 * Return the name of the action this object responds to
282
	 * @since 1.17
283
	 *
284
	 * @return string Lowercase name
285
	 */
286
	abstract public function getName();
287
288
	/**
289
	 * Get the permission required to perform this action.  Often, but not always,
290
	 * the same as the action name
291
	 * @since 1.17
292
	 *
293
	 * @return string|null
294
	 */
295
	public function getRestriction() {
296
		return null;
297
	}
298
299
	/**
300
	 * Checks if the given user (identified by an object) can perform this action.  Can be
301
	 * overridden by sub-classes with more complicated permissions schemes.  Failures here
302
	 * must throw subclasses of ErrorPageError
303
	 * @since 1.17
304
	 *
305
	 * @param User $user The user to check, or null to use the context user
306
	 * @throws UserBlockedError|ReadOnlyError|PermissionsError
307
	 */
308
	protected function checkCanExecute( User $user ) {
309
		$right = $this->getRestriction();
310
		if ( $right !== null ) {
311
			$errors = $this->getTitle()->getUserPermissionsErrors( $right, $user );
312
			if ( count( $errors ) ) {
313
				throw new PermissionsError( $right, $errors );
314
			}
315
		}
316
317
		if ( $this->requiresUnblock() && $user->isBlocked() ) {
318
			$block = $user->getBlock();
319
			throw new UserBlockedError( $block );
0 ignored issues
show
It seems like $block defined by $user->getBlock() on line 318 can be null; however, UserBlockedError::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
320
		}
321
322
		// This should be checked at the end so that the user won't think the
323
		// error is only temporary when he also don't have the rights to execute
324
		// this action
325
		if ( $this->requiresWrite() && wfReadOnly() ) {
326
			throw new ReadOnlyError();
327
		}
328
	}
329
330
	/**
331
	 * Whether this action requires the wiki not to be locked
332
	 * @since 1.17
333
	 *
334
	 * @return bool
335
	 */
336
	public function requiresWrite() {
337
		return true;
338
	}
339
340
	/**
341
	 * Whether this action can still be executed by a blocked user
342
	 * @since 1.17
343
	 *
344
	 * @return bool
345
	 */
346
	public function requiresUnblock() {
347
		return true;
348
	}
349
350
	/**
351
	 * Set output headers for noindexing etc.  This function will not be called through
352
	 * the execute() entry point, so only put UI-related stuff in here.
353
	 * @since 1.17
354
	 */
355
	protected function setHeaders() {
356
		$out = $this->getOutput();
357
		$out->setRobotPolicy( "noindex,nofollow" );
358
		$out->setPageTitle( $this->getPageTitle() );
359
		$out->setSubtitle( $this->getDescription() );
360
		$out->setArticleRelated( true );
361
	}
362
363
	/**
364
	 * Returns the name that goes in the \<h1\> page title
365
	 *
366
	 * @return string
367
	 */
368
	protected function getPageTitle() {
369
		return $this->getTitle()->getPrefixedText();
370
	}
371
372
	/**
373
	 * Returns the description that goes below the \<h1\> tag
374
	 * @since 1.17
375
	 *
376
	 * @return string HTML
377
	 */
378
	protected function getDescription() {
379
		return $this->msg( strtolower( $this->getName() ) )->escaped();
380
	}
381
382
	/**
383
	 * Adds help link with an icon via page indicators.
384
	 * Link target can be overridden by a local message containing a wikilink:
385
	 * the message key is: lowercase action name + '-helppage'.
386
	 * @param string $to Target MediaWiki.org page title or encoded URL.
387
	 * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
388
	 * @since 1.25
389
	 */
390 View Code Duplication
	public function addHelpLink( $to, $overrideBaseUrl = false ) {
391
		global $wgContLang;
392
		$msg = wfMessage( $wgContLang->lc(
393
			Action::getActionName( $this->getContext() )
394
			) . '-helppage' );
395
396
		if ( !$msg->isDisabled() ) {
397
			$helpUrl = Skin::makeUrl( $msg->plain() );
398
			$this->getOutput()->addHelpLink( $helpUrl, true );
399
		} else {
400
			$this->getOutput()->addHelpLink( $to, $overrideBaseUrl );
401
		}
402
	}
403
404
	/**
405
	 * The main action entry point.  Do all output for display and send it to the context
406
	 * output.  Do not use globals $wgOut, $wgRequest, etc, in implementations; use
407
	 * $this->getOutput(), etc.
408
	 * @since 1.17
409
	 *
410
	 * @throws ErrorPageError
411
	 */
412
	abstract public function show();
413
414
	/**
415
	 * Call wfTransactionalTimeLimit() if this request was POSTed
416
	 * @since 1.26
417
	 */
418
	protected function useTransactionalTimeLimit() {
419
		if ( $this->getRequest()->wasPosted() ) {
420
			wfTransactionalTimeLimit();
421
		}
422
	}
423
424
	/**
425
	 * Indicates whether this action may perform database writes
426
	 * @return bool
427
	 * @since 1.27
428
	 */
429
	public function doesWrites() {
430
		return false;
431
	}
432
}
433