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

EditPage   F

Complexity

Total Complexity 608

Size/Duplication

Total Lines 4170
Duplicated Lines 3.62 %

Coupling/Cohesion

Components 1
Dependencies 41

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 151
loc 4170
rs 0.5217
wmc 608
lcom 1
cbo 41

82 Methods

Rating   Name   Duplication   Size   Complexity  
F showHeader() 11 220 51
A __construct() 0 10 1
A getArticle() 0 3 1
A getTitle() 0 3 1
A setContextTitle() 0 3 1
A getContextTitle() 0 8 2
A isSupportedContentModel() 0 4 2
A setApiEditOverride() 0 3 1
A submit() 0 3 1
D edit() 0 112 23
C getEditPermissionErrors() 0 27 7
C displayPermissionsError() 0 25 7
B displayViewSourcePage() 0 49 5
D previewOnOpen() 0 26 10
A isWrongCaseCssJsPage() 0 13 3
A isSectionEditSupported() 0 4 1
F importFormData() 0 203 32
A importContentFormData() 0 3 1
D initialiseForm() 0 30 9
D getContentObject() 0 115 24
A getOriginalContent() 0 16 4
A getParentRevId() 0 7 2
B getCurrentContent() 0 19 5
A setPreloadedContent() 0 3 1
C getPreloadedContent() 8 56 13
A tokenOk() 0 7 1
A setPostEditCookie() 0 16 3
A attemptSave() 0 11 2
D handleStatus() 0 121 38
C runPostMergeFilters() 0 47 7
B newSectionSummary() 0 23 4
F internalAttemptSave() 30 438 80
A addContentModelChangeLogEntry() 0 13 2
A updateWatchlist() 0 18 3
B mergeChangesIntoContent() 0 33 6
A getBaseRevision() 0 8 2
A matchSpamRegex() 0 6 1
A matchSummarySpamRegex() 0 5 1
A matchSpamRegexInternal() 0 9 3
F setHeaders() 0 55 14
D showIntro() 28 108 22
B showCustomIntro() 0 15 5
B toEditText() 0 12 5
A toEditContent() 0 15 4
F showEditForm() 18 220 27
A extractSectionTitle() 0 9 2
B getSummaryInput() 0 31 6
B showSummaryInput() 0 23 6
B getSummaryPreview() 0 20 6
A showFormBeforeText() 0 15 2
A showFormAfterText() 0 16 1
A showContentForm() 0 3 1
C showTextbox1() 0 41 11
A showTextbox2() 0 3 1
B showTextbox() 0 28 2
B displayPreviewArea() 13 41 6
A showPreview() 0 13 3
C showDiff() 0 54 11
A showHeaderCopyrightWarning() 0 8 2
A showTosSummary() 0 10 2
A showEditTools() 0 6 1
A getCopywarn() 0 3 1
A getCopyrightWarning() 0 16 2
C getPreviewLimitReport() 0 41 8
B showStandardInputs() 4 46 3
B showConflict() 0 24 2
A getCancelLink() 0 14 3
A getActionURL() 0 3 1
B wasDeletedSinceLastEdit() 0 19 6
B getLastDelete() 0 38 4
F getPreviewText() 0 160 29
B getTemplates() 0 16 6
C getEditToolbar() 0 128 8
B getCheckboxes() 39 55 6
B getEditButtons() 0 32 1
A noSuchSectionPage() 0 11 1
B spamPageWithContent() 0 24 3
A checkUnicodeCompliantBrowser() 0 16 4
A safeUnicodeInput() 0 6 2
A safeUnicodeOutput() 0 7 2
C makeSafe() 0 33 7
C unmakeSafe() 0 28 8

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 EditPage 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 EditPage, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * User interface for page editing.
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 along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
23
/**
24
 * The edit page/HTML interface (split from Article)
25
 * The actual database and text munging is still in Article,
26
 * but it should get easier to call those from alternate
27
 * interfaces.
28
 *
29
 * EditPage cares about two distinct titles:
30
 * $this->mContextTitle is the page that forms submit to, links point to,
31
 * redirects go to, etc. $this->mTitle (as well as $mArticle) is the
32
 * page in the database that is actually being edited. These are
33
 * usually the same, but they are now allowed to be different.
34
 *
35
 * Surgeon General's Warning: prolonged exposure to this class is known to cause
36
 * headaches, which may be fatal.
37
 */
38
class EditPage {
39
	/**
40
	 * Status: Article successfully updated
41
	 */
42
	const AS_SUCCESS_UPDATE = 200;
43
44
	/**
45
	 * Status: Article successfully created
46
	 */
47
	const AS_SUCCESS_NEW_ARTICLE = 201;
48
49
	/**
50
	 * Status: Article update aborted by a hook function
51
	 */
52
	const AS_HOOK_ERROR = 210;
53
54
	/**
55
	 * Status: A hook function returned an error
56
	 */
57
	const AS_HOOK_ERROR_EXPECTED = 212;
58
59
	/**
60
	 * Status: User is blocked from editing this page
61
	 */
62
	const AS_BLOCKED_PAGE_FOR_USER = 215;
63
64
	/**
65
	 * Status: Content too big (> $wgMaxArticleSize)
66
	 */
67
	const AS_CONTENT_TOO_BIG = 216;
68
69
	/**
70
	 * Status: this anonymous user is not allowed to edit this page
71
	 */
72
	const AS_READ_ONLY_PAGE_ANON = 218;
73
74
	/**
75
	 * Status: this logged in user is not allowed to edit this page
76
	 */
77
	const AS_READ_ONLY_PAGE_LOGGED = 219;
78
79
	/**
80
	 * Status: wiki is in readonly mode (wfReadOnly() == true)
81
	 */
82
	const AS_READ_ONLY_PAGE = 220;
83
84
	/**
85
	 * Status: rate limiter for action 'edit' was tripped
86
	 */
87
	const AS_RATE_LIMITED = 221;
88
89
	/**
90
	 * Status: article was deleted while editing and param wpRecreate == false or form
91
	 * was not posted
92
	 */
93
	const AS_ARTICLE_WAS_DELETED = 222;
94
95
	/**
96
	 * Status: user tried to create this page, but is not allowed to do that
97
	 * ( Title->userCan('create') == false )
98
	 */
99
	const AS_NO_CREATE_PERMISSION = 223;
100
101
	/**
102
	 * Status: user tried to create a blank page and wpIgnoreBlankArticle == false
103
	 */
104
	const AS_BLANK_ARTICLE = 224;
105
106
	/**
107
	 * Status: (non-resolvable) edit conflict
108
	 */
109
	const AS_CONFLICT_DETECTED = 225;
110
111
	/**
112
	 * Status: no edit summary given and the user has forceeditsummary set and the user is not
113
	 * editing in his own userspace or talkspace and wpIgnoreBlankSummary == false
114
	 */
115
	const AS_SUMMARY_NEEDED = 226;
116
117
	/**
118
	 * Status: user tried to create a new section without content
119
	 */
120
	const AS_TEXTBOX_EMPTY = 228;
121
122
	/**
123
	 * Status: article is too big (> $wgMaxArticleSize), after merging in the new section
124
	 */
125
	const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229;
126
127
	/**
128
	 * Status: WikiPage::doEdit() was unsuccessful
129
	 */
130
	const AS_END = 231;
131
132
	/**
133
	 * Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex
134
	 */
135
	const AS_SPAM_ERROR = 232;
136
137
	/**
138
	 * Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
139
	 */
140
	const AS_IMAGE_REDIRECT_ANON = 233;
141
142
	/**
143
	 * Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
144
	 */
145
	const AS_IMAGE_REDIRECT_LOGGED = 234;
146
147
	/**
148
	 * Status: user tried to modify the content model, but is not allowed to do that
149
	 * ( User::isAllowed('editcontentmodel') == false )
150
	 */
151
	const AS_NO_CHANGE_CONTENT_MODEL = 235;
152
153
	/**
154
	 * Status: user tried to create self-redirect (redirect to the same article) and
155
	 * wpIgnoreSelfRedirect == false
156
	 */
157
	const AS_SELF_REDIRECT = 236;
158
159
	/**
160
	 * Status: an error relating to change tagging. Look at the message key for
161
	 * more details
162
	 */
163
	const AS_CHANGE_TAG_ERROR = 237;
164
165
	/**
166
	 * Status: can't parse content
167
	 */
168
	const AS_PARSE_ERROR = 240;
169
170
	/**
171
	 * Status: when changing the content model is disallowed due to
172
	 * $wgContentHandlerUseDB being false
173
	 */
174
	const AS_CANNOT_USE_CUSTOM_MODEL = 241;
175
176
	/**
177
	 * HTML id and name for the beginning of the edit form.
178
	 */
179
	const EDITFORM_ID = 'editform';
180
181
	/**
182
	 * Prefix of key for cookie used to pass post-edit state.
183
	 * The revision id edited is added after this
184
	 */
185
	const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
186
187
	/**
188
	 * Duration of PostEdit cookie, in seconds.
189
	 * The cookie will be removed instantly if the JavaScript runs.
190
	 *
191
	 * Otherwise, though, we don't want the cookies to accumulate.
192
	 * RFC 2109 ( https://www.ietf.org/rfc/rfc2109.txt ) specifies a possible
193
	 * limit of only 20 cookies per domain. This still applies at least to some
194
	 * versions of IE without full updates:
195
	 * https://blogs.msdn.com/b/ieinternals/archive/2009/08/20/wininet-ie-cookie-internals-faq.aspx
196
	 *
197
	 * A value of 20 minutes should be enough to take into account slow loads and minor
198
	 * clock skew while still avoiding cookie accumulation when JavaScript is turned off.
199
	 */
200
	const POST_EDIT_COOKIE_DURATION = 1200;
201
202
	/** @var Article */
203
	public $mArticle;
204
	/** @var WikiPage */
205
	private $page;
206
207
	/** @var Title */
208
	public $mTitle;
209
210
	/** @var null|Title */
211
	private $mContextTitle = null;
212
213
	/** @var string */
214
	public $action = 'submit';
215
216
	/** @var bool */
217
	public $isConflict = false;
218
219
	/** @var bool */
220
	public $isCssJsSubpage = false;
221
222
	/** @var bool */
223
	public $isCssSubpage = false;
224
225
	/** @var bool */
226
	public $isJsSubpage = false;
227
228
	/** @var bool */
229
	public $isWrongCaseCssJsPage = false;
230
231
	/** @var bool New page or new section */
232
	public $isNew = false;
233
234
	/** @var bool */
235
	public $deletedSinceEdit;
236
237
	/** @var string */
238
	public $formtype;
239
240
	/** @var bool */
241
	public $firsttime;
242
243
	/** @var bool|stdClass */
244
	public $lastDelete;
245
246
	/** @var bool */
247
	public $mTokenOk = false;
248
249
	/** @var bool */
250
	public $mTokenOkExceptSuffix = false;
251
252
	/** @var bool */
253
	public $mTriedSave = false;
254
255
	/** @var bool */
256
	public $incompleteForm = false;
257
258
	/** @var bool */
259
	public $tooBig = false;
260
261
	/** @var bool */
262
	public $kblength = false;
263
264
	/** @var bool */
265
	public $missingComment = false;
266
267
	/** @var bool */
268
	public $missingSummary = false;
269
270
	/** @var bool */
271
	public $allowBlankSummary = false;
272
273
	/** @var bool */
274
	protected $blankArticle = false;
275
276
	/** @var bool */
277
	protected $allowBlankArticle = false;
278
279
	/** @var bool */
280
	protected $selfRedirect = false;
281
282
	/** @var bool */
283
	protected $allowSelfRedirect = false;
284
285
	/** @var string */
286
	public $autoSumm = '';
287
288
	/** @var string */
289
	public $hookError = '';
290
291
	/** @var ParserOutput */
292
	public $mParserOutput;
293
294
	/** @var bool Has a summary been preset using GET parameter &summary= ? */
295
	public $hasPresetSummary = false;
296
297
	/** @var bool */
298
	public $mBaseRevision = false;
299
300
	/** @var bool */
301
	public $mShowSummaryField = true;
302
303
	# Form values
304
305
	/** @var bool */
306
	public $save = false;
307
308
	/** @var bool */
309
	public $preview = false;
310
311
	/** @var bool */
312
	public $diff = false;
313
314
	/** @var bool */
315
	public $minoredit = false;
316
317
	/** @var bool */
318
	public $watchthis = false;
319
320
	/** @var bool */
321
	public $recreate = false;
322
323
	/** @var string */
324
	public $textbox1 = '';
325
326
	/** @var string */
327
	public $textbox2 = '';
328
329
	/** @var string */
330
	public $summary = '';
331
332
	/** @var bool */
333
	public $nosummary = false;
334
335
	/** @var string */
336
	public $edittime = '';
337
338
	/** @var string */
339
	public $section = '';
340
341
	/** @var string */
342
	public $sectiontitle = '';
343
344
	/** @var string */
345
	public $starttime = '';
346
347
	/** @var int */
348
	public $oldid = 0;
349
350
	/** @var int */
351
	public $parentRevId = 0;
352
353
	/** @var string */
354
	public $editintro = '';
355
356
	/** @var null */
357
	public $scrolltop = null;
358
359
	/** @var bool */
360
	public $bot = true;
361
362
	/** @var null|string */
363
	public $contentModel = null;
364
365
	/** @var null|string */
366
	public $contentFormat = null;
367
368
	/** @var null|array */
369
	private $changeTags = null;
370
371
	# Placeholders for text injection by hooks (must be HTML)
372
	# extensions should take care to _append_ to the present value
373
374
	/** @var string Before even the preview */
375
	public $editFormPageTop = '';
376
	public $editFormTextTop = '';
377
	public $editFormTextBeforeContent = '';
378
	public $editFormTextAfterWarn = '';
379
	public $editFormTextAfterTools = '';
380
	public $editFormTextBottom = '';
381
	public $editFormTextAfterContent = '';
382
	public $previewTextAfterContent = '';
383
	public $mPreloadContent = null;
384
385
	/* $didSave should be set to true whenever an article was successfully altered. */
386
	public $didSave = false;
387
	public $undidRev = 0;
388
389
	public $suppressIntro = false;
390
391
	/** @var bool */
392
	protected $edit;
393
394
	/**
395
	 * @var bool Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing
396
	 */
397
	private $enableApiEditOverride = false;
398
399
	/**
400
	 * @param Article $article
401
	 */
402
	public function __construct( Article $article ) {
403
		$this->mArticle = $article;
404
		$this->page = $article->getPage(); // model object
405
		$this->mTitle = $article->getTitle();
406
407
		$this->contentModel = $this->mTitle->getContentModel();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->mTitle->getContentModel() can also be of type integer or boolean. However, the property $contentModel is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
408
409
		$handler = ContentHandler::getForModelID( $this->contentModel );
410
		$this->contentFormat = $handler->getDefaultFormat();
411
	}
412
413
	/**
414
	 * @return Article
415
	 */
416
	public function getArticle() {
417
		return $this->mArticle;
418
	}
419
420
	/**
421
	 * @since 1.19
422
	 * @return Title
423
	 */
424
	public function getTitle() {
425
		return $this->mTitle;
426
	}
427
428
	/**
429
	 * Set the context Title object
430
	 *
431
	 * @param Title|null $title Title object or null
432
	 */
433
	public function setContextTitle( $title ) {
434
		$this->mContextTitle = $title;
435
	}
436
437
	/**
438
	 * Get the context title object.
439
	 * If not set, $wgTitle will be returned. This behavior might change in
440
	 * the future to return $this->mTitle instead.
441
	 *
442
	 * @return Title
443
	 */
444
	public function getContextTitle() {
445
		if ( is_null( $this->mContextTitle ) ) {
446
			global $wgTitle;
447
			return $wgTitle;
448
		} else {
449
			return $this->mContextTitle;
450
		}
451
	}
452
453
	/**
454
	 * Returns if the given content model is editable.
455
	 *
456
	 * @param string $modelId The ID of the content model to test. Use CONTENT_MODEL_XXX constants.
457
	 * @return bool
458
	 * @throws MWException If $modelId has no known handler
459
	 */
460
	public function isSupportedContentModel( $modelId ) {
461
		return $this->enableApiEditOverride === true ||
462
			ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
463
	}
464
465
	/**
466
	 * Allow editing of content that supports API direct editing, but not general
467
	 * direct editing. Set to false by default.
468
	 *
469
	 * @param bool $enableOverride
470
	 */
471
	public function setApiEditOverride( $enableOverride ) {
472
		$this->enableApiEditOverride = $enableOverride;
473
	}
474
475
	function submit() {
476
		$this->edit();
477
	}
478
479
	/**
480
	 * This is the function that gets called for "action=edit". It
481
	 * sets up various member variables, then passes execution to
482
	 * another function, usually showEditForm()
483
	 *
484
	 * The edit form is self-submitting, so that when things like
485
	 * preview and edit conflicts occur, we get the same form back
486
	 * with the extra stuff added.  Only when the final submission
487
	 * is made and all is well do we actually save and redirect to
488
	 * the newly-edited page.
489
	 */
490
	function edit() {
491
		global $wgOut, $wgRequest, $wgUser;
492
		// Allow extensions to modify/prevent this form or submission
493
		if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
494
			return;
495
		}
496
497
		wfDebug( __METHOD__ . ": enter\n" );
498
499
		// If they used redlink=1 and the page exists, redirect to the main article
500
		if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
501
			$wgOut->redirect( $this->mTitle->getFullURL() );
502
			return;
503
		}
504
505
		$this->importFormData( $wgRequest );
506
		$this->firsttime = false;
507
508
		if ( wfReadOnly() && $this->save ) {
509
			// Force preview
510
			$this->save = false;
511
			$this->preview = true;
512
		}
513
514
		if ( $this->save ) {
515
			$this->formtype = 'save';
516
		} elseif ( $this->preview ) {
517
			$this->formtype = 'preview';
518
		} elseif ( $this->diff ) {
519
			$this->formtype = 'diff';
520
		} else { # First time through
521
			$this->firsttime = true;
522
			if ( $this->previewOnOpen() ) {
523
				$this->formtype = 'preview';
524
			} else {
525
				$this->formtype = 'initial';
526
			}
527
		}
528
529
		$permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
530
		if ( $permErrors ) {
531
			wfDebug( __METHOD__ . ": User can't edit\n" );
532
			// Auto-block user's IP if the account was "hard" blocked
533
			if ( !wfReadOnly() ) {
534
				$user = $wgUser;
535
				DeferredUpdates::addCallableUpdate( function () use ( $user ) {
536
					$user->spreadAnyEditBlock();
537
				} );
538
			}
539
			$this->displayPermissionsError( $permErrors );
540
541
			return;
542
		}
543
544
		$revision = $this->mArticle->getRevisionFetched();
545
		// Disallow editing revisions with content models different from the current one
546
		if ( $revision && $revision->getContentModel() !== $this->contentModel ) {
547
			$this->displayViewSourcePage(
548
				$this->getContentObject(),
0 ignored issues
show
Bug introduced by
It seems like $this->getContentObject() targeting EditPage::getContentObject() can also be of type boolean or null; however, EditPage::displayViewSourcePage() does only seem to accept object<Content>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
549
				wfMessage(
550
					'contentmodelediterror',
551
					$revision->getContentModel(),
552
					$this->contentModel
553
				)->plain()
554
			);
555
			return;
556
		}
557
558
		$this->isConflict = false;
559
		// css / js subpages of user pages get a special treatment
560
		$this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
561
		$this->isCssSubpage = $this->mTitle->isCssSubpage();
562
		$this->isJsSubpage = $this->mTitle->isJsSubpage();
563
		// @todo FIXME: Silly assignment.
564
		$this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage();
565
566
		# Show applicable editing introductions
567
		if ( $this->formtype == 'initial' || $this->firsttime ) {
568
			$this->showIntro();
569
		}
570
571
		# Attempt submission here.  This will check for edit conflicts,
572
		# and redundantly check for locked database, blocked IPs, etc.
573
		# that edit() already checked just in case someone tries to sneak
574
		# in the back door with a hand-edited submission URL.
575
576
		if ( 'save' == $this->formtype ) {
577
			$resultDetails = null;
578
			$status = $this->attemptSave( $resultDetails );
579
			if ( !$this->handleStatus( $status, $resultDetails ) ) {
580
				return;
581
			}
582
		}
583
584
		# First time through: get contents, set time for conflict
585
		# checking, etc.
586
		if ( 'initial' == $this->formtype || $this->firsttime ) {
587
			if ( $this->initialiseForm() === false ) {
588
				$this->noSuchSectionPage();
589
				return;
590
			}
591
592
			if ( !$this->mTitle->getArticleID() ) {
593
				Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
594
			} else {
595
				Hooks::run( 'EditFormInitialText', [ $this ] );
596
			}
597
598
		}
599
600
		$this->showEditForm();
601
	}
602
603
	/**
604
	 * @param string $rigor Same format as Title::getUserPermissionErrors()
605
	 * @return array
606
	 */
607
	protected function getEditPermissionErrors( $rigor = 'secure' ) {
608
		global $wgUser;
609
610
		$permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser, $rigor );
611
		# Can this title be created?
612
		if ( !$this->mTitle->exists() ) {
613
			$permErrors = array_merge(
614
				$permErrors,
615
				wfArrayDiff2(
616
					$this->mTitle->getUserPermissionsErrors( 'create', $wgUser, $rigor ),
617
					$permErrors
618
				)
619
			);
620
		}
621
		# Ignore some permissions errors when a user is just previewing/viewing diffs
622
		$remove = [];
623
		foreach ( $permErrors as $error ) {
624
			if ( ( $this->preview || $this->diff )
625
				&& ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' )
626
			) {
627
				$remove[] = $error;
628
			}
629
		}
630
		$permErrors = wfArrayDiff2( $permErrors, $remove );
631
632
		return $permErrors;
633
	}
634
635
	/**
636
	 * Display a permissions error page, like OutputPage::showPermissionsErrorPage(),
637
	 * but with the following differences:
638
	 * - If redlink=1, the user will be redirected to the page
639
	 * - If there is content to display or the error occurs while either saving,
640
	 *   previewing or showing the difference, it will be a
641
	 *   "View source for ..." page displaying the source code after the error message.
642
	 *
643
	 * @since 1.19
644
	 * @param array $permErrors Array of permissions errors, as returned by
645
	 *    Title::getUserPermissionsErrors().
646
	 * @throws PermissionsError
647
	 */
648
	protected function displayPermissionsError( array $permErrors ) {
649
		global $wgRequest, $wgOut;
650
651
		if ( $wgRequest->getBool( 'redlink' ) ) {
652
			// The edit page was reached via a red link.
653
			// Redirect to the article page and let them click the edit tab if
654
			// they really want a permission error.
655
			$wgOut->redirect( $this->mTitle->getFullURL() );
656
			return;
657
		}
658
659
		$content = $this->getContentObject();
660
661
		# Use the normal message if there's nothing to display
662
		if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
663
			$action = $this->mTitle->exists() ? 'edit' :
664
				( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
665
			throw new PermissionsError( $action, $permErrors );
666
		}
667
668
		$this->displayViewSourcePage(
669
			$content,
0 ignored issues
show
Bug introduced by
It seems like $content defined by $this->getContentObject() on line 659 can also be of type boolean or null; however, EditPage::displayViewSourcePage() does only seem to accept object<Content>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
670
			$wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
671
		);
672
	}
673
674
	/**
675
	 * Display a read-only View Source page
676
	 * @param Content $content content object
677
	 * @param string $errorMessage additional wikitext error message to display
678
	 */
679
	protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
680
		global $wgOut;
681
682
		Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] );
683
684
		$wgOut->setRobotPolicy( 'noindex,nofollow' );
685
		$wgOut->setPageTitle( wfMessage(
686
			'viewsource-title',
687
			$this->getContextTitle()->getPrefixedText()
688
		) );
689
		$wgOut->addBacklinkSubtitle( $this->getContextTitle() );
690
		$wgOut->addHTML( $this->editFormPageTop );
691
		$wgOut->addHTML( $this->editFormTextTop );
692
693
		if ( $errorMessage !== '' ) {
694
			$wgOut->addWikiText( $errorMessage );
695
			$wgOut->addHTML( "<hr />\n" );
696
		}
697
698
		# If the user made changes, preserve them when showing the markup
699
		# (This happens when a user is blocked during edit, for instance)
700
		if ( !$this->firsttime ) {
701
			$text = $this->textbox1;
702
			$wgOut->addWikiMsg( 'viewyourtext' );
703
		} else {
704
			try {
705
				$text = $this->toEditText( $content );
706
			} catch ( MWException $e ) {
707
				# Serialize using the default format if the content model is not supported
708
				# (e.g. for an old revision with a different model)
709
				$text = $content->serialize();
710
			}
711
			$wgOut->addWikiMsg( 'viewsourcetext' );
712
		}
713
714
		$wgOut->addHTML( $this->editFormTextBeforeContent );
715
		$this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
716
		$wgOut->addHTML( $this->editFormTextAfterContent );
717
718
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
719
			Linker::formatTemplates( $this->getTemplates() ) ) );
720
721
		$wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
722
723
		$wgOut->addHTML( $this->editFormTextBottom );
724
		if ( $this->mTitle->exists() ) {
725
			$wgOut->returnToMain( null, $this->mTitle );
726
		}
727
	}
728
729
	/**
730
	 * Should we show a preview when the edit form is first shown?
731
	 *
732
	 * @return bool
733
	 */
734
	protected function previewOnOpen() {
735
		global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
736
		if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
737
			// Explicit override from request
738
			return true;
739
		} elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
740
			// Explicit override from request
741
			return false;
742
		} elseif ( $this->section == 'new' ) {
743
			// Nothing *to* preview for new sections
744
			return false;
745
		} elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() )
746
			&& $wgUser->getOption( 'previewonfirst' )
747
		) {
748
			// Standard preference behavior
749
			return true;
750
		} elseif ( !$this->mTitle->exists()
751
			&& isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
752
			&& $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]
753
		) {
754
			// Categories are special
755
			return true;
756
		} else {
757
			return false;
758
		}
759
	}
760
761
	/**
762
	 * Checks whether the user entered a skin name in uppercase,
763
	 * e.g. "User:Example/Monobook.css" instead of "monobook.css"
764
	 *
765
	 * @return bool
766
	 */
767
	protected function isWrongCaseCssJsPage() {
768
		if ( $this->mTitle->isCssJsSubpage() ) {
769
			$name = $this->mTitle->getSkinFromCssJsSubpage();
770
			$skins = array_merge(
771
				array_keys( Skin::getSkinNames() ),
772
				[ 'common' ]
773
			);
774
			return !in_array( $name, $skins )
775
				&& in_array( strtolower( $name ), $skins );
776
		} else {
777
			return false;
778
		}
779
	}
780
781
	/**
782
	 * Returns whether section editing is supported for the current page.
783
	 * Subclasses may override this to replace the default behavior, which is
784
	 * to check ContentHandler::supportsSections.
785
	 *
786
	 * @return bool True if this edit page supports sections, false otherwise.
787
	 */
788
	protected function isSectionEditSupported() {
789
		$contentHandler = ContentHandler::getForTitle( $this->mTitle );
790
		return $contentHandler->supportsSections();
791
	}
792
793
	/**
794
	 * This function collects the form data and uses it to populate various member variables.
795
	 * @param WebRequest $request
796
	 * @throws ErrorPageError
797
	 */
798
	function importFormData( &$request ) {
0 ignored issues
show
Coding Style introduced by
importFormData uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
799
		global $wgContLang, $wgUser;
800
801
		# Section edit can come from either the form or a link
802
		$this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
803
804
		if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
805
			throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
806
		}
807
808
		$this->isNew = !$this->mTitle->exists() || $this->section == 'new';
809
810
		if ( $request->wasPosted() ) {
811
			# These fields need to be checked for encoding.
812
			# Also remove trailing whitespace, but don't remove _initial_
813
			# whitespace from the text boxes. This may be significant formatting.
814
			$this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' );
815
			if ( !$request->getCheck( 'wpTextbox2' ) ) {
816
				// Skip this if wpTextbox2 has input, it indicates that we came
817
				// from a conflict page with raw page text, not a custom form
818
				// modified by subclasses
819
				$textbox1 = $this->importContentFormData( $request );
820
				if ( $textbox1 !== null ) {
821
					$this->textbox1 = $textbox1;
822
				}
823
			}
824
825
			# Truncate for whole multibyte characters
826
			$this->summary = $wgContLang->truncate( $request->getText( 'wpSummary' ), 255 );
827
828
			# If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
829
			# header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
830
			# section titles.
831
			$this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
832
833
			# Treat sectiontitle the same way as summary.
834
			# Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
835
			# currently doing double duty as both edit summary and section title. Right now this
836
			# is just to allow API edits to work around this limitation, but this should be
837
			# incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
838
			$this->sectiontitle = $wgContLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
839
			$this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
840
841
			$this->edittime = $request->getVal( 'wpEdittime' );
842
			$this->starttime = $request->getVal( 'wpStarttime' );
843
844
			$undidRev = $request->getInt( 'wpUndidRevision' );
845
			if ( $undidRev ) {
846
				$this->undidRev = $undidRev;
847
			}
848
849
			$this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
0 ignored issues
show
Documentation Bug introduced by
It seems like $request->getIntOrNull('wpScrolltop') can also be of type integer. However, the property $scrolltop is declared as type null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
850
851
			if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
852
				// wpTextbox1 field is missing, possibly due to being "too big"
853
				// according to some filter rules such as Suhosin's setting for
854
				// suhosin.request.max_value_length (d'oh)
855
				$this->incompleteForm = true;
856
			} else {
857
				// If we receive the last parameter of the request, we can fairly
858
				// claim the POST request has not been truncated.
859
860
				// TODO: softened the check for cutover.  Once we determine
861
				// that it is safe, we should complete the transition by
862
				// removing the "edittime" clause.
863
				$this->incompleteForm = ( !$request->getVal( 'wpUltimateParam' )
0 ignored issues
show
Bug Best Practice introduced by
The expression $request->getVal('wpUltimateParam') of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
864
					&& is_null( $this->edittime ) );
865
			}
866
			if ( $this->incompleteForm ) {
867
				# If the form is incomplete, force to preview.
868
				wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
869
				wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
870
				$this->preview = true;
871
			} else {
872
				$this->preview = $request->getCheck( 'wpPreview' );
873
				$this->diff = $request->getCheck( 'wpDiff' );
874
875
				// Remember whether a save was requested, so we can indicate
876
				// if we forced preview due to session failure.
877
				$this->mTriedSave = !$this->preview;
878
879
				if ( $this->tokenOk( $request ) ) {
880
					# Some browsers will not report any submit button
881
					# if the user hits enter in the comment box.
882
					# The unmarked state will be assumed to be a save,
883
					# if the form seems otherwise complete.
884
					wfDebug( __METHOD__ . ": Passed token check.\n" );
885
				} elseif ( $this->diff ) {
886
					# Failed token check, but only requested "Show Changes".
887
					wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
888
				} else {
889
					# Page might be a hack attempt posted from
890
					# an external site. Preview instead of saving.
891
					wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
892
					$this->preview = true;
893
				}
894
			}
895
			$this->save = !$this->preview && !$this->diff;
896
			if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
897
				$this->edittime = null;
898
			}
899
900
			if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
901
				$this->starttime = null;
902
			}
903
904
			$this->recreate = $request->getCheck( 'wpRecreate' );
905
906
			$this->minoredit = $request->getCheck( 'wpMinoredit' );
907
			$this->watchthis = $request->getCheck( 'wpWatchthis' );
908
909
			# Don't force edit summaries when a user is editing their own user or talk page
910
			if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
911
				&& $this->mTitle->getText() == $wgUser->getName()
912
			) {
913
				$this->allowBlankSummary = true;
914
			} else {
915
				$this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
916
					|| !$wgUser->getOption( 'forceeditsummary' );
917
			}
918
919
			$this->autoSumm = $request->getText( 'wpAutoSummary' );
920
921
			$this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
922
			$this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
923
924
			$changeTags = $request->getVal( 'wpChangeTags' );
925
			if ( is_null( $changeTags ) || $changeTags === '' ) {
926
				$this->changeTags = [];
927
			} else {
928
				$this->changeTags = array_filter( array_map( 'trim', explode( ',',
929
					$changeTags ) ) );
930
			}
931
		} else {
932
			# Not a posted form? Start with nothing.
933
			wfDebug( __METHOD__ . ": Not a posted form.\n" );
934
			$this->textbox1 = '';
935
			$this->summary = '';
936
			$this->sectiontitle = '';
937
			$this->edittime = '';
938
			$this->starttime = wfTimestampNow();
0 ignored issues
show
Documentation Bug introduced by
It seems like wfTimestampNow() can also be of type false. However, the property $starttime is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
939
			$this->edit = false;
940
			$this->preview = false;
941
			$this->save = false;
942
			$this->diff = false;
943
			$this->minoredit = false;
944
			// Watch may be overridden by request parameters
945
			$this->watchthis = $request->getBool( 'watchthis', false );
946
			$this->recreate = false;
947
948
			// When creating a new section, we can preload a section title by passing it as the
949
			// preloadtitle parameter in the URL (Bug 13100)
950
			if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $request->getVal('preloadtitle') of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
951
				$this->sectiontitle = $request->getVal( 'preloadtitle' );
952
				// Once wpSummary isn't being use for setting section titles, we should delete this.
953
				$this->summary = $request->getVal( 'preloadtitle' );
954
			} elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $request->getVal('summary') of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
955
				$this->summary = $request->getText( 'summary' );
956
				if ( $this->summary !== '' ) {
957
					$this->hasPresetSummary = true;
958
				}
959
			}
960
961
			if ( $request->getVal( 'minor' ) ) {
962
				$this->minoredit = true;
963
			}
964
		}
965
966
		$this->oldid = $request->getInt( 'oldid' );
967
		$this->parentRevId = $request->getInt( 'parentRevId' );
968
969
		$this->bot = $request->getBool( 'bot', true );
970
		$this->nosummary = $request->getBool( 'nosummary' );
971
972
		// May be overridden by revision.
973
		$this->contentModel = $request->getText( 'model', $this->contentModel );
974
		// May be overridden by revision.
975
		$this->contentFormat = $request->getText( 'format', $this->contentFormat );
976
977
		if ( !ContentHandler::getForModelID( $this->contentModel )
978
			->isSupportedFormat( $this->contentFormat )
979
		) {
980
			throw new ErrorPageError(
981
				'editpage-notsupportedcontentformat-title',
982
				'editpage-notsupportedcontentformat-text',
983
				[ $this->contentFormat, ContentHandler::getLocalizedName( $this->contentModel ) ]
984
			);
985
		}
986
987
		/**
988
		 * @todo Check if the desired model is allowed in this namespace, and if
989
		 *   a transition from the page's current model to the new model is
990
		 *   allowed.
991
		 */
992
993
		$this->editintro = $request->getText( 'editintro',
994
			// Custom edit intro for new sections
995
			$this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
996
997
		// Allow extensions to modify form data
998
		Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
999
1000
	}
1001
1002
	/**
1003
	 * Subpage overridable method for extracting the page content data from the
1004
	 * posted form to be placed in $this->textbox1, if using customized input
1005
	 * this method should be overridden and return the page text that will be used
1006
	 * for saving, preview parsing and so on...
1007
	 *
1008
	 * @param WebRequest $request
1009
	 * @return string|null
1010
	 */
1011
	protected function importContentFormData( &$request ) {
1012
		return; // Don't do anything, EditPage already extracted wpTextbox1
1013
	}
1014
1015
	/**
1016
	 * Initialise form fields in the object
1017
	 * Called on the first invocation, e.g. when a user clicks an edit link
1018
	 * @return bool If the requested section is valid
1019
	 */
1020
	function initialiseForm() {
1021
		global $wgUser;
1022
		$this->edittime = $this->page->getTimestamp();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->page->getTimestamp() can also be of type false. However, the property $edittime is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1023
1024
		$content = $this->getContentObject( false ); # TODO: track content object?!
1025
		if ( $content === false ) {
1026
			return false;
1027
		}
1028
		$this->textbox1 = $this->toEditText( $content );
1029
1030
		// activate checkboxes if user wants them to be always active
1031
		# Sort out the "watch" checkbox
1032
		if ( $wgUser->getOption( 'watchdefault' ) ) {
1033
			# Watch all edits
1034
			$this->watchthis = true;
1035
		} elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1036
			# Watch creations
1037
			$this->watchthis = true;
1038
		} elseif ( $wgUser->isWatched( $this->mTitle ) ) {
1039
			# Already watched
1040
			$this->watchthis = true;
1041
		}
1042
		if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
1043
			$this->minoredit = true;
1044
		}
1045
		if ( $this->textbox1 === false ) {
1046
			return false;
1047
		}
1048
		return true;
1049
	}
1050
1051
	/**
1052
	 * @param Content|null $def_content The default value to return
1053
	 *
1054
	 * @return Content|null Content on success, $def_content for invalid sections
1055
	 *
1056
	 * @since 1.21
1057
	 */
1058
	protected function getContentObject( $def_content = null ) {
1059
		global $wgOut, $wgRequest, $wgUser, $wgContLang;
1060
1061
		$content = false;
1062
1063
		// For message page not locally set, use the i18n message.
1064
		// For other non-existent articles, use preload text if any.
1065
		if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1066
			if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1067
				# If this is a system message, get the default text.
1068
				$msg = $this->mTitle->getDefaultMessageText();
1069
1070
				$content = $this->toEditContent( $msg );
1071
			}
1072
			if ( $content === false ) {
1073
				# If requested, preload some text.
1074
				$preload = $wgRequest->getVal( 'preload',
1075
					// Custom preload text for new sections
1076
					$this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1077
				$params = $wgRequest->getArray( 'preloadparams', [] );
1078
1079
				$content = $this->getPreloadedContent( $preload, $params );
1080
			}
1081
		// For existing pages, get text based on "undo" or section parameters.
1082
		} else {
1083
			if ( $this->section != '' ) {
1084
				// Get section edit text (returns $def_text for invalid sections)
1085
				$orig = $this->getOriginalContent( $wgUser );
1086
				$content = $orig ? $orig->getSection( $this->section ) : null;
1087
1088
				if ( !$content ) {
1089
					$content = $def_content;
1090
				}
1091
			} else {
1092
				$undoafter = $wgRequest->getInt( 'undoafter' );
1093
				$undo = $wgRequest->getInt( 'undo' );
1094
1095
				if ( $undo > 0 && $undoafter > 0 ) {
1096
					$undorev = Revision::newFromId( $undo );
1097
					$oldrev = Revision::newFromId( $undoafter );
1098
1099
					# Sanity check, make sure it's the right page,
1100
					# the revisions exist and they were not deleted.
1101
					# Otherwise, $content will be left as-is.
1102
					if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1103
						!$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1104
						!$oldrev->isDeleted( Revision::DELETED_TEXT )
1105
					) {
1106
						$content = $this->page->getUndoContent( $undorev, $oldrev );
1107
1108
						if ( $content === false ) {
1109
							# Warn the user that something went wrong
1110
							$undoMsg = 'failure';
1111
						} else {
1112
							$oldContent = $this->page->getContent( Revision::RAW );
1113
							$popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
1114
							$newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
1115
1116
							if ( $newContent->equals( $oldContent ) ) {
1117
								# Tell the user that the undo results in no change,
1118
								# i.e. the revisions were already undone.
1119
								$undoMsg = 'nochange';
1120
								$content = false;
1121
							} else {
1122
								# Inform the user of our success and set an automatic edit summary
1123
								$undoMsg = 'success';
1124
1125
								# If we just undid one rev, use an autosummary
1126
								$firstrev = $oldrev->getNext();
1127
								if ( $firstrev && $firstrev->getId() == $undo ) {
1128
									$userText = $undorev->getUserText();
1129
									if ( $userText === '' ) {
1130
										$undoSummary = wfMessage(
1131
											'undo-summary-username-hidden',
1132
											$undo
1133
										)->inContentLanguage()->text();
1134
									} else {
1135
										$undoSummary = wfMessage(
1136
											'undo-summary',
1137
											$undo,
1138
											$userText
1139
										)->inContentLanguage()->text();
1140
									}
1141
									if ( $this->summary === '' ) {
1142
										$this->summary = $undoSummary;
1143
									} else {
1144
										$this->summary = $undoSummary . wfMessage( 'colon-separator' )
1145
											->inContentLanguage()->text() . $this->summary;
1146
									}
1147
									$this->undidRev = $undo;
1148
								}
1149
								$this->formtype = 'diff';
1150
							}
1151
						}
1152
					} else {
1153
						// Failed basic sanity checks.
1154
						// Older revisions may have been removed since the link
1155
						// was created, or we may simply have got bogus input.
1156
						$undoMsg = 'norev';
1157
					}
1158
1159
					// Messages: undo-success, undo-failure, undo-norev, undo-nochange
1160
					$class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1161
					$this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
1162
						wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1163
				}
1164
1165
				if ( $content === false ) {
1166
					$content = $this->getOriginalContent( $wgUser );
1167
				}
1168
			}
1169
		}
1170
1171
		return $content;
1172
	}
1173
1174
	/**
1175
	 * Get the content of the wanted revision, without section extraction.
1176
	 *
1177
	 * The result of this function can be used to compare user's input with
1178
	 * section replaced in its context (using WikiPage::replaceSectionAtRev())
1179
	 * to the original text of the edit.
1180
	 *
1181
	 * This differs from Article::getContent() that when a missing revision is
1182
	 * encountered the result will be null and not the
1183
	 * 'missing-revision' message.
1184
	 *
1185
	 * @since 1.19
1186
	 * @param User $user The user to get the revision for
1187
	 * @return Content|null
1188
	 */
1189
	private function getOriginalContent( User $user ) {
1190
		if ( $this->section == 'new' ) {
1191
			return $this->getCurrentContent();
1192
		}
1193
		$revision = $this->mArticle->getRevisionFetched();
1194
		if ( $revision === null ) {
1195
			if ( !$this->contentModel ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->contentModel of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1196
				$this->contentModel = $this->getTitle()->getContentModel();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getTitle()->getContentModel() can also be of type integer or boolean. However, the property $contentModel is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1197
			}
1198
			$handler = ContentHandler::getForModelID( $this->contentModel );
1199
1200
			return $handler->makeEmptyContent();
1201
		}
1202
		$content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1203
		return $content;
1204
	}
1205
1206
	/**
1207
	 * Get the edit's parent revision ID
1208
	 *
1209
	 * The "parent" revision is the ancestor that should be recorded in this
1210
	 * page's revision history.  It is either the revision ID of the in-memory
1211
	 * article content, or in the case of a 3-way merge in order to rebase
1212
	 * across a recoverable edit conflict, the ID of the newer revision to
1213
	 * which we have rebased this page.
1214
	 *
1215
	 * @since 1.27
1216
	 * @return int Revision ID
1217
	 */
1218
	public function getParentRevId() {
1219
		if ( $this->parentRevId ) {
1220
			return $this->parentRevId;
1221
		} else {
1222
			return $this->mArticle->getRevIdFetched();
1223
		}
1224
	}
1225
1226
	/**
1227
	 * Get the current content of the page. This is basically similar to
1228
	 * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
1229
	 * content object is returned instead of null.
1230
	 *
1231
	 * @since 1.21
1232
	 * @return Content
1233
	 */
1234
	protected function getCurrentContent() {
1235
		$rev = $this->page->getRevision();
1236
		$content = $rev ? $rev->getContent( Revision::RAW ) : null;
1237
1238
		if ( $content === false || $content === null ) {
1239
			if ( !$this->contentModel ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->contentModel of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1240
				$this->contentModel = $this->getTitle()->getContentModel();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getTitle()->getContentModel() can also be of type integer or boolean. However, the property $contentModel is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1241
			}
1242
			$handler = ContentHandler::getForModelID( $this->contentModel );
1243
1244
			return $handler->makeEmptyContent();
1245
		} else {
1246
			# nasty side-effect, but needed for consistency
1247
			$this->contentModel = $rev->getContentModel();
1248
			$this->contentFormat = $rev->getContentFormat();
1249
1250
			return $content;
1251
		}
1252
	}
1253
1254
	/**
1255
	 * Use this method before edit() to preload some content into the edit box
1256
	 *
1257
	 * @param Content $content
1258
	 *
1259
	 * @since 1.21
1260
	 */
1261
	public function setPreloadedContent( Content $content ) {
1262
		$this->mPreloadContent = $content;
1263
	}
1264
1265
	/**
1266
	 * Get the contents to be preloaded into the box, either set by
1267
	 * an earlier setPreloadText() or by loading the given page.
1268
	 *
1269
	 * @param string $preload Representing the title to preload from.
1270
	 * @param array $params Parameters to use (interface-message style) in the preloaded text
1271
	 *
1272
	 * @return Content
1273
	 *
1274
	 * @since 1.21
1275
	 */
1276
	protected function getPreloadedContent( $preload, $params = [] ) {
1277
		global $wgUser;
1278
1279
		if ( !empty( $this->mPreloadContent ) ) {
1280
			return $this->mPreloadContent;
1281
		}
1282
1283
		$handler = ContentHandler::getForTitle( $this->getTitle() );
1284
1285
		if ( $preload === '' ) {
1286
			return $handler->makeEmptyContent();
1287
		}
1288
1289
		$title = Title::newFromText( $preload );
1290
		# Check for existence to avoid getting MediaWiki:Noarticletext
1291 View Code Duplication
		if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1292
			// TODO: somehow show a warning to the user!
1293
			return $handler->makeEmptyContent();
1294
		}
1295
1296
		$page = WikiPage::factory( $title );
1297
		if ( $page->isRedirect() ) {
1298
			$title = $page->getRedirectTarget();
1299
			# Same as before
1300 View Code Duplication
			if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1301
				// TODO: somehow show a warning to the user!
1302
				return $handler->makeEmptyContent();
1303
			}
1304
			$page = WikiPage::factory( $title );
1305
		}
1306
1307
		$parserOptions = ParserOptions::newFromUser( $wgUser );
1308
		$content = $page->getContent( Revision::RAW );
1309
1310
		if ( !$content ) {
1311
			// TODO: somehow show a warning to the user!
1312
			return $handler->makeEmptyContent();
1313
		}
1314
1315
		if ( $content->getModel() !== $handler->getModelID() ) {
1316
			$converted = $content->convert( $handler->getModelID() );
1317
1318
			if ( !$converted ) {
1319
				// TODO: somehow show a warning to the user!
1320
				wfDebug( "Attempt to preload incompatible content: " .
1321
					"can't convert " . $content->getModel() .
1322
					" to " . $handler->getModelID() );
1323
1324
				return $handler->makeEmptyContent();
1325
			}
1326
1327
			$content = $converted;
1328
		}
1329
1330
		return $content->preloadTransform( $title, $parserOptions, $params );
1331
	}
1332
1333
	/**
1334
	 * Make sure the form isn't faking a user's credentials.
1335
	 *
1336
	 * @param WebRequest $request
1337
	 * @return bool
1338
	 * @private
1339
	 */
1340
	function tokenOk( &$request ) {
1341
		global $wgUser;
1342
		$token = $request->getVal( 'wpEditToken' );
1343
		$this->mTokenOk = $wgUser->matchEditToken( $token );
1344
		$this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
1345
		return $this->mTokenOk;
1346
	}
1347
1348
	/**
1349
	 * Sets post-edit cookie indicating the user just saved a particular revision.
1350
	 *
1351
	 * This uses a temporary cookie for each revision ID so separate saves will never
1352
	 * interfere with each other.
1353
	 *
1354
	 * The cookie is deleted in the mediawiki.action.view.postEdit JS module after
1355
	 * the redirect.  It must be clearable by JavaScript code, so it must not be
1356
	 * marked HttpOnly. The JavaScript code converts the cookie to a wgPostEdit config
1357
	 * variable.
1358
	 *
1359
	 * If the variable were set on the server, it would be cached, which is unwanted
1360
	 * since the post-edit state should only apply to the load right after the save.
1361
	 *
1362
	 * @param int $statusValue The status value (to check for new article status)
1363
	 */
1364
	protected function setPostEditCookie( $statusValue ) {
1365
		$revisionId = $this->page->getLatest();
1366
		$postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1367
1368
		$val = 'saved';
1369
		if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1370
			$val = 'created';
1371
		} elseif ( $this->oldid ) {
1372
			$val = 'restored';
1373
		}
1374
1375
		$response = RequestContext::getMain()->getRequest()->response();
1376
		$response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
1377
			'httpOnly' => false,
1378
		] );
1379
	}
1380
1381
	/**
1382
	 * Attempt submission
1383
	 * @param array $resultDetails See docs for $result in internalAttemptSave
1384
	 * @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError
1385
	 * @return Status The resulting status object.
1386
	 */
1387
	public function attemptSave( &$resultDetails = false ) {
1388
		global $wgUser;
1389
1390
		# Allow bots to exempt some edits from bot flagging
1391
		$bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
1392
		$status = $this->internalAttemptSave( $resultDetails, $bot );
0 ignored issues
show
Bug introduced by
It seems like $resultDetails defined by parameter $resultDetails on line 1387 can also be of type false; however, EditPage::internalAttemptSave() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1393
1394
		Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1395
1396
		return $status;
1397
	}
1398
1399
	/**
1400
	 * Handle status, such as after attempt save
1401
	 *
1402
	 * @param Status $status
1403
	 * @param array|bool $resultDetails
1404
	 *
1405
	 * @throws ErrorPageError
1406
	 * @return bool False, if output is done, true if rest of the form should be displayed
1407
	 */
1408
	private function handleStatus( Status $status, $resultDetails ) {
1409
		global $wgUser, $wgOut;
1410
1411
		/**
1412
		 * @todo FIXME: once the interface for internalAttemptSave() is made
1413
		 *   nicer, this should use the message in $status
1414
		 */
1415
		if ( $status->value == self::AS_SUCCESS_UPDATE
1416
			|| $status->value == self::AS_SUCCESS_NEW_ARTICLE
1417
		) {
1418
			$this->didSave = true;
1419
			if ( !$resultDetails['nullEdit'] ) {
1420
				$this->setPostEditCookie( $status->value );
1421
			}
1422
		}
1423
1424
		// "wpExtraQueryRedirect" is a hidden input to modify
1425
		// after save URL and is not used by actual edit form
1426
		$request = RequestContext::getMain()->getRequest();
1427
		$extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1428
1429
		switch ( $status->value ) {
1430
			case self::AS_HOOK_ERROR_EXPECTED:
1431
			case self::AS_CONTENT_TOO_BIG:
1432
			case self::AS_ARTICLE_WAS_DELETED:
1433
			case self::AS_CONFLICT_DETECTED:
1434
			case self::AS_SUMMARY_NEEDED:
1435
			case self::AS_TEXTBOX_EMPTY:
1436
			case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1437
			case self::AS_END:
1438
			case self::AS_BLANK_ARTICLE:
1439
			case self::AS_SELF_REDIRECT:
1440
				return true;
1441
1442
			case self::AS_HOOK_ERROR:
1443
				return false;
1444
1445
			case self::AS_CANNOT_USE_CUSTOM_MODEL:
1446
			case self::AS_PARSE_ERROR:
1447
				$wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' );
1448
				return true;
1449
1450
			case self::AS_SUCCESS_NEW_ARTICLE:
1451
				$query = $resultDetails['redirect'] ? 'redirect=no' : '';
1452
				if ( $extraQueryRedirect ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extraQueryRedirect of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1453
					if ( $query === '' ) {
1454
						$query = $extraQueryRedirect;
1455
					} else {
1456
						$query = $query . '&' . $extraQueryRedirect;
1457
					}
1458
				}
1459
				$anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1460
				$wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1461
				return false;
1462
1463
			case self::AS_SUCCESS_UPDATE:
1464
				$extraQuery = '';
1465
				$sectionanchor = $resultDetails['sectionanchor'];
1466
1467
				// Give extensions a chance to modify URL query on update
1468
				Hooks::run(
1469
					'ArticleUpdateBeforeRedirect',
1470
					[ $this->mArticle, &$sectionanchor, &$extraQuery ]
1471
				);
1472
1473
				if ( $resultDetails['redirect'] ) {
1474
					if ( $extraQuery == '' ) {
1475
						$extraQuery = 'redirect=no';
1476
					} else {
1477
						$extraQuery = 'redirect=no&' . $extraQuery;
1478
					}
1479
				}
1480
				if ( $extraQueryRedirect ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extraQueryRedirect of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1481
					if ( $extraQuery === '' ) {
1482
						$extraQuery = $extraQueryRedirect;
1483
					} else {
1484
						$extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1485
					}
1486
				}
1487
1488
				$wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1489
				return false;
1490
1491
			case self::AS_SPAM_ERROR:
1492
				$this->spamPageWithContent( $resultDetails['spam'] );
1493
				return false;
1494
1495
			case self::AS_BLOCKED_PAGE_FOR_USER:
1496
				throw new UserBlockedError( $wgUser->getBlock() );
1497
1498
			case self::AS_IMAGE_REDIRECT_ANON:
1499
			case self::AS_IMAGE_REDIRECT_LOGGED:
1500
				throw new PermissionsError( 'upload' );
1501
1502
			case self::AS_READ_ONLY_PAGE_ANON:
1503
			case self::AS_READ_ONLY_PAGE_LOGGED:
1504
				throw new PermissionsError( 'edit' );
1505
1506
			case self::AS_READ_ONLY_PAGE:
1507
				throw new ReadOnlyError;
1508
1509
			case self::AS_RATE_LIMITED:
1510
				throw new ThrottledError();
1511
1512
			case self::AS_NO_CREATE_PERMISSION:
1513
				$permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1514
				throw new PermissionsError( $permission );
1515
1516
			case self::AS_NO_CHANGE_CONTENT_MODEL:
1517
				throw new PermissionsError( 'editcontentmodel' );
1518
1519
			default:
1520
				// We don't recognize $status->value. The only way that can happen
1521
				// is if an extension hook aborted from inside ArticleSave.
1522
				// Render the status object into $this->hookError
1523
				// FIXME this sucks, we should just use the Status object throughout
1524
				$this->hookError = '<div class="error">' . $status->getWikiText() .
1525
					'</div>';
1526
				return true;
1527
		}
1528
	}
1529
1530
	/**
1531
	 * Run hooks that can filter edits just before they get saved.
1532
	 *
1533
	 * @param Content $content The Content to filter.
1534
	 * @param Status $status For reporting the outcome to the caller
1535
	 * @param User $user The user performing the edit
1536
	 *
1537
	 * @return bool
1538
	 */
1539
	protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1540
		// Run old style post-section-merge edit filter
1541
		if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged',
1542
			[ $this, $content, &$this->hookError, $this->summary ] )
1543
		) {
1544
			# Error messages etc. could be handled within the hook...
1545
			$status->fatal( 'hookaborted' );
1546
			$status->value = self::AS_HOOK_ERROR;
1547
			return false;
1548
		} elseif ( $this->hookError != '' ) {
1549
			# ...or the hook could be expecting us to produce an error
1550
			$status->fatal( 'hookaborted' );
1551
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1552
			return false;
1553
		}
1554
1555
		// Run new style post-section-merge edit filter
1556
		if ( !Hooks::run( 'EditFilterMergedContent',
1557
				[ $this->mArticle->getContext(), $content, $status, $this->summary,
1558
				$user, $this->minoredit ] )
1559
		) {
1560
			# Error messages etc. could be handled within the hook...
1561
			if ( $status->isGood() ) {
1562
				$status->fatal( 'hookaborted' );
1563
				// Not setting $this->hookError here is a hack to allow the hook
1564
				// to cause a return to the edit page without $this->hookError
1565
				// being set. This is used by ConfirmEdit to display a captcha
1566
				// without any error message cruft.
1567
			} else {
1568
				$this->hookError = $status->getWikiText();
1569
			}
1570
			// Use the existing $status->value if the hook set it
1571
			if ( !$status->value ) {
1572
				$status->value = self::AS_HOOK_ERROR;
1573
			}
1574
			return false;
1575
		} elseif ( !$status->isOK() ) {
1576
			# ...or the hook could be expecting us to produce an error
1577
			// FIXME this sucks, we should just use the Status object throughout
1578
			$this->hookError = $status->getWikiText();
1579
			$status->fatal( 'hookaborted' );
1580
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1581
			return false;
1582
		}
1583
1584
		return true;
1585
	}
1586
1587
	/**
1588
	 * Return the summary to be used for a new section.
1589
	 *
1590
	 * @param string $sectionanchor Set to the section anchor text
1591
	 * @return string
1592
	 */
1593
	private function newSectionSummary( &$sectionanchor = null ) {
1594
		global $wgParser;
1595
1596
		if ( $this->sectiontitle !== '' ) {
1597
			$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
1598
			// If no edit summary was specified, create one automatically from the section
1599
			// title and have it link to the new section. Otherwise, respect the summary as
1600
			// passed.
1601
			if ( $this->summary === '' ) {
1602
				$cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1603
				return wfMessage( 'newsectionsummary' )
1604
					->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1605
			}
1606
		} elseif ( $this->summary !== '' ) {
1607
			$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
1608
			# This is a new section, so create a link to the new section
1609
			# in the revision summary.
1610
			$cleanSummary = $wgParser->stripSectionName( $this->summary );
1611
			return wfMessage( 'newsectionsummary' )
1612
				->rawParams( $cleanSummary )->inContentLanguage()->text();
1613
		}
1614
		return $this->summary;
1615
	}
1616
1617
	/**
1618
	 * Attempt submission (no UI)
1619
	 *
1620
	 * @param array $result Array to add statuses to, currently with the
1621
	 *   possible keys:
1622
	 *   - spam (string): Spam string from content if any spam is detected by
1623
	 *     matchSpamRegex.
1624
	 *   - sectionanchor (string): Section anchor for a section save.
1625
	 *   - nullEdit (boolean): Set if doEditContent is OK.  True if null edit,
1626
	 *     false otherwise.
1627
	 *   - redirect (bool): Set if doEditContent is OK. True if resulting
1628
	 *     revision is a redirect.
1629
	 * @param bool $bot True if edit is being made under the bot right.
1630
	 *
1631
	 * @return Status Status object, possibly with a message, but always with
1632
	 *   one of the AS_* constants in $status->value,
1633
	 *
1634
	 * @todo FIXME: This interface is TERRIBLE, but hard to get rid of due to
1635
	 *   various error display idiosyncrasies. There are also lots of cases
1636
	 *   where error metadata is set in the object and retrieved later instead
1637
	 *   of being returned, e.g. AS_CONTENT_TOO_BIG and
1638
	 *   AS_BLOCKED_PAGE_FOR_USER. All that stuff needs to be cleaned up some
1639
	 * time.
1640
	 */
1641
	function internalAttemptSave( &$result, $bot = false ) {
1642
		global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
1643
		global $wgContentHandlerUseDB;
1644
1645
		$status = Status::newGood();
1646
1647
		if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1648
			wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1649
			$status->fatal( 'hookaborted' );
1650
			$status->value = self::AS_HOOK_ERROR;
1651
			return $status;
1652
		}
1653
1654
		$spam = $wgRequest->getText( 'wpAntispam' );
1655
		if ( $spam !== '' ) {
1656
			wfDebugLog(
1657
				'SimpleAntiSpam',
1658
				$wgUser->getName() .
1659
				' editing "' .
1660
				$this->mTitle->getPrefixedText() .
1661
				'" submitted bogus field "' .
1662
				$spam .
1663
				'"'
1664
			);
1665
			$status->fatal( 'spamprotectionmatch', false );
1666
			$status->value = self::AS_SPAM_ERROR;
1667
			return $status;
1668
		}
1669
1670
		try {
1671
			# Construct Content object
1672
			$textbox_content = $this->toEditContent( $this->textbox1 );
1673
		} catch ( MWContentSerializationException $ex ) {
1674
			$status->fatal(
1675
				'content-failed-to-parse',
1676
				$this->contentModel,
1677
				$this->contentFormat,
1678
				$ex->getMessage()
1679
			);
1680
			$status->value = self::AS_PARSE_ERROR;
1681
			return $status;
1682
		}
1683
1684
		# Check image redirect
1685
		if ( $this->mTitle->getNamespace() == NS_FILE &&
1686
			$textbox_content->isRedirect() &&
1687
			!$wgUser->isAllowed( 'upload' )
1688
		) {
1689
				$code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1690
				$status->setResult( false, $code );
1691
1692
				return $status;
1693
		}
1694
1695
		# Check for spam
1696
		$match = self::matchSummarySpamRegex( $this->summary );
1697
		if ( $match === false && $this->section == 'new' ) {
1698
			# $wgSpamRegex is enforced on this new heading/summary because, unlike
1699
			# regular summaries, it is added to the actual wikitext.
1700
			if ( $this->sectiontitle !== '' ) {
1701
				# This branch is taken when the API is used with the 'sectiontitle' parameter.
1702
				$match = self::matchSpamRegex( $this->sectiontitle );
1703
			} else {
1704
				# This branch is taken when the "Add Topic" user interface is used, or the API
1705
				# is used with the 'summary' parameter.
1706
				$match = self::matchSpamRegex( $this->summary );
1707
			}
1708
		}
1709
		if ( $match === false ) {
1710
			$match = self::matchSpamRegex( $this->textbox1 );
1711
		}
1712
		if ( $match !== false ) {
1713
			$result['spam'] = $match;
1714
			$ip = $wgRequest->getIP();
1715
			$pdbk = $this->mTitle->getPrefixedDBkey();
1716
			$match = str_replace( "\n", '', $match );
1717
			wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1718
			$status->fatal( 'spamprotectionmatch', $match );
1719
			$status->value = self::AS_SPAM_ERROR;
1720
			return $status;
1721
		}
1722
		if ( !Hooks::run(
1723
			'EditFilter',
1724
			[ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1725
		) {
1726
			# Error messages etc. could be handled within the hook...
1727
			$status->fatal( 'hookaborted' );
1728
			$status->value = self::AS_HOOK_ERROR;
1729
			return $status;
1730
		} elseif ( $this->hookError != '' ) {
1731
			# ...or the hook could be expecting us to produce an error
1732
			$status->fatal( 'hookaborted' );
1733
			$status->value = self::AS_HOOK_ERROR_EXPECTED;
1734
			return $status;
1735
		}
1736
1737
		if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
1738
			// Auto-block user's IP if the account was "hard" blocked
1739
			if ( !wfReadOnly() ) {
1740
				$wgUser->spreadAnyEditBlock();
1741
			}
1742
			# Check block state against master, thus 'false'.
1743
			$status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1744
			return $status;
1745
		}
1746
1747
		$this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
0 ignored issues
show
Documentation Bug introduced by
The property $kblength was declared of type boolean, but (int) (strlen($this->textbox1) / 1024) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1748 View Code Duplication
		if ( $this->kblength > $wgMaxArticleSize ) {
1749
			// Error will be displayed by showEditForm()
1750
			$this->tooBig = true;
1751
			$status->setResult( false, self::AS_CONTENT_TOO_BIG );
1752
			return $status;
1753
		}
1754
1755 View Code Duplication
		if ( !$wgUser->isAllowed( 'edit' ) ) {
1756
			if ( $wgUser->isAnon() ) {
1757
				$status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1758
				return $status;
1759
			} else {
1760
				$status->fatal( 'readonlytext' );
1761
				$status->value = self::AS_READ_ONLY_PAGE_LOGGED;
1762
				return $status;
1763
			}
1764
		}
1765
1766
		$changingContentModel = false;
1767
		if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1768 View Code Duplication
			if ( !$wgContentHandlerUseDB ) {
1769
				$status->fatal( 'editpage-cannot-use-custom-model' );
1770
				$status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
1771
				return $status;
1772
			} elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
1773
				$status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1774
				return $status;
1775
1776
			}
1777
			$changingContentModel = true;
1778
			$oldContentModel = $this->mTitle->getContentModel();
1779
		}
1780
1781
		if ( $this->changeTags ) {
1782
			$changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
1783
				$this->changeTags, $wgUser );
1784
			if ( !$changeTagsStatus->isOK() ) {
1785
				$changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
1786
				return $changeTagsStatus;
1787
			}
1788
		}
1789
1790
		if ( wfReadOnly() ) {
1791
			$status->fatal( 'readonlytext' );
1792
			$status->value = self::AS_READ_ONLY_PAGE;
1793
			return $status;
1794
		}
1795
		if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
1796
			$status->fatal( 'actionthrottledtext' );
1797
			$status->value = self::AS_RATE_LIMITED;
1798
			return $status;
1799
		}
1800
1801
		# If the article has been deleted while editing, don't save it without
1802
		# confirmation
1803
		if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1804
			$status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1805
			return $status;
1806
		}
1807
1808
		# Load the page data from the master. If anything changes in the meantime,
1809
		# we detect it by using page_latest like a token in a 1 try compare-and-swap.
1810
		$this->page->loadPageData( 'fromdbmaster' );
1811
		$new = !$this->page->exists();
1812
1813
		if ( $new ) {
1814
			// Late check for create permission, just in case *PARANOIA*
1815
			if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
1816
				$status->fatal( 'nocreatetext' );
1817
				$status->value = self::AS_NO_CREATE_PERMISSION;
1818
				wfDebug( __METHOD__ . ": no create permission\n" );
1819
				return $status;
1820
			}
1821
1822
			// Don't save a new page if it's blank or if it's a MediaWiki:
1823
			// message with content equivalent to default (allow empty pages
1824
			// in this case to disable messages, see bug 50124)
1825
			$defaultMessageText = $this->mTitle->getDefaultMessageText();
1826
			if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1827
				$defaultText = $defaultMessageText;
1828
			} else {
1829
				$defaultText = '';
1830
			}
1831
1832
			if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1833
				$this->blankArticle = true;
1834
				$status->fatal( 'blankarticle' );
1835
				$status->setResult( false, self::AS_BLANK_ARTICLE );
1836
				return $status;
1837
			}
1838
1839
			if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) {
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1672 can be null; however, EditPage::runPostMergeFilters() 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...
1840
				return $status;
1841
			}
1842
1843
			$content = $textbox_content;
1844
1845
			$result['sectionanchor'] = '';
1846
			if ( $this->section == 'new' ) {
1847
				if ( $this->sectiontitle !== '' ) {
1848
					// Insert the section title above the content.
1849
					$content = $content->addSectionHeader( $this->sectiontitle );
1850
				} elseif ( $this->summary !== '' ) {
1851
					// Insert the section title above the content.
1852
					$content = $content->addSectionHeader( $this->summary );
1853
				}
1854
				$this->summary = $this->newSectionSummary( $result['sectionanchor'] );
1855
			}
1856
1857
			$status->value = self::AS_SUCCESS_NEW_ARTICLE;
1858
1859
		} else { # not $new
1860
1861
			# Article exists. Check for edit conflict.
1862
1863
			$this->page->clear(); # Force reload of dates, etc.
1864
			$timestamp = $this->page->getTimestamp();
1865
1866
			wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
1867
1868
			if ( $timestamp != $this->edittime ) {
1869
				$this->isConflict = true;
1870
				if ( $this->section == 'new' ) {
1871
					if ( $this->page->getUserText() == $wgUser->getName() &&
1872
						$this->page->getComment() == $this->newSectionSummary()
1873
					) {
1874
						// Probably a duplicate submission of a new comment.
1875
						// This can happen when CDN resends a request after
1876
						// a timeout but the first one actually went through.
1877
						wfDebug( __METHOD__
1878
							. ": duplicate new section submission; trigger edit conflict!\n" );
1879
					} else {
1880
						// New comment; suppress conflict.
1881
						$this->isConflict = false;
1882
						wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
1883
					}
1884
				} elseif ( $this->section == ''
1885
					&& Revision::userWasLastToEdit(
0 ignored issues
show
Deprecated Code introduced by
The method Revision::userWasLastToEdit() has been deprecated with message: since 1.24

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...
1886
						DB_MASTER, $this->mTitle->getArticleID(),
1887
						$wgUser->getId(), $this->edittime
1888
					)
1889
				) {
1890
					# Suppress edit conflict with self, except for section edits where merging is required.
1891
					wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
1892
					$this->isConflict = false;
1893
				}
1894
			}
1895
1896
			// If sectiontitle is set, use it, otherwise use the summary as the section title.
1897
			if ( $this->sectiontitle !== '' ) {
1898
				$sectionTitle = $this->sectiontitle;
1899
			} else {
1900
				$sectionTitle = $this->summary;
1901
			}
1902
1903
			$content = null;
0 ignored issues
show
Unused Code introduced by
$content is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1904
1905
			if ( $this->isConflict ) {
1906
				wfDebug( __METHOD__
1907
					. ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
1908
					. " (article time '{$timestamp}')\n" );
1909
1910
				$content = $this->page->replaceSectionContent(
0 ignored issues
show
Deprecated Code introduced by
The method WikiPage::replaceSectionContent() has been deprecated with message: since 1.24, use replaceSectionAtRev instead

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...
1911
					$this->section,
1912
					$textbox_content,
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1672 can be null; however, WikiPage::replaceSectionContent() 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...
1913
					$sectionTitle,
1914
					$this->edittime
1915
				);
1916
			} else {
1917
				wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
1918
				$content = $this->page->replaceSectionContent(
0 ignored issues
show
Deprecated Code introduced by
The method WikiPage::replaceSectionContent() has been deprecated with message: since 1.24, use replaceSectionAtRev instead

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...
1919
					$this->section,
1920
					$textbox_content,
0 ignored issues
show
Bug introduced by
It seems like $textbox_content defined by $this->toEditContent($this->textbox1) on line 1672 can be null; however, WikiPage::replaceSectionContent() 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...
1921
					$sectionTitle
1922
				);
1923
			}
1924
1925
			if ( is_null( $content ) ) {
1926
				wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
1927
				$this->isConflict = true;
1928
				$content = $textbox_content; // do not try to merge here!
1929
			} elseif ( $this->isConflict ) {
1930
				# Attempt merge
1931
				if ( $this->mergeChangesIntoContent( $content ) ) {
0 ignored issues
show
Bug introduced by
It seems like $content can also be of type string; however, EditPage::mergeChangesIntoContent() does only seem to accept object<Content>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1932
					// Successful merge! Maybe we should tell the user the good news?
1933
					$this->isConflict = false;
1934
					wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
1935
				} else {
1936
					$this->section = '';
1937
					$this->textbox1 = ContentHandler::getContentText( $content );
1938
					wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
1939
				}
1940
			}
1941
1942
			if ( $this->isConflict ) {
1943
				$status->setResult( false, self::AS_CONFLICT_DETECTED );
1944
				return $status;
1945
			}
1946
1947
			if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
0 ignored issues
show
Bug introduced by
It seems like $content can also be of type null or string; however, EditPage::runPostMergeFilters() does only seem to accept object<Content>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1948
				return $status;
1949
			}
1950
1951
			if ( $this->section == 'new' ) {
1952
				// Handle the user preference to force summaries here
1953
				if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
1954
					$this->missingSummary = true;
1955
					$status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
1956
					$status->value = self::AS_SUMMARY_NEEDED;
1957
					return $status;
1958
				}
1959
1960
				// Do not allow the user to post an empty comment
1961
				if ( $this->textbox1 == '' ) {
1962
					$this->missingComment = true;
1963
					$status->fatal( 'missingcommenttext' );
1964
					$status->value = self::AS_TEXTBOX_EMPTY;
1965
					return $status;
1966
				}
1967
			} elseif ( !$this->allowBlankSummary
1968
				&& !$content->equals( $this->getOriginalContent( $wgUser ) )
1969
				&& !$content->isRedirect()
1970
				&& md5( $this->summary ) == $this->autoSumm
1971
			) {
1972
				$this->missingSummary = true;
1973
				$status->fatal( 'missingsummary' );
1974
				$status->value = self::AS_SUMMARY_NEEDED;
1975
				return $status;
1976
			}
1977
1978
			# All's well
1979
			$sectionanchor = '';
1980
			if ( $this->section == 'new' ) {
1981
				$this->summary = $this->newSectionSummary( $sectionanchor );
1982
			} elseif ( $this->section != '' ) {
1983
				# Try to get a section anchor from the section source, redirect
1984
				# to edited section if header found.
1985
				# XXX: Might be better to integrate this into Article::replaceSectionAtRev
1986
				# for duplicate heading checking and maybe parsing.
1987
				$hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
1988
				# We can't deal with anchors, includes, html etc in the header for now,
1989
				# headline would need to be parsed to improve this.
1990
				if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
1991
					$sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
1992
				}
1993
			}
1994
			$result['sectionanchor'] = $sectionanchor;
1995
1996
			// Save errors may fall down to the edit form, but we've now
1997
			// merged the section into full text. Clear the section field
1998
			// so that later submission of conflict forms won't try to
1999
			// replace that into a duplicated mess.
2000
			$this->textbox1 = $this->toEditText( $content );
2001
			$this->section = '';
2002
2003
			$status->value = self::AS_SUCCESS_UPDATE;
2004
		}
2005
2006
		if ( !$this->allowSelfRedirect
2007
			&& $content->isRedirect()
2008
			&& $content->getRedirectTarget()->equals( $this->getTitle() )
2009
		) {
2010
			// If the page already redirects to itself, don't warn.
2011
			$currentTarget = $this->getCurrentContent()->getRedirectTarget();
2012
			if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2013
				$this->selfRedirect = true;
2014
				$status->fatal( 'selfredirect' );
2015
				$status->value = self::AS_SELF_REDIRECT;
2016
				return $status;
2017
			}
2018
		}
2019
2020
		// Check for length errors again now that the section is merged in
2021
		$this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 );
0 ignored issues
show
Documentation Bug introduced by
The property $kblength was declared of type boolean, but (int) (strlen($this->toE...Text($content)) / 1024) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
2022 View Code Duplication
		if ( $this->kblength > $wgMaxArticleSize ) {
2023
			$this->tooBig = true;
2024
			$status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2025
			return $status;
2026
		}
2027
2028
		$flags = EDIT_AUTOSUMMARY |
2029
			( $new ? EDIT_NEW : EDIT_UPDATE ) |
2030
			( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2031
			( $bot ? EDIT_FORCE_BOT : 0 );
2032
2033
		$doEditStatus = $this->page->doEditContent(
2034
			$content,
2035
			$this->summary,
2036
			$flags,
2037
			false,
2038
			$wgUser,
2039
			$content->getDefaultFormat(),
2040
			$this->changeTags
2041
		);
2042
2043
		if ( !$doEditStatus->isOK() ) {
2044
			// Failure from doEdit()
2045
			// Show the edit conflict page for certain recognized errors from doEdit(),
2046
			// but don't show it for errors from extension hooks
2047
			$errors = $doEditStatus->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...
2048
			if ( in_array( $errors[0][0],
2049
					[ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2050
			) {
2051
				$this->isConflict = true;
2052
				// Destroys data doEdit() put in $status->value but who cares
2053
				$doEditStatus->value = self::AS_END;
2054
			}
2055
			return $doEditStatus;
2056
		}
2057
2058
		$result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2059
		if ( $result['nullEdit'] ) {
2060
			// We don't know if it was a null edit until now, so increment here
2061
			$wgUser->pingLimiter( 'linkpurge' );
2062
		}
2063
		$result['redirect'] = $content->isRedirect();
2064
2065
		$this->updateWatchlist();
2066
2067
		// If the content model changed, add a log entry
2068
		if ( $changingContentModel ) {
2069
			$this->addContentModelChangeLogEntry(
2070
				$wgUser,
2071
				$new ? false : $oldContentModel,
0 ignored issues
show
Bug introduced by
The variable $oldContentModel 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...
2072
				$this->contentModel,
2073
				$this->summary
2074
			);
2075
		}
2076
2077
		return $status;
2078
	}
2079
2080
	/**
2081
	 * @param User $user
2082
	 * @param string|false $oldModel false if the page is being newly created
2083
	 * @param string $newModel
2084
	 * @param string $reason
2085
	 */
2086
	protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2087
		$new = $oldModel === false;
2088
		$log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2089
		$log->setPerformer( $user );
2090
		$log->setTarget( $this->mTitle );
2091
		$log->setComment( $reason );
2092
		$log->setParameters( [
2093
			'4::oldmodel' => $oldModel,
2094
			'5::newmodel' => $newModel
2095
		] );
2096
		$logid = $log->insert();
2097
		$log->publish( $logid );
2098
	}
2099
2100
	/**
2101
	 * Register the change of watch status
2102
	 */
2103
	protected function updateWatchlist() {
2104
		global $wgUser;
2105
2106
		if ( !$wgUser->isLoggedIn() ) {
2107
			return;
2108
		}
2109
2110
		$user = $wgUser;
2111
		$title = $this->mTitle;
2112
		$watch = $this->watchthis;
2113
		// Do this in its own transaction to reduce contention...
2114
		DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2115
			if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2116
				return; // nothing to change
2117
			}
2118
			WatchAction::doWatchOrUnwatch( $watch, $title, $user );
2119
		} );
2120
	}
2121
2122
	/**
2123
	 * Attempts to do 3-way merge of edit content with a base revision
2124
	 * and current content, in case of edit conflict, in whichever way appropriate
2125
	 * for the content type.
2126
	 *
2127
	 * @since 1.21
2128
	 *
2129
	 * @param Content $editContent
2130
	 *
2131
	 * @return bool
2132
	 */
2133
	private function mergeChangesIntoContent( &$editContent ) {
2134
2135
		$db = wfGetDB( DB_MASTER );
2136
2137
		// This is the revision the editor started from
2138
		$baseRevision = $this->getBaseRevision();
2139
		$baseContent = $baseRevision ? $baseRevision->getContent() : null;
2140
2141
		if ( is_null( $baseContent ) ) {
2142
			return false;
2143
		}
2144
2145
		// The current state, we want to merge updates into it
2146
		$currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2147
		$currentContent = $currentRevision ? $currentRevision->getContent() : null;
2148
2149
		if ( is_null( $currentContent ) ) {
2150
			return false;
2151
		}
2152
2153
		$handler = ContentHandler::getForModelID( $baseContent->getModel() );
2154
2155
		$result = $handler->merge3( $baseContent, $editContent, $currentContent );
2156
2157
		if ( $result ) {
2158
			$editContent = $result;
2159
			// Update parentRevId to what we just merged.
2160
			$this->parentRevId = $currentRevision->getId();
2161
			return true;
2162
		}
2163
2164
		return false;
2165
	}
2166
2167
	/**
2168
	 * @note: this method is very poorly named. If the user opened the form with ?oldid=X,
2169
	 *        one might think of X as the "base revision", which is NOT what this returns.
2170
	 * @return Revision Current version when the edit was started
2171
	 */
2172
	function getBaseRevision() {
2173
		if ( !$this->mBaseRevision ) {
2174
			$db = wfGetDB( DB_MASTER );
2175
			$this->mBaseRevision = Revision::loadFromTimestamp(
0 ignored issues
show
Documentation Bug introduced by
It seems like \Revision::loadFromTimes...Title, $this->edittime) can also be of type object<Revision>. However, the property $mBaseRevision is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2176
				$db, $this->mTitle, $this->edittime );
2177
		}
2178
		return $this->mBaseRevision;
2179
	}
2180
2181
	/**
2182
	 * Check given input text against $wgSpamRegex, and return the text of the first match.
2183
	 *
2184
	 * @param string $text
2185
	 *
2186
	 * @return string|bool Matching string or false
2187
	 */
2188
	public static function matchSpamRegex( $text ) {
2189
		global $wgSpamRegex;
2190
		// For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2191
		$regexes = (array)$wgSpamRegex;
2192
		return self::matchSpamRegexInternal( $text, $regexes );
2193
	}
2194
2195
	/**
2196
	 * Check given input text against $wgSummarySpamRegex, and return the text of the first match.
2197
	 *
2198
	 * @param string $text
2199
	 *
2200
	 * @return string|bool Matching string or false
2201
	 */
2202
	public static function matchSummarySpamRegex( $text ) {
2203
		global $wgSummarySpamRegex;
2204
		$regexes = (array)$wgSummarySpamRegex;
2205
		return self::matchSpamRegexInternal( $text, $regexes );
2206
	}
2207
2208
	/**
2209
	 * @param string $text
2210
	 * @param array $regexes
2211
	 * @return bool|string
2212
	 */
2213
	protected static function matchSpamRegexInternal( $text, $regexes ) {
2214
		foreach ( $regexes as $regex ) {
2215
			$matches = [];
2216
			if ( preg_match( $regex, $text, $matches ) ) {
2217
				return $matches[0];
2218
			}
2219
		}
2220
		return false;
2221
	}
2222
2223
	function setHeaders() {
2224
		global $wgOut, $wgUser, $wgAjaxEditStash;
2225
2226
		$wgOut->addModules( 'mediawiki.action.edit' );
2227
		$wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
2228
2229
		if ( $wgUser->getOption( 'showtoolbar' ) ) {
2230
			// The addition of default buttons is handled by getEditToolbar() which
2231
			// has its own dependency on this module. The call here ensures the module
2232
			// is loaded in time (it has position "top") for other modules to register
2233
			// buttons (e.g. extensions, gadgets, user scripts).
2234
			$wgOut->addModules( 'mediawiki.toolbar' );
2235
		}
2236
2237
		if ( $wgUser->getOption( 'uselivepreview' ) ) {
2238
			$wgOut->addModules( 'mediawiki.action.edit.preview' );
2239
		}
2240
2241
		if ( $wgUser->getOption( 'useeditwarning' ) ) {
2242
			$wgOut->addModules( 'mediawiki.action.edit.editWarning' );
2243
		}
2244
2245
		if ( $wgAjaxEditStash ) {
2246
			$wgOut->addModules( 'mediawiki.action.edit.stash' );
2247
		}
2248
2249
		# Enabled article-related sidebar, toplinks, etc.
2250
		$wgOut->setArticleRelated( true );
2251
2252
		$contextTitle = $this->getContextTitle();
2253
		if ( $this->isConflict ) {
2254
			$msg = 'editconflict';
2255
		} elseif ( $contextTitle->exists() && $this->section != '' ) {
2256
			$msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2257
		} else {
2258
			$msg = $contextTitle->exists()
2259
				|| ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2260
					&& $contextTitle->getDefaultMessageText() !== false
2261
				)
2262
				? 'editing'
2263
				: 'creating';
2264
		}
2265
2266
		# Use the title defined by DISPLAYTITLE magic word when present
2267
		# NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2268
		#       setPageTitle() treats the input as wikitext, which should be safe in either case.
2269
		$displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2270
		if ( $displayTitle === false ) {
2271
			$displayTitle = $contextTitle->getPrefixedText();
2272
		}
2273
		$wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
2274
		# Transmit the name of the message to JavaScript for live preview
2275
		# Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2276
		$wgOut->addJsConfigVars( 'wgEditMessage', $msg );
2277
	}
2278
2279
	/**
2280
	 * Show all applicable editing introductions
2281
	 */
2282
	protected function showIntro() {
2283
		global $wgOut, $wgUser;
2284
		if ( $this->suppressIntro ) {
2285
			return;
2286
		}
2287
2288
		$namespace = $this->mTitle->getNamespace();
2289
2290
		if ( $namespace == NS_MEDIAWIKI ) {
2291
			# Show a warning if editing an interface message
2292
			$wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2293
			# If this is a default message (but not css or js),
2294
			# show a hint that it is translatable on translatewiki.net
2295
			if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2296
				&& !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2297
			) {
2298
				$defaultMessageText = $this->mTitle->getDefaultMessageText();
2299
				if ( $defaultMessageText !== false ) {
2300
					$wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2301
						'translateinterface' );
2302
				}
2303
			}
2304
		} elseif ( $namespace == NS_FILE ) {
2305
			# Show a hint to shared repo
2306
			$file = wfFindFile( $this->mTitle );
2307
			if ( $file && !$file->isLocal() ) {
2308
				$descUrl = $file->getDescriptionUrl();
2309
				# there must be a description url to show a hint to shared repo
2310
				if ( $descUrl ) {
2311
					if ( !$this->mTitle->exists() ) {
2312
						$wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2313
									'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2314
						] );
2315
					} else {
2316
						$wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2317
									'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2318
						] );
2319
					}
2320
				}
2321
			}
2322
		}
2323
2324
		# Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2325
		# Show log extract when the user is currently blocked
2326
		if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2327
			$username = explode( '/', $this->mTitle->getText(), 2 )[0];
2328
			$user = User::newFromName( $username, false /* allow IP users*/ );
2329
			$ip = User::isIP( $username );
2330
			$block = Block::newFromTarget( $user, $user );
0 ignored issues
show
Security Bug introduced by
It seems like $user defined by \User::newFromName($username, false) on line 2328 can also be of type false; however, Block::newFromTarget() does only seem to accept string|object<User>|integer, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
Security Bug introduced by
It seems like $user defined by \User::newFromName($username, false) on line 2328 can also be of type false; however, Block::newFromTarget() does only seem to accept string|object<User>|integer|null, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2331
			if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2332
				$wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2333
					[ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2334 View Code Duplication
			} elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2335
				# Show log extract if the user is currently blocked
2336
				LogEventsList::showLogExtract(
2337
					$wgOut,
2338
					'block',
2339
					MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2340
					'',
2341
					[
2342
						'lim' => 1,
2343
						'showIfEmpty' => false,
2344
						'msgKey' => [
2345
							'blocked-notice-logextract',
2346
							$user->getName() # Support GENDER in notice
2347
						]
2348
					]
2349
				);
2350
			}
2351
		}
2352
		# Try to add a custom edit intro, or use the standard one if this is not possible.
2353
		if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2354
			$helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
2355
				wfMessage( 'helppage' )->inContentLanguage()->text()
2356
			) );
2357
			if ( $wgUser->isLoggedIn() ) {
2358
				$wgOut->wrapWikiMsg(
2359
					// Suppress the external link icon, consider the help url an internal one
2360
					"<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2361
					[
2362
						'newarticletext',
2363
						$helpLink
2364
					]
2365
				);
2366
			} else {
2367
				$wgOut->wrapWikiMsg(
2368
					// Suppress the external link icon, consider the help url an internal one
2369
					"<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2370
					[
2371
						'newarticletextanon',
2372
						$helpLink
2373
					]
2374
				);
2375
			}
2376
		}
2377
		# Give a notice if the user is editing a deleted/moved page...
2378 View Code Duplication
		if ( !$this->mTitle->exists() ) {
2379
			LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
2380
				'',
2381
				[
2382
					'lim' => 10,
2383
					'conds' => [ "log_action != 'revision'" ],
2384
					'showIfEmpty' => false,
2385
					'msgKey' => [ 'recreate-moveddeleted-warn' ]
2386
				]
2387
			);
2388
		}
2389
	}
2390
2391
	/**
2392
	 * Attempt to show a custom editing introduction, if supplied
2393
	 *
2394
	 * @return bool
2395
	 */
2396
	protected function showCustomIntro() {
2397
		if ( $this->editintro ) {
2398
			$title = Title::newFromText( $this->editintro );
2399
			if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2400
				global $wgOut;
2401
				// Added using template syntax, to take <noinclude>'s into account.
2402
				$wgOut->addWikiTextTitleTidy(
2403
					'<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2404
					$this->mTitle
2405
				);
2406
				return true;
2407
			}
2408
		}
2409
		return false;
2410
	}
2411
2412
	/**
2413
	 * Gets an editable textual representation of $content.
2414
	 * The textual representation can be turned by into a Content object by the
2415
	 * toEditContent() method.
2416
	 *
2417
	 * If $content is null or false or a string, $content is returned unchanged.
2418
	 *
2419
	 * If the given Content object is not of a type that can be edited using
2420
	 * the text base EditPage, an exception will be raised. Set
2421
	 * $this->allowNonTextContent to true to allow editing of non-textual
2422
	 * content.
2423
	 *
2424
	 * @param Content|null|bool|string $content
2425
	 * @return string The editable text form of the content.
2426
	 *
2427
	 * @throws MWException If $content is not an instance of TextContent and
2428
	 *   $this->allowNonTextContent is not true.
2429
	 */
2430
	protected function toEditText( $content ) {
2431
		if ( $content === null || $content === false || is_string( $content ) ) {
2432
			return $content;
2433
		}
2434
2435
		if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2436
			throw new MWException( 'This content model is not supported: '
2437
				. ContentHandler::getLocalizedName( $content->getModel() ) );
0 ignored issues
show
Bug introduced by
It seems like $content is not always an object, but can also be of type boolean. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
2438
		}
2439
2440
		return $content->serialize( $this->contentFormat );
2441
	}
2442
2443
	/**
2444
	 * Turns the given text into a Content object by unserializing it.
2445
	 *
2446
	 * If the resulting Content object is not of a type that can be edited using
2447
	 * the text base EditPage, an exception will be raised. Set
2448
	 * $this->allowNonTextContent to true to allow editing of non-textual
2449
	 * content.
2450
	 *
2451
	 * @param string|null|bool $text Text to unserialize
2452
	 * @return Content The content object created from $text. If $text was false
2453
	 *   or null, false resp. null will be  returned instead.
2454
	 *
2455
	 * @throws MWException If unserializing the text results in a Content
2456
	 *   object that is not an instance of TextContent and
2457
	 *   $this->allowNonTextContent is not true.
2458
	 */
2459
	protected function toEditContent( $text ) {
2460
		if ( $text === false || $text === null ) {
2461
			return $text;
2462
		}
2463
2464
		$content = ContentHandler::makeContent( $text, $this->getTitle(),
0 ignored issues
show
Bug introduced by
It seems like $text defined by parameter $text on line 2459 can also be of type boolean; however, ContentHandler::makeContent() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2465
			$this->contentModel, $this->contentFormat );
2466
2467
		if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2468
			throw new MWException( 'This content model is not supported: '
2469
				. ContentHandler::getLocalizedName( $content->getModel() ) );
2470
		}
2471
2472
		return $content;
2473
	}
2474
2475
	/**
2476
	 * Send the edit form and related headers to $wgOut
2477
	 * @param callable|null $formCallback That takes an OutputPage parameter; will be called
2478
	 *     during form output near the top, for captchas and the like.
2479
	 *
2480
	 * The $formCallback parameter is deprecated since MediaWiki 1.25. Please
2481
	 * use the EditPage::showEditForm:fields hook instead.
2482
	 */
2483
	function showEditForm( $formCallback = null ) {
2484
		global $wgOut, $wgUser;
2485
2486
		# need to parse the preview early so that we know which templates are used,
2487
		# otherwise users with "show preview after edit box" will get a blank list
2488
		# we parse this near the beginning so that setHeaders can do the title
2489
		# setting work instead of leaving it in getPreviewText
2490
		$previewOutput = '';
2491
		if ( $this->formtype == 'preview' ) {
2492
			$previewOutput = $this->getPreviewText();
2493
		}
2494
2495
		Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$wgOut ] );
2496
2497
		$this->setHeaders();
2498
2499
		if ( $this->showHeader() === false ) {
2500
			return;
2501
		}
2502
2503
		$wgOut->addHTML( $this->editFormPageTop );
2504
2505
		if ( $wgUser->getOption( 'previewontop' ) ) {
2506
			$this->displayPreviewArea( $previewOutput, true );
2507
		}
2508
2509
		$wgOut->addHTML( $this->editFormTextTop );
2510
2511
		$showToolbar = true;
2512
		if ( $this->wasDeletedSinceLastEdit() ) {
2513
			if ( $this->formtype == 'save' ) {
2514
				// Hide the toolbar and edit area, user can click preview to get it back
2515
				// Add an confirmation checkbox and explanation.
2516
				$showToolbar = false;
2517
			} else {
2518
				$wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2519
					'deletedwhileediting' );
2520
			}
2521
		}
2522
2523
		// @todo add EditForm plugin interface and use it here!
2524
		//       search for textarea1 and textares2, and allow EditForm to override all uses.
2525
		$wgOut->addHTML( Html::openElement(
2526
			'form',
2527
			[
2528
				'id' => self::EDITFORM_ID,
2529
				'name' => self::EDITFORM_ID,
2530
				'method' => 'post',
2531
				'action' => $this->getActionURL( $this->getContextTitle() ),
2532
				'enctype' => 'multipart/form-data'
2533
			]
2534
		) );
2535
2536
		if ( is_callable( $formCallback ) ) {
2537
			wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2538
			call_user_func_array( $formCallback, [ &$wgOut ] );
2539
		}
2540
2541
		// Add an empty field to trip up spambots
2542
		$wgOut->addHTML(
2543
			Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2544
			. Html::rawElement(
2545
				'label',
2546
				[ 'for' => 'wpAntispam' ],
2547
				wfMessage( 'simpleantispam-label' )->parse()
2548
			)
2549
			. Xml::element(
2550
				'input',
2551
				[
2552
					'type' => 'text',
2553
					'name' => 'wpAntispam',
2554
					'id' => 'wpAntispam',
2555
					'value' => ''
2556
				]
2557
			)
2558
			. Xml::closeElement( 'div' )
2559
		);
2560
2561
		Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$wgOut ] );
2562
2563
		// Put these up at the top to ensure they aren't lost on early form submission
2564
		$this->showFormBeforeText();
2565
2566
		if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2567
			$username = $this->lastDelete->user_name;
2568
			$comment = $this->lastDelete->log_comment;
2569
2570
			// It is better to not parse the comment at all than to have templates expanded in the middle
2571
			// TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2572
			$key = $comment === ''
2573
				? 'confirmrecreate-noreason'
2574
				: 'confirmrecreate';
2575
			$wgOut->addHTML(
2576
				'<div class="mw-confirm-recreate">' .
2577
					wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2578
				Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2579
					[ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2580
				) .
2581
				'</div>'
2582
			);
2583
		}
2584
2585
		# When the summary is hidden, also hide them on preview/show changes
2586
		if ( $this->nosummary ) {
2587
			$wgOut->addHTML( Html::hidden( 'nosummary', true ) );
2588
		}
2589
2590
		# If a blank edit summary was previously provided, and the appropriate
2591
		# user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2592
		# user being bounced back more than once in the event that a summary
2593
		# is not required.
2594
		# ####
2595
		# For a bit more sophisticated detection of blank summaries, hash the
2596
		# automatic one and pass that in the hidden field wpAutoSummary.
2597
		if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2598
			$wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2599
		}
2600
2601
		if ( $this->undidRev ) {
2602
			$wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2603
		}
2604
2605
		if ( $this->selfRedirect ) {
2606
			$wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2607
		}
2608
2609
		if ( $this->hasPresetSummary ) {
2610
			// If a summary has been preset using &summary= we don't want to prompt for
2611
			// a different summary. Only prompt for a summary if the summary is blanked.
2612
			// (Bug 17416)
2613
			$this->autoSumm = md5( '' );
2614
		}
2615
2616
		$autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2617
		$wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2618
2619
		$wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2620
		$wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2621
2622
		$wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2623
		$wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
2624
2625 View Code Duplication
		if ( $this->section == 'new' ) {
2626
			$this->showSummaryInput( true, $this->summary );
2627
			$wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2628
		}
2629
2630
		$wgOut->addHTML( $this->editFormTextBeforeContent );
2631
2632
		if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
2633
			$wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
2634
		}
2635
2636
		if ( $this->blankArticle ) {
2637
			$wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2638
		}
2639
2640
		if ( $this->isConflict ) {
2641
			// In an edit conflict bypass the overridable content form method
2642
			// and fallback to the raw wpTextbox1 since editconflicts can't be
2643
			// resolved between page source edits and custom ui edits using the
2644
			// custom edit ui.
2645
			$this->textbox2 = $this->textbox1;
2646
2647
			$content = $this->getCurrentContent();
2648
			$this->textbox1 = $this->toEditText( $content );
2649
2650
			$this->showTextbox1();
2651
		} else {
2652
			$this->showContentForm();
2653
		}
2654
2655
		$wgOut->addHTML( $this->editFormTextAfterContent );
2656
2657
		$this->showStandardInputs();
2658
2659
		$this->showFormAfterText();
2660
2661
		$this->showTosSummary();
2662
2663
		$this->showEditTools();
2664
2665
		$wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
2666
2667
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2668
			Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
2669
2670
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2671
			Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2672
2673
		$wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2674
			self::getPreviewLimitReport( $this->mParserOutput ) ) );
2675
2676
		$wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2677
2678 View Code Duplication
		if ( $this->isConflict ) {
2679
			try {
2680
				$this->showConflict();
2681
			} catch ( MWContentSerializationException $ex ) {
2682
				// this can't really happen, but be nice if it does.
2683
				$msg = wfMessage(
2684
					'content-failed-to-parse',
2685
					$this->contentModel,
2686
					$this->contentFormat,
2687
					$ex->getMessage()
2688
				);
2689
				$wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2690
			}
2691
		}
2692
2693
		// Marker for detecting truncated form data.  This must be the last
2694
		// parameter sent in order to be of use, so do not move me.
2695
		$wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2696
		$wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2697
2698
		if ( !$wgUser->getOption( 'previewontop' ) ) {
2699
			$this->displayPreviewArea( $previewOutput, false );
2700
		}
2701
2702
	}
2703
2704
	/**
2705
	 * Extract the section title from current section text, if any.
2706
	 *
2707
	 * @param string $text
2708
	 * @return string|bool String or false
2709
	 */
2710
	public static function extractSectionTitle( $text ) {
2711
		preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2712
		if ( !empty( $matches[2] ) ) {
2713
			global $wgParser;
2714
			return $wgParser->stripSectionName( trim( $matches[2] ) );
2715
		} else {
2716
			return false;
2717
		}
2718
	}
2719
2720
	/**
2721
	 * @return bool
2722
	 */
2723
	protected function showHeader() {
2724
		global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang;
2725
		global $wgAllowUserCss, $wgAllowUserJs;
2726
2727
		if ( $this->mTitle->isTalkPage() ) {
2728
			$wgOut->addWikiMsg( 'talkpagetext' );
2729
		}
2730
2731
		// Add edit notices
2732
		$editNotices = $this->mTitle->getEditNotices( $this->oldid );
2733
		if ( count( $editNotices ) ) {
2734
			$wgOut->addHTML( implode( "\n", $editNotices ) );
2735
		} else {
2736
			$msg = wfMessage( 'editnotice-notext' );
2737
			if ( !$msg->isDisabled() ) {
2738
				$wgOut->addHTML(
2739
					'<div class="mw-editnotice-notext">'
2740
					. $msg->parseAsBlock()
2741
					. '</div>'
2742
				);
2743
			}
2744
		}
2745
2746
		if ( $this->isConflict ) {
2747
			$wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
2748
			$this->edittime = $this->page->getTimestamp();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->page->getTimestamp() can also be of type false. However, the property $edittime is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2749
		} else {
2750
			if ( $this->section != '' && !$this->isSectionEditSupported() ) {
2751
				// We use $this->section to much before this and getVal('wgSection') directly in other places
2752
				// at this point we can't reset $this->section to '' to fallback to non-section editing.
2753
				// Someone is welcome to try refactoring though
2754
				$wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2755
				return false;
2756
			}
2757
2758
			if ( $this->section != '' && $this->section != 'new' ) {
2759
				if ( !$this->summary && !$this->preview && !$this->diff ) {
2760
					$sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2761
					if ( $sectionTitle !== false ) {
2762
						$this->summary = "/* $sectionTitle */ ";
2763
					}
2764
				}
2765
			}
2766
2767
			if ( $this->missingComment ) {
2768
				$wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2769
			}
2770
2771
			if ( $this->missingSummary && $this->section != 'new' ) {
2772
				$wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
2773
			}
2774
2775
			if ( $this->missingSummary && $this->section == 'new' ) {
2776
				$wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
2777
			}
2778
2779
			if ( $this->blankArticle ) {
2780
				$wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
2781
			}
2782
2783
			if ( $this->selfRedirect ) {
2784
				$wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
2785
			}
2786
2787
			if ( $this->hookError !== '' ) {
2788
				$wgOut->addWikiText( $this->hookError );
2789
			}
2790
2791
			if ( !$this->checkUnicodeCompliantBrowser() ) {
2792
				$wgOut->addWikiMsg( 'nonunicodebrowser' );
2793
			}
2794
2795
			if ( $this->section != 'new' ) {
2796
				$revision = $this->mArticle->getRevisionFetched();
2797
				if ( $revision ) {
2798
					// Let sysop know that this will make private content public if saved
2799
2800 View Code Duplication
					if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
2801
						$wgOut->wrapWikiMsg(
2802
							"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2803
							'rev-deleted-text-permission'
2804
						);
2805
					} elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
2806
						$wgOut->wrapWikiMsg(
2807
							"<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2808
							'rev-deleted-text-view'
2809
						);
2810
					}
2811
2812
					if ( !$revision->isCurrent() ) {
2813
						$this->mArticle->setOldSubtitle( $revision->getId() );
2814
						$wgOut->addWikiMsg( 'editingold' );
2815
					}
2816
				} elseif ( $this->mTitle->exists() ) {
2817
					// Something went wrong
2818
2819
					$wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
2820
						[ 'missing-revision', $this->oldid ] );
2821
				}
2822
			}
2823
		}
2824
2825
		if ( wfReadOnly() ) {
2826
			$wgOut->wrapWikiMsg(
2827
				"<div id=\"mw-read-only-warning\">\n$1\n</div>",
2828
				[ 'readonlywarning', wfReadOnlyReason() ]
2829
			);
2830
		} elseif ( $wgUser->isAnon() ) {
2831
			if ( $this->formtype != 'preview' ) {
2832
				$wgOut->wrapWikiMsg(
2833
					"<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
2834
					[ 'anoneditwarning',
2835
						// Log-in link
2836
						SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
2837
							'returnto' => $this->getTitle()->getPrefixedDBkey()
2838
						] ),
2839
						// Sign-up link
2840
						SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
2841
							'returnto' => $this->getTitle()->getPrefixedDBkey()
2842
						] )
2843
					]
2844
				);
2845
			} else {
2846
				$wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
2847
					'anonpreviewwarning'
2848
				);
2849
			}
2850
		} else {
2851
			if ( $this->isCssJsSubpage ) {
2852
				# Check the skin exists
2853
				if ( $this->isWrongCaseCssJsPage ) {
2854
					$wgOut->wrapWikiMsg(
2855
						"<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
2856
						[ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
2857
					);
2858
				}
2859
				if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
2860
					if ( $this->formtype !== 'preview' ) {
2861
						if ( $this->isCssSubpage && $wgAllowUserCss ) {
2862
							$wgOut->wrapWikiMsg(
2863
								"<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
2864
								[ 'usercssyoucanpreview' ]
2865
							);
2866
						}
2867
2868
						if ( $this->isJsSubpage && $wgAllowUserJs ) {
2869
							$wgOut->wrapWikiMsg(
2870
								"<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
2871
								[ 'userjsyoucanpreview' ]
2872
							);
2873
						}
2874
					}
2875
				}
2876
			}
2877
		}
2878
2879
		if ( $this->mTitle->isProtected( 'edit' ) &&
2880
			MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
2881
		) {
2882
			# Is the title semi-protected?
2883
			if ( $this->mTitle->isSemiProtected() ) {
2884
				$noticeMsg = 'semiprotectedpagewarning';
2885
			} else {
2886
				# Then it must be protected based on static groups (regular)
2887
				$noticeMsg = 'protectedpagewarning';
2888
			}
2889
			LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2890
				[ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
2891
		}
2892
		if ( $this->mTitle->isCascadeProtected() ) {
2893
			# Is this page under cascading protection from some source pages?
2894
			/** @var Title[] $cascadeSources */
2895
			list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
2896
			$notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
2897
			$cascadeSourcesCount = count( $cascadeSources );
2898
			if ( $cascadeSourcesCount > 0 ) {
2899
				# Explain, and list the titles responsible
2900
				foreach ( $cascadeSources as $page ) {
2901
					$notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
2902
				}
2903
			}
2904
			$notice .= '</div>';
2905
			$wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
2906
		}
2907
		if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
2908
			LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2909
				[ 'lim' => 1,
2910
					'showIfEmpty' => false,
2911
					'msgKey' => [ 'titleprotectedwarning' ],
2912
					'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
2913
		}
2914
2915
		if ( $this->kblength === false ) {
2916
			$this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
0 ignored issues
show
Documentation Bug introduced by
The property $kblength was declared of type boolean, but (int) (strlen($this->textbox1) / 1024) is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
2917
		}
2918
2919
		if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
2920
			$wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
2921
				[
2922
					'longpageerror',
2923
					$wgLang->formatNum( $this->kblength ),
2924
					$wgLang->formatNum( $wgMaxArticleSize )
2925
				]
2926
			);
2927
		} else {
2928
			if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
2929
				$wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
2930
					[
2931
						'longpage-hint',
2932
						$wgLang->formatSize( strlen( $this->textbox1 ) ),
2933
						strlen( $this->textbox1 )
2934
					]
2935
				);
2936
			}
2937
		}
2938
		# Add header copyright warning
2939
		$this->showHeaderCopyrightWarning();
2940
2941
		return true;
2942
	}
2943
2944
	/**
2945
	 * Standard summary input and label (wgSummary), abstracted so EditPage
2946
	 * subclasses may reorganize the form.
2947
	 * Note that you do not need to worry about the label's for=, it will be
2948
	 * inferred by the id given to the input. You can remove them both by
2949
	 * passing array( 'id' => false ) to $userInputAttrs.
2950
	 *
2951
	 * @param string $summary The value of the summary input
2952
	 * @param string $labelText The html to place inside the label
2953
	 * @param array $inputAttrs Array of attrs to use on the input
2954
	 * @param array $spanLabelAttrs Array of attrs to use on the span inside the label
2955
	 *
2956
	 * @return array An array in the format array( $label, $input )
2957
	 */
2958
	function getSummaryInput( $summary = "", $labelText = null,
2959
		$inputAttrs = null, $spanLabelAttrs = null
2960
	) {
2961
		// Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
2962
		$inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
2963
			'id' => 'wpSummary',
2964
			'maxlength' => '200',
2965
			'tabindex' => '1',
2966
			'size' => 60,
2967
			'spellcheck' => 'true',
2968
		] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
2969
2970
		$spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
2971
			'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
2972
			'id' => "wpSummaryLabel"
2973
		];
2974
2975
		$label = null;
2976
		if ( $labelText ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $labelText of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2977
			$label = Xml::tags(
2978
				'label',
2979
				$inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
2980
				$labelText
2981
			);
2982
			$label = Xml::tags( 'span', $spanLabelAttrs, $label );
2983
		}
2984
2985
		$input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
2986
2987
		return [ $label, $input ];
2988
	}
2989
2990
	/**
2991
	 * @param bool $isSubjectPreview True if this is the section subject/title
2992
	 *   up top, or false if this is the comment summary
2993
	 *   down below the textarea
2994
	 * @param string $summary The text of the summary to display
2995
	 */
2996
	protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
2997
		global $wgOut, $wgContLang;
2998
		# Add a class if 'missingsummary' is triggered to allow styling of the summary line
2999
		$summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3000
		if ( $isSubjectPreview ) {
3001
			if ( $this->nosummary ) {
3002
				return;
3003
			}
3004
		} else {
3005
			if ( !$this->mShowSummaryField ) {
3006
				return;
3007
			}
3008
		}
3009
		$summary = $wgContLang->recodeForEdit( $summary );
3010
		$labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3011
		list( $label, $input ) = $this->getSummaryInput(
3012
			$summary,
3013
			$labelText,
3014
			[ 'class' => $summaryClass ],
3015
			[]
3016
		);
3017
		$wgOut->addHTML( "{$label} {$input}" );
3018
	}
3019
3020
	/**
3021
	 * @param bool $isSubjectPreview True if this is the section subject/title
3022
	 *   up top, or false if this is the comment summary
3023
	 *   down below the textarea
3024
	 * @param string $summary The text of the summary to display
3025
	 * @return string
3026
	 */
3027
	protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3028
		// avoid spaces in preview, gets always trimmed on save
3029
		$summary = trim( $summary );
3030
		if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3031
			return "";
3032
		}
3033
3034
		global $wgParser;
3035
3036
		if ( $isSubjectPreview ) {
3037
			$summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
3038
				->inContentLanguage()->text();
3039
		}
3040
3041
		$message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3042
3043
		$summary = wfMessage( $message )->parse()
3044
			. Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3045
		return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3046
	}
3047
3048
	protected function showFormBeforeText() {
3049
		global $wgOut;
3050
		$section = htmlspecialchars( $this->section );
3051
		$wgOut->addHTML( <<<HTML
3052
<input type='hidden' value="{$section}" name="wpSection"/>
3053
<input type='hidden' value="{$this->starttime}" name="wpStarttime" />
3054
<input type='hidden' value="{$this->edittime}" name="wpEdittime" />
3055
<input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
3056
3057
HTML
3058
		);
3059
		if ( !$this->checkUnicodeCompliantBrowser() ) {
3060
			$wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
3061
		}
3062
	}
3063
3064
	protected function showFormAfterText() {
3065
		global $wgOut, $wgUser;
3066
		/**
3067
		 * To make it harder for someone to slip a user a page
3068
		 * which submits an edit form to the wiki without their
3069
		 * knowledge, a random token is associated with the login
3070
		 * session. If it's not passed back with the submission,
3071
		 * we won't save the page, or render user JavaScript and
3072
		 * CSS previews.
3073
		 *
3074
		 * For anon editors, who may not have a session, we just
3075
		 * include the constant suffix to prevent editing from
3076
		 * broken text-mangling proxies.
3077
		 */
3078
		$wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
3079
	}
3080
3081
	/**
3082
	 * Subpage overridable method for printing the form for page content editing
3083
	 * By default this simply outputs wpTextbox1
3084
	 * Subclasses can override this to provide a custom UI for editing;
3085
	 * be it a form, or simply wpTextbox1 with a modified content that will be
3086
	 * reverse modified when extracted from the post data.
3087
	 * Note that this is basically the inverse for importContentFormData
3088
	 */
3089
	protected function showContentForm() {
3090
		$this->showTextbox1();
3091
	}
3092
3093
	/**
3094
	 * Method to output wpTextbox1
3095
	 * The $textoverride method can be used by subclasses overriding showContentForm
3096
	 * to pass back to this method.
3097
	 *
3098
	 * @param array $customAttribs Array of html attributes to use in the textarea
3099
	 * @param string $textoverride Optional text to override $this->textarea1 with
3100
	 */
3101
	protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3102
		if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3103
			$attribs = [ 'style' => 'display:none;' ];
3104
		} else {
3105
			$classes = []; // Textarea CSS
3106
			if ( $this->mTitle->isProtected( 'edit' ) &&
3107
				MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3108
			) {
3109
				# Is the title semi-protected?
3110
				if ( $this->mTitle->isSemiProtected() ) {
3111
					$classes[] = 'mw-textarea-sprotected';
3112
				} else {
3113
					# Then it must be protected based on static groups (regular)
3114
					$classes[] = 'mw-textarea-protected';
3115
				}
3116
				# Is the title cascade-protected?
3117
				if ( $this->mTitle->isCascadeProtected() ) {
3118
					$classes[] = 'mw-textarea-cprotected';
3119
				}
3120
			}
3121
3122
			$attribs = [ 'tabindex' => 1 ];
3123
3124
			if ( is_array( $customAttribs ) ) {
3125
				$attribs += $customAttribs;
3126
			}
3127
3128
			if ( count( $classes ) ) {
3129
				if ( isset( $attribs['class'] ) ) {
3130
					$classes[] = $attribs['class'];
3131
				}
3132
				$attribs['class'] = implode( ' ', $classes );
3133
			}
3134
		}
3135
3136
		$this->showTextbox(
3137
			$textoverride !== null ? $textoverride : $this->textbox1,
3138
			'wpTextbox1',
3139
			$attribs
3140
		);
3141
	}
3142
3143
	protected function showTextbox2() {
3144
		$this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3145
	}
3146
3147
	protected function showTextbox( $text, $name, $customAttribs = [] ) {
3148
		global $wgOut, $wgUser;
3149
3150
		$wikitext = $this->safeUnicodeOutput( $text );
3151
		if ( strval( $wikitext ) !== '' ) {
3152
			// Ensure there's a newline at the end, otherwise adding lines
3153
			// is awkward.
3154
			// But don't add a newline if the ext is empty, or Firefox in XHTML
3155
			// mode will show an extra newline. A bit annoying.
3156
			$wikitext .= "\n";
3157
		}
3158
3159
		$attribs = $customAttribs + [
3160
			'accesskey' => ',',
3161
			'id' => $name,
3162
			'cols' => $wgUser->getIntOption( 'cols' ),
3163
			'rows' => $wgUser->getIntOption( 'rows' ),
3164
			// Avoid PHP notices when appending preferences
3165
			// (appending allows customAttribs['style'] to still work).
3166
			'style' => ''
3167
		];
3168
3169
		$pageLang = $this->mTitle->getPageLanguage();
3170
		$attribs['lang'] = $pageLang->getHtmlCode();
3171
		$attribs['dir'] = $pageLang->getDir();
3172
3173
		$wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
3174
	}
3175
3176
	protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3177
		global $wgOut;
3178
		$classes = [];
3179
		if ( $isOnTop ) {
3180
			$classes[] = 'ontop';
3181
		}
3182
3183
		$attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3184
3185
		if ( $this->formtype != 'preview' ) {
3186
			$attribs['style'] = 'display: none;';
3187
		}
3188
3189
		$wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
3190
3191
		if ( $this->formtype == 'preview' ) {
3192
			$this->showPreview( $previewOutput );
3193
		} else {
3194
			// Empty content container for LivePreview
3195
			$pageViewLang = $this->mTitle->getPageViewLanguage();
3196
			$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3197
				'class' => 'mw-content-' . $pageViewLang->getDir() ];
3198
			$wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
3199
		}
3200
3201
		$wgOut->addHTML( '</div>' );
3202
3203 View Code Duplication
		if ( $this->formtype == 'diff' ) {
3204
			try {
3205
				$this->showDiff();
3206
			} catch ( MWContentSerializationException $ex ) {
3207
				$msg = wfMessage(
3208
					'content-failed-to-parse',
3209
					$this->contentModel,
3210
					$this->contentFormat,
3211
					$ex->getMessage()
3212
				);
3213
				$wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3214
			}
3215
		}
3216
	}
3217
3218
	/**
3219
	 * Append preview output to $wgOut.
3220
	 * Includes category rendering if this is a category page.
3221
	 *
3222
	 * @param string $text The HTML to be output for the preview.
3223
	 */
3224
	protected function showPreview( $text ) {
3225
		global $wgOut;
3226
		if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3227
			$this->mArticle->openShowCategory();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Article as the method openShowCategory() does only exist in the following sub-classes of Article: CategoryPage. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
3228
		}
3229
		# This hook seems slightly odd here, but makes things more
3230
		# consistent for extensions.
3231
		Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
3232
		$wgOut->addHTML( $text );
3233
		if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3234
			$this->mArticle->closeShowCategory();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Article as the method closeShowCategory() does only exist in the following sub-classes of Article: CategoryPage. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
3235
		}
3236
	}
3237
3238
	/**
3239
	 * Get a diff between the current contents of the edit box and the
3240
	 * version of the page we're editing from.
3241
	 *
3242
	 * If this is a section edit, we'll replace the section as for final
3243
	 * save and then make a comparison.
3244
	 */
3245
	function showDiff() {
3246
		global $wgUser, $wgContLang, $wgOut;
3247
3248
		$oldtitlemsg = 'currentrev';
3249
		# if message does not exist, show diff against the preloaded default
3250
		if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3251
			$oldtext = $this->mTitle->getDefaultMessageText();
3252
			if ( $oldtext !== false ) {
3253
				$oldtitlemsg = 'defaultmessagetext';
3254
				$oldContent = $this->toEditContent( $oldtext );
3255
			} else {
3256
				$oldContent = null;
3257
			}
3258
		} else {
3259
			$oldContent = $this->getCurrentContent();
3260
		}
3261
3262
		$textboxContent = $this->toEditContent( $this->textbox1 );
3263
3264
		$newContent = $this->page->replaceSectionContent(
0 ignored issues
show
Deprecated Code introduced by
The method WikiPage::replaceSectionContent() has been deprecated with message: since 1.24, use replaceSectionAtRev instead

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...
3265
							$this->section, $textboxContent,
0 ignored issues
show
Bug introduced by
It seems like $textboxContent defined by $this->toEditContent($this->textbox1) on line 3262 can be null; however, WikiPage::replaceSectionContent() 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...
3266
							$this->summary, $this->edittime );
3267
3268
		if ( $newContent ) {
3269
			ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
3270
			Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3271
3272
			$popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
3273
			$newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
3274
		}
3275
3276
		if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3277
			$oldtitle = wfMessage( $oldtitlemsg )->parse();
3278
			$newtitle = wfMessage( 'yourtext' )->parse();
3279
3280
			if ( !$oldContent ) {
3281
				$oldContent = $newContent->getContentHandler()->makeEmptyContent();
3282
			}
3283
3284
			if ( !$newContent ) {
3285
				$newContent = $oldContent->getContentHandler()->makeEmptyContent();
3286
			}
3287
3288
			$de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
3289
			$de->setContent( $oldContent, $newContent );
3290
3291
			$difftext = $de->getDiff( $oldtitle, $newtitle );
3292
			$de->showDiffStyle();
3293
		} else {
3294
			$difftext = '';
3295
		}
3296
3297
		$wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3298
	}
3299
3300
	/**
3301
	 * Show the header copyright warning.
3302
	 */
3303
	protected function showHeaderCopyrightWarning() {
3304
		$msg = 'editpage-head-copy-warn';
3305
		if ( !wfMessage( $msg )->isDisabled() ) {
3306
			global $wgOut;
3307
			$wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3308
				'editpage-head-copy-warn' );
3309
		}
3310
	}
3311
3312
	/**
3313
	 * Give a chance for site and per-namespace customizations of
3314
	 * terms of service summary link that might exist separately
3315
	 * from the copyright notice.
3316
	 *
3317
	 * This will display between the save button and the edit tools,
3318
	 * so should remain short!
3319
	 */
3320
	protected function showTosSummary() {
3321
		$msg = 'editpage-tos-summary';
3322
		Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3323
		if ( !wfMessage( $msg )->isDisabled() ) {
3324
			global $wgOut;
3325
			$wgOut->addHTML( '<div class="mw-tos-summary">' );
3326
			$wgOut->addWikiMsg( $msg );
3327
			$wgOut->addHTML( '</div>' );
3328
		}
3329
	}
3330
3331
	protected function showEditTools() {
3332
		global $wgOut;
3333
		$wgOut->addHTML( '<div class="mw-editTools">' .
3334
			wfMessage( 'edittools' )->inContentLanguage()->parse() .
3335
			'</div>' );
3336
	}
3337
3338
	/**
3339
	 * Get the copyright warning
3340
	 *
3341
	 * Renamed to getCopyrightWarning(), old name kept around for backwards compatibility
3342
	 * @return string
3343
	 */
3344
	protected function getCopywarn() {
3345
		return self::getCopyrightWarning( $this->mTitle );
3346
	}
3347
3348
	/**
3349
	 * Get the copyright warning, by default returns wikitext
3350
	 *
3351
	 * @param Title $title
3352
	 * @param string $format Output format, valid values are any function of a Message object
3353
	 * @return string
3354
	 */
3355
	public static function getCopyrightWarning( $title, $format = 'plain' ) {
3356
		global $wgRightsText;
3357
		if ( $wgRightsText ) {
3358
			$copywarnMsg = [ 'copyrightwarning',
3359
				'[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3360
				$wgRightsText ];
3361
		} else {
3362
			$copywarnMsg = [ 'copyrightwarning2',
3363
				'[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3364
		}
3365
		// Allow for site and per-namespace customization of contribution/copyright notice.
3366
		Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3367
3368
		return "<div id=\"editpage-copywarn\">\n" .
3369
			call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n</div>";
3370
	}
3371
3372
	/**
3373
	 * Get the Limit report for page previews
3374
	 *
3375
	 * @since 1.22
3376
	 * @param ParserOutput $output ParserOutput object from the parse
3377
	 * @return string HTML
3378
	 */
3379
	public static function getPreviewLimitReport( $output ) {
3380
		if ( !$output || !$output->getLimitReportData() ) {
3381
			return '';
3382
		}
3383
3384
		$limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3385
			wfMessage( 'limitreport-title' )->parseAsBlock()
3386
		);
3387
3388
		// Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3389
		$limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3390
3391
		$limitReport .= Html::openElement( 'table', [
3392
			'class' => 'preview-limit-report wikitable'
3393
		] ) .
3394
			Html::openElement( 'tbody' );
3395
3396
		foreach ( $output->getLimitReportData() as $key => $value ) {
3397
			if ( Hooks::run( 'ParserLimitReportFormat',
3398
				[ $key, &$value, &$limitReport, true, true ]
3399
			) ) {
3400
				$keyMsg = wfMessage( $key );
3401
				$valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3402
				if ( !$valueMsg->exists() ) {
3403
					$valueMsg = new RawMessage( '$1' );
3404
				}
3405
				if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3406
					$limitReport .= Html::openElement( 'tr' ) .
3407
						Html::rawElement( 'th', null, $keyMsg->parse() ) .
3408
						Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
3409
						Html::closeElement( 'tr' );
3410
				}
3411
			}
3412
		}
3413
3414
		$limitReport .= Html::closeElement( 'tbody' ) .
3415
			Html::closeElement( 'table' ) .
3416
			Html::closeElement( 'div' );
3417
3418
		return $limitReport;
3419
	}
3420
3421
	protected function showStandardInputs( &$tabindex = 2 ) {
3422
		global $wgOut;
3423
		$wgOut->addHTML( "<div class='editOptions'>\n" );
3424
3425 View Code Duplication
		if ( $this->section != 'new' ) {
3426
			$this->showSummaryInput( false, $this->summary );
3427
			$wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3428
		}
3429
3430
		$checkboxes = $this->getCheckboxes( $tabindex,
3431
			[ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
3432
		$wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
3433
3434
		// Show copyright warning.
3435
		$wgOut->addWikiText( $this->getCopywarn() );
3436
		$wgOut->addHTML( $this->editFormTextAfterWarn );
3437
3438
		$wgOut->addHTML( "<div class='editButtons'>\n" );
3439
		$wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3440
3441
		$cancel = $this->getCancelLink();
3442
		if ( $cancel !== '' ) {
3443
			$cancel .= Html::element( 'span',
3444
				[ 'class' => 'mw-editButtons-pipe-separator' ],
3445
				wfMessage( 'pipe-separator' )->text() );
3446
		}
3447
3448
		$message = wfMessage( 'edithelppage' )->inContentLanguage()->text();
3449
		$edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3450
		$attrs = [
3451
			'target' => 'helpwindow',
3452
			'href' => $edithelpurl,
3453
		];
3454
		$edithelp = Html::linkButton( wfMessage( 'edithelp' )->text(),
3455
			$attrs, [ 'mw-ui-quiet' ] ) .
3456
			wfMessage( 'word-separator' )->escaped() .
3457
			wfMessage( 'newwindow' )->parse();
3458
3459
		$wgOut->addHTML( "	<span class='cancelLink'>{$cancel}</span>\n" );
3460
		$wgOut->addHTML( "	<span class='editHelp'>{$edithelp}</span>\n" );
3461
		$wgOut->addHTML( "</div><!-- editButtons -->\n" );
3462
3463
		Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
3464
3465
		$wgOut->addHTML( "</div><!-- editOptions -->\n" );
3466
	}
3467
3468
	/**
3469
	 * Show an edit conflict. textbox1 is already shown in showEditForm().
3470
	 * If you want to use another entry point to this function, be careful.
3471
	 */
3472
	protected function showConflict() {
3473
		global $wgOut;
3474
3475
		if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
3476
			$stats = $wgOut->getContext()->getStats();
3477
			$stats->increment( 'edit.failures.conflict' );
3478
3479
			$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3480
3481
			$content1 = $this->toEditContent( $this->textbox1 );
3482
			$content2 = $this->toEditContent( $this->textbox2 );
3483
3484
			$handler = ContentHandler::getForModelID( $this->contentModel );
3485
			$de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
3486
			$de->setContent( $content2, $content1 );
0 ignored issues
show
Bug introduced by
It seems like $content2 defined by $this->toEditContent($this->textbox2) on line 3482 can be null; however, DifferenceEngine::setContent() 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...
Bug introduced by
It seems like $content1 defined by $this->toEditContent($this->textbox1) on line 3481 can be null; however, DifferenceEngine::setContent() 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...
3487
			$de->showDiff(
3488
				wfMessage( 'yourtext' )->parse(),
3489
				wfMessage( 'storedversion' )->text()
3490
			);
3491
3492
			$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3493
			$this->showTextbox2();
3494
		}
3495
	}
3496
3497
	/**
3498
	 * @return string
3499
	 */
3500
	public function getCancelLink() {
3501
		$cancelParams = [];
3502
		if ( !$this->isConflict && $this->oldid > 0 ) {
3503
			$cancelParams['oldid'] = $this->oldid;
3504
		}
3505
		$attrs = [ 'id' => 'mw-editform-cancel' ];
3506
3507
		return Linker::linkKnown(
3508
			$this->getContextTitle(),
3509
			wfMessage( 'cancel' )->parse(),
3510
			Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ),
3511
			$cancelParams
3512
		);
3513
	}
3514
3515
	/**
3516
	 * Returns the URL to use in the form's action attribute.
3517
	 * This is used by EditPage subclasses when simply customizing the action
3518
	 * variable in the constructor is not enough. This can be used when the
3519
	 * EditPage lives inside of a Special page rather than a custom page action.
3520
	 *
3521
	 * @param Title $title Title object for which is being edited (where we go to for &action= links)
3522
	 * @return string
3523
	 */
3524
	protected function getActionURL( Title $title ) {
3525
		return $title->getLocalURL( [ 'action' => $this->action ] );
3526
	}
3527
3528
	/**
3529
	 * Check if a page was deleted while the user was editing it, before submit.
3530
	 * Note that we rely on the logging table, which hasn't been always there,
3531
	 * but that doesn't matter, because this only applies to brand new
3532
	 * deletes.
3533
	 * @return bool
3534
	 */
3535
	protected function wasDeletedSinceLastEdit() {
3536
		if ( $this->deletedSinceEdit !== null ) {
3537
			return $this->deletedSinceEdit;
3538
		}
3539
3540
		$this->deletedSinceEdit = false;
3541
3542
		if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3543
			$this->lastDelete = $this->getLastDelete();
3544
			if ( $this->lastDelete ) {
3545
				$deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3546
				if ( $deleteTime > $this->starttime ) {
3547
					$this->deletedSinceEdit = true;
3548
				}
3549
			}
3550
		}
3551
3552
		return $this->deletedSinceEdit;
3553
	}
3554
3555
	/**
3556
	 * @return bool|stdClass
3557
	 */
3558
	protected function getLastDelete() {
3559
		$dbr = wfGetDB( DB_SLAVE );
3560
		$data = $dbr->selectRow(
3561
			[ 'logging', 'user' ],
3562
			[
3563
				'log_type',
3564
				'log_action',
3565
				'log_timestamp',
3566
				'log_user',
3567
				'log_namespace',
3568
				'log_title',
3569
				'log_comment',
3570
				'log_params',
3571
				'log_deleted',
3572
				'user_name'
3573
			], [
3574
				'log_namespace' => $this->mTitle->getNamespace(),
3575
				'log_title' => $this->mTitle->getDBkey(),
3576
				'log_type' => 'delete',
3577
				'log_action' => 'delete',
3578
				'user_id=log_user'
3579
			],
3580
			__METHOD__,
3581
			[ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ]
3582
		);
3583
		// Quick paranoid permission checks...
3584
		if ( is_object( $data ) ) {
3585
			if ( $data->log_deleted & LogPage::DELETED_USER ) {
3586
				$data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
3587
			}
3588
3589
			if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3590
				$data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
3591
			}
3592
		}
3593
3594
		return $data;
3595
	}
3596
3597
	/**
3598
	 * Get the rendered text for previewing.
3599
	 * @throws MWException
3600
	 * @return string
3601
	 */
3602
	function getPreviewText() {
3603
		global $wgOut, $wgUser, $wgRawHtml, $wgLang;
3604
		global $wgAllowUserCss, $wgAllowUserJs, $wgAjaxEditStash;
3605
3606
		$stats = $wgOut->getContext()->getStats();
3607
3608
		if ( $wgRawHtml && !$this->mTokenOk ) {
3609
			// Could be an offsite preview attempt. This is very unsafe if
3610
			// HTML is enabled, as it could be an attack.
3611
			$parsedNote = '';
3612
			if ( $this->textbox1 !== '' ) {
3613
				// Do not put big scary notice, if previewing the empty
3614
				// string, which happens when you initially edit
3615
				// a category page, due to automatic preview-on-open.
3616
				$parsedNote = $wgOut->parse( "<div class='previewnote'>" .
3617
					wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
3618
			}
3619
			$stats->increment( 'edit.failures.session_loss' );
3620
			return $parsedNote;
3621
		}
3622
3623
		$note = '';
3624
3625
		try {
3626
			$content = $this->toEditContent( $this->textbox1 );
3627
3628
			$previewHTML = '';
3629
			if ( !Hooks::run(
3630
				'AlternateEditPreview',
3631
				[ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3632
			) {
3633
				return $previewHTML;
3634
			}
3635
3636
			# provide a anchor link to the editform
3637
			$continueEditing = '<span class="mw-continue-editing">' .
3638
				'[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
3639
				wfMessage( 'continue-editing' )->text() . ']]</span>';
3640
			if ( $this->mTriedSave && !$this->mTokenOk ) {
3641
				if ( $this->mTokenOkExceptSuffix ) {
3642
					$note = wfMessage( 'token_suffix_mismatch' )->plain();
3643
					$stats->increment( 'edit.failures.bad_token' );
3644
				} else {
3645
					$note = wfMessage( 'session_fail_preview' )->plain();
3646
					$stats->increment( 'edit.failures.session_loss' );
3647
				}
3648
			} elseif ( $this->incompleteForm ) {
3649
				$note = wfMessage( 'edit_form_incomplete' )->plain();
3650
				if ( $this->mTriedSave ) {
3651
					$stats->increment( 'edit.failures.incomplete_form' );
3652
				}
3653
			} else {
3654
				$note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
3655
			}
3656
3657
			$parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
3658
			$parserOptions->setIsPreview( true );
3659
			$parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3660
3661
			# don't parse non-wikitext pages, show message about preview
3662
			if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3663
				if ( $this->mTitle->isCssJsSubpage() ) {
3664
					$level = 'user';
3665
				} elseif ( $this->mTitle->isCssOrJsPage() ) {
3666
					$level = 'site';
3667
				} else {
3668
					$level = false;
3669
				}
3670
3671
				if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3672
					$format = 'css';
3673
					if ( $level === 'user' && !$wgAllowUserCss ) {
3674
						$format = false;
3675
					}
3676
				} elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3677
					$format = 'js';
3678
					if ( $level === 'user' && !$wgAllowUserJs ) {
3679
						$format = false;
3680
					}
3681
				} else {
3682
					$format = false;
3683
				}
3684
3685
				# Used messages to make sure grep find them:
3686
				# Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3687
				if ( $level && $format ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $level of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $format of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
3688
					$note = "<div id='mw-{$level}{$format}preview'>" .
3689
						wfMessage( "{$level}{$format}preview" )->text() .
3690
						' ' . $continueEditing . "</div>";
3691
				}
3692
			}
3693
3694
			# If we're adding a comment, we need to show the
3695
			# summary as the headline
3696
			if ( $this->section === "new" && $this->summary !== "" ) {
3697
				$content = $content->addSectionHeader( $this->summary );
3698
			}
3699
3700
			$hook_args = [ $this, &$content ];
3701
			ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
3702
			Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3703
3704
			$parserOptions->enableLimitReport();
3705
3706
			# For CSS/JS pages, we should have called the ShowRawCssJs hook here.
3707
			# But it's now deprecated, so never mind
3708
3709
			$pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
3710
			$scopedCallback = $parserOptions->setupFakeRevision(
3711
				$this->mTitle, $pstContent, $wgUser );
3712
			$parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3713
3714
			# Try to stash the edit for the final submission step
3715
			# @todo: different date format preferences cause cache misses
3716
			if ( $wgAjaxEditStash ) {
3717
				ApiStashEdit::stashEditFromPreview(
3718
					$this->getArticle(), $content, $pstContent,
3719
					$parserOutput, $parserOptions, $parserOptions, wfTimestampNow()
0 ignored issues
show
Security Bug introduced by
It seems like wfTimestampNow() can also be of type false; however, ApiStashEdit::stashEditFromPreview() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
3720
				);
3721
			}
3722
3723
			$parserOutput->setEditSectionTokens( false ); // no section edit links
3724
			$previewHTML = $parserOutput->getText();
3725
			$this->mParserOutput = $parserOutput;
3726
			$wgOut->addParserOutputMetadata( $parserOutput );
3727
3728
			if ( count( $parserOutput->getWarnings() ) ) {
3729
				$note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3730
			}
3731
3732
			ScopedCallback::consume( $scopedCallback );
3733
		} catch ( MWContentSerializationException $ex ) {
3734
			$m = wfMessage(
3735
				'content-failed-to-parse',
3736
				$this->contentModel,
3737
				$this->contentFormat,
3738
				$ex->getMessage()
3739
			);
3740
			$note .= "\n\n" . $m->parse();
3741
			$previewHTML = '';
3742
		}
3743
3744
		if ( $this->isConflict ) {
3745
			$conflict = '<h2 id="mw-previewconflict">'
3746
				. wfMessage( 'previewconflict' )->escaped() . "</h2>\n";
3747
		} else {
3748
			$conflict = '<hr />';
3749
		}
3750
3751
		$previewhead = "<div class='previewnote'>\n" .
3752
			'<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
3753
			$wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3754
3755
		$pageViewLang = $this->mTitle->getPageViewLanguage();
3756
		$attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3757
			'class' => 'mw-content-' . $pageViewLang->getDir() ];
3758
		$previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3759
3760
		return $previewhead . $previewHTML . $this->previewTextAfterContent;
3761
	}
3762
3763
	/**
3764
	 * @return array
3765
	 */
3766
	function getTemplates() {
3767
		if ( $this->preview || $this->section != '' ) {
3768
			$templates = [];
3769
			if ( !isset( $this->mParserOutput ) ) {
3770
				return $templates;
3771
			}
3772
			foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3773
				foreach ( array_keys( $template ) as $dbk ) {
3774
					$templates[] = Title::makeTitle( $ns, $dbk );
3775
				}
3776
			}
3777
			return $templates;
3778
		} else {
3779
			return $this->mTitle->getTemplateLinksFrom();
3780
		}
3781
	}
3782
3783
	/**
3784
	 * Shows a bulletin board style toolbar for common editing functions.
3785
	 * It can be disabled in the user preferences.
3786
	 *
3787
	 * @param Title $title Title object for the page being edited (optional)
3788
	 * @return string
3789
	 */
3790
	static function getEditToolbar( $title = null ) {
3791
		global $wgContLang, $wgOut;
3792
		global $wgEnableUploads, $wgForeignFileRepos;
3793
3794
		$imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3795
		$showSignature = true;
3796
		if ( $title ) {
3797
			$showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
3798
		}
3799
3800
		/**
3801
		 * $toolarray is an array of arrays each of which includes the
3802
		 * opening tag, the closing tag, optionally a sample text that is
3803
		 * inserted between the two when no selection is highlighted
3804
		 * and.  The tip text is shown when the user moves the mouse
3805
		 * over the button.
3806
		 *
3807
		 * Images are defined in ResourceLoaderEditToolbarModule.
3808
		 */
3809
		$toolarray = [
3810
			[
3811
				'id'     => 'mw-editbutton-bold',
3812
				'open'   => '\'\'\'',
3813
				'close'  => '\'\'\'',
3814
				'sample' => wfMessage( 'bold_sample' )->text(),
3815
				'tip'    => wfMessage( 'bold_tip' )->text(),
3816
			],
3817
			[
3818
				'id'     => 'mw-editbutton-italic',
3819
				'open'   => '\'\'',
3820
				'close'  => '\'\'',
3821
				'sample' => wfMessage( 'italic_sample' )->text(),
3822
				'tip'    => wfMessage( 'italic_tip' )->text(),
3823
			],
3824
			[
3825
				'id'     => 'mw-editbutton-link',
3826
				'open'   => '[[',
3827
				'close'  => ']]',
3828
				'sample' => wfMessage( 'link_sample' )->text(),
3829
				'tip'    => wfMessage( 'link_tip' )->text(),
3830
			],
3831
			[
3832
				'id'     => 'mw-editbutton-extlink',
3833
				'open'   => '[',
3834
				'close'  => ']',
3835
				'sample' => wfMessage( 'extlink_sample' )->text(),
3836
				'tip'    => wfMessage( 'extlink_tip' )->text(),
3837
			],
3838
			[
3839
				'id'     => 'mw-editbutton-headline',
3840
				'open'   => "\n== ",
3841
				'close'  => " ==\n",
3842
				'sample' => wfMessage( 'headline_sample' )->text(),
3843
				'tip'    => wfMessage( 'headline_tip' )->text(),
3844
			],
3845
			$imagesAvailable ? [
3846
				'id'     => 'mw-editbutton-image',
3847
				'open'   => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
3848
				'close'  => ']]',
3849
				'sample' => wfMessage( 'image_sample' )->text(),
3850
				'tip'    => wfMessage( 'image_tip' )->text(),
3851
			] : false,
3852
			$imagesAvailable ? [
3853
				'id'     => 'mw-editbutton-media',
3854
				'open'   => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
3855
				'close'  => ']]',
3856
				'sample' => wfMessage( 'media_sample' )->text(),
3857
				'tip'    => wfMessage( 'media_tip' )->text(),
3858
			] : false,
3859
			[
3860
				'id'     => 'mw-editbutton-nowiki',
3861
				'open'   => "<nowiki>",
3862
				'close'  => "</nowiki>",
3863
				'sample' => wfMessage( 'nowiki_sample' )->text(),
3864
				'tip'    => wfMessage( 'nowiki_tip' )->text(),
3865
			],
3866
			$showSignature ? [
3867
				'id'     => 'mw-editbutton-signature',
3868
				'open'   => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
3869
				'close'  => '',
3870
				'sample' => '',
3871
				'tip'    => wfMessage( 'sig_tip' )->text(),
3872
			] : false,
3873
			[
3874
				'id'     => 'mw-editbutton-hr',
3875
				'open'   => "\n----\n",
3876
				'close'  => '',
3877
				'sample' => '',
3878
				'tip'    => wfMessage( 'hr_tip' )->text(),
3879
			]
3880
		];
3881
3882
		$script = 'mw.loader.using("mediawiki.toolbar", function () {';
3883
		foreach ( $toolarray as $tool ) {
3884
			if ( !$tool ) {
3885
				continue;
3886
			}
3887
3888
			$params = [
3889
				// Images are defined in ResourceLoaderEditToolbarModule
3890
				false,
3891
				// Note that we use the tip both for the ALT tag and the TITLE tag of the image.
3892
				// Older browsers show a "speedtip" type message only for ALT.
3893
				// Ideally these should be different, realistically they
3894
				// probably don't need to be.
3895
				$tool['tip'],
3896
				$tool['open'],
3897
				$tool['close'],
3898
				$tool['sample'],
3899
				$tool['id'],
3900
			];
3901
3902
			$script .= Xml::encodeJsCall(
3903
				'mw.toolbar.addButton',
3904
				$params,
3905
				ResourceLoader::inDebugMode()
3906
			);
3907
		}
3908
3909
		$script .= '});';
3910
		$wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
3911
3912
		$toolbar = '<div id="toolbar"></div>';
3913
3914
		Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] );
3915
3916
		return $toolbar;
3917
	}
3918
3919
	/**
3920
	 * Returns an array of html code of the following checkboxes:
3921
	 * minor and watch
3922
	 *
3923
	 * @param int $tabindex Current tabindex
3924
	 * @param array $checked Array of checkbox => bool, where bool indicates the checked
3925
	 *                 status of the checkbox
3926
	 *
3927
	 * @return array
3928
	 */
3929
	public function getCheckboxes( &$tabindex, $checked ) {
3930
		global $wgUser, $wgUseMediaWikiUIEverywhere;
3931
3932
		$checkboxes = [];
3933
3934
		// don't show the minor edit checkbox if it's a new page or section
3935
		if ( !$this->isNew ) {
3936
			$checkboxes['minor'] = '';
3937
			$minorLabel = wfMessage( 'minoredit' )->parse();
3938 View Code Duplication
			if ( $wgUser->isAllowed( 'minoredit' ) ) {
3939
				$attribs = [
3940
					'tabindex' => ++$tabindex,
3941
					'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
3942
					'id' => 'wpMinoredit',
3943
				];
3944
				$minorEditHtml =
3945
					Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
3946
					"&#160;<label for='wpMinoredit' id='mw-editpage-minoredit'" .
3947
					Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ] ) .
3948
					">{$minorLabel}</label>";
3949
3950
				if ( $wgUseMediaWikiUIEverywhere ) {
3951
					$checkboxes['minor'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
3952
						$minorEditHtml .
3953
					Html::closeElement( 'div' );
3954
				} else {
3955
					$checkboxes['minor'] = $minorEditHtml;
3956
				}
3957
			}
3958
		}
3959
3960
		$watchLabel = wfMessage( 'watchthis' )->parse();
3961
		$checkboxes['watch'] = '';
3962 View Code Duplication
		if ( $wgUser->isLoggedIn() ) {
3963
			$attribs = [
3964
				'tabindex' => ++$tabindex,
3965
				'accesskey' => wfMessage( 'accesskey-watch' )->text(),
3966
				'id' => 'wpWatchthis',
3967
			];
3968
			$watchThisHtml =
3969
				Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
3970
				"&#160;<label for='wpWatchthis' id='mw-editpage-watch'" .
3971
				Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ] ) .
3972
				">{$watchLabel}</label>";
3973
			if ( $wgUseMediaWikiUIEverywhere ) {
3974
				$checkboxes['watch'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
3975
					$watchThisHtml .
3976
					Html::closeElement( 'div' );
3977
			} else {
3978
				$checkboxes['watch'] = $watchThisHtml;
3979
			}
3980
		}
3981
		Hooks::run( 'EditPageBeforeEditChecks', [ &$this, &$checkboxes, &$tabindex ] );
3982
		return $checkboxes;
3983
	}
3984
3985
	/**
3986
	 * Returns an array of html code of the following buttons:
3987
	 * save, diff, preview and live
3988
	 *
3989
	 * @param int $tabindex Current tabindex
3990
	 *
3991
	 * @return array
3992
	 */
3993
	public function getEditButtons( &$tabindex ) {
3994
		$buttons = [];
3995
3996
		$attribs = [
3997
			'id' => 'wpSave',
3998
			'name' => 'wpSave',
3999
			'tabindex' => ++$tabindex,
4000
		] + Linker::tooltipAndAccesskeyAttribs( 'save' );
4001
		$buttons['save'] = Html::submitButton( wfMessage( 'savearticle' )->text(),
4002
			$attribs, [ 'mw-ui-constructive' ] );
4003
4004
		++$tabindex; // use the same for preview and live preview
4005
		$attribs = [
4006
			'id' => 'wpPreview',
4007
			'name' => 'wpPreview',
4008
			'tabindex' => $tabindex,
4009
		] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
4010
		$buttons['preview'] = Html::submitButton( wfMessage( 'showpreview' )->text(),
4011
			$attribs );
4012
		$buttons['live'] = '';
4013
4014
		$attribs = [
4015
			'id' => 'wpDiff',
4016
			'name' => 'wpDiff',
4017
			'tabindex' => ++$tabindex,
4018
		] + Linker::tooltipAndAccesskeyAttribs( 'diff' );
4019
		$buttons['diff'] = Html::submitButton( wfMessage( 'showdiff' )->text(),
4020
			$attribs );
4021
4022
		Hooks::run( 'EditPageBeforeEditButtons', [ &$this, &$buttons, &$tabindex ] );
4023
		return $buttons;
4024
	}
4025
4026
	/**
4027
	 * Creates a basic error page which informs the user that
4028
	 * they have attempted to edit a nonexistent section.
4029
	 */
4030
	function noSuchSectionPage() {
4031
		global $wgOut;
4032
4033
		$wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
4034
4035
		$res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
4036
		Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
4037
		$wgOut->addHTML( $res );
4038
4039
		$wgOut->returnToMain( false, $this->mTitle );
4040
	}
4041
4042
	/**
4043
	 * Show "your edit contains spam" page with your diff and text
4044
	 *
4045
	 * @param string|array|bool $match Text (or array of texts) which triggered one or more filters
4046
	 */
4047
	public function spamPageWithContent( $match = false ) {
4048
		global $wgOut, $wgLang;
4049
		$this->textbox2 = $this->textbox1;
4050
4051
		if ( is_array( $match ) ) {
4052
			$match = $wgLang->listToText( $match );
4053
		}
4054
		$wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
4055
4056
		$wgOut->addHTML( '<div id="spamprotected">' );
4057
		$wgOut->addWikiMsg( 'spamprotectiontext' );
4058
		if ( $match ) {
4059
			$wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4060
		}
4061
		$wgOut->addHTML( '</div>' );
4062
4063
		$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4064
		$this->showDiff();
4065
4066
		$wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4067
		$this->showTextbox2();
4068
4069
		$wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4070
	}
4071
4072
	/**
4073
	 * Check if the browser is on a blacklist of user-agents known to
4074
	 * mangle UTF-8 data on form submission. Returns true if Unicode
4075
	 * should make it through, false if it's known to be a problem.
4076
	 * @return bool
4077
	 */
4078
	private function checkUnicodeCompliantBrowser() {
4079
		global $wgBrowserBlackList, $wgRequest;
4080
4081
		$currentbrowser = $wgRequest->getHeader( 'User-Agent' );
4082
		if ( $currentbrowser === false ) {
4083
			// No User-Agent header sent? Trust it by default...
4084
			return true;
4085
		}
4086
4087
		foreach ( $wgBrowserBlackList as $browser ) {
4088
			if ( preg_match( $browser, $currentbrowser ) ) {
4089
				return false;
4090
			}
4091
		}
4092
		return true;
4093
	}
4094
4095
	/**
4096
	 * Filter an input field through a Unicode de-armoring process if it
4097
	 * came from an old browser with known broken Unicode editing issues.
4098
	 *
4099
	 * @param WebRequest $request
4100
	 * @param string $field
4101
	 * @return string
4102
	 */
4103
	protected function safeUnicodeInput( $request, $field ) {
4104
		$text = rtrim( $request->getText( $field ) );
4105
		return $request->getBool( 'safemode' )
4106
			? $this->unmakeSafe( $text )
4107
			: $text;
4108
	}
4109
4110
	/**
4111
	 * Filter an output field through a Unicode armoring process if it is
4112
	 * going to an old browser with known broken Unicode editing issues.
4113
	 *
4114
	 * @param string $text
4115
	 * @return string
4116
	 */
4117
	protected function safeUnicodeOutput( $text ) {
4118
		global $wgContLang;
4119
		$codedText = $wgContLang->recodeForEdit( $text );
4120
		return $this->checkUnicodeCompliantBrowser()
4121
			? $codedText
4122
			: $this->makeSafe( $codedText );
4123
	}
4124
4125
	/**
4126
	 * A number of web browsers are known to corrupt non-ASCII characters
4127
	 * in a UTF-8 text editing environment. To protect against this,
4128
	 * detected browsers will be served an armored version of the text,
4129
	 * with non-ASCII chars converted to numeric HTML character references.
4130
	 *
4131
	 * Preexisting such character references will have a 0 added to them
4132
	 * to ensure that round-trips do not alter the original data.
4133
	 *
4134
	 * @param string $invalue
4135
	 * @return string
4136
	 */
4137
	private function makeSafe( $invalue ) {
4138
		// Armor existing references for reversibility.
4139
		$invalue = strtr( $invalue, [ "&#x" => "&#x0" ] );
4140
4141
		$bytesleft = 0;
4142
		$result = "";
4143
		$working = 0;
4144
		$valueLength = strlen( $invalue );
4145
		for ( $i = 0; $i < $valueLength; $i++ ) {
4146
			$bytevalue = ord( $invalue[$i] );
4147
			if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
4148
				$result .= chr( $bytevalue );
4149
				$bytesleft = 0;
4150
			} elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
4151
				$working = $working << 6;
4152
				$working += ( $bytevalue & 0x3F );
4153
				$bytesleft--;
4154
				if ( $bytesleft <= 0 ) {
4155
					$result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
4156
				}
4157
			} elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
4158
				$working = $bytevalue & 0x1F;
4159
				$bytesleft = 1;
4160
			} elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
4161
				$working = $bytevalue & 0x0F;
4162
				$bytesleft = 2;
4163
			} else { // 1111 0xxx
4164
				$working = $bytevalue & 0x07;
4165
				$bytesleft = 3;
4166
			}
4167
		}
4168
		return $result;
4169
	}
4170
4171
	/**
4172
	 * Reverse the previously applied transliteration of non-ASCII characters
4173
	 * back to UTF-8. Used to protect data from corruption by broken web browsers
4174
	 * as listed in $wgBrowserBlackList.
4175
	 *
4176
	 * @param string $invalue
4177
	 * @return string
4178
	 */
4179
	private function unmakeSafe( $invalue ) {
4180
		$result = "";
4181
		$valueLength = strlen( $invalue );
4182
		for ( $i = 0; $i < $valueLength; $i++ ) {
4183
			if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
4184
				$i += 3;
4185
				$hexstring = "";
4186
				do {
4187
					$hexstring .= $invalue[$i];
4188
					$i++;
4189
				} while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
4190
4191
				// Do some sanity checks. These aren't needed for reversibility,
4192
				// but should help keep the breakage down if the editor
4193
				// breaks one of the entities whilst editing.
4194
				if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) {
4195
					$codepoint = hexdec( $hexstring );
4196
					$result .= UtfNormal\Utils::codepointToUtf8( $codepoint );
4197
				} else {
4198
					$result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
4199
				}
4200
			} else {
4201
				$result .= substr( $invalue, $i, 1 );
4202
			}
4203
		}
4204
		// reverse the transform that we made for reversibility reasons.
4205
		return strtr( $result, [ "&#x0" => "&#x" ] );
4206
	}
4207
}
4208