Completed
Branch master (62f6c6)
by
unknown
21:31
created

ApiEditPage   F

Complexity

Total Complexity 115

Size/Duplication

Total Lines 631
Duplicated Lines 2.22 %

Coupling/Cohesion

Components 1
Dependencies 20

Importance

Changes 0
Metric Value
dl 14
loc 631
rs 1.481
c 0
b 0
f 0
wmc 115
lcom 1
cbo 20

7 Methods

Rating   Name   Duplication   Size   Complexity  
F execute() 14 520 109
A mustBePosted() 0 3 1
A isWriteMode() 0 3 1
B getAllowedParams() 0 78 1
A needsToken() 0 3 1
A getExamplesMessages() 0 13 1
A getHelpUrls() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ApiEditPage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApiEditPage, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 *
4
 *
5
 * Created on August 16, 2007
6
 *
7
 * Copyright © 2007 Iker Labarga "<Firstname><Lastname>@gmail.com"
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 2 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License along
20
 * with this program; if not, write to the Free Software Foundation, Inc.,
21
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
 * http://www.gnu.org/copyleft/gpl.html
23
 *
24
 * @file
25
 */
26
27
/**
28
 * A module that allows for editing and creating pages.
29
 *
30
 * Currently, this wraps around the EditPage class in an ugly way,
31
 * EditPage.php should be rewritten to provide a cleaner interface,
32
 * see T20654 if you're inspired to fix this.
33
 *
34
 * @ingroup API
35
 */
36
class ApiEditPage extends ApiBase {
37
	public function execute() {
38
		$this->useTransactionalTimeLimit();
39
40
		$user = $this->getUser();
41
		$params = $this->extractRequestParams();
42
43
		if ( is_null( $params['text'] ) && is_null( $params['appendtext'] ) &&
44
			is_null( $params['prependtext'] ) &&
45
			$params['undo'] == 0
46
		) {
47
			$this->dieUsageMsg( 'missingtext' );
48
		}
49
50
		$pageObj = $this->getTitleOrPageId( $params );
51
		$titleObj = $pageObj->getTitle();
52
		$apiResult = $this->getResult();
53
54
		if ( $params['redirect'] ) {
55
			if ( $params['prependtext'] === null && $params['appendtext'] === null
56
				&& $params['section'] !== 'new'
57
			) {
58
				$this->dieUsage( 'You have attempted to edit using the "redirect"-following'
59
					. ' mode, which must be used in conjuction with section=new, prependtext'
60
					. ', or appendtext.', 'redirect-appendonly' );
61
			}
62
			if ( $titleObj->isRedirect() ) {
63
				$oldTitle = $titleObj;
64
65
				$titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST )
66
					->getContent( Revision::FOR_THIS_USER, $user )
67
					->getRedirectChain();
68
				// array_shift( $titles );
69
70
				$redirValues = [];
71
72
				/** @var $newTitle Title */
73
				foreach ( $titles as $id => $newTitle ) {
74
75
					if ( !isset( $titles[$id - 1] ) ) {
76
						$titles[$id - 1] = $oldTitle;
77
					}
78
79
					$redirValues[] = [
80
						'from' => $titles[$id - 1]->getPrefixedText(),
81
						'to' => $newTitle->getPrefixedText()
82
					];
83
84
					$titleObj = $newTitle;
85
				}
86
87
				ApiResult::setIndexedTagName( $redirValues, 'r' );
88
				$apiResult->addValue( null, 'redirects', $redirValues );
89
90
				// Since the page changed, update $pageObj
91
				$pageObj = WikiPage::factory( $titleObj );
92
			}
93
		}
94
95
		if ( !isset( $params['contentmodel'] ) || $params['contentmodel'] == '' ) {
96
			$contentHandler = $pageObj->getContentHandler();
97
		} else {
98
			$contentHandler = ContentHandler::getForModelID( $params['contentmodel'] );
99
		}
100
101
		$name = $titleObj->getPrefixedDBkey();
102
		$model = $contentHandler->getModelID();
103
104
		if ( $params['undo'] > 0 ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
105
			// allow undo via api
106
		} elseif ( $contentHandler->supportsDirectApiEditing() === false ) {
107
			$this->dieUsage(
108
				"Direct editing via API is not supported for content model $model used by $name",
109
				'no-direct-editing'
110
			);
111
		}
112
113
		if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) {
114
			$params['contentformat'] = $contentHandler->getDefaultFormat();
115
		}
116
117
		$contentFormat = $params['contentformat'];
118
119
		if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
120
121
			$this->dieUsage( "The requested format $contentFormat is not supported for content model " .
122
				" $model used by $name", 'badformat' );
123
		}
124
125
		if ( $params['createonly'] && $titleObj->exists() ) {
126
			$this->dieUsageMsg( 'createonly-exists' );
127
		}
128
		if ( $params['nocreate'] && !$titleObj->exists() ) {
129
			$this->dieUsageMsg( 'nocreate-missing' );
130
		}
131
132
		// Now let's check whether we're even allowed to do this
133
		$errors = $titleObj->getUserPermissionsErrors( 'edit', $user );
134
		if ( !$titleObj->exists() ) {
135
			$errors = array_merge( $errors, $titleObj->getUserPermissionsErrors( 'create', $user ) );
136
		}
137
		if ( count( $errors ) ) {
138
			if ( is_array( $errors[0] ) ) {
139
				switch ( $errors[0][0] ) {
140
					case 'blockedtext':
141
						$this->dieUsage(
142
							'You have been blocked from editing',
143
							'blocked',
144
							0,
145
							[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
0 ignored issues
show
Bug introduced by
It seems like $user->getBlock() can be null; however, getBlockInfo() 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...
146
						);
147
						break;
148
					case 'autoblockedtext':
149
						$this->dieUsage(
150
							'Your IP address has been blocked automatically, because it was used by a blocked user',
151
							'autoblocked',
152
							0,
153
							[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
0 ignored issues
show
Bug introduced by
It seems like $user->getBlock() can be null; however, getBlockInfo() 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...
154
						);
155
						break;
156
					default:
157
						$this->dieUsageMsg( $errors[0] );
158
				}
159
			} else {
160
				$this->dieUsageMsg( $errors[0] );
161
			}
162
		}
163
164
		$toMD5 = $params['text'];
165
		if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) ) {
166
			$content = $pageObj->getContent();
167
168
			if ( !$content ) {
169
				if ( $titleObj->getNamespace() == NS_MEDIAWIKI ) {
170
					# If this is a MediaWiki:x message, then load the messages
171
					# and return the message value for x.
172
					$text = $titleObj->getDefaultMessageText();
173
					if ( $text === false ) {
174
						$text = '';
175
					}
176
177
					try {
178
						$content = ContentHandler::makeContent( $text, $this->getTitle() );
179
					} catch ( MWContentSerializationException $ex ) {
180
						$this->dieUsage( $ex->getMessage(), 'parseerror' );
181
182
						return;
183
					}
184
				} else {
185
					# Otherwise, make a new empty content.
186
					$content = $contentHandler->makeEmptyContent();
187
				}
188
			}
189
190
			// @todo Add support for appending/prepending to the Content interface
191
192
			if ( !( $content instanceof TextContent ) ) {
193
				$mode = $contentHandler->getModelID();
194
				$this->dieUsage( "Can't append to pages using content model $mode", 'appendnotsupported' );
195
			}
196
197
			if ( !is_null( $params['section'] ) ) {
198
				if ( !$contentHandler->supportsSections() ) {
199
					$modelName = $contentHandler->getModelID();
200
					$this->dieUsage(
201
						"Sections are not supported for this content model: $modelName.",
202
						'sectionsnotsupported'
203
					);
204
				}
205
206
				if ( $params['section'] == 'new' ) {
207
					// DWIM if they're trying to prepend/append to a new section.
208
					$content = null;
209
				} else {
210
					// Process the content for section edits
211
					$section = $params['section'];
212
					$content = $content->getSection( $section );
213
214
					if ( !$content ) {
215
						$this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
216
					}
217
				}
218
			}
219
220
			if ( !$content ) {
221
				$text = '';
222
			} else {
223
				$text = $content->serialize( $contentFormat );
224
			}
225
226
			$params['text'] = $params['prependtext'] . $text . $params['appendtext'];
227
			$toMD5 = $params['prependtext'] . $params['appendtext'];
228
		}
229
230
		if ( $params['undo'] > 0 ) {
231
			if ( $params['undoafter'] > 0 ) {
232
				if ( $params['undo'] < $params['undoafter'] ) {
233
					list( $params['undo'], $params['undoafter'] ) =
234
						[ $params['undoafter'], $params['undo'] ];
235
				}
236
				$undoafterRev = Revision::newFromId( $params['undoafter'] );
237
			}
238
			$undoRev = Revision::newFromId( $params['undo'] );
239 View Code Duplication
			if ( is_null( $undoRev ) || $undoRev->isDeleted( Revision::DELETED_TEXT ) ) {
240
				$this->dieUsageMsg( [ 'nosuchrevid', $params['undo'] ] );
241
			}
242
243
			if ( $params['undoafter'] == 0 ) {
244
				$undoafterRev = $undoRev->getPrevious();
245
			}
246 View Code Duplication
			if ( is_null( $undoafterRev ) || $undoafterRev->isDeleted( Revision::DELETED_TEXT ) ) {
0 ignored issues
show
Bug introduced by
The variable $undoafterRev does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
247
				$this->dieUsageMsg( [ 'nosuchrevid', $params['undoafter'] ] );
248
			}
249
250 View Code Duplication
			if ( $undoRev->getPage() != $pageObj->getId() ) {
251
				$this->dieUsageMsg( [ 'revwrongpage', $undoRev->getId(),
252
					$titleObj->getPrefixedText() ] );
253
			}
254 View Code Duplication
			if ( $undoafterRev->getPage() != $pageObj->getId() ) {
255
				$this->dieUsageMsg( [ 'revwrongpage', $undoafterRev->getId(),
256
					$titleObj->getPrefixedText() ] );
257
			}
258
259
			$newContent = $contentHandler->getUndoContent(
260
				$pageObj->getRevision(),
261
				$undoRev,
262
				$undoafterRev
263
			);
264
265
			if ( !$newContent ) {
266
				$this->dieUsageMsg( 'undo-failure' );
267
			}
268
269
			$params['text'] = $newContent->serialize( $params['contentformat'] );
270
271
			// If no summary was given and we only undid one rev,
272
			// use an autosummary
273
			if ( is_null( $params['summary'] ) &&
274
				$titleObj->getNextRevisionID( $undoafterRev->getId() ) == $params['undo']
275
			) {
276
				$params['summary'] = wfMessage( 'undo-summary' )
277
					->params( $params['undo'], $undoRev->getUserText() )->inContentLanguage()->text();
278
			}
279
		}
280
281
		// See if the MD5 hash checks out
282
		if ( !is_null( $params['md5'] ) && md5( $toMD5 ) !== $params['md5'] ) {
283
			$this->dieUsageMsg( 'hashcheckfailed' );
284
		}
285
286
		// EditPage wants to parse its stuff from a WebRequest
287
		// That interface kind of sucks, but it's workable
288
		$requestArray = [
289
			'wpTextbox1' => $params['text'],
290
			'format' => $contentFormat,
291
			'model' => $contentHandler->getModelID(),
292
			'wpEditToken' => $params['token'],
293
			'wpIgnoreBlankSummary' => true,
294
			'wpIgnoreBlankArticle' => true,
295
			'wpIgnoreSelfRedirect' => true,
296
			'bot' => $params['bot'],
297
		];
298
299
		if ( !is_null( $params['summary'] ) ) {
300
			$requestArray['wpSummary'] = $params['summary'];
301
		}
302
303
		if ( !is_null( $params['sectiontitle'] ) ) {
304
			$requestArray['wpSectionTitle'] = $params['sectiontitle'];
305
		}
306
307
		// TODO: Pass along information from 'undoafter' as well
308
		if ( $params['undo'] > 0 ) {
309
			$requestArray['wpUndidRevision'] = $params['undo'];
310
		}
311
312
		// Watch out for basetimestamp == '' or '0'
313
		// It gets treated as NOW, almost certainly causing an edit conflict
314
		if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
315
			$requestArray['wpEdittime'] = $params['basetimestamp'];
316
		} else {
317
			$requestArray['wpEdittime'] = $pageObj->getTimestamp();
318
		}
319
320
		if ( $params['starttimestamp'] !== null ) {
321
			$requestArray['wpStarttime'] = $params['starttimestamp'];
322
		} else {
323
			$requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
324
		}
325
326
		if ( $params['minor'] || ( !$params['notminor'] && $user->getOption( 'minordefault' ) ) ) {
327
			$requestArray['wpMinoredit'] = '';
328
		}
329
330
		if ( $params['recreate'] ) {
331
			$requestArray['wpRecreate'] = '';
332
		}
333
334
		if ( !is_null( $params['section'] ) ) {
335
			$section = $params['section'];
336
			if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
337
				$this->dieUsage( "The section parameter must be a valid section id or 'new'",
338
					'invalidsection' );
339
			}
340
			$content = $pageObj->getContent();
341
			if ( $section !== '0' && $section != 'new'
342
				&& ( !$content || !$content->getSection( $section ) )
343
			) {
344
				$this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
345
			}
346
			$requestArray['wpSection'] = $params['section'];
347
		} else {
348
			$requestArray['wpSection'] = '';
349
		}
350
351
		$watch = $this->getWatchlistValue( $params['watchlist'], $titleObj );
352
353
		// Deprecated parameters
354
		if ( $params['watch'] ) {
355
			$watch = true;
356
		} elseif ( $params['unwatch'] ) {
357
			$watch = false;
358
		}
359
360
		if ( $watch ) {
361
			$requestArray['wpWatchthis'] = '';
362
		}
363
364
		// Apply change tags
365
		if ( count( $params['tags'] ) ) {
366
			$tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $user );
367
			if ( $tagStatus->isOK() ) {
368
				$requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
369
			} else {
370
				$this->dieStatus( $tagStatus );
371
			}
372
		}
373
374
		// Pass through anything else we might have been given, to support extensions
375
		// This is kind of a hack but it's the best we can do to make extensions work
376
		$requestArray += $this->getRequest()->getValues();
377
378
		global $wgTitle, $wgRequest;
379
380
		$req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
381
382
		// Some functions depend on $wgTitle == $ep->mTitle
383
		// TODO: Make them not or check if they still do
384
		$wgTitle = $titleObj;
385
386
		$articleContext = new RequestContext;
387
		$articleContext->setRequest( $req );
388
		$articleContext->setWikiPage( $pageObj );
0 ignored issues
show
Bug introduced by
It seems like $pageObj defined by $this->getTitleOrPageId($params) on line 50 can be null; however, RequestContext::setWikiPage() 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...
389
		$articleContext->setUser( $this->getUser() );
390
391
		/** @var $articleObject Article */
392
		$articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
0 ignored issues
show
Bug introduced by
It seems like $pageObj defined by $this->getTitleOrPageId($params) on line 50 can be null; however, Article::newFromWikiPage() 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...
393
394
		$ep = new EditPage( $articleObject );
395
396
		$ep->setApiEditOverride( true );
397
		$ep->setContextTitle( $titleObj );
398
		$ep->importFormData( $req );
399
		$content = $ep->textbox1;
400
401
		// The following is needed to give the hook the full content of the
402
		// new revision rather than just the current section. (Bug 52077)
403
		if ( !is_null( $params['section'] ) &&
404
			$contentHandler->supportsSections() && $titleObj->exists()
405
		) {
406
			// If sectiontitle is set, use it, otherwise use the summary as the section title (for
407
			// backwards compatibility with old forms/bots).
408
			if ( $ep->sectiontitle !== '' ) {
409
				$sectionTitle = $ep->sectiontitle;
410
			} else {
411
				$sectionTitle = $ep->summary;
412
			}
413
414
			$contentObj = $contentHandler->unserializeContent( $content, $contentFormat );
415
416
			$fullContentObj = $articleObject->replaceSectionContent(
417
				$params['section'],
418
				$contentObj,
419
				$sectionTitle
420
			);
421
			if ( $fullContentObj ) {
422
				$content = $fullContentObj->serialize( $contentFormat );
423
			} else {
424
				// This most likely means we have an edit conflict which means that the edit
425
				// wont succeed anyway.
426
				$this->dieUsageMsg( 'editconflict' );
427
			}
428
		}
429
430
		// Run hooks
431
		// Handle APIEditBeforeSave parameters
432
		$r = [];
433
		if ( !Hooks::run( 'APIEditBeforeSave', [ $ep, $content, &$r ] ) ) {
434
			if ( count( $r ) ) {
435
				$r['result'] = 'Failure';
436
				$apiResult->addValue( null, $this->getModuleName(), $r );
437
438
				return;
439
			}
440
441
			$this->dieUsageMsg( 'hookaborted' );
442
		}
443
444
		// Do the actual save
445
		$oldRevId = $articleObject->getRevIdFetched();
446
		$result = null;
447
		// Fake $wgRequest for some hooks inside EditPage
448
		// @todo FIXME: This interface SUCKS
449
		$oldRequest = $wgRequest;
450
		$wgRequest = $req;
451
452
		$status = $ep->attemptSave( $result );
453
		$wgRequest = $oldRequest;
454
455
		switch ( $status->value ) {
456
			case EditPage::AS_HOOK_ERROR:
457
			case EditPage::AS_HOOK_ERROR_EXPECTED:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
458
				if ( isset( $status->apiHookResult ) ) {
459
					$r = $status->apiHookResult;
0 ignored issues
show
Documentation introduced by
The property apiHookResult does not exist on object<Status>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
460
					$r['result'] = 'Failure';
461
					$apiResult->addValue( null, $this->getModuleName(), $r );
462
					return;
463
				} else {
464
					$this->dieUsageMsg( 'hookaborted' );
465
				}
466
467
			case EditPage::AS_PARSE_ERROR:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
468
				$this->dieUsage( $status->getMessage(), 'parseerror' );
469
470
			case EditPage::AS_IMAGE_REDIRECT_ANON:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
471
				$this->dieUsageMsg( 'noimageredirect-anon' );
472
473
			case EditPage::AS_IMAGE_REDIRECT_LOGGED:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
474
				$this->dieUsageMsg( 'noimageredirect-logged' );
475
476
			case EditPage::AS_SPAM_ERROR:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
477
				$this->dieUsageMsg( [ 'spamdetected', $result['spam'] ] );
478
479
			case EditPage::AS_BLOCKED_PAGE_FOR_USER:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
480
				$this->dieUsage(
481
					'You have been blocked from editing',
482
					'blocked',
483
					0,
484
					[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
0 ignored issues
show
Bug introduced by
It seems like $user->getBlock() can be null; however, getBlockInfo() 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...
485
				);
486
487
			case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
488
			case EditPage::AS_CONTENT_TOO_BIG:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
489
				$this->dieUsageMsg( [ 'contenttoobig', $this->getConfig()->get( 'MaxArticleSize' ) ] );
490
491
			case EditPage::AS_READ_ONLY_PAGE_ANON:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
492
				$this->dieUsageMsg( 'noedit-anon' );
493
494
			case EditPage::AS_READ_ONLY_PAGE_LOGGED:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
495
				$this->dieUsageMsg( 'noedit' );
496
497
			case EditPage::AS_READ_ONLY_PAGE:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
498
				$this->dieReadOnly();
499
500
			case EditPage::AS_RATE_LIMITED:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
501
				$this->dieUsageMsg( 'actionthrottledtext' );
502
503
			case EditPage::AS_ARTICLE_WAS_DELETED:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
504
				$this->dieUsageMsg( 'wasdeleted' );
505
506
			case EditPage::AS_NO_CREATE_PERMISSION:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
507
				$this->dieUsageMsg( 'nocreate-loggedin' );
508
509
			case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
510
				$this->dieUsageMsg( 'cantchangecontentmodel' );
511
512
			case EditPage::AS_BLANK_ARTICLE:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
513
				$this->dieUsageMsg( 'blankpage' );
514
515
			case EditPage::AS_CONFLICT_DETECTED:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
516
				$this->dieUsageMsg( 'editconflict' );
517
518
			case EditPage::AS_TEXTBOX_EMPTY:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
519
				$this->dieUsageMsg( 'emptynewsection' );
520
521
			case EditPage::AS_CHANGE_TAG_ERROR:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
522
				$this->dieStatus( $status );
523
524
			case EditPage::AS_SUCCESS_NEW_ARTICLE:
525
				$r['new'] = true;
526
				// fall-through
527
528
			case EditPage::AS_SUCCESS_UPDATE:
529
				$r['result'] = 'Success';
530
				$r['pageid'] = intval( $titleObj->getArticleID() );
531
				$r['title'] = $titleObj->getPrefixedText();
532
				$r['contentmodel'] = $articleObject->getContentModel();
533
				$newRevId = $articleObject->getLatest();
534
				if ( $newRevId == $oldRevId ) {
535
					$r['nochange'] = true;
536
				} else {
537
					$r['oldrevid'] = intval( $oldRevId );
538
					$r['newrevid'] = intval( $newRevId );
539
					$r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
540
						$pageObj->getTimestamp() );
541
				}
542
				break;
543
544
			case EditPage::AS_SUMMARY_NEEDED:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
545
				// Shouldn't happen since we set wpIgnoreBlankSummary, but just in case
546
				$this->dieUsageMsg( 'summaryrequired' );
547
548
			case EditPage::AS_END:
549
			default:
550
				// $status came from WikiPage::doEdit()
551
				$errors = $status->getErrorsArray();
0 ignored issues
show
Deprecated Code introduced by
The method Status::getErrorsArray() has been deprecated with message: 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
552
				$this->dieUsageMsg( $errors[0] ); // TODO: Add new errors to message map
553
				break;
554
		}
555
		$apiResult->addValue( null, $this->getModuleName(), $r );
556
	}
557
558
	public function mustBePosted() {
559
		return true;
560
	}
561
562
	public function isWriteMode() {
563
		return true;
564
	}
565
566
	public function getAllowedParams() {
567
		return [
568
			'title' => [
569
				ApiBase::PARAM_TYPE => 'string',
570
			],
571
			'pageid' => [
572
				ApiBase::PARAM_TYPE => 'integer',
573
			],
574
			'section' => null,
575
			'sectiontitle' => [
576
				ApiBase::PARAM_TYPE => 'string',
577
			],
578
			'text' => [
579
				ApiBase::PARAM_TYPE => 'text',
580
			],
581
			'summary' => null,
582
			'tags' => [
583
				ApiBase::PARAM_TYPE => 'tags',
584
				ApiBase::PARAM_ISMULTI => true,
585
			],
586
			'minor' => false,
587
			'notminor' => false,
588
			'bot' => false,
589
			'basetimestamp' => [
590
				ApiBase::PARAM_TYPE => 'timestamp',
591
			],
592
			'starttimestamp' => [
593
				ApiBase::PARAM_TYPE => 'timestamp',
594
			],
595
			'recreate' => false,
596
			'createonly' => false,
597
			'nocreate' => false,
598
			'watch' => [
599
				ApiBase::PARAM_DFLT => false,
600
				ApiBase::PARAM_DEPRECATED => true,
601
			],
602
			'unwatch' => [
603
				ApiBase::PARAM_DFLT => false,
604
				ApiBase::PARAM_DEPRECATED => true,
605
			],
606
			'watchlist' => [
607
				ApiBase::PARAM_DFLT => 'preferences',
608
				ApiBase::PARAM_TYPE => [
609
					'watch',
610
					'unwatch',
611
					'preferences',
612
					'nochange'
613
				],
614
			],
615
			'md5' => null,
616
			'prependtext' => [
617
				ApiBase::PARAM_TYPE => 'text',
618
			],
619
			'appendtext' => [
620
				ApiBase::PARAM_TYPE => 'text',
621
			],
622
			'undo' => [
623
				ApiBase::PARAM_TYPE => 'integer'
624
			],
625
			'undoafter' => [
626
				ApiBase::PARAM_TYPE => 'integer'
627
			],
628
			'redirect' => [
629
				ApiBase::PARAM_TYPE => 'boolean',
630
				ApiBase::PARAM_DFLT => false,
631
			],
632
			'contentformat' => [
633
				ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
634
			],
635
			'contentmodel' => [
636
				ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
637
			],
638
			'token' => [
639
				// Standard definition automatically inserted
640
				ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
641
			],
642
		];
643
	}
644
645
	public function needsToken() {
646
		return 'csrf';
647
	}
648
649
	protected function getExamplesMessages() {
650
		return [
651
			'action=edit&title=Test&summary=test%20summary&' .
652
				'text=article%20content&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
653
				=> 'apihelp-edit-example-edit',
654
			'action=edit&title=Test&summary=NOTOC&minor=&' .
655
				'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
656
				=> 'apihelp-edit-example-prepend',
657
			'action=edit&title=Test&undo=13585&undoafter=13579&' .
658
				'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
659
				=> 'apihelp-edit-example-undo',
660
		];
661
	}
662
663
	public function getHelpUrls() {
664
		return 'https://www.mediawiki.org/wiki/API:Edit';
665
	}
666
}
667